Libseccomp: BOGUE : compatibilité avec openmp

Créé le 2 sept. 2017  ·  19Commentaires  ·  Source: seccomp/libseccomp

La nature non déterministe du multi-threading rend celui-ci difficile à déboguer, mais je pense que je suis arrivé à un cas de test plutôt reproductible seccomp_omp.c.gz . Compilez le programme avec -O0 -ggdb -fopenmp .

Si nous l'exécutons avec un seul thread et le laissons faire l'appel système interdit, le programme meurt comme prévu.

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

Cependant, étant donné plus de threads, il cesse de fonctionner.

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

Le programme n'est pas tué, il s'arrête simplement de faire quoi que ce soit. Je ne comprends pas ce qui est arrivé au signal SIGSYS et pourquoi il n'est pas traité ?

Ce n'est peut-être pas du tout un problème de libseccomp, mais je ne suis pas assez compétent avec seccomp, les signaux et le système de threads en général pour déboguer la cause première. J'espère que vous pouvez le comprendre.

bug prioritmedium

Tous les 19 commentaires

REMARQUE : je n'ai pas encore examiné votre cas de test, je suppose simplement en me basant sur votre description bien écrite

Compte tenu de la nature multithread de cela, avez-vous essayé de définir l'attribut de filtre SCMP_FLTATR_CTL_TSYNC sur true avant de charger le filtre dans le noyau ?

Je n'ai pas défini SCMP_FLTATR_CTL_TSYNC car j'ajoute le filtre seccomp, avant de le diviser en threads. Ainsi, le noyau devrait appliquer le filtre à tous les threads, de toute façon (et apparemment, il le fait). L'ajout de SCMP_FLTATR_CTL_TSYNC au testcase ne fait aucune différence (ce qui est inférieur à 100 lignes avec une gestion des erreurs pédante, d'ailleurs).

D'accord, je voulais juste le mentionner ; il semble que vous fassiez les choses correctement (définir le filtre avant de générer de nouveaux threads). Je vais devoir regarder de plus près, mais je n'aurai peut-être pas l'occasion de le faire très bientôt.

Une confirmation que je ne fais pas les choses de manière flagrante est assez bonne pour moi. Prends ton temps!

Dans l'exemple à thread unique, ./seccomp_omp -t 1 -k 0 , openmp reconnaît qu'un seul thread va s'exécuter, donc openmp contourne une grande partie de la synchronisation qu'il ferait normalement dans une boucle for multi-thread. J'ai vérifié cela en observant les appels système qui ont été traités dans seccomp_run_filters() dans le noyau. Comme on pouvait s'y attendre, j'ai vu un appel à __NR_write() et un appel à __NR_madvise() qui ont incité seccomp à demander au noyau de tuer le thread.

Dans l'exemple multithread, ./seccomp_omp -t 2 -k 0 , openmp essaie de paralléliser la boucle for. Ainsi, une plus grande partie de la bibliothèque openmp est utilisée. Cela est évident lorsque vous regardez à nouveau les appels système via seccomp_run_filters(). __NR_mmap(), __NR_mprotect(), __NR_clone(), __NR_futex() et d'autres appels système sont appelés avant l'appel à madvise(). En fin de compte, l'un des deux threads appelle finalement madvise() et seccomp tue correctement ce thread. Mais openmp a une synchronisation entre les threads et avant que le deuxième thread puisse appeler madvise() (et être tué), le deuxième thread appelle futex() car il attend quelque chose du thread mort. Et c'est pourquoi le programme se bloque. Un thread a été tué et le deuxième thread attend des données (du thread mort) qui n'arriveront jamais.

Je n'ai pas beaucoup travaillé avec openmp, mais il semble y avoir une discussion [ 1 , 2 , 3 , ...] sur les signaux dans les blocs parallèles. En fin de compte, cela ressemble à un problème difficile que vous feriez mieux d'éviter.

Il semble qu'il existe plusieurs solutions simples potentielles :

  1. N'utilisez pas SCMP_ACT_KILL comme gestionnaire par défaut. Utilisez plutôt un code d'erreur, par exemple SCMP_ACT_ERROR(5) . J'ai apporté cette modification dans votre exemple de programme et vérifié qu'il s'est terminé correctement.

  2. Si vous n'avez pas besoin de la puissance d'openmp, une autre option peut être de passer à une solution plus courante comme pthread_create() ou fork()

@drakenclimber donc il semble que le problème soit vraiment juste openmp faisant des appels système supplémentaires qui ne font normalement pas partie du filtre? Ou est-ce qu'il manque un point plus important ?

@drakenclimber Merci beaucoup d'avoir investi votre temps. L'attente d'un fil mort explique les symptômes.

Pour donner plus de contexte : j'écris du code scientifique, donc j'aime garder les choses simples avec OpenMP. Cependant, je veux aussi protéger mes utilisateurs d'eux-mêmes. Ainsi, si mon programme fait quelque chose qui sort de l'ordinaire, il suffit de le neutraliser. Je suppose que ce que je veux finalement atteindre, c'est simplement tuer le processus (#96) avec un message d'erreur quelque peu raisonnable.

Quelle valeur dois-je utiliser pour errno dans SCMP_ACT_ERROR pour imiter étroitement SECCOMP_RET_KILL_PROCESS ? Y a-t-il quelque chose qui fonctionne bien sur tous les appels système ?

