Mejoras en el análisis estático en GCC 14
(developers.redhat.com)- El
-fanalyzerde GCC 14 sigue múltiples rutas de ejecución del código C mediante ejecución simbólica, con foco en encontrar en tiempo de compilación defectos difíciles de detectar antes del runtime - Esta mejora se divide en detección de bucles infinitos, visualización de desbordamientos de búfer, seguimiento de cadenas C y activación predeterminada del análisis de contaminación, con énfasis en hacer que los diagnósticos sean más fáciles de entender
-Wanalyzer-out-of-boundsva más allá de una advertencia simple y muestra con un diagrama de texto la relación entre el búfer y la posición de escritura, e incluso indica en qué punto se desborda dentro de una cadena UTF-8- El nuevo atributo
null_terminated_string_arg(PARAM_IDX)informa al analizador y a quienes leen el código sobre la condición de una API que espera una cadena terminada en nulo, haciendo más visibles las rutas donde se pasa un búfer no terminado en nulo - El análisis de contaminación se activa por defecto al seleccionar
-fanalyzer, lo que facilita ver flujos donde valores controlados por un atacante se usan como tamaño, índice u offset sin validación superior
Alcance de los problemas que cubre -fanalyzer
-fanalyzeres una pasada de análisis estático de GCC que intenta encontrar defectos en tiempo de compilación mediante ejecución simbólica de múltiples rutas de ejecución del código fuente en C- Las mejoras de GCC 14 se resumieron antes del lanzamiento oficial de abril de 2024, y al momento de escribirse el artículo se esperaba que GCC 14.1 saliera durante abril de 2024
- La prerrelease de GCC 14.0 ya se estaba usando en Fedora 40 Beta
- Los ejemplos pueden probarse en Compiler Explorer junto con las nuevas opciones del compilador
Detección simple de bucles infinitos
- GCC 14 agrega la nueva advertencia
-Wanalyzer-infinite-loop - El código de ejemplo tiene una estructura donde, en un
foranidado, la condición del segundo bucle esj < npero la expresión de incremento sigue siendoi++- Corresponde al error de copiar el primer
fory no cambiar laiporjen el incremento - El analizador sigue continuamente la rama verdadera de
j < nen esa ruta y advierte sobre la repetición del bucle
- Corresponde al error de copiar el primer
- La salida de diagnóstico actual se entiende mejor si se lee en orden desde el evento
(1)hasta el(5) - Para GCC 15 sigue quedando como deseo una mejora de legibilidad con algo parecido a arte ASCII que resalte la ruta del flujo de control
- Ese código puede probarse en este ejemplo de Compiler Explorer
Visualizar desbordamientos de búfer con texto
- En GCC 13 el analizador empezó a admitir verificaciones de límites mediante
-Wanalyzer-out-of-bounds - En GCC 14 puede mostrar la relación espacial del desbordamiento de búfer previsto mediante un diagrama basado en texto
- En el ejemplo que hace
strcpy(buf, "hello")sobrechar buf[10]y luego llama astrcat(buf, " world!"), se detecta un desbordamiento de búfer en la pila- El mensaje anterior indicaba que
buftenía capacidad de 10 bytes y que ocurría una escritura fuera de rango desde el byte 10 hasta el 12 - El diagrama de GCC 14 muestra tanto el búfer de destino llenado por
strcpycomo el byteNULterminador existente desde donde empiezastrcat
- El mensaje anterior indicaba que
- También en ejemplos con cadenas no ASCII se marca la posición del desbordamiento siguiendo la representación UTF-8
- Se usa como ejemplo código que copia
"サツキ"achar buf[11]y luego concatena"メイ" - El diagrama muestra que el desbordamiento ocurre en medio del carácter
メ, es decir, U+30E1
- Se usa como ejemplo código que copia
- El código relacionado puede verse en el ejemplo de cadena ASCII y el ejemplo de cadena no ASCII
Mejor seguimiento de operaciones con cadenas C
- El analizador de GCC 14 mejora el seguimiento de operaciones con cadenas C, simulando APIs que escanean búferes buscando el byte terminador nulo
- Si existe una ruta donde un puntero apunta a un búfer no terminado en nulo y se pasa a una de esas APIs, se genera una advertencia
- El nuevo atributo de función
null_terminated_string_arg(PARAM_IDX)informa al analizador y a quienes leen el código que cierto parámetro debe ser una cadena terminada en nulo - En el ejemplo,
example_fn(const char *p)lleva los atributosnull_terminated_string_arg(1)ynonnullchar str[3] = "abc";no tiene espacio para almacenar el byte terminador nulo- Al llamar
example_fn(str), el analizador advierte sobre una lectura excesiva de búfer en la pila de 1 byte más allá del final destr - El diagnóstico también incluye una nota indicando que el argumento 1 de
example_fndebe ser un puntero a cadena terminada en nulo
- Este ejemplo puede probarse en Compiler Explorer
Activación predeterminada del análisis de contaminación
- El análisis de contaminación del analizador rastrea entradas controladas por un atacante, puntos de saneamiento y puntos donde se usan sin saneamiento
- En versiones anteriores de GCC no se activaba por defecto debido a bugs y a muchos falsos positivos, y estaba oculto detrás de un argumento separado de línea de comandos
- En GCC 14, tras varias correcciones, el análisis de contaminación se activa por defecto al seleccionar
-fanalyzer - Con este cambio también se habilitan las siguientes 6 advertencias basadas en contaminación
Un caso de análisis del kernel a partir de CVE-2011-2210
- Se usa como ejemplo de análisis de contaminación un extracto del CVE-2011-2210 del kernel de Linux
- Se añade
__attribute__((tainted_args))a la macro__SYSCALL_DEFINExpara informar al analizador que el argumento deosf_getsysinfoproviene de un límite de confianza y debe tratarse como un valor contaminado - El analizador de GCC 14 advierte en
copy_to_user(buffer, hwrpb, nbytes)que el valor controlado por un atacantenbytesse usa como tamaño sin una validación superior- El diagnóstico muestra que
nbytessolo recibió una validación inferior enif (nbytes < sizeof(*hwrpb)) - El tercer parámetro de
copy_to_userse indica como parámetro de tamaño mediante el atributoaccess(write_only, 1, 3)
- El diagnóstico muestra que
- El problema estaba en que la condición de saneamiento se había escrito como
if (nbytes < sizeof(*hwrpb))- La condición prevista era algo como
if (nbytes > sizeof(*hwrpb)) - En la versión con la condición corregida, el analizador ya no advierte
- La condición prevista era algo como
- El trabajo de ejecutar el analizador sobre el kernel para encontrar vulnerabilidades y corregir falsos positivos del analizador sigue en marcha
1 comentarios
Opiniones en Hacker News
Para mí, fanalyzer es una de las funciones estrella de GCC frente a Clang. Hace que programar en C sea mucho más fácil al explicar los errores, y en cuanto a mensajes de error empieza a sentirse parecido a Rust por lo amigables que son para el desarrollador.
Muchas veces, la principal razón para abandonar el aprendizaje de una tecnología son errores frustrantes o poco claros. Me fui un poco del tema, pero lo que quería decir es que me gusta C y me gustaría aún más si tuviera mensajes de error al nivel de Rust.
error: missing semicolon, solía recibir más de 1000 líneas de mensajes de error relacionados con la instanciación de templates.He usado el analizador de GCC algunas veces, pero no encontré un buen frontend que hiciera legible la salida. Clang tiene varias opciones, como una salida HTML bastante buena, CodeChecker e integración con Xcode; me pregunto cómo se lee la salida del lado de GCC. Además, GCC parece generar muchos más falsos positivos que Clang.
Hay 36 comentarios más en otro hilo: https://news.ycombinator.com/item?id=39918278
“GCC 14 Boasts Nice ASCII Art for Visualizing Buffer Overflows (phoronix.com)”, publicado hace 2 horas.
Hace unos meses hice una pequeña utilidad de Linux. Era un shim drop-in que reemplazaba cualquier ejecutable: al invocarse, fingía ser el programa original, hacía fork del original y se conectaba a stdout/stderr.
La salida de errores se enviaba a un asistente GPT personalizado que conocía el contexto de ese programa, y el asistente convertía el error original a una forma legible para humanos antes de enviarlo al stderr del shim. Lo usaba porque me harté de ver volcados anidados del compilador relacionados con concepts/templates de GCC/Clang, pero podía usarse con cualquier programa si uno quería. Funcionaba bien, pero me enfermé bastante y no pude seguir trabajando en ello; creo que sería un buen proyecto si alguien lo retomara, lo hiciera bien y lo generalizara.
Me gustaría que hubiera un mejor formato de salida para los resultados del análisis. La forma actual es un infierno para los lectores de pantalla.
Un ejemplo de salida se puede ver aquí: https://godbolt.org/z/aan6Kfxds
Es el primer ejemplo del artículo con la opción de línea de comandos
-fdiagnostics-format=sarif-stderragregada. También experimenté con sacar los diagramas como SVG, pero no lo pulí lo suficiente como para incluirlo en GCC 14.El código original devolvía
-1sinbytes < sizeof(*hwrpb)y devolvía-2si fallabacopy_to_user(buffer, hwrpb, nbytes). El arreglo aplicado fue comprobarnbytes > sizeof(*hwrpb), pero me parece que la corrección correcta escopy_to_user(buffer, hwrpb, sizeof(*hwrpb)).No tiene sentido copiar desde el puntero
hwrpbun tamaño que no seasizeof(*hwrpb).nbytes = 4ysizeof(hwrpb)es 16 bytes, podrías copiar 12 bytes de más desde el búfer del llamador y leer memoria que no te pertenece. Creo que eso debería evitarse.Una mejor solución es copiar solo el mínimo de bytes que soportan tanto el llamador como el llamado. Por ejemplo, algo como
nbytes = MIN(nbytes, sizeof(hwrpb));. Suponiendo que se respete la información de versión dehwrpb->size, se puede garantizar compatibilidad hacia atrás y hacia adelante aunque parte de la estructura no esté inicializada.nbytes.Realmente excelente. La cantidad de trabajo invertida parece enorme. La dificultad se ve comparable a introducir fat pointers/vistas de arreglos en la biblioteca estándar y en el estándar de C.
-Wstringop-overflowtiene tantos falsos positivos que es el primer warning que desactivo. Dudo que una variante del analizador sea mejor.Muy bueno. Últimamente no hago mucho desarrollo en C, así que me pregunto qué tan seguido se usan
strcpyystrcat. La última vez que revisé, estaban casi tan mal vistos comogoto.Claro, sé que en desarrollo del kernel
gotosuele preferirse. Me pregunto qué tan útil es realmente el análisis de cadenas en C.gotoes claramente correcto y elegante. Evitarlo a toda costa puede llevar a código feo, enredado y difícil de mantener. Aunque no sea común, tiene usos válidos.Funciones como
strcpyson menos recomendables, pero hay situaciones en las que puede garantizarse que son correctas mientras no se rompan invariantes más fuertes, por ejemplo invariantes a nivel de lenguaje. Si esas invariantes se rompen, ya tienes un problema mucho mayor. Es raro, pero también se puede argumentar que una alternativa nominalmente más segura podría ser un poco menos eficiente sin aportar beneficios.gotosiguen siendo idiomáticos en C. Hay que tener cuidado, pero al fin y al cabo es C, así que puede pasar.Eso sí,
longjmpno me gusta nada.goto. En cambio, la familia strxcpy es un desastre total y no debería usarse por ningún motivo. Que se use en el kernel es horrible.Esas funciones y todos los intentos fallidos de “arreglarlas” debieron haberse destruido desde la órbita.
gotoestá bien si se usa con cuidado.strcpyystrcattambién están “bien” en el sentido de que sabes que el código es correcto y, si no lo es, tienes un problema grande. Lamentablemente, esa descripción aplica a la mayor parte de C.gotosea tan tabú comostrcatystrcpy.gotoestá bien;strcatystrcpyusados en el mismo scope sin unmallocdel tamaño exacto son más bien un code smell.Muy bueno. Me alegra que todo venga con reportes detallados que explican qué salió mal.