Libseccomp: ОШИБКА: совместимость с openmp

Созданный на 2 сент. 2017  ·  19Комментарии  ·  Источник: seccomp/libseccomp

Недетерминированная природа многопоточности усложняет отладку, но я думаю, что я получил довольно воспроизводимый тестовый пример -O0 -ggdb -fopenmp .

Если мы выполним его только с одним потоком и позволим ему выполнить запрещенный системный вызов, программа умрет, как и ожидалось.

$ ./seccomp_omp -t 1 -k 0      
86195973
[1]    10103 invalid system call (core dumped)  ./seccomp_omp -t 1 -k 0

Однако при увеличении количества потоков он просто перестает функционировать.

$ ./seccomp_omp -t 2 -k 0
86198868
86195973
86200317
^C

Программу не убивают, она просто перестает что-либо делать. Я не понимаю, что случилось с сигналом SIGSYS и почему он не обрабатывается?

Возможно, это вообще не проблема libseccomp, но я недостаточно разбираюсь в seccomp, сигналах и системе потоковой передачи в целом, чтобы отладить основную причину. Надеюсь, ты сможешь понять это.

bug prioritmedium

Все 19 Комментарий

ПРИМЕЧАНИЕ: я еще не смотрел ваш тестовый пример, я просто предполагаю, основываясь на вашем хорошо написанном описании

Учитывая многопоточный характер этого, пробовали ли вы установить для атрибута SCMP_FLTATR_CTL_TSYNC filter значение true перед загрузкой фильтра в ядро?

Я не установил SCMP_FLTATR_CTL_TSYNC потому что добавляю фильтр seccomp перед разделением на потоки. Таким образом, ядро ​​в любом случае должно применять фильтр ко всем потокам (и, по-видимому, оно это делает). Добавление SCMP_FLTATR_CTL_TSYNC в тестовый пример не имеет никакого значения (кстати, это меньше 100 строк с педантичной обработкой ошибок).

Хорошо, я просто хотел об этом упомянуть; похоже, что вы все делаете правильно (устанавливаете фильтр перед созданием новых потоков). Придется присмотреться, но, возможно, в ближайшее время у меня не будет возможности сделать это.

Для меня достаточно подтверждения того, что я не делаю что-то явно неправильно. Не торопитесь!

В однопоточном примере ./seccomp_omp -t 1 -k 0 openmp распознает, что будет выполняться только один поток, поэтому openmp обходит большую часть синхронизации, которая обычно выполняется в многопоточном цикле for. Я проверил это, наблюдая за системными вызовами, которые обрабатывались в seccomp_run_filters () в ядре. Как и следовало ожидать, я увидел вызов __NR_write () и вызов __NR_madvise (), которые побудили seccomp дать команду ядру убить поток.

В многопоточном примере ./seccomp_omp -t 2 -k 0 openmp пытается распараллелить цикл for. Таким образом, используется гораздо больше библиотеки openmp. Это становится очевидным при повторном просмотре системных вызовов через seccomp_run_filters (). __NR_mmap (), __NR_mprotect (), __NR_clone (), __NR_futex () и другие системные вызовы вызываются до вызова madvise (). В конце концов, один из двух потоков вызывает madvise (), и seccomp должным образом уничтожает этот поток. Но openmp имеет синхронизацию между потоками, и прежде чем второй поток сможет вызвать madvise () (и быть убитым), второй поток вызывает futex (), поскольку он ожидает чего-то от мертвого потока. И поэтому программа зависает. Один поток был убит, а второй поток ожидает данных (от мертвого потока), которые никогда не поступят.

Я мало работал с openmp, но, похоже, есть некоторые обсуждения [ 1 , 2 , 3 , ...] сигналов в параллельных блоках. В конечном итоге это выглядит как сложная проблема, которой лучше всего избегать.

Похоже, есть несколько простых решений:

  1. Не используйте SCMP_ACT_KILL качестве обработчика по умолчанию. Лучше вместо этого переключиться на использование кода ошибки, например, SCMP_ACT_ERROR(5) . Я внес это изменение в ваш пример программы и убедился, что она завершилась правильно.

  2. Если вам не нужна мощь openmp, другим вариантом может быть переключение на использование более распространенного решения, такого как pthread_create () или fork ().

@drakenclimber, так что похоже, что проблема в том, что openmp делает дополнительные системные вызовы, которые обычно не могут быть частью фильтра? Или здесь не хватает более крупной точки?

@drakenclimber Большое спасибо за то, что

Чтобы дать больше контекста: я пишу научный код, поэтому мне нравится упрощать работу с OpenMP. Однако я также хочу защитить своих пользователей от самих себя. Таким образом, если моя программа делает что-то необычное, просто уничтожьте ее. Я предполагаю, что в конечном итоге я хочу просто убить процесс (# 96) с довольно разумным сообщением об ошибке.

Какое значение я должен использовать для errno в SCMP_ACT_ERROR чтобы точно имитировать SECCOMP_RET_KILL_PROCESS ? Есть ли что-то, что отлично работает со всеми системными вызовами?

@pcmoore - вроде как. Но я думаю, что более серьезная проблема заключается в том, что openmp не обрабатывает сигналы изящно внутри своей параллельной конструкции. Я предполагаю, что это сделано специально.

@kloetzl - Не беспокойтесь - вы можете полностью придерживаться OpenMP. Похоже, что другие участники сообщества OpenMP делают что-то вроде следующего:

ctx = seccomp_init(SCMP_ACT_ERRNO(ENOTBLK));
...
bool kill_process = false;
int rc;
#pragma omp parallel for num_threads(THREADS)
  for (int i = 0; i < THREADS * 2; i++) {
    fprintf(stderr, "%u\n", func(i));
    if (i == K) {
      rc = madvise(NULL, 0, 0); 
      if (rc < 0)
        kill_process = true;
      }   
    // sleep(10);
  }

  if (kill_process)
    goto error;

  seccomp_release(ctx);
  return 0;
error:
  seccomp_release(ctx);
  return -1; 
}

