De ¿Cuál es el propósito de la función _chkstk()? :
Al final de la pila, hay una página de protección asignada como memoria inaccesible; si el programa accede a ella (porque está tratando de usar más pila de la asignada actualmente), hay una infracción de acceso.
_chkstk() es una función auxiliar de compilación especial que
asegura que hay suficiente espacio para las variables locales
es decir, está haciendo un sondeo de pila (aquí hay un ejemplo de LLVM ).
Este caso es específico de Windows. Entonces Windows tiene alguna solución al problema.
Consideremos las condiciones similares bajo Linux (o algún otro tipo de Unix): tenemos muchas variables locales de funciones. El primer acceso a la variable de la pila está detrás del segmento de la pila (por ejemplo mov eax, [esp-LARGE_NUMBER] , aquí esp-LARGE_NUMBER es algo detrás del segmento de la pila). ¿Hay alguna función para evitar posibles fallos de página o lo que sea en Linux (quizás otro similar a Unix) o herramientas de desarrollo como gcc , clang , etc.? ¿ -fstack-check (verificación de pila GCC ) de alguna manera resuelve este problema? Esta respuesta indica que es algo muy similar a _chkstk() .
PD Estas publicaciones 1 , 2 no ayudaron mucho.
PPS En general, la pregunta es sobre las diferencias de implementación entre los enfoques de los sistemas operativos (principalmente Linux vs Windows) de luchar con una gran cantidad de variables de pila, que se ubican detrás del segmento de pila . Se agregan etiquetas C++ y C porque se trata de la producción binaria nativa de Linux, pero el código ensamblador está relacionado con el compilador.
_chkstk sondas para asegurarse de que cada página se toca en orden después de una asignación (potencialmente) grande, por ejemplo, una asignación. Porque Windows solo hará crecer la pila una página a la vez hasta el límite de tamaño de la pila.
Tocar esa "página de protección" activa el crecimiento de la pila. No protege contra el desbordamiento de pila; Creo que está malinterpretando el significado de "página de protección" en este uso.
El nombre de la función también es potencialmente engañoso. Los documentos _chkstk simplemente dicen: Llamado por el compilador cuando tiene más de una página de variables locales en su función. Realmente no verifica nada, solo se asegura de que las páginas intermedias se hayan tocado antes de que se use la memoria alrededor de esp / rsp . es decir, los únicos efectos posibles son: nada (posiblemente incluido un error de página suave válido) o un error de página no válido en el desbordamiento de la pila (intentar tocar una página que Windows se negó a incluir en la pila). Garantiza que las páginas de la pila sean asignados escribiéndolos incondicionalmente.
Supongo que podría ver esto como una verificación de un conflicto de pila asegurándose de tocar una página que no se puede asignar antes de continuar en el caso de un desbordamiento de pila.
Linux hará crecer la pila 1 del subproceso principal en cualquier número de páginas (hasta el límite de tamaño de pila establecido por ulimit -s ; predeterminado 8MiB) cuando toque la memoria debajo de las páginas de pila antiguas si está por encima del puntero de pila actual .
Si toca la memoria fuera del límite de crecimiento, o no mueve el puntero de la pila primero, simplemente fallará. Por lo tanto, Linux no necesita sondas de pila, simplemente para mover el puntero de la pila tantos bytes como desee reservar. Los compiladores saben esto y emiten código en consecuencia.
Consulte también ¿Cómo se asigna la memoria de pila cuando se usan instrucciones x86 'push' o 'sub'? para obtener más detalles de bajo nivel sobre lo que hace el kernel de Linux y lo que hace glibc pthreads en Linux.
Una alloca lo suficientemente grande en Linux puede mover la pila más allá de la parte inferior de la región de crecimiento de la pila, más allá de las páginas de protección debajo de eso, y hacia otra asignación; esto es un choque de pila. https://blog.qualys.com/securitylabs/2017/06/19/the-stack-clash Por supuesto, requiere que el programa use un tamaño potencialmente enorme para alloca, dependiendo de la entrada del usuario. La mitigación para CVE-2017-1000364 es dejar una región de protección de 1MiB, lo que requiere una asignación mucho mayor de lo normal para pasar las páginas de protección.
Esta región de protección de 1MiB está por debajo del límite de crecimiento de ulimit ulimit -s (8MiB), no por debajo del puntero de pila actual. Es independiente del mecanismo de crecimiento de pila normal de Linux.
gcc -fstack-check El efecto de gcc -fstack-check es esencialmente el mismo que el que siempre se necesita en Windows (lo que hace MSVC llamando a _chkstk ): toque las páginas de la pila entre el puntero de la pila anterior y el nuevo cuando lo mueve en una cantidad grande o variable en el tiempo de ejecución.
Pero el propósito/beneficio de estas sondas es diferente en Linux; nunca es necesario para la corrección en un programa libre de errores en GNU/Linux. "Solo" defiende contra errores/exploits de choque de pila.
En x86-64 GNU/Linux, gcc -fstack-check agregará (para funciones con un VLA o una matriz grande de tamaño fijo) un bucle que apila sondas con or qword ptr [rsp], 0 junto con sub rsp,4096 . Para tamaños de matriz fijos conocidos, puede ser solo una sonda. El code-gen no parece muy eficiente; normalmente nunca se usa en este objetivo. (Ejemplo del explorador del compilador Godbolt que pasa una matriz de pila a una función no en línea).
https://gcc.gnu.org/onlinedocs/gccnt/Stack-Checking.html describe algunos parámetros internos de GCC que controlan lo que -fstack-check .
Si desea una seguridad absoluta contra los ataques de choque de pila, esto debería ser suficiente. Sin embargo, no es necesario para el funcionamiento normal, y una página de protección de 1MiB es suficiente para la mayoría de las personas.
Tenga en cuenta que -fstack-protector-strong es completamente diferente y protege contra la sobrescritura de la dirección de retorno por desbordamientos del búfer en los arreglos locales. No tiene nada que ver con los conflictos de pila, y el ataque es contra cosas que ya están en la pila por encima de una matriz local pequeña, no contra otras regiones de la memoria moviendo mucho la pila.
Nota al pie 1: las pilas de subprocesos en Linux (para subprocesos que no sean el inicial) deben asignarse completamente por adelantado porque la función de crecimiento mágico no funciona. Solo el subproceso principal, también conocido como inicial, de un proceso puede tener eso.
(Hay una función mmap(MAP_GROWSDOWN) pero no es segura porque no hay límite, y porque nada impide que otras asignaciones dinámicas seleccionen aleatoriamente una página cerca de la pila actual, lo que limita el crecimiento futuro a un tamaño pequeño antes de que la pila choque. También porque solo crece si toca la página de protección, por lo que necesitaría sondas de pila. Por estas razones sensacionales, MAP_GROWSDOWN no se usa para pilas de subprocesos . El mecanismo interno para la pila principal se basa en una magia diferente en el kernel que evita que otras asignaciones roben espacio.)