1 puntos por GN⁺ 2024-04-05 | 1 comentarios | Compartir por WhatsApp
  • El -fanalyzer de 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-bounds va 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

  • -fanalyzer es 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 for anidado, la condición del segundo bucle es j < n pero la expresión de incremento sigue siendo i++
    • Corresponde al error de copiar el primer for y no cambiar la i por j en el incremento
    • El analizador sigue continuamente la rama verdadera de j < n en esa ruta y advierte sobre la repetición del bucle
  • 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") sobre char buf[10] y luego llama a strcat(buf, " world!"), se detecta un desbordamiento de búfer en la pila
    • El mensaje anterior indicaba que buf tení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 strcpy como el byte NUL terminador existente desde donde empieza strcat
  • 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 "サツキ" a char buf[11] y luego concatena "メイ"
    • El diagrama muestra que el desbordamiento ocurre en medio del carácter , es decir, U+30E1
  • 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 atributos null_terminated_string_arg(1) y nonnull
    • char 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 de str
    • El diagnóstico también incluye una nota indicando que el argumento 1 de example_fn debe ser un puntero a cadena terminada en nulo
  • Este ejemplo puede probarse en Compiler Explorer

Activación predeterminada del análisis de 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_DEFINEx para informar al analizador que el argumento de osf_getsysinfo proviene 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 atacante nbytes se usa como tamaño sin una validación superior
    • El diagnóstico muestra que nbytes solo recibió una validación inferior en if (nbytes < sizeof(*hwrpb))
    • El tercer parámetro de copy_to_user se indica como parámetro de tamaño mediante el atributo access(write_only, 1, 3)
  • 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
  • El trabajo de ejecutar el analizador sobre el kernel para encontrar vulnerabilidades y corregir falsos positivos del analizador sigue en marcha

1 comentarios

 
GN⁺ 2024-04-05
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.

    • Sé que Rust recibe mucha atención, especialmente en HN, por la seguridad de memoria y sus buenas abstracciones, pero me pregunto qué tanto de su popularidad se debe a los mensajes de error.
      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.
    • Clang también tiene una herramienta similar, Clang Static Analyzer: https://clang-analyzer.llvm.org/
    • Tuve la experiencia opuesta. Clang suele mostrar mensajes de error mucho mejores que GCC, algunas implementaciones de warnings o errores detectan más casos, y clang-tidy hace análisis estático mucho mejor.
    • Me recuerda una de las razones por las que odiaba tanto C++. En lugar de error: missing semicolon, solía recibir más de 1000 líneas de mensajes de error relacionados con la instanciación de templates.
    • Esto suena bastante sorprendente. Me pregunto qué detecta el analizador de GCC que el analizador estático de Clang no reporte ya.
      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.

    • Como referencia, en GCC 13 implementé salida SARIF, que por ejemplo puede verse en VS Code con un plugin. Eso sí, no incluye el arte ASCII.
      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-stderr agregada. 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 -1 si nbytes < sizeof(*hwrpb) y devolvía -2 si fallaba copy_to_user(buffer, hwrpb, nbytes). El arreglo aplicado fue comprobar nbytes > sizeof(*hwrpb), pero me parece que la corrección correcta es copy_to_user(buffer, hwrpb, sizeof(*hwrpb)).
    No tiene sentido copiar desde el puntero hwrpb un tamaño que no sea sizeof(*hwrpb).

    • Si el llamador pasa nbytes = 4 y sizeof(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 de hwrpb->size, se puede garantizar compatibilidad hacia atrás y hacia adelante aunque parte de la estructura no esté inicializada.
    • Correcto. Pero dado que se proporciona el tamaño del búfer, tampoco tiene sentido sobrescribir más allá del final del búfer del llamador, así que tampoco puedes pasar un valor mayor que 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-overflow tiene tantos falsos positivos que es el primer warning que desactivo. Dudo que una variante del analizador sea mejor.

    • Me parece un poco como quitarle la batería al detector de monóxido de carbono porque no deja de pitar, te da dolor de cabeza y sueño.
  • Muy bueno. Últimamente no hago mucho desarrollo en C, así que me pregunto qué tan seguido se usan strcpy y strcat. La última vez que revisé, estaban casi tan mal vistos como goto.
    Claro, sé que en desarrollo del kernel goto suele preferirse. Me pregunto qué tan útil es realmente el análisis de cadenas en C.

    • En algunos contextos, usar goto es 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 strcpy son 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.
    • Mientras se use como equivalente lógico de construcciones de programación estructurada que C no tiene, algunos usos de goto siguen siendo idiomáticos en C. Hay que tener cuidado, pero al fin y al cabo es C, así que puede pasar.
      Eso sí, longjmp no me gusta nada.
    • No hay problema con un uso simple de 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.
    • goto está bien si se usa con cuidado. strcpy y strcat tambié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.
    • No veo que goto sea tan tabú como strcat y strcpy. goto está bien; strcat y strcpy usados en el mismo scope sin un malloc del 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.