Что касается того, какой результат должен вернуть SCMP_ACT_ERRNO() , решать вам. SCMP_ACT_ERRNO() будет работать со всеми системными вызовами. И ваша программа будет обрабатывать ее и возвращать пользователю ошибку, так что вы можете выбрать любой errno, который лучше всего подходит для вас. Фактически, выбор непонятного варианта - скажем, ENOTBLK - может быть способом узнать, что ошибка возникла из-за блокировки вызова seccomp.

tl; dr - Обнаружить проблему в параллельном цикле, но подождать, пока не завершится цикл, чтобы обработать ее. Вам может потребоваться добавить дополнительное защитное кодирование в цикл из-за сбоя системного вызова.

@kloetzl - Я рассмотрю вопрос № 96 и посмотрю, насколько хорошо он работает с OpenMP. Это могло быть и другим решением. Спасибо!

И ваша программа будет обрабатывать ее и возвращать пользователю ошибку, так что вы можете выбрать любой errno, который лучше всего подходит для вас. Фактически, выбор непонятного варианта - скажем, ENOTBLK - может быть способом узнать, что ошибка возникла из-за блокировки вызова seccomp.

Дело в том, что madvise вызывается глубоко в недрах malloc . Таким образом, это не я обрабатываю ошибку, а glibc. Итак, вопрос в том, знает ли glibc (и все другие библиотеки, выполняющие системные вызовы), что системный вызов может возвращать другие ошибки, чем указано на его странице руководства, и обрабатывать их соответствующим образом?

Дело в том, что мэдвизе называют глубоко в недрах malloc. Таким образом, это не я обрабатываю ошибку, а glibc. Итак, вопрос в том, знает ли glibc (и все другие библиотеки, выполняющие системные вызовы), что системный вызов может возвращать другие ошибки, чем указано на его странице руководства, и обрабатывать их соответствующим образом?

Аааааааааааааааааааааааааааааааааа речь. Чтобы убедиться в этом, мне придется просмотреть код glibc, но я готов рискнуть и сделать следующие предположения:

  • Если madvise возвращает ошибку, glibc _definally_ также вернет ошибку
  • Определенно возможно, что glibc может возвращать ошибку, отличную от возвращаемой seccomp, поэтому вам следует проявлять осторожность, если вы ожидаете "пользовательский" код возврата, например ENOTBLK

Короче говоря, glibc не должен подавлять _ любой_ код ошибки, но вместо этого он может возвращать другой код ошибки.

Я стараюсь не отставать от вас, ребята, но боюсь, что отсутствие опыта работы с OpenMP заставляет меня немного отставать ... судя по комментариям выше, похоже, что KILL_PROCESS может быть здесь работоспособным решением? Если это так, мы можем повысить приоритет этой проблемы, хотя она входит в мой список вещей, которые нужно решить до выпуска v2.4.

@pcmoore - Да, я считаю, что KILL_PROCESS - жизнеспособное решение этой ошибки. Я протестировал тестовую программу @kloetzl, описанную выше, с помощью действия KILL_PROCESS и зависание больше не происходит.

Я реализовал это, но в настоящее время я сталкиваюсь с парой ошибок в автотестах python. На следующей неделе у меня должен быть патч.

@drakenclimber ох, патчи? Обожаю патчи :)

Спасибо ребята.

Я могу подтвердить, что KILL_PROCESS решает проблему: я тоже взломал локальную версию libseccomp на Arch, и тестовая программа не прошла должным образом. Однако создание надежных тестов и запуск их на разных ядрах может оказаться более сложной задачей.

Спасибо за подтверждение @kloetzl.

Да, написание правильных тестов может быть утомительным, но это важно. Так же важно, как и код, это проверка ИМХО.

Я с нетерпением жду PR от @drakenclimber , мы сможем обсудить еще немного, как только этот код будет опубликован.

Я провожу генеральную уборку от

Спасибо, что напомнили мне, я немного тестировал со своей стороны.

Эта проблема в основном исправлена, но есть небольшая загвоздка. Программа больше не зависает в подвешенном состоянии при обнаружении недопустимого системного вызова, ура! С помощью SCMP_ACT_TRAP можно даже создать имя плохого системного вызова. Однако OpenMP перезаписывает мой обработчик сигналов, поэтому он возвращается к сообщению об ошибке по умолчанию после создания нескольких потоков. С точки зрения пользовательского опыта мне не нравится такое поведение, но я считаю, что оно настолько хорошо, насколько возможно.

Ах, да, я не уверен, что мы можем многое сказать о перезаписи обработчика сигналов в libseccomp, извините за это.

Спасибо, что сообщили нам, что остальное сработало!

Была ли эта страница полезной?
0 / 5 - 0 рейтинги