Здравствуй,
Во-первых, спасибо за libseccomp - мы успешно используем его в продакшене уже несколько лет и не сталкивались с какими-либо проблемами (до сих пор). Я не уверен, является ли это ошибкой в нашем коде, неправильным пониманием документации или чем-то еще, но я потратил последний месяц, пытаясь отследить это, но безрезультатно.
Недавно мы обновили пакеты в наших контейнерах Docker, включая обновление с libseccomp 2.3.3 (версия в стабильных репозиториях Debian) до 2.4.3. Были и другие системные пакеты, которые также были обновлены, но я их не записывал. Наше ядро не обновлялось и имеет версию 4.19.0-8-amd64.
Мы используем SCMP_ACT_TRACE
и создаем фильтр, состоящий только из правил SCMP_ACT_ALLOW
, которые добавляются с использованием собственных номеров системных вызовов, а не псевдо-чисел libseccomp. Мы отключаем 64-битный вспомогательный процесс, который создает и загружает фильтр seccomp перед exec
-ингом другого 64-битного двоичного файла.
Для справки, это вся наша процедура инициализации seccomp с использованием проверки ошибок, аналогичной странице руководства
Однако наш вызов seccomp_load
начал возвращать -EINVAL
порядка 1/100 000 инициализаций процесса. (Отсутствие возможности надежного воспроизведения сделало эту отладку утомительной.) В это время в нашем приложении не было изменений кода. Системные вызовы, добавленные к фильтру, идентичны для всех запусков.
Есть идеи о том, что может пойти не так (или даже как глубже разобраться в том, что не так), или ожидается ли это каким-то образом? Не так много динамических движущихся частей, и я не смог найти в документации ничего о том, почему это может происходить.
Привет @Xyene!
В пути кода seccomp_load () не так много мест, которые возвращают -EINVAL. Основываясь на быстром изучении кода libseccomp v2.4.3, похоже, что это связано либо с недопустимым scmp_filter_ctx
либо с жалобой ядра на вызов prctl(...)
который загружает фильтр.
Учитывая, что v2.4.3 в целом работает, и вы не меняли свое ядро, кажется сомнительным, что вызов prctl(...)
является причиной, которая приводит нас к недопустимому контексту фильтра. Заметили ли вы какое-либо другое странное поведение в вашей программе после обновления? Мне интересно, есть ли проблема с повреждением памяти где-либо еще, которая вызывает проблему.
Хотя всегда возможно, что ошибка связана с libseccomp, мы запускаем каждый выпуск через серию проверок, которые включают запуск valgrind для всех наших регрессионных тестов, а также статический анализ с использованием как clang, так и Coverity.
Кроме того, хотя это не помогает для v2.4.3, одно из улучшений, на которые мы нацелены для почти готовой версии v2.5.0, - это улучшенная документация и обработка кодов ошибок.
Недавно мы обновили пакеты в наших контейнерах Docker, включая обновление с libseccomp 2.3.3 (версия в стабильных репозиториях Debian) до 2.4.3. Были и другие системные пакеты, которые также были обновлены, но я их не записывал. Наше ядро не обновлялось и имеет версию 4.19.0-8-amd64.
Спасибо за проверку того, что ваш код и базовое ядро не изменились. Это должно помочь отследить проблему.
Для справки, это вся наша процедура инициализации seccomp с использованием проверки ошибок, аналогичной описанной на странице руководства
seccomp_rule_add
.
Ваш фильтр мне кажется разумным.
Есть идеи о том, что может пойти не так (или даже как глубже разобраться в том, что не так), или ожидается ли это каким-то образом? Не так много динамических движущихся частей, и я не смог найти в документации ничего о том, почему это может происходить.
Я просмотрел код v2.4.3 seccomp_load()
и думаю, что есть только два места, где libseccomp генерирует код возврата -EINVAL
:
seccomp_load()
в строке 283_gen_bpf_build_bpf()
на линии 1657Обе указанные выше ошибки фактически вызваны неправильным фильтром. Это кажется мне маловероятным, исходя из вашего кода фильтра.
Стоит отметить, что возвращаемое ядром умолчанию в seccomp_set_mode_filter()
равно -EINVAL
, поэтому возможно, что что-то еще в системе изменилось, что привело к тому, что мы упали по этому пути. Вы упомянули, что работаете в Docker; Вы отключаете фильтр Docker seccomp по умолчанию?
У меня возникнет соблазн добавить еще немного отладки в ваш код внутри if после сбоя seccomp_load()
. Например, мы могли бы вывести PFC и / или BPF самого фильтра, чтобы убедиться, что он выглядит разумным. См. seccomp_export_pfc()
и seccomp_export_bpf()
.
Я просмотрел код v2.4.3
seccomp_load()
и думаю, что есть только два места, где libseccomp генерирует код возврата-EINVAL
:
seccomp_load()
в строке 283_gen_bpf_build_bpf()
на линии 1657
Имейте в виду, что любые сбои, обнаруженные в gen_bpf_generate(...)
или ниже, эффективно объединяются в -ENOMEM с помощью sys_filter_load(...)
в src / system.c: 267 .
Ненавижу возвращаться к «порче памяти»! так быстро, но похоже, что это может иметь место здесь.
Спасибо за быстрые и подробные ответы! Они открыли для себя несколько направлений исследования.
Заметили ли вы какое-либо другое странное поведение в вашей программе после обновления? Мне интересно, есть ли проблема с повреждением памяти где-либо еще, которая вызывает проблему.
Нет, только это. Наши модульные и интеграционные тесты продолжают проходить, и, за исключением этого очень редкого EINVAL
, в prod не регистрируются никакие ошибки. Это, конечно, вызывает недоумение; Я также подозревал повреждение памяти, но не смог найти никаких доказательств, подтверждающих это: слегка_frowning_face:
Для более подробного контекста:
seccomp_init
и т. д.Набирая это, у меня возникла идея: я слышал ужасные истории о том, что malloc
небезопасно использовать после разветвления, и у нас есть некоторые из них в самой libseccomp. Само приложение Python _ является многопоточным, но мы всегда храним GIL в нативном коде, так что это должно быть безопасно (?). Однако я слышал только о тупиках, возникающих через malloc-after-fork. (Я предполагаю, что это заставляет следующий порядок бизнеса перемещать seccomp_init
и др. Перед форком, вызывая только seccomp_load
post-fork и проверяя, продолжают ли возникать ошибки.)
У меня возникнет соблазн добавить дополнительную отладку в ваш код внутри if после сбоя seccomp_load ().
Спасибо за предложение! Я добавил вызов seccomp_export_pfc
, а также выгрузил содержимое ввода в фильтр ( config->syscall_whitelist
). Я свяжусь с вами в следующий раз, когда это не удастся.
Привет, @Xyene, так как прошло около недели, я просто хотел проверить, есть ли что-нибудь новое, что вы нашли?
К сожалению, пока нет. После добавления патча к seccomp_export_pfc
молчал. Вчера я установил этот патч на все наши виртуальные машины (а не только на тестовый) в надежде зафиксировать проблему, когда она в конечном итоге возникнет.
Я нахожу молчание странным, но сейчас я списываю его на совпадение, поскольку вся логика отладки / экспорта происходит _после_ сбоя seccomp_load
, поэтому это не должно влиять на сам сбой.
Прогресс!
Оказывается, причина молчания в том, что в seccomp_export_bpf
произошел сбой (если он был вызван после seccomp_load
?), И об этом сообщалось в другом месте, а не там, где я искал сбои seccomp. Что еще более важно, я столкнулся со случаем, когда я могу надежно воспроизвести проблему в ~ 150 вызовах, поэтому с некоторыми сантехническими работами я смогу извлечь некоторые дампы ядра.
Хорошо, я вытащил дамп ядра, и это был след: https://gist.github.com/Xyene/920f1cb098784a031f53c66a2f49d167
Это было немного подозрительно, поскольку происходит сбой внутри подпрограммы jemalloc realloc
. Более того, использование glibc malloc вместо этого решает проблему (к сожалению, в данном случае это не долгосрочный вариант из-за проблем фрагментации).
Затем я вытащил jemalloc, скомпилировал его с помощью -O0
и отладочных символов и перезапустил воспроизведение. На этот раз он разбился в seccomp_load
, а не после! Я загрузил эту трассировку здесь: https://gist.github.com/Xyene/5da56168bcea337da85b2cd30704d12e
Фрагмент этого следа:
#9 0x00007ff962698495 in free (ptr=0x5a5a5a5a5a5a5a5a) at src/jemalloc.c:2867
No locals.
#10 0x00007ff96062d087 in _program_free (prg=prg@entry=0x7ff95e963010) at gen_bpf.c:511
No locals.
#11 0x00007ff96062f605 in gen_bpf_release (program=program@entry=0x7ff95e963010) at gen_bpf.c:1986
No locals.
#12 0x00007ff96062c04f in sys_filter_load (col=col@entry=0x7ff95e9a5000) at system.c:293
rc = -1
prgm = 0x7ff95e963010
#13 0x00007ff96062b666 in seccomp_load (ctx=ctx@entry=0x7ff95e9a5000) at api.c:286
col = 0x7ff95e9a5000
При поиске в jemalloc выясняется, что 0x5a
используется для пометки свободных байтов как свободных с конкретным намерением сбой кода, который пытается освободить что-то, что уже было освобождено.
gen_bpf.c:511
в v2.4.3: https://github.com/seccomp/libseccomp/blob/1dde9d94e0848e12da20602ca38032b91d521427/src/gen_bpf.c#L505 -L513
Но в этом нет особого смысла, поскольку время жизни программы - это всего лишь тело sys_filter_load
:
Думаю, я заметил по крайней мере одну проблему. В gen_bpf_generate
;
state.bpf = prgm
до тех пор, пока не завершится ошибка zmalloc
. Затем вызывается _gen_bpf_build_bpf
, и на основе его rc
state.bpf
устанавливается значение NULL
.
Учитывая случай, когда rc != 0
, state.bpf
по-прежнему имеет значение prgm
во время вызова _state_release
. Это приведет к освобождению памяти, на которую указывает prgm
.
Затем gen_bpf_generate
будет return prgm
, который, несмотря на то, что был освобожден, все еще является ненулевым указателем.
Вернувшись в sys_filter_load
, gen_bpf_generate
возвращается, а prgm
равно NULL
так что это продолжается.
Наконец, в конце sys_filter_load
вызывается gen_bpf_release
для уже освобожденного prgm
.
Это не решает вопроса о том, почему _gen_bpf_build_bpf
вообще не сработает, но похоже на что-то плохое, что могло бы случиться, если бы это произошло.
Изменить: на самом деле, похоже, что это, вероятно, было исправлено как побочный эффект https://github.com/seccomp/libseccomp/commit/3a1d1c977065f204b96293cccfe7d3e5aa0d7ace.
Учитывая случай, когда rc! = 0, state.bpf по-прежнему имеет значение prgm во время вызова _state_release. Это приведет к освобождению памяти, на которую указывает prgm.
Ага! Хороший улов @Xyene!
Я думаю, нам нужно исправить это помимо 3a1d1c977065f204b96293cccfe7d3e5aa0d7ace, позвольте мне подумать об этом на минутку ... Я не думаю, что исправление будет слишком сложным ... и посмотрю, смогу ли я придумать PR.
Я думаю, нам нужно исправить это помимо 3a1d1c9, позвольте мне подумать об этом на минуту ... Я не думаю, что исправление будет слишком сложным ... и посмотрю, смогу ли я придумать PR.
Ой, когда я писал это, я смотрел на старый код; да, я считаю, что 3a1d1c9 исправит это для нас, но нам понадобится патч для ветки release-2.4. Я сейчас над этим поработаю.
_ (Мета: Я буду продолжать обновлять это сообщение своими выводами по мере продвижения, так что у меня есть место, чтобы записать их, не рассылая вам спам по электронной почте, ребята :) _
Хорошо, вернувшись к 2.4.3 с примененным патчем, я смог вытащить фильтр, который не работал: ссылка .
Сообщенная причина теперь ENOMEM
вместо EINVAL
, что, я думаю, ожидается, учитывая, что _gen_bpf_build_bpf
не работает и возвращает программу NULL
. Однако PFC печатает нормально. При изменении кода seccomp для сообщения о возвращаемом значении _gen_bpf_build_bpf
EFAULT
в качестве причины указывается
В качестве быстрого взлома я запустил :%s/return -EFAULT/abort()
поверх src/gen_bpf.c
и смог извлечь эту трассировку стека:
EFAULT трассировка стека
(gdb) bt full
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
set = {__val = {0, 140084028365964, 140083248439464, 140083248438968, 140083248431088, 140084028368143, 28659884033, 140083965300736,
140083248439464, 140083248438968, 140083248431088, 140084028351031, 140084019988760, 140083248439624, 140083248431200, 140084028372597}}
pid = <optimized out>
tid = <optimized out>
ret = <optimized out>
#1 0x00007f67daa4d55b in __GI_abort () at abort.c:79
save_stage = 1
act = {__sigaction_handler = {sa_handler = 0x7f67d6f3eec0, sa_sigaction = 0x7f67d6f3eec0}, sa_mask = {__val = {140083965300736,
140083965300736, 0, 0, 140083248438968, 140083248438968, 140083248439464, 140083248431504, 140084028417173, 140083964793344,
140083965300736, 140083248431552, 140083994791895, 140083248431552, 140083994787642, 140083965300736}}, sa_flags = -1404894496,
sa_restorer = 0x0}
sigs = {__val = {32, 0 <repeats 15 times>}}
#2 0x00007f67d8bfd455 in _gen_bpf_build_bpf (state=0x7f67ac4302e0, col=0x7f67d6f63040) at gen_bpf.c:1943
rc = 0
iter = 1
h_val = 1425818561
res_cnt = 0
jmp_len = 0
arch_x86_64 = 0
arch_x32 = -1
instr = {op = 32, jt = {tgt = {imm_j = 0 '\000', imm_k = 0, hash = 0, db = 0x0, blk = 0x0, nxt = 0}, type = TGT_NONE}, jf = {tgt = {
imm_j = 0 '\000', imm_k = 0, hash = 0, db = 0x0, blk = 0x0, nxt = 0}, type = TGT_NONE}, k = {tgt = {imm_j = 4 '\004', imm_k = 4,
hash = 4, db = 0x4, blk = 0x4, nxt = 4}, type = TGT_K}}
i_iter = 0x7f67d6fdcb60
b_badarch = 0x7f67d6fd9000
b_default = 0x7f67d6fd9060
b_head = 0x7f67d6fda1a0
b_tail = 0x7f67d6fd9000
b_iter = 0x0
b_new = 0x7f67d6fe3300
b_jmp = 0x0
db_secondary = 0x0
pseudo_arch = {token = 0, token_bpf = 0, size = ARCH_SIZE_UNSPEC, endian = ARCH_ENDIAN_LITTLE, syscall_resolve_name = 0x0,
syscall_resolve_num = 0x0, syscall_rewrite = 0x0, rule_add = 0x0}
#3 0x00007f67d8bfd560 in gen_bpf_generate (col=0x7f67d6f63040) at gen_bpf.c:1971
rc = 0
state = {htbl = {0x0 <repeats 256 times>}, attr = 0x7f67d6f63044, bad_arch_hsh = 889798935, def_hsh = 742199527, arch = 0x7f67ac4301e0,
bpf = 0x7f67d6f64010}
prgm = 0x7f67d6f64010
#4 0x00007f67d8bf64a7 in sys_filter_load (col=0x7f67d6f63040) at system.c:265
rc = 32615
prgm = 0x0
#5 0x00007f67d8bf4f10 in seccomp_load (ctx=0x7f67d6f63040) at api.c:287
col = 0x7f67d6f63040
Это соответствует строке 1943:
Учитывая характер замены, я думаю, что мы можем исключить любые EFAULT
из любой вспомогательной функции, так как они сначала были бы прерваны.
После этого я попробовал воспроизвести то же самое с HEAD - все еще происходит. Далее %s:/goto build_bpf_free_blks/abort()
и повторяем. Причина была:
К счастью, эта функция была короткой и имела лишь несколько точек отказа. Еще один раунд вставок abort
позже;
След
(gdb) bt full
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
set = {__val = {0, 140050183343588, 0, 448, 140049402494880, 140049402509040, 140049402494832, 140050183342988, 140049402495088,
140049402509040, 140049402494896, 140050183343588, 4294967296, 140049402509040, 140049402509040, 140049402509040}}
pid = <optimized out>
tid = <optimized out>
ret = <optimized out>
#1 0x00007f5ff953055b in __GI_abort () at abort.c:79
save_stage = 1
act = {__sigaction_handler = {sa_handler = 0x7f5ff595d260, sa_sigaction = 0x7f5ff595d260}, sa_mask = {__val = {139642271694862,
140050119389792, 0, 0, 140049402502840, 0, 140049402503336, 140049402502888, 140049402502840, 112, 384, 140049402502840, 140050149861504,
140049402495328, 140050149857273, 392}}, sa_flags = 448, sa_restorer = 0x7f5ff595d240}
sigs = {__val = {32, 0 <repeats 15 times>}}
#2 0x00007f5ff76edee5 in _bpf_append_blk (prg=0x7f5ff5964010, blk=0x7f5ff59df1a0) at gen_bpf.c:452
rc = -12
i_new = 0x0
i_iter = 0x7f5ff59fa178
old_cnt = 48
iter = 1
#3 0x00007f5ff76f3716 in _gen_bpf_build_bpf (state=0x7f5fcae302d0, col=0x7f5ff59c5000) at gen_bpf.c:2223
rc = 0
iter = 1
h_val = 1425818561
res_cnt = 0
jmp_len = 0
arch_x86_64 = 0
arch_x32 = -1
instr = {op = 32, jt = {tgt = {imm_j = 0 '\000', imm_k = 0, hash = 0, db = 0x0, blk = 0x0, nxt = 0}, type = TGT_NONE}, jf = {tgt = {
imm_j = 0 '\000', imm_k = 0, hash = 0, db = 0x0, blk = 0x0, nxt = 0}, type = TGT_NONE}, k = {tgt = {imm_j = 4 '\004', imm_k = 4,
hash = 4, db = 0x4, blk = 0x4, nxt = 4}, type = TGT_K}}
i_iter = 0x7f5ff59e1b60
b_badarch = 0x7f5ff59de000
b_default = 0x7f5ff59de060
b_head = 0x7f5ff59df1a0
b_tail = 0x7f5ff59de000
b_iter = 0x7f5ff59df1a0
b_new = 0x7f5ff59e8300
b_jmp = 0x7f5ff59df0e0
db_secondary = 0x0
pseudo_arch = {token = 0, token_bpf = 0, size = ARCH_SIZE_UNSPEC, endian = ARCH_ENDIAN_LITTLE, syscall_resolve_name = 0x0,
syscall_resolve_num = 0x0, syscall_rewrite = 0x0, rule_add = 0x0}
#4 0x00007f5ff76f3874 in gen_bpf_generate (col=0x7f5ff59c5000, prgm_ptr=0x7f5fcae30b40) at gen_bpf.c:2270
rc = 0
state = {htbl = {0x0, 0x7f5ff593ef80, 0x7f5ff593efe0, 0x7f5ff593efc0, 0x0, 0x7f5ff595d000, 0x7f5ff593ef60, 0x7f5ff593ef00,
0x0 <repeats 248 times>}, attr = 0x7f5ff59c5004, bad_arch_hsh = 889798935, def_hsh = 742199527, bpf = 0x7f5ff5964010,
arch = 0x7f5fcae301c0, b_head = 0x7f5ff59e8300, b_tail = 0x7f5ff59de120, b_new = 0x7f5ff59e8300}
prgm = <optimized out>
#5 0x00007f5ff76eb275 in sys_filter_load (col=0x7f5ff59c5000, rawrc=false) at system.c:307
rc = 0
prgm = 0x0
#6 0x00007f5ff76e9505 in seccomp_load (ctx=0x7f5ff59c5000) at api.c:386
col = 0x7f5ff59c5000
rawrc = false
Итак, realloc
снова терпит неудачу, а _bpf_append_blk
возвращает -ENOMEM
который маскируется _gen_bpf_build_bpf
и превращается в -EFAULT
. Это не имеет большого значения, но, поскольку вы сказали, что улучшенная отчетность об ошибках - это цель 2,5, я подумал, что я упомянул бы об этом, поскольку это выглядит в рамках: немного_smiling_face:
Некоторые ковыряются в GDB:
(gdb) f 2
#2 0x00007f5ff76edee5 in _bpf_append_blk (prg=0x7f5ff5964010, blk=0x7f5ff59df1a0) at gen_bpf.c:452
452 abort();
(gdb) info args
prg = 0x7f5ff5964010
blk = 0x7f5ff59df1a0
(gdb) print prg->blks
$4 = (bpf_instr_raw *) 0x7f5ff59fa000
(gdb) x/32bx &prg->blks
0x7f5ff5964018: 0x00 0xa0 0x9f 0xf5 0x5f 0x7f 0x00 0x00
0x7f5ff5964020: 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a
0x7f5ff5964028: 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a
0x7f5ff5964030: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
(gdb) print ((prg)->blk_cnt * sizeof(*((prg)->blks)))
$5 = 392
(gdb) print prg->blk_cnt
$6 = 49
Это действительно начинает выглядеть как сбой распределителя ...
Ага, эта история наконец-то достигла своего _влекательного_ завершения - я понял, что происходит, и проверил исправление: слегка_smiling_face:
Поскольку это может быть интересная история, вот она:
Основной процесс, который отделяет воркера, обычно занимает около 80 МБ RSS. После разветвления он ограничивает использование памяти через rlimit
, иногда до 64 МБ. Это ставит его в положение, когда его текущее использование памяти превышает его предел, но это разрешено rlimit
. В большинстве случаев у распределителя памяти будет достаточно свободной памяти для обслуживания процедур инициализации libseccomp без дополнительных запросов от ядра. Но когда это _не__ и ему нужно запросить место для дополнительной арены или чего-то еще, ядро не предоставит его, поскольку процесс уже превысил свой предел.
В 2.4.3 эта ошибка при получении памяти проявлялась в EINVAL
и двойном освобождении. В главном посте - https://github.com/seccomp/libseccomp/commit/3a1d1c977065f204b96293cccfe7d3e5aa0d7ace вместо этого сообщается EFAULT
. При применении https://github.com/seccomp/libseccomp/pull/257 ENOMEM
отображается правильно.
Причина, по которой это происходит так редко, становится очевидной: это полностью зависит от того, достаточно ли памяти у распределителя для создания программы BPF без дополнительных запросов от ядра. Распределитель glibc более свободен в разрешении нарастания фрагментации, поэтому с ним этого никогда не происходило. jemalloc устанавливает более жесткие границы и приводит к увеличению вероятности необходимости запрашивать память во время seccomp_load
- достаточно, чтобы заметить возникающие в результате сбои, но по-прежнему раздражает отслеживание.
Тогда исправление состоит в том, чтобы просто переместить все вызовы setrlimit
в _after_ seccomp_load
. При этом realloc
больше не дает сбой в _bpf_append_blk
, и фильтр загружается успешно. Это означает, что фильтр должен разрешать setrlimit
, но в моем случае это было приемлемо. В целом, я думаю, что эту проблему можно решить чем-то вроде https://github.com/seccomp/libseccomp/issues/123.
@pcmoore , @drakenclimber - еще раз спасибо за вашу помощь в устранении этой проблемы! Я рад, что могу оставить это позади, но ваши указатели оказались неоценимыми в достижении цели: smiley:
Эта ошибка была исправлена фиксацией https://github.com/seccomp/libseccomp/commit/c0a6e6fd15f74c429a0b74e0dfd4de5a29aabebd
Самый полезный комментарий
К сожалению, пока нет. После добавления патча к
seccomp_export_pfc
молчал. Вчера я установил этот патч на все наши виртуальные машины (а не только на тестовый) в надежде зафиксировать проблему, когда она в конечном итоге возникнет.Я нахожу молчание странным, но сейчас я списываю его на совпадение, поскольку вся логика отладки / экспорта происходит _после_ сбоя
seccomp_load
, поэтому это не должно влиять на сам сбой.