Libseccomp: ERROR: compatibilidad con openmp

Creado en 2 sept. 2017  ·  19Comentarios  ·  Fuente: seccomp/libseccomp

La naturaleza no determinista de los subprocesos múltiples hace que este sea difícil de depurar, pero creo que he llegado a un caso de prueba bastante reproducible seccomp_omp.c.gz . Compile el programa con -O0 -ggdb -fopenmp .

Si lo ejecutamos con un solo hilo y dejamos que haga la llamada al sistema prohibida, el programa muere como se esperaba.

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

Sin embargo, dados más hilos, simplemente deja de funcionar.

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

El programa no se mata, simplemente deja de hacer nada, en absoluto. No sé qué pasó con la señal SIGSYS y por qué no se maneja.

Puede que esto no sea un problema de libseccomp en absoluto, pero no soy lo suficientemente competente con seccomp, señales y el sistema de subprocesos en general para depurar la causa raíz. Espero que puedas encontrarle algún sentido.

bug prioritmedium

Todos 19 comentarios

NOTA: Todavía no he examinado su caso de prueba, solo supongo que basándome en su descripción bien escrita

Teniendo en cuenta la naturaleza multiproceso de esto, ¿ha intentado establecer el atributo de filtro SCMP_FLTATR_CTL_TSYNC en verdadero antes de cargar el filtro en el kernel?

No configuré SCMP_FLTATR_CTL_TSYNC porque agregué el filtro seccomp, antes de dividirlo en subprocesos. Por lo tanto, el kernel debería aplicar el filtro a todos los subprocesos, de todos modos (y aparentemente lo hace). Agregar SCMP_FLTATR_CTL_TSYNC al caso de prueba no hace ninguna diferencia (que es menos de 100 líneas con un manejo de errores pedante, por cierto).

Está bien, solo quería mencionarlo; parece que estás haciendo las cosas correctamente (configurando el filtro antes de generar nuevos hilos). Tendré que mirar más de cerca, pero es posible que no tenga la oportunidad de hacerlo muy pronto.

Una confirmación de que no estoy haciendo las cosas descaradamente mal es suficiente para mí. ¡Tome su tiempo!

En el ejemplo de un solo subproceso, ./seccomp_omp -t 1 -k 0 , openmp reconoce que solo se va a ejecutar un solo subproceso, por lo que openmp omite gran parte de la sincronización que normalmente haría en un bucle for multiproceso. Verifiqué esto observando las llamadas al sistema que se procesaron en seccomp_run_filters () en el kernel. Como era de esperar, vi una llamada a __NR_write () y una llamada a __NR_madvise () que hizo que seccomp instruyera al kernel para que matara el hilo.

En el ejemplo de varios subprocesos, ./seccomp_omp -t 2 -k 0 , openmp intenta paralelizar el ciclo for. Por lo tanto, se utiliza mucho más de la biblioteca openmp. Esto es evidente cuando se vuelven a ver las llamadas al sistema a través de seccomp_run_filters (). __NR_mmap (), __NR_mprotect (), __NR_clone (), __NR_futex () y más llamadas al sistema se llaman antes de la llamada a madvise (). En última instancia, uno de los dos subprocesos finalmente llama a madvise () y seccomp mata correctamente ese subproceso. Pero openmp tiene sincronización entre los subprocesos y antes de que el segundo subproceso pueda llamar a madvise () (y ser eliminado), el segundo subproceso llama a futex () ya que está esperando algo del subproceso inactivo. Y es por eso que el programa se cuelga. Se ha eliminado un hilo y el segundo hilo está esperando datos (del hilo inactivo) que nunca llegarán.

No he trabajado mucho con openmp, pero parece haber algo de discusión [ 1 , 2 , 3 , ...] sobre señales en bloques paralelos. En última instancia, parece un problema difícil que sería mejor evitar.

Parece que hay un par de posibles soluciones fáciles:

  1. No use SCMP_ACT_KILL como su controlador predeterminado. En su lugar, cambie a usar un código de error, por ejemplo, SCMP_ACT_ERROR(5) . Hice este cambio en su programa de ejemplo y verifiqué que finalizó correctamente.

  2. Si no necesita el poder de openmp, otra opción puede ser cambiar a una solución más común como pthread_create () o fork ()

@drakenclimber, por lo que parece que el problema es realmente que openmp hace llamadas al sistema adicionales que normalmente no forman parte del filtro. ¿O le falta un punto más importante?

@drakenclimber Muchas gracias por invertir su tiempo. Esperar un hilo muerto explica los síntomas.

