Los comportamientos de GCC indefinidos se vuelven locos

Satisfecho con mi reciente avance en la comprensión de las divisiones enteras de C después de semanas de lucha, estaba ocupándome de mis propios asuntos divirtiéndome escribiendo código aritmético de enteros. La vida era buena, cuando de repente... zsh: segfault (core dumped).

Este código no alteró mucho la memoria, por lo que era más probable que fuera un efecto secundario de un desbordamiento o algo así. El uso de -fsanitize=undefined identificó rápidamente el problema, lo que confirmó la presencia de un desbordamiento de enteros. La solución fue fácil, pero algo andaba mal. Sentí que mi código era bastante robusto contra este tipo de error honesto. Resulta que la condición de guardia que tenía debería haber sido suficiente, así que traté de extraer un caso mínimo repetible:

#comprender #comprender #comprender pestaña uint8_t[0x1ff + 1]; uint8_tf(int32_tx) { si (x < 0) devuelve 0; int32_t i = x * 0x1ff / 0xffff; if (i >= 0 && i < tamaño de (pestaña)) { printf("tab[%d] parece seguro porque %d está entre [0;%d[\n", i, i, (int)sizeof(tab)); pestaña de retorno[i]; } devuelve 0; } int principal(int ac, char **av) { return f(atoi(av[1])); }

Puede ocurrir un desbordamiento en una tarea. Dado que un desbordamiento de enteros no está definido, GCC asume que nunca puede suceder. En pratique, dans ce cas, c'est le cas, mais la condition i >= 0 && i < sizeof(tab) devrait suffire à s'en occuper, quelle que soit la valeur folle qu'elle devient, n'est- No es ? Bueno, tengo malas noticias:

%cc -Pared -O2 desbordamiento.c -o desbordamiento && ./desbordamiento 50000000 tab[62183] parece seguro porque 62183 está entre [0;512[ zsh: segfault (núcleo volcado) ./overflow 50000000

Nota: Este es GCC 12.2.0 en x86-64.

Tenemos i=62183 como resultado del desbordamiento y, sin embargo, la ejecución viola la condición de puerta, dice una mentira sin sentido, salta directamente a la pestaña de desreferenciación y muere miserablemente.

Echemos un vistazo a lo que hace GCC aquí. Ejecutando Ghidra, vemos el siguiente código descompilado:

uint8_t f(intx) { int tmp; si (-1 < x) { tmp=x*0x1ff; si (tmp < 0x1fffe00) { printf("tab[%d] parece seguro porque %d está entre [0;%d[\n",(ulong)(uint)tmp / 0xffff, (ulong)(uint)tmp / 0xffff,0x200); volver tabulador[(int)((uint)tmp / 0xffff)]; } } devuelve '\0'; }

Cuando dije que GCC asume que no puede suceder, eso es lo que quise decir: se supone que tmp no debe desbordarse, por lo que parte de la condición que tenía en su lugar simplemente se eliminó.

Informé este problema exacto a GCC para asegurarme de que no era un error y, de hecho, me confirmaron que el comportamiento indefinido de un desbordamiento de enteros n no se limita al valor insano que podría tomar: es aparentemente perfectamente aceptable para estropear por completo el flujo de código.

Si bien entiendo lo atractivo que puede ser esto desde una perspectiva de optimización, el desarrollador paranoico que hay en mí está francamente aterrorizado ante la posibilidad de que un solo desbordamiento de enteros elimine la protección de seguridad y cause tantos estragos. Trabajé durante varios años en un proyecto en el que los desbordamientos de enteros eran (y probablemente todavía lo sean) legión. Identificarlos y repararlos es probablemente la misión de toda una vida para muchas personas obstinadas.

Espero que este artículo empuje al equipo de Rust a una cruzada nuevamente, y creo que podría estar con ellos esta vez.

Para actualizaciones y contenido más frecuentes, puedes seguirme en Twitter o Mastodon. No dude en suscribirse a la fuente RSS para recibir notificaciones de nuevos artículos. Por lo general, también es posible comunicarse conmigo por otros medios (consulte el pie de página a continuación). Finalmente, las discusiones de ciertos artículos a veces se pueden encontrar en HackerNews, Lobste.rs y Reddit.

Satisfecho con mi reciente avance en la comprensión de las divisiones enteras de C después de semanas de lucha, estaba ocupándome de mis propios asuntos divirtiéndome escribiendo código aritmético de enteros. La vida era buena, cuando de repente... zsh: segfault (core dumped).

Este código no alteró mucho la memoria, por lo que era más probable que fuera un efecto secundario de un desbordamiento o algo así. El uso de -fsanitize=undefined identificó rápidamente el problema, lo que confirmó la presencia de un desbordamiento de enteros. La solución fue fácil, pero algo andaba mal. Sentí que mi código era bastante robusto contra este tipo de error honesto. Resulta que la condición de guardia que tenía debería haber sido suficiente, así que traté de extraer un caso mínimo repetible:

#comprender #comprender #comprender pestaña uint8_t[0x1ff + 1]; uint8_tf(int32_tx) { si (x < 0) devuelve 0; int32_t i = x * 0x1ff / 0xffff; if (i >= 0 && i < tamaño de (pestaña)) { printf("tab[%d] parece seguro porque %d está entre [0;%d[\n", i, i, (int)sizeof(tab)); pestaña de retorno[i]; } devuelve 0; } int principal(int ac, char **av) { return f(atoi(av[1])); }

Puede ocurrir un desbordamiento en una tarea. Dado que un desbordamiento de enteros no está definido, GCC asume que nunca puede suceder. En pratique, dans ce cas, c'est le cas, mais la condition i >= 0 && i < sizeof(tab) devrait suffire à s'en occuper, quelle que soit la valeur folle qu'elle devient, n'est- No es ? Bueno, tengo malas noticias:

%cc -Pared -O2 desbordamiento.c -o desbordamiento && ./desbordamiento 50000000 tab[62183] parece seguro porque 62183 está entre [0;512[ zsh: segfault (núcleo volcado) ./overflow 50000000

Nota: Este es GCC 12.2.0 en x86-64.

Tenemos i=62183 como resultado del desbordamiento y, sin embargo, la ejecución viola la condición de puerta, dice una mentira sin sentido, salta directamente a la pestaña de desreferenciación y muere miserablemente.

Echemos un vistazo a lo que hace GCC aquí. Ejecutando Ghidra, vemos el siguiente código descompilado:

uint8_t f(intx) { int tmp; si (-1 < x) { tmp=x*0x1ff; si (tmp < 0x1fffe00) { printf("tab[%d] parece seguro porque %d está entre [0;%d[\n",(ulong)(uint)tmp / 0xffff, (ulong)(uint)tmp / 0xffff,0x200); volver tabulador[(int)((uint)tmp / 0xffff)]; } } devuelve '\0'; }

Cuando dije que GCC asume que no puede suceder, eso es lo que quise decir: se supone que tmp no debe desbordarse, por lo que parte de la condición que tenía en su lugar simplemente se eliminó.

Informé este problema exacto a GCC para asegurarme de que no era un error y, de hecho, me confirmaron que el comportamiento indefinido de un desbordamiento de enteros n no se limita al valor insano que podría tomar: es aparentemente perfectamente aceptable para estropear por completo el flujo de código.

Si bien entiendo lo atractivo que puede ser esto desde una perspectiva de optimización, el desarrollador paranoico que hay en mí está francamente aterrorizado ante la posibilidad de que un solo desbordamiento de enteros elimine la protección de seguridad y cause tantos estragos. Trabajé durante varios años en un proyecto en el que los desbordamientos de enteros eran (y probablemente todavía lo sean) legión. Identificarlos y repararlos es probablemente la misión de toda una vida para muchas personas obstinadas.

Espero que este artículo empuje al equipo de Rust a una cruzada nuevamente, y creo que podría estar con ellos esta vez.

Para actualizaciones y contenido más frecuentes, puedes seguirme en Twitter o Mastodon. No dude en suscribirse a la fuente RSS para recibir notificaciones de nuevos artículos. Por lo general, también es posible comunicarse conmigo por otros medios (consulte el pie de página a continuación). Finalmente, las discusiones de ciertos artículos a veces se pueden encontrar en HackerNews, Lobste.rs y Reddit.

What's Your Reaction?

like

dislike

love

funny

angry

sad

wow