- Al hacer fuzzing de una implementación de zlib verificada formalmente, se descubrió un desbordamiento de búfer en el heap en
lean_alloc_sarraydel runtime de Lean 4 - Tras más de 100 millones de pruebas con el fuzzer de IA Claude, AFL++ y AddressSanitizer, entre otras herramientas, se confirmaron 4 fallos y 1 vulnerabilidad de memoria
- Los problemas encontrados se dividen en dos tipos: desbordamiento en el runtime y denegación de servicio (DoS) basada en falta de memoria en
Archive.lean - Los algoritmos verificados de compresión y descompresión eran seguros, pero existían vulnerabilidades en el parser de archivos no verificado y en la capa del runtime
- En consecuencia, la verificación formal es poderosa, pero sin garantizar la seguridad de la base de cómputo confiable (TCB) no se puede asegurar la estabilidad de todo el sistema
Bugs encontrados en un programa que había pasado la verificación de Lean
- Al hacer fuzzing de una implementación de zlib verificada formalmente con Lean, se encontró un desbordamiento de búfer en el heap en el runtime de Lean 4
- El código verificado de la aplicación no tenía vulnerabilidades de memoria
- Sin embargo, se produjo un desbordamiento en la función
lean_alloc_sarraydel runtime de Lean, lo que afecta a todas las versiones de Lean 4
- Se realizaron más de 100 millones de ejecuciones de fuzzing con el fuzzer basado en IA Claude, AFL++, AddressSanitizer, Valgrind, UBSan y otras herramientas
- Se ejecutaron 16 fuzzers en paralelo contra 6 superficies de ataque, incluidas compresión, descompresión y manejo de archivos en
lean-zip - En 19 horas se encontraron 4 entradas que provocaban fallos y 1 vulnerabilidad de memoria
- Se ejecutaron 16 fuzzers en paralelo contra 6 superficies de ataque, incluidas compresión, descompresión y manejo de archivos en
- Se confirmaron dos bugs principales
- Desbordamiento de búfer en el heap en
lean_alloc_sarraydel runtime de Lean - Denegación de servicio (DoS) por falta de memoria en el módulo
Archive.leandelean-zip
- Desbordamiento de búfer en el heap en
- Quedaron expuestos los límites de la verificación formal
- Aunque los algoritmos de compresión y descompresión de
lean-zipestaban completamente verificados, el parser de archivos (Archive.lean) no lo estaba y presentaba una vulnerabilidad DoS - El bug del runtime está dentro de la base de cómputo confiable (Trusted Computing Base), por lo que afecta a todos los programas de Lean
- Aunque los algoritmos de compresión y descompresión de
- En conclusión, la verificación formal de Lean demostró la estabilidad del código de la aplicación, pero siguen existiendo vulnerabilidades en componentes fuera del alcance de la verificación
- La verificación solo es poderosa dentro del alcance en que se aplica, y es indispensable garantizar la seguridad de la capa base de confianza
Resumen del experimento de fuzzing
- El código objetivo fue la implementación verificada de zlib de
lean-zip- Se eliminaron todos los teoremas (theorem), especificaciones (specification), documentación y enlaces C FFI para dejar solo código puro en Lean
- Se evitó el sesgo haciendo que Claude no supiera que el código había sido verificado
-
Entorno del experimento
- Se ejecutaron 16 fuzzers en paralelo sobre 6 superficies de ataque: ZIP, gzip, DEFLATE, tar, tar.gz, compression
- También se usaron AddressSanitizer, UndefinedBehaviorSanitizer, Valgrind memcheck, cppcheck y flawfinder
- Incluyendo 48 archivos de explotación hechos a mano, se realizaron 105,823,818 ejecuciones en total, usando 359 archivos semilla
- En 19 horas se encontraron 4 entradas con fallo y 1 vulnerabilidad de memoria
Bug 1: desbordamiento de búfer en el heap en el runtime de Lean
- Función vulnerable:
lean_alloc_sarray- Es la función que asigna todos los arreglos escalares como
ByteArrayyFloatArray - Puede ocurrir un desbordamiento entero al calcular
sizeof(lean_sarray_object) + elem_size * capacity
- Es la función que asigna todos los arreglos escalares como
-
Causa del problema
- Cuando
capacityestá cerca deSIZE_MAX, la suma se desborda y se asigna un búfer pequeño - Luego el llamador intenta leer
nbytes, provocando un desbordamiento de búfer en el heap
- Cuando
-
Condiciones para activarlo
- Ocurre cuando
nbytestiene un valor extremadamente grande enlean_io_prim_handle_read - Puede reproducirse con un archivo de 156 bytes cuyo encabezado ZIP64 tenga
compressedSizeigual a0xFFFFFFFFFFFFFFFF - Afecta a todas las versiones de Lean 4, incluida la nightly más reciente
- Ejemplo de código para reproducirlo
def main : IO Unit := do IO.FS.writeFile "test.bin" "hello" let h ← IO.FS.Handle.mk "test.bin" .read let n : USize := (0 : USize) - (1 : USize) let _ ← h.read n- Al ejecutar ese código se produce el desbordamiento en
lean_alloc_sarray - Ya se presentó un PR de corrección
- Ocurre cuando
Bug 2: denegación de servicio (DoS) en el parser de archivos de lean-zip
- Función vulnerable:
readExact(Archive.lean)- Usa directamente el campo
compressedSizedel directorio central ZIP sin validarlo - Al llamar a
h.read, solicita un tamaño anormalmente grande, lo que provoca asignación excesiva de memoria y OOM
- Usa directamente el campo
-
Reproducción del problema
- Si un archivo ZIP de 156 bytes afirma tener un tamaño de varios exabytes,
el proceso termina con
INTERNAL PANIC: out of memory - El
unzipdel sistema valida el tamaño del encabezado, perolean-zipno lo hace
- Si un archivo ZIP de 156 bytes afirma tener un tamaño de varios exabytes,
el proceso termina con
Lo que la verificación formal no cubrió
-
Causa de la vulnerabilidad DoS
- El módulo
Archive.leanes una zona no verificada - El pipeline de compresión y descompresión (por ejemplo, DEFLATE, Huffman, CRC32) estaba completamente verificado y no presentó problemas
- La vulnerabilidad apareció en una parte a la que no se aplicó la verificación
- El módulo
-
Naturaleza del desbordamiento en el runtime
- El runtime de Lean pertenece a la base de cómputo confiable (TCB)
- Todas las pruebas de Lean suponen que el runtime es correcto
- Por eso, un bug en el runtime afecta la seguridad de todos los programas de Lean
Estabilidad del código verificado
-
Resultado tras 105 millones de ejecuciones
- En el código C generado por Lean no se observaron desbordamientos de heap, use-after-free, desbordamientos de pila, UB ni accesos fuera de rango en arreglos
- Según la evaluación de Claude, fue analizado como uno de los codebases más seguros en memoria
-
Efecto del sistema de tipos de Lean
- Con tipos dependientes (dependent types) y una estructura de recursión bien fundada (well-founded recursion), se bloquean de forma estructural vulnerabilidades comunes (clases de CVE) típicas de implementaciones zlib basadas en C/C++
-
Conclusión
- La parte de código verificada fue muy robusta y al fuzzer le costó encontrar errores
- Sin embargo, seguían existiendo vulnerabilidades en partes no verificadas y en la capa del runtime
- La verificación está limitada por el alcance en que se aplica y por la seguridad de su base de confianza
Lección principal
- La verificación formal es muy poderosa dentro del código al que se aplica, pero componentes periféricos no verificados o la capa del runtime pueden poner en riesgo la estabilidad total
- Es indispensable garantizar la seguridad de la base de cómputo confiable, y aun en un sistema verificado sigue vigente la pregunta: “¿quién vigila a los vigilantes? (Quis custodiet ipsos custodes)”
1 comentarios
Comentarios en Hacker News
El título y el enfoque de este artículo me parecieron un poco raros
En realidad, el autor deja claro que no pudo encontrar bugs ni errores dentro del código demostrado
Los dos bugs encontrados estaban fuera del alcance de la prueba: uno era una omisión en la especificación y el otro un heap overflow en el runtime de C++
Quiero enfatizar que el bug se encontró en el runtime de Lean, no en el kernel que realiza la verificación
Véase el enlace a la documentación de Lean
Si pierdes tus bitcoins por un buffer overflow, no consuela saber que el bug estaba en el runtime
Además, si el programa se cae, la mayoría de los usuarios lo considerará un bug
Probablemente también haya bastante gente ejecutando Lean en producción
Si no es el kernel, entonces no afecta mucho la confiabilidad de la prueba en sí
Aunque verifiques un programa de “Hello world”, tienes que especificar no solo la salida, sino también los efectos secundarios
Si hay corrupción de memoria en el límite entre el runtime y el kernel, la confiabilidad de la prueba puede verse afectada
Al final, lo clave es definir con claridad “qué debe verificarse”
Lista de artículos relacionados
Parece que el autor intentó inducir a error deliberadamente
Yo también tuve una experiencia parecida con código demostrado
En mi caso, el problema no fue un overflow sino un defecto en la especificación (spec bug)
Si la especificación está mal, el código y la prueba pueden ser perfectos y aun así comportarse distinto de lo que se pretendía
Al final, la dificultad de la verificación está en expresar con precisión la intención humana
Creer que la IA resolverá perfectamente la verificación puede dar una falsa confianza
Las especificaciones que se pueden demostrar con herramientas como Lean, TLA+ o Z3 están simplificadas y requieren muchas suposiciones
Aun así, es una herramienta poderosa
Ayuda a acotar bugs en algoritmos complejos y a ver con claridad los límites de la especificación
Gracias a la IA, este tipo de trabajo de prueba se ha vuelto mucho más fácil
Otro programa que verifica programas también puede tener bugs al final
La imposibilidad de una autoverificación completa hace pensar en el principio de incertidumbre de Heisenberg
En última instancia, la verificación es un proceso de aumentar la confianza mediante una “segunda implementación independiente”
(1) cuando se comporta distinto a lo que yo quería
(2) cuando se comporta tal como yo quería, pero esa intención estaba equivocada
Los asistentes de prueba ayudan a evitar (1), pero no pueden evitar (2)
Es decir, no se puede demostrar la solidez del diseño
Verificarlo todo, como en seL4, cuesta demasiado y toma demasiado tiempo
Lo ideal sería combinar lógica formal + pensamiento adversarial (adversarial thinking) para enumerar por completo qué debe hacer el programa y qué no debe hacer
El título se sintió como clickbait
No había bugs en la parte demostrada, así que no entiendo por qué lo redactaron así
En HN quiero ver publicaciones basadas en hechos, y esto fue una pérdida de tiempo
Se publicita como “sin bugs”, pero en realidad significa “solo dentro del alcance perfectamente expresado por la especificación”
Incluso si es perfectamente correcto, en la realidad puede estar correcto pero muerto — o sea, teóricamente correcto pero roto en la práctica
No hay referencias al código, y si uno mira el código real ve que fue un malentendido
Este problema aparece a menudo también en la verificación de sistemas distribuidos
Las pruebas solo son válidas dentro de ciertas condiciones (rango operativo), y los fallos interesantes ocurren en esos bordes
Con TLA+ pasa lo mismo: si la realidad se sale de las suposiciones, la prueba deja de tener sentido
Lo que yo quisiera es una función que declare mecánicamente el rango operativo y lo vigile en runtime
Pero la mayoría de los bugs no vienen del hardware, sino de que la gente no lee la documentación
Solo con modelado formal ya se puede reducir muchísimo la cantidad de bugs
Esta es una buena noticia predecible
Significa que los LLM pueden generar código que pase verificación formal
De ahora en adelante, los bugs se irán desplazando cada vez más hacia la capa de hardware
Al final, hará falta la formalización de las especificaciones de hardware
Si los fabricantes no la publican, podría haber conflicto con la comunidad
A largo plazo, esto se dividirá en dos caminos: la extinción de las vulnerabilidades o su inserción intencional
Al principio pensé que el autor no había probado la parte demostrada
Pero al seguir leyendo, vi que los bugs se encontraron fuera del alcance de la prueba
Por eso el título me pareció una exageración
Sostiene que, si hablamos de un bug en un sistema verificado, debe considerarse el binario completo
Y dice que de hecho encontró una entrada que hace colapsar la parte de parseo de cabeceras comprimidas en Archive.lean
Me hizo recordar la famosa frase de Donald Knuth
“Cuidado, el código de arriba puede tener bugs. Yo solo demostré que es correcto; no lo he ejecutado”
No haber verificado el parser parece un gran error
El parseo de formatos binarios siempre es una zona de riesgo
El punto clave es que un agente de IA colaboró con un fuzzer para encontrar un heap buffer overflow en Lean
Eso es un logro bastante grande
Me llamó la atención la parte donde Claude dice que “este es uno de los codebases más memory-safe que he analizado”
Pero sonó como el chiste de que era el primer código que Claude analizaba
Justamente por eso es tan bueno en esto