Libseccomp: BUG: compatibilidade com openmp

Criado em 2 set. 2017  ·  19Comentários  ·  Fonte: seccomp/libseccomp

A natureza não determinística do multi-threading torna este difícil de depurar, mas acho que cheguei a um caso de teste bastante reproduzível seccomp_omp.c.gz . Compile o programa com -O0 -ggdb -fopenmp .

Se o executarmos com apenas uma thread e permitirmos o syscall proibido, o programa morre conforme o esperado.

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

No entanto, com mais threads, ele simplesmente para de funcionar.

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

O programa não morre, ele simplesmente para de fazer qualquer coisa. Não estou entendendo o que aconteceu com o sinal SIGSYS e por que ele não foi tratado?

Isso pode não ser um problema do libseccomp, mas não sou proficiente o suficiente com o seccomp, os sinais e o sistema de threading em geral para depurar a causa raiz. Espero que você consiga entender isso.

bug prioritmedium

Todos 19 comentários

NOTA: Eu não olhei para o seu caso de teste ainda, estou apenas supondo com base na sua descrição bem escrita

Considerando a natureza multithread disso, você tentou definir o atributo SCMP_FLTATR_CTL_TSYNC filter como true antes de carregar o filtro no kernel?

Não configurei SCMP_FLTATR_CTL_TSYNC porque adicionei o filtro seccomp, antes de dividir em threads. Assim, o kernel deve aplicar o filtro a todos os threads, de qualquer maneira (e aparentemente ele faz isso). Adicionar SCMP_FLTATR_CTL_TSYNC ao caso de teste não faz diferença (que tem menos de 100 linhas com tratamento pedante de erros, aliás).

Ok, eu só queria mencionar isso; parece que você está fazendo as coisas corretamente (configurando o filtro antes de gerar novos tópicos). Vou ter que olhar mais de perto, mas talvez não tenha a chance de fazer isso tão cedo.

A confirmação de que não estou fazendo coisas claramente erradas é boa o suficiente para mim. Sem pressa!

No exemplo de thread único, ./seccomp_omp -t 1 -k 0 , o openmp reconhece que apenas um único thread será executado, portanto, o openmp ignora grande parte da sincronização que normalmente faria em um loop for multiencadeado. Eu verifiquei isso observando as syscalls que foram processadas em seccomp_run_filters () no kernel. Como seria de se esperar, vi uma chamada para __NR_write () e uma chamada para __NR_madvise () que fez com que o seccomp instruísse o kernel a encerrar o thread.

No exemplo multithread, ./seccomp_omp -t 2 -k 0 , openmp tenta paralelizar o loop for. Assim, muito mais da biblioteca openmp é utilizada. Isso é aparente ao assistir novamente as syscalls por meio de seccomp_run_filters (). __NR_mmap (), __NR_mprotect (), __NR_clone (), __NR_futex (), e mais syscalls são chamados antes da chamada para madvise (). Por fim, um dos dois encadeamentos finalmente chama madvise () e seccomp elimina esse encadeamento de maneira adequada. Mas openmp tem sincronização entre os threads e antes que o segundo thread possa chamar madvise () (e ser eliminado), o segundo thread chama futex () porque está esperando algo do thread morto. E é por isso que o programa trava. Um encadeamento foi eliminado e o segundo encadeamento está esperando por dados (do encadeamento morto) que nunca chegarão.

Não trabalhei muito com openmp, mas parece haver alguma discussão [ 1 , 2 , 3 , ...] sobre sinais em blocos paralelos. Em última análise, parece um problema difícil que seria melhor evitar.

Parece que existem algumas soluções fáceis em potencial:

  1. Não use SCMP_ACT_KILL como seu manipulador padrão. Em vez disso, mude para um código de erro, por exemplo, SCMP_ACT_ERROR(5) . Fiz essa alteração em seu programa de exemplo e verifiquei se ele foi encerrado corretamente.

  2. Se você não precisa do poder do openmp, outra opção pode ser mudar para uma solução mais comum como pthread_create () ou fork ()

@drakenclimber então parece que o problema é realmente apenas o openmp criando syscalls adicionais que normalmente não fazem parte do filtro? Ou está faltando um ponto mais importante?

@drakenclimber Muito obrigado por investir seu tempo. Esperar por um thread morto explica os sintomas.

Para dar mais contexto: Eu escrevo código científico, portanto, gosto de manter as coisas simples com OpenMP. No entanto, também quero proteger meus usuários de si mesmos. Portanto, se meu programa fizer algo fora do comum, basta detoná-lo. Eu estou supondo, o que eu quero atingir no final das contas é apenas eliminar o processo (# 96) com uma mensagem de erro um tanto razoável.

Qual valor devo usar para errno em SCMP_ACT_ERROR para imitar SECCOMP_RET_KILL_PROCESS ? Existe algo que funciona bem em todas as syscalls?

@pcmoore - meio. Mas acho que o maior problema é que o openmp não lida com sinais normalmente dentro de sua construção paralela. Eu acho que isso é por design.

@kloetzl - Não se preocupe - você pode ficar totalmente com o OpenMP. Parece que outros membros da comunidade OpenMP estão fazendo algo como o seguinte:

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; 
}

