Libseccomp: BUG: Kompatibilität mit openmp

Erstellt am 2. Sept. 2017  ·  19Kommentare  ·  Quelle: seccomp/libseccomp

Die nicht deterministische Natur von Multithreading macht es schwierig, dieses Problem zu debuggen, aber ich denke, ich bin bei einem ziemlich reproduzierbaren Testfall -O0 -ggdb -fopenmp .

Wenn wir es mit nur einem Thread ausführen und es den verbotenen Systemaufruf ausführen lassen, stirbt das Programm wie erwartet ab.

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

Bei mehr Threads funktioniert es jedoch einfach nicht mehr.

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

Das Programm wird nicht getötet, es hört einfach auf, überhaupt etwas zu tun. Ich weiß nicht, was mit dem SIGSYS-Signal passiert ist und warum es nicht verarbeitet wird?

Dies mag überhaupt kein libseccomp-Problem sein, aber ich bin mit seccomp, Signalen und dem Threading-System im Allgemeinen nicht kompetent genug, um die Ursache zu debuggen. Ich hoffe, Sie können es ein wenig nachvollziehen.

bug prioritmedium

Alle 19 Kommentare

HINWEIS: Ich habe mir Ihren Testfall noch nicht angesehen, ich vermute nur aufgrund Ihrer gut geschriebenen Beschreibung

Haben Sie in Anbetracht der Multithread-Natur dieses Problems versucht, das Filterattribut SCMP_FLTATR_CTL_TSYNC auf true zu setzen, bevor Sie den Filter in den Kernel laden?

Ich habe SCMP_FLTATR_CTL_TSYNC nicht gesetzt, weil ich den seccomp-Filter hinzufüge, bevor ich ihn in Threads aufteile. Daher sollte der Kernel den Filter sowieso auf alle Threads anwenden (und das tut er anscheinend). Das Hinzufügen von SCMP_FLTATR_CTL_TSYNC zum Testfall macht keinen Unterschied (was übrigens weniger als 100 Zeilen mit pedantischer Fehlerbehandlung sind).

Okay, ich wollte es nur erwähnen; es hört sich so an, als ob Sie die Dinge richtig machen (den Filter setzen, bevor Sie neue Threads erstellen). Ich muss mir das mal genauer anschauen, aber dazu komme ich wohl nicht so bald.

Eine Bestätigung, dass ich Dinge nicht eklatant falsch mache, ist mir gut genug. Lass dir Zeit!

Im Singlethread-Beispiel ./seccomp_omp -t 1 -k 0 erkennt openmp, dass nur ein einzelner Thread ausgeführt wird, sodass openmp einen Großteil der Synchronisation umgeht, die es normalerweise in einer Multithread-for-Schleife durchführen würde. Ich habe dies überprüft, indem ich die Systemaufrufe beobachtet habe, die in seccomp_run_filters() im Kernel verarbeitet wurden. Wie zu erwarten, sah ich einen Aufruf von __NR_write() und einen Aufruf von __NR_madvise(), was seccomp dazu veranlasste, den Kernel anzuweisen, den Thread zu beenden.

Im Multithread-Beispiel ./seccomp_omp -t 2 -k 0 versucht openmp, die for-Schleife zu parallelisieren. Somit wird viel mehr von der openmp-Bibliothek verwendet. Dies wird offensichtlich, wenn man erneut Systemaufrufe durch seccomp_run_filters() beobachtet. __NR_mmap(), __NR_mprotect(), __NR_clone(), __NR_futex() und weitere Systemaufrufe werden vor dem Aufruf von madvise() aufgerufen. Schließlich ruft einer der beiden Threads schließlich madvise() auf und seccomp beendet diesen Thread ordnungsgemäß. Aber openmp hat Synchronisation zwischen den Threads und bevor der zweite Thread madvise() aufrufen kann (und getötet wird), ruft der zweite Thread futex() auf, da er auf etwas vom toten Thread wartet. Und deshalb hängt das Programm. Ein Thread wurde beendet und der zweite Thread wartet auf Daten (vom toten Thread), die nie ankommen werden.