Para dar más contexto: escribo código científico, por lo que me gusta simplificar las cosas con OpenMP. Sin embargo, también quiero proteger a mis usuarios de ellos mismos. Por lo tanto, si mi programa hace algo fuera de lo común, simplemente atóquelo. Supongo que, en última instancia, lo que quiero lograr es simplemente matar el proceso (# 96) con un mensaje de error algo razonable.

¿Qué valor debo usar para errno en SCMP_ACT_ERROR para imitar de cerca SECCOMP_RET_KILL_PROCESS ? ¿Hay algo que funcione bien en todas las llamadas al sistema?

@pcmoore - un poco. Pero creo que el problema más grande es que openmp no maneja las señales con gracia dentro de su construcción paralela. Supongo que esto es por diseño.

@kloetzl - No se preocupe, puede seguir totalmente con OpenMP. Parece que otros miembros de la comunidad OpenMP están haciendo algo como lo siguiente:

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

En cuanto a lo que errno debería devolver SCMP_ACT_ERRNO() , realmente depende de usted. SCMP_ACT_ERRNO() funcionará en todas las llamadas al sistema. Y su programa será el que lo maneje y devuelva un error al usuario, para que pueda elegir el error que mejor se adapte a sus necesidades. De hecho, elegir uno oscuro, digamos ENOTBLK , podría ser una forma de saber que el error proviene de seccomp que bloquea la llamada.

tl; dr: detecta el problema en el bucle paralelo, pero espera para solucionarlo hasta que se complete el bucle. Es posible que deba agregar codificación defensiva adicional dentro del bucle debido a una falla en la llamada del sistema.

@kloetzl -

Y su programa será el que lo maneje y devuelva un error al usuario, para que pueda elegir el error que mejor se adapte a sus necesidades. De hecho, elegir uno oscuro, digamos ENOTBLK, podría ser una forma de saber que el error proviene de seccomp que bloquea la llamada.

La cuestión es que madvise se llama en lo más profundo de las entrañas de malloc . Por lo tanto, no soy yo quien maneja el error, glibc es. Entonces, la pregunta es, ¿glibc (y todas las otras bibliotecas que realizan llamadas al sistema) saben que una llamada al sistema puede devolver otros errores distintos a los dados en su página de manual y manejarlos adecuadamente?

El caso es que madvise se llama profundo en las entrañas de malloc. Por lo tanto, no soy yo quien maneja el error, glibc es. Entonces, la pregunta es, ¿glibc (y todas las otras bibliotecas que realizan llamadas al sistema) saben que una llamada al sistema puede devolver otros errores distintos a los dados en su página de manual y manejarlos adecuadamente?

Ahhh ... te tengo. Tendría que revisar el código glibc para estar seguro, pero estaría dispuesto a arriesgarme con las siguientes suposiciones:

  • Si madvise devuelve un error, glibc _definitely_ también devolverá un error
  • Definitivamente es posible que glibc devuelva un error diferente al devuelto por seccomp, por lo que debe tener cuidado si espera un código de retorno "personalizado" como ENOTBLK

Para resumir, glibc no debería suprimir _cualquier_ código de error, pero podría devolver un código de error diferente en su lugar

Estoy tratando de mantenerme al día con ustedes, pero me temo que mi falta de experiencia con OpenMP me tiene un poco atrasado ... según los comentarios anteriores, parece que KILL_PROCESS podría ser una solución viable aquí. Si es así, podemos aumentar la prioridad de ese problema, aunque está en mi lista de cosas que abordar antes del lanzamiento de la v2.4.

@pcmoore - Sí, creo que KILL_PROCESS es una solución viable para este error. Probé el programa de prueba de KILL_PROCESS y el bloqueo ya no ocurre.

Lo tengo implementado, pero actualmente tengo un par de inconvenientes en las pruebas automáticas de Python. Debería tener un parche la semana que viene.

@drakenclimber ooh, ¿parches? Me encantan los parches :)

Gracias chicos.

Puedo confirmar que KILL_PROCESS resuelve el problema: yo también pirateé una versión local de libseccomp en Arch y el programa de prueba falla según lo previsto. Sin embargo, proporcionar pruebas sólidas y ejecutarlas en diferentes núcleos podría ser el mayor desafío.

Gracias por la confirmación @kloetzl.

Sí, escribir pruebas adecuadas puede ser tedioso, pero es importante. Tan importante como el código que prueba en mi humilde opinión.

Espero las relaciones públicas de @drakenclimber , podemos discutir las cosas un poco más una vez que se publique el código.

Estoy haciendo una limpieza de primavera de

Gracias por recordarme, probé un poco al final.

Este problema está mayormente solucionado, con un pequeño inconveniente. El programa ya no se queda en el limbo cuando se encuentra una llamada al sistema no válida, ¡yay! Con SCMP_ACT_TRAP uno puede incluso producir el nombre de la llamada al sistema incorrecta. Sin embargo, OpenMP sobrescribe mi controlador de señal, por lo que vuelve al mensaje de error predeterminado una vez que se generan varios subprocesos. Desde la perspectiva de la experiencia del usuario, no me gusta este comportamiento, pero creo que es tan bueno como parece.

Ah, sí, no estoy seguro de que haya mucho que podamos sobre la sobrescritura del manejador de señales en libseccomp, lo siento.

¡Gracias por informarnos que el resto funcionó!

¿Fue útil esta página
0 / 5 - 0 calificaciones