@pcmoore - un peu. Mais je pense que le plus gros problème est que openmp ne gère pas les signaux avec élégance à l'intérieur de sa construction parallèle. Je suppose que c'est par conception.

@kloetzl - Pas de soucis - vous pouvez totalement vous en tenir à OpenMP. Il semble que d'autres membres de la communauté OpenMP fassent quelque chose comme ce qui suit :

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

Quant à ce que errno doit renvoyer SCMP_ACT_ERRNO() , c'est vraiment à vous de décider. SCMP_ACT_ERRNO() fonctionnera sur tous les appels système. Et votre programme sera celui qui le gérera et renverra une erreur à l'utilisateur, vous pouvez donc choisir celui qui vous convient le mieux. En fait, en choisir un obscur - disons ENOTBLK - pourrait être un moyen de savoir que l'erreur provient du blocage de l'appel par seccomp.

tl;dr - Détecte le problème dans la boucle parallèle, mais attend de le gérer jusqu'à la fin de la boucle. Vous devrez peut-être ajouter un codage défensif supplémentaire dans la boucle en raison d'un échec d'appel système.

@kloetzl - Je vais examiner le numéro 96 et voir à quel point cela fonctionne avec OpenMP. Cela pourrait aussi être une autre solution. Merci!

Et votre programme sera celui qui le gérera et renverra une erreur à l'utilisateur, vous pouvez donc choisir celui qui vous convient le mieux. En fait, en choisir un obscur - disons ENOTBLK - pourrait être un moyen de savoir que l'erreur provient du blocage de l'appel par seccomp.

Le fait est que madvise est appelé au fond des entrailles de malloc . Ainsi, je ne suis pas celui qui gère l'erreur, c'est la glibc. La question est donc de savoir si la glibc (et toutes les autres bibliothèques effectuant des appels système) sait qu'un appel système peut renvoyer d'autres erreurs que celles indiquées dans sa page de manuel et les gérer de manière appropriée ?

Le truc, c'est que madvise s'appelle au fond des entrailles de malloc. Ainsi, je ne suis pas celui qui gère l'erreur, c'est la glibc. La question est donc de savoir si la glibc (et toutes les autres bibliothèques effectuant des appels système) sait qu'un appel système peut renvoyer d'autres erreurs que celles indiquées dans sa page de manuel et les gérer de manière appropriée ?

Ahhh... je sais. Il faudrait que je parcoure le code glibc pour être sûr, mais je serais prêt à risquer les suppositions suivantes :

  • Si madvise renvoie une erreur, la glibc renverra _définitivement_ également une erreur
  • Il est certainement possible que la glibc renvoie une erreur différente de celle renvoyée par seccomp, vous devez donc faire preuve de prudence si vous attendez un code de retour "personnalisé" comme ENOTBLK

Pour faire court, la glibc ne devrait pas supprimer _aucun_ code d'erreur, mais elle pourrait renvoyer un code d'erreur différent à la place

J'essaie de vous suivre, mais je crains que mon manque d'expérience avec OpenMP ne me laisse un peu en retard ... d'après les commentaires ci-dessus, il semble que KILL_PROCESS pourrait être une solution viable ici ? Si c'est le cas, nous pouvons augmenter la priorité de ce problème, bien qu'il soit sur ma liste de choses à régler avant la version v2.4.

@pcmoore - Oui, je pense que KILL_PROCESS est une solution viable à ce bogue. J'ai testé le programme de test de KILL_PROCESS et le blocage ne se produit plus.

Je l'ai implémenté, mais je rencontre actuellement quelques problèmes dans les autotests python. Je devrais avoir un patch la semaine prochaine.

@drakenclimber ooh, des patchs ? J'adore les patchs :)

Merci les gars.

Je peux confirmer que KILL_PROCESS résout le problème : j'ai aussi piraté une version locale de libseccomp sur Arch et le programme de test échoue comme prévu. Cependant, fournir des tests solides et les exécuter sur différents noyaux pourrait être le plus grand défi.

Merci pour la confirmation @kloetzl.

Oui, écrire des tests appropriés peut être fastidieux, mais c'est important. Tout aussi important que le code qu'il teste à mon humble avis.

J'attends avec impatience le PR de @drakenclimber , nous pourrons discuter un peu plus une fois ce code publié.

Je fais un ménage de printemps

Merci de me le rappeler, j'ai testé un peu de mon côté.

Ce problème est en grande partie résolu, avec un petit hic. Le programme ne se bloque plus dans les limbes lorsqu'un appel système non valide est rencontré, oui ! Avec SCMP_ACT_TRAP on peut même produire le nom du mauvais syscall. Cependant, OpenMP écrase mon gestionnaire de signaux, il revient donc au message d'erreur par défaut une fois que plusieurs threads sont générés. Du point de vue de l'expérience utilisateur, je n'aime pas ce comportement, mais je pense qu'il est aussi bon que possible.

Ah, oui, je ne suis pas sûr que nous puissions faire grand-chose pour que le gestionnaire de signal soit écrasé dans libseccomp, désolé pour ça.

Merci de nous avoir fait savoir que le reste a fonctionné !

Cette page vous a été utile?
0 / 5 - 0 notes