Quanto ao que errno deve SCMP_ACT_ERRNO() retornar, é realmente com você. SCMP_ACT_ERRNO() funcionará em todas as syscalls. E seu programa será aquele que tratará disso e retornará um erro ao usuário, então você pode escolher o errno que funcionar melhor para você. Na verdade, escolher um obscuro - digamos ENOTBLK - pode ser uma maneira de saber que o erro veio do bloqueio da chamada por seccomp.

tl; dr - Detecta o problema no loop paralelo, mas espere para resolvê-lo até que o loop seja concluído. Pode ser necessário adicionar codificação defensiva extra dentro do loop devido a uma falha na chamada do sistema.

@kloetzl -

E seu programa será aquele que tratará disso e retornará um erro ao usuário, então você pode escolher o errno que funcionar melhor para você. Na verdade, escolher um obscuro - digamos ENOTBLK - pode ser uma maneira de saber que o erro veio do seccomp bloqueando a chamada.

O fato é que madvise é chamado nas entranhas de malloc . Portanto, não sou eu quem está lidando com o erro, é a glibc. Portanto, a questão é: glibc (e todas as outras bibliotecas que executam syscalls) sabem que um syscall pode retornar outros erros além dos fornecidos em sua página de manual e tratá-los apropriadamente?

O que acontece é que madvise é chamado nas entranhas de malloc. Portanto, não sou eu quem está lidando com o erro, é a glibc. Portanto, a questão é: glibc (e todas as outras bibliotecas que executam syscalls) sabem que um syscall pode retornar outros erros além dos fornecidos em sua página de manual e tratá-los apropriadamente?

Ahhh ... entendi. Eu teria que examinar o código glibc para ter certeza, mas estaria disposto a arriscar as seguintes suposições:

  • Se madvise retornar um erro, glibc irá _definitivamente_ também retornar um erro
  • É definitivamente possível que glibc possa retornar um erro diferente do seccomp retornado, portanto, você deve ter cuidado se estiver esperando um código de retorno "personalizado" como ENOTBLK

Resumindo, glibc não deve suprimir _qualquer_ código de erro, mas pode retornar um código de erro diferente em vez disso

Estou tentando acompanhar vocês, mas temo que minha falta de experiência com OpenMP me deixou um pouco para trás ... com base nos comentários acima, parece que KILL_PROCESS poderia ser uma solução viável aqui? Nesse caso, podemos aumentar a prioridade desse problema, embora esteja na minha lista de itens a serem resolvidos antes do lançamento v2.4.

@pcmoore - Sim, acredito que KILL_PROCESS é uma solução viável para esse bug. Testei o programa de teste de KILL_PROCESS e o travamento não ocorre mais.

Eu o implementei, mas atualmente estou encontrando alguns obstáculos nos autotestes do python. Devo tirar um patch na próxima semana.

@drakenclimber ooh, patches? Eu amo patches :)

Obrigado rapazes.

Posso confirmar que KILL_PROCESS resolve o problema: eu também hackeei uma versão local de libseccomp no Arch e o programa de teste falhou conforme o planejado. No entanto, fornecer testes sólidos e executá-los em kernels diferentes pode ser o maior desafio.

Obrigado pela confirmação @kloetzl.

Sim, escrever testes adequados pode ser entediante, mas é importante. Tão importante quanto o código que testa IMHO.

Estou ansioso pelo PR de @drakenclimber , podemos discutir um pouco mais as coisas assim que o código for publicado.

Estou fazendo uma limpeza de primavera no COVID-19 e acho que resolvemos seu problema, correto @kloetzl? Vou encerrar este problema, mas se estiver errado e você ainda tiver problemas, avise-nos e o reabriremos!

Obrigado por me lembrar, eu testei um pouco do meu lado.

Esse problema foi corrigido em grande parte, com um pequeno obstáculo. O programa não trava mais no limbo quando uma syscall inválida é encontrada, yay! Com SCMP_ACT_TRAP pode-se até mesmo produzir o nome da syscall ruim. No entanto, o OpenMP sobrescreve meu manipulador de sinal, portanto, ele volta para a mensagem de erro padrão quando vários threads são gerados. Do ponto de vista da experiência do usuário, não gosto desse comportamento, mas acho que é o melhor que pode acontecer.

Ah, sim, não tenho certeza se podemos fazer sobre o manipulador de sinais ser sobrescrito em libseccomp, desculpe por isso.

Obrigado por nos informar que o resto funcionou!

Esta página foi útil?
0 / 5 - 0 avaliações