Oi,
Em primeiro lugar, obrigado por libseccomp - estamos usando-o felizmente em produção há vários anos e não tivemos nenhum problema (até agora). Não tenho certeza se isso é um bug em nosso código, um mal-entendido da documentação ou outra coisa - mas passei o mês passado tentando rastrear isso sem sucesso.
Recentemente, atualizamos pacotes em nossos contêineres Docker, que incluíam uma atualização de libseccomp 2.3.3 (versão em repositórios estáveis do Debian) para 2.4.3. Outros pacotes de sistema também foram atualizados, mas não os gravei. Nosso kernel não foi atualizado e é a versão 4.19.0-8-amd64.
Usamos SCMP_ACT_TRACE
e construímos um filtro que consiste apenas em regras SCMP_ACT_ALLOW
que são adicionadas usando números de syscall nativos, em vez dos pseudo-números de libseccomp. Nós bifuramos um processo auxiliar de 64 bits que constrói e carrega o filtro seccomp antes de exec
-ing outro binário de 64 bits.
Para referência, esta é toda a nossa rotina de inicialização do seccomp, usando verificação de erros semelhante à página do manual seccomp_rule_add
.
No entanto, nossa chamada para seccomp_load
começou a retornar -EINVAL
, na ordem de magnitude de 1 / 100.000 inicializações de processo. (Não ser capaz de reproduzi-lo de forma confiável tornou a depuração entediante.) Não houve alterações de código em nosso aplicativo durante esse tempo. As syscalls adicionadas ao filtro são idênticas em todas as execuções.
Alguma ideia sobre o que pode estar errado (ou mesmo como investigar mais a fundo o que está errado), ou se isso é esperado de alguma forma? Não há muitas partes móveis dinâmicas e não consegui encontrar nada na documentação sobre por que isso pode estar acontecendo.
Olá @Xyene ,
Não há muitos lugares que retornam -EINVAL no caminho do código seccomp_load (). Com base em um rápido exame do código libseccomp v2.4.3, parece que é devido a um scmp_filter_ctx
inválido ou ao kernel reclamando da chamada prctl(...)
que carrega o filtro.
Considerando que a v2.4.3 geralmente funciona, e você não mudou seu kernel, parece duvidoso que a chamada prctl(...)
seja a causa que nos leva a um contexto de filtro inválido. Você notou algum outro comportamento estranho em seu programa desde a atualização? Estou me perguntando se há um problema de corrupção de memória em outro lugar que está causando o problema.
Embora seja sempre possível que a falha seja com libseccomp, executamos cada versão por meio de uma série de verificações que incluem execuções de valgrind para todos os nossos testes de regressão, bem como análise estática usando clang e Coverity.
Além disso, embora isso não ajude para a v2.4.3, uma das melhorias que pretendemos para a versão v2.5.0 quase pronta é a documentação aprimorada e o tratamento de códigos de erro.
Recentemente, atualizamos pacotes em nossos contêineres Docker, que incluíam uma atualização de libseccomp 2.3.3 (versão em repositórios estáveis do Debian) para 2.4.3. Outros pacotes de sistema também foram atualizados, mas não os gravei. Nosso kernel não foi atualizado e é a versão 4.19.0-8-amd64.
Obrigado por verificar se o seu código e o kernel subjacente não mudaram. Isso deve ajudar a rastrear o problema.
Para referência, esta é a totalidade de nossa rotina de inicialização do seccomp, usando verificação de erro semelhante à página do manual
seccomp_rule_add
.
Seu filtro parece razoável para mim.
Alguma ideia sobre o que pode estar errado (ou mesmo como investigar mais a fundo o que está errado), ou se isso é esperado de alguma forma? Não há muitas partes móveis dinâmicas e não consegui encontrar nada na documentação sobre por que isso pode estar acontecendo.
Eu olhei através do código v2.4.3 seccomp_load()
e acho que há apenas dois lugares onde libseccomp gera um código de retorno de -EINVAL
:
seccomp_load()
na linha 283_gen_bpf_build_bpf()
na linha 1657Ambos os erros acima são efetivamente causados por um filtro inválido. Isso me parece improvável com base no código do seu filtro.
É importante notar que o valor de retorno padrão do kernel em seccomp_set_mode_filter()
é -EINVAL
, então é possível que algo mais no sistema tenha mudado, levando-nos a cair nesse caminho. Você mencionou que está executando no Docker; você está desativando o filtro Docker seccomp padrão?
Eu ficaria tentado a adicionar mais depuração ao seu código dentro do if após seccomp_load()
falhar. Por exemplo, podemos gerar o PFC e / ou o BPF do próprio filtro para verificar se ele parece razoável. Veja seccomp_export_pfc()
e seccomp_export_bpf()
.
Eu olhei através do código v2.4.3
seccomp_load()
e acho que há apenas dois lugares onde libseccomp gera um código de retorno de-EINVAL
:
seccomp_load()
na linha 283_gen_bpf_build_bpf()
na linha 1657
Tenha em mente que quaisquer falhas encontradas em gen_bpf_generate(...)
, ou abaixo, são efetivamente combinadas em -ENOMEM por sys_filter_load(...)
em src / system.c: 267 .
Eu odeio voltar para "corrupção de memória!" tão rapidamente, mas parece que pode ser o caso aqui.
Obrigado pelas respostas rápidas e detalhadas! Eles geraram vários caminhos de exploração: ligeiramente_smiling_face:
Você notou algum outro comportamento estranho em seu programa desde a atualização? Estou me perguntando se há um problema de corrupção de memória em outro lugar que está causando o problema.
Não, só isso. Nossos testes de unidade e integração continuam a passar e, além deste muito raro EINVAL
, nenhum erro está sendo registrado no prod. Isso certamente o torna intrigante; Eu também suspeitei de corrupção de memória, mas não consegui encontrar nenhuma evidência para apoiá-la: ligeiramente_frowning_face:
Para um pouco mais de contexto:
seccomp_init
etc.Enquanto digitava, tive uma ideia: ouvi histórias horríveis sobre malloc
não ser seguro para usar após a bifurcação e temos algumas dentro do próprio libseccomp. O próprio aplicativo Python _é_ multithread, mas sempre mantemos o GIL enquanto estamos no código nativo, então isso deve ser seguro (?). Eu só ouvi falar de deadlocks acontecendo através de malloc-after-fork, no entanto. (Eu acho que isso faz com que a próxima ordem de negócios mova seccomp_init
et al. Antes do fork, apenas chamando seccomp_load
post-fork e vendo se os erros continuam acontecendo.)
Eu ficaria tentado a adicionar mais depuração ao seu código dentro do if after seccomp_load () falhar.
Obrigado pela sugestão! Eu adicionei uma chamada para seccomp_export_pfc
, bem como despejei o conteúdo da entrada no filtro ( config->syscall_whitelist
). Farei o acompanhamento na próxima vez que isso falhar.
Olá @Xyene - já se passou cerca de uma semana, só queria verificar se há algo novo que você encontrou.
Ainda não, infelizmente. Depois de adicionar um patch a seccomp_export_pfc
, ele ficou em silêncio. Ontem eu empurrei esse patch para todas as nossas VMs (em vez de apenas um de teste) na esperança de capturar o problema quando ele eventualmente ocorrer.
Eu acho o silêncio estranho, mas por agora estou atribuindo isso a uma coincidência, já que toda a lógica de depuração / exportação acontece _após_ a falha seccomp_load
, então isso não deveria estar afetando a falha em si.
Progresso!
Acontece que o motivo do silêncio é porque seccomp_export_bpf
estava em segfaulting (deveria, se chamado depois de seccomp_load
?), E isso estava sendo relatado em outro lugar e não onde eu estava procurando por falhas de seccomp. Mais importante, encontrei um caso em que posso reproduzir o problema de forma confiável em aproximadamente 150 invocações, portanto, com algum trabalho de encanamento, devo ser capaz de extrair alguns despejos de memória.
Tudo bem, retirei um coredump e este foi o rastreamento: https://gist.github.com/Xyene/920f1cb098784a031f53c66a2f49d167
Isso era um pouco suspeito, já que estava travando dentro da rotina realloc
do jemalloc. Além disso, o uso de glibc malloc resolve o problema (infelizmente, não é uma opção de longo prazo neste caso devido a problemas de fragmentação).
Em seguida, puxei o jemalloc, compilei-o com -O0
e símbolos de depuração e fiz a reprodução novamente. Desta vez, ele travou em seccomp_load
, ao invés de depois! Enviei esse rastreamento aqui: https://gist.github.com/Xyene/5da56168bcea337da85b2cd30704d12e
Um trecho desse traço:
#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
Pesquisando em jemalloc, parece que 0x5a
é usado para marcar bytes livres como livres , com a intenção específica de travar o código que está tentando liberar algo que já foi liberado.
gen_bpf.c:511
na v2.4.3 é: https://github.com/seccomp/libseccomp/blob/1dde9d94e0848e12da20602ca38032b91d521427/src/gen_bpf.c#L505 -L513
Mas, isso não faz muito sentido, já que a vida útil do programa é apenas o corpo de sys_filter_load
:
Acho que identifiquei pelo menos um problema. Em gen_bpf_generate
;
state.bpf = prgm
desde que zmalloc
não falhe. Em seguida, _gen_bpf_build_bpf
é chamado e baseado em rc
, state.bpf
é definido como NULL
.
Considerando o caso em que rc != 0
, state.bpf
ainda está definido como prgm
no momento da chamada para _state_release
. Isso fará com que a memória apontada por prgm
seja liberada.
Em seguida, gen_bpf_generate
will return prgm
, que apesar de ter sido liberado, ainda é um ponteiro diferente de zero.
De volta a sys_filter_load
, gen_bpf_generate
retorna, e prgm
não é- NULL
então continua.
Finalmente, no final de sys_filter_load
, gen_bpf_release
é chamado no já livre prgm
.
Isso não resolve a preocupação de por que _gen_bpf_build_bpf
falharia em primeiro lugar, mas parece algo ruim que poderia acontecer se isso acontecesse.
Editar: na verdade, parece que provavelmente foi corrigido como um efeito colateral de https://github.com/seccomp/libseccomp/commit/3a1d1c977065f204b96293cccfe7d3e5aa0d7ace.
Considerando o caso em que rc! = 0, state.bpf ainda está definido como prgm no momento da chamada para _state_release. Isso fará com que a memória apontada por prgm seja liberada.
Ah ha! Boa captura @Xyene!
Acho que precisamos consertar isso além de 3a1d1c977065f204b96293cccfe7d3e5aa0d7ace, deixe-me pensar nisso por um minuto ... Não acho que a correção será muito difícil ... e ver se consigo um PR.
Acho que precisamos corrigir isso além de 3a1d1c9, deixe-me pensar nisso por um minuto ... Não acho que a correção será muito difícil ... e ver se consigo um PR.
Opa, eu estava olhando para um código antigo quando o escrevi; sim, acredito que 3a1d1c9 corrige isso para nós, mas precisaremos de um patch para o branch release-2.4. Vou trabalhar nisso agora.
_ (Meta: Vou continuar atualizando esta mensagem com minhas descobertas à medida que prossigo, então tenho um lugar para anotá-las sem enviar spam por e-mail para vocês:) _
Tudo bem, de volta ao 2.4.3 com o patch aplicado, consegui retirar o filtro que estava falhando: link .
A causa relatada agora é ENOMEM
vez de EINVAL
, o que eu acho que é esperado, dado que _gen_bpf_build_bpf
está falhando e retornando um programa NULL
. O PFC imprime bem, no entanto. Modificar o código seccomp para relatar o valor de retorno de _gen_bpf_build_bpf
mostra EFAULT
como a causa.
Como um hack rápido, executei :%s/return -EFAULT/abort()
em src/gen_bpf.c
e consegui extrair este rastreamento de pilha:
Rastreamento de pilha 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
Isso corresponde à linha 1943:
Dada a natureza da substituição, acho que podemos excluir qualquer EFAULT
em qualquer função auxiliar, uma vez que elas teriam sido abortadas primeiro.
Depois disso, tentei reproduzir o mesmo com HEAD - ainda acontece. Em seguida, %s:/goto build_bpf_free_blks/abort()
e repita. A causa foi:
Felizmente, essa função era curta e tinha apenas alguns pontos de falha. Outra rodada de abort
inserções mais tarde;
Vestígio
(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
Portanto, está realloc
falhando novamente, e _bpf_append_blk
está retornando -ENOMEM
que foi mascarado por _gen_bpf_build_bpf
e se transformou em -EFAULT
. Isso não é grande coisa, mas como você disse que um relatório de erro melhor é uma meta de 2,5, pensei em mencioná-lo, pois parece estar dentro do escopo: ligeiramente_smiling_face:
Algumas cutucadas com 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
Este realmente começa a parecer uma falha de alocador ...
Aha, esta história finalmente chegou à sua conclusão _trincante_ - descobri o que está acontecendo e verifiquei uma correção: ligeiramente_sorrindo_face:
Uma vez que pode ser uma história interessante, aqui está:
O processo principal que separa o trabalhador geralmente fica em ~ 80 MB RSS. Depois de se bifurcar, ele restringe o uso de memória por meio de rlimit
, às vezes até 64 MB. Isso o coloca em uma posição em que seu uso de memória atual excede seu limite, mas isso é permitido por rlimit
. _ Na maioria das vezes, o alocador de memória terá memória livre suficiente disponível para atender às rotinas de inicialização do libseccomp sem solicitar mais do kernel. Mas quando _não_ e precisa solicitar espaço para uma arena extra ou algo assim, o kernel não vai fornecer, pois o processo já está acima do seu limite.
Em 2.4.3, essa falha em obter memória se manifestou em EINVAL
e um double-free. No mestre post- https://github.com/seccomp/libseccomp/commit/3a1d1c977065f204b96293cccfe7d3e5aa0d7ace , EFAULT
é relatado em seu lugar. Com https://github.com/seccomp/libseccomp/pull/257 aplicado, ENOMEM
é informado corretamente.
O motivo pelo qual isso acontece tão raramente torna-se óbvio: é inteiramente dependente se o alocador tem memória suficiente disponível para construir o programa BPF sem solicitar mais do kernel. O alocador da glibc é mais flexível quanto a permitir o acúmulo de fragmentação, então isso nunca aconteceu com ela no lugar. jemalloc coloca limites mais estreitos e leva a um aumento da probabilidade de precisar solicitar memória durante seccomp_load
- apenas o suficiente para notar as falhas resultantes, mas ainda é irritante para rastrear.
A correção, então, é simplesmente mover todas as chamadas de setrlimit
para _após_ seccomp_load
. Ao fazer isso, realloc
não falha mais em _bpf_append_blk
e o filtro é carregado com êxito. Isso significa que o filtro precisa permitir setrlimit
, mas no meu caso isso era aceitável. De maneira mais geral, acho que esse problema seria resolvido por algo como https://github.com/seccomp/libseccomp/issues/123.
@pcmoore , @drakenclimber - obrigado novamente por toda a sua ajuda na depuração deste problema! Estou feliz por poder deixar isso para trás agora, mas suas dicas foram inestimáveis para chegar lá: smiley:
Este bug foi corrigido por commit https://github.com/seccomp/libseccomp/commit/c0a6e6fd15f74c429a0b74e0dfd4de5a29aabebd
Comentários muito úteis
Ainda não, infelizmente. Depois de adicionar um patch a
seccomp_export_pfc
, ele ficou em silêncio. Ontem eu empurrei esse patch para todas as nossas VMs (em vez de apenas um de teste) na esperança de capturar o problema quando ele eventualmente ocorrer.Eu acho o silêncio estranho, mas por agora estou atribuindo isso a uma coincidência, já que toda a lógica de depuração / exportação acontece _após_ a falha
seccomp_load
, então isso não deveria estar afetando a falha em si.