Ich habe nicht viel mit openmp gearbeitet, aber es scheint eine Diskussion [ 1 , 2 , 3 , ...] über Signale in parallelen Blöcken zu geben. Letztendlich sieht es nach einem schwierigen Problem aus, das Sie am besten vermeiden sollten.

Es scheint, als gäbe es ein paar mögliche einfache Lösungen:

  1. Verwenden Sie nicht SCMP_ACT_KILL als Standardhandler. Wechseln Sie stattdessen zur Verwendung eines Fehlercodes, zB SCMP_ACT_ERROR(5) . Ich habe diese Änderung in Ihrem Beispielprogramm vorgenommen und überprüft, ob es ordnungsgemäß beendet wurde.

  2. Wenn Sie die Leistung von openmp nicht benötigen, können Sie auch zu einer gebräuchlicheren Lösung wie pthread_create() oder fork() wechseln.

@drakenclimber, also hört es sich so an, als ob das Problem wirklich nur darin besteht, dass openmp zusätzliche Systemaufrufe durchführt, die normalerweise nicht Teil des Filters sind? Oder fehlt da ein größerer Punkt?

@drakenclimber Vielen Dank, dass Sie Ihre Zeit investiert haben. Das Warten auf einen toten Thread erklärt die Symptome.

Um mehr Kontext zu geben: Ich schreibe wissenschaftlichen Code, daher halte ich die Dinge mit OpenMP gerne einfach. Allerdings möchte ich meine Nutzer auch vor sich selbst schützen. Wenn mein Programm also etwas Außergewöhnliches macht, nuke es einfach. Ich vermute, was ich letztendlich erreichen möchte, ist, den Prozess (#96) mit einer einigermaßen vernünftigen Fehlermeldung zu beenden.

Welchen Wert sollte ich für errno in SCMP_ACT_ERROR , um SECCOMP_RET_KILL_PROCESS genau nachzuahmen? Gibt es etwas, das über alle Systemaufrufe hinweg gut funktioniert?

@pcmoore - irgendwie. Aber ich denke, das größere Problem ist, dass openmp Signale innerhalb seines parallelen Konstrukts nicht elegant behandelt. Ich würde vermuten, dass dies beabsichtigt ist.

@kloetzl - Keine Sorge - du kannst voll und ganz bei OpenMP bleiben. Es sieht so aus, als ob andere in der OpenMP-Community Folgendes tun:

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

Welche Fehler SCMP_ACT_ERRNO() zurückgeben soll, liegt ganz bei Ihnen. SCMP_ACT_ERRNO() funktioniert über alle Systemaufrufe hinweg. Und Ihr Programm wird es handhaben und einen Fehler an den Benutzer zurückgeben, sodass Sie den Fehler auswählen können, der für Sie am besten geeignet ist. In der Tat könnte die Auswahl eines obskuren - sagen wir ENOTBLK - ein Weg sein, um zu wissen , dass der Fehler von der Blockierung des Anrufs durch seccomp herrührt.

tl;dr - Das Problem in der parallelen Schleife erkennen, aber warten, bis die Schleife abgeschlossen ist. Möglicherweise müssen Sie aufgrund eines fehlgeschlagenen Systemaufrufs zusätzliche defensive Codierung innerhalb der Schleife hinzufügen.

@kloetzl - Ich werde mir Ausgabe 96 ansehen und sehen, wie gut das mit OpenMP funktioniert. Das könnte auch eine andere Lösung sein. Vielen Dank!

Und Ihr Programm wird es handhaben und einen Fehler an den Benutzer zurückgeben, sodass Sie den Fehler auswählen können, der für Sie am besten geeignet ist. Tatsächlich könnte die Auswahl eines obskuren Anrufs - sagen wir ENOTBLK - ein Weg sein, um zu wissen, dass der Fehler von der Blockierung des Anrufs durch seccomp herrührt.

Die Sache ist, madvise wird tief in den Eingeweiden von malloc . Daher bin ich nicht derjenige, der den Fehler behandelt, glibc ist es. Die Frage ist also, ob die glibc (und alle anderen Bibliotheken, die Systemaufrufe durchführen) wissen, dass ein Systemaufruf andere Fehler zurückgeben kann, als in der Manpage angegeben, und sie entsprechend behandeln?

Die Sache ist die, Madvise wird tief in den Eingeweiden von Malloc genannt. Daher bin ich nicht derjenige, der den Fehler behandelt, glibc ist es. Die Frage ist also, ob die glibc (und alle anderen Bibliotheken, die Systemaufrufe durchführen) wissen, dass ein Systemaufruf andere Fehler zurückgeben kann, als in der Manpage angegeben, und sie entsprechend behandeln?

Ahhh... kapiert. Ich müsste den glibc-Code durchsuchen, um sicher zu sein, aber ich wäre bereit, die folgenden Vermutungen zu riskieren:

  • Wenn madvise einen Fehler zurückgibt, wird glibc _definitiv_ auch einen Fehler zurückgeben
  • Es ist definitiv möglich, dass glibc einen anderen Fehler als seccomp zurückgibt, daher sollten Sie vorsichtig sein, wenn Sie einen "benutzerdefinierten" Rückgabecode wie ENOTBLK erwarten

Lange Rede, kurzer Sinn, glibc sollte keinen _jeden_ Fehlercode unterdrücken, sondern könnte stattdessen einen anderen Fehlercode zurückgeben

Ich versuche, mit euch Schritt zu halten, aber ich fürchte, mein fehlender Hintergrund mit OpenMP hat mich etwas hinterherhinkt ... basierend auf den obigen Kommentaren sieht es so aus, als ob KILL_PROCESS hier eine praktikable Lösung sein könnte? Wenn ja, können wir die Priorität dieses Problems erhöhen, obwohl es auf meiner Liste der Dinge steht, die vor der Veröffentlichung von v2.4 behoben werden müssen.

@pcmoore - Ja, ich glaube, dass KILL_PROCESS eine praktikable Lösung für diesen Fehler ist. Ich habe das obige Testprogramm von KILL_PROCESS getestet und das Aufhängen tritt nicht mehr auf.

Ich habe es implementiert, stoße aber derzeit bei den Python-Autotests auf ein paar Haken. Ich sollte nächste Woche einen Patch rausbringen.

@drakenclimber ooh, Patches? Ich liebe Patches :)

Danke Leute.

Ich kann bestätigen, dass KILL_PROCESS das Problem löst: Ich habe auch eine lokale Version von libseccomp auf Arch gehackt und das Testprogramm schlägt wie beabsichtigt fehl. Allerdings könnte es die größere Herausforderung sein, solide Tests bereitzustellen und sie auf verschiedenen Kerneln auszuführen.

Danke für die Bestätigung @kloetzl.

Ja, richtige Tests zu schreiben kann mühsam sein, aber es ist wichtig. Genauso wichtig wie der Code, den es IMHO testet.

Ich freue mich auf die PR von @drakenclimber , wir können die Dinge ein bisschen mehr besprechen, sobald der Code veröffentlicht ist.

Ich mache einen COVID-19- Frühjahrsputz und ich denke, wir haben Ihr Problem gelöst, ist das richtig @kloetzl? Ich werde dieses Problem schließen, aber wenn ich falsch liege und Sie immer noch Probleme sehen, lassen Sie es uns bitte wissen und wir werden es wieder öffnen!

Danke für die Erinnerung, ich habe ein bisschen an meinem Ende getestet.

Dieses Problem ist größtenteils behoben, mit einem kleinen Haken. Das Programm hängt nicht mehr in der Schwebe, wenn ein ungültiger Systemaufruf auftritt, juhu! Mit SCMP_ACT_TRAP kann man sogar den Namen des fehlerhaften Systemaufrufs erzeugen. OpenMP überschreibt jedoch meinen Signalhandler, sodass er auf die Standardfehlermeldung zurückfällt, sobald mehrere Threads erzeugt werden. Aus Sicht der Benutzererfahrung mag ich dieses Verhalten nicht, aber ich denke, es ist so gut wie es nur geht.

Ah, ja, ich bin mir nicht sicher, ob wir viel gegen das Überschreiben des Signalhandlers in libseccomp tun können, sorry dafür.

Danke, dass Sie uns wissen lassen, dass der Rest funktioniert hat!

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen