Недетерминированная природа многопоточности усложняет отладку, но я думаю, что я получил довольно воспроизводимый тестовый пример -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, сигналах и системе потоковой передачи в целом, чтобы отладить основную причину. Надеюсь, ты сможешь понять это.
ПРИМЕЧАНИЕ: я еще не смотрел ваш тестовый пример, я просто предполагаю, основываясь на вашем хорошо написанном описании
Учитывая многопоточный характер этого, пробовали ли вы установить для атрибута 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 , ...] сигналов в параллельных блоках. В конечном итоге это выглядит как сложная проблема, которой лучше всего избегать.
Похоже, есть несколько простых решений:
Не используйте SCMP_ACT_KILL
качестве обработчика по умолчанию. Лучше вместо этого переключиться на использование кода ошибки, например, SCMP_ACT_ERROR(5)
. Я внес это изменение в ваш пример программы и убедился, что она завершилась правильно.
Если вам не нужна мощь 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_ также вернет ошибку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, извините за это.
Спасибо, что сообщили нам, что остальное сработало!