Análisis estático y fuzzing
Encontrar bugs sin (y con) ejecutar: analizadores estáticos como clang-tidy y -fanalyzer, Valgrind, y el fuzzing que descubre entradas que ni imaginabas.
Los sanitizers cazan bugs cuando el código se ejecuta con una entrada dada. Pero ¿y los bugs en caminos que tus tests no recorren? Para eso están dos familias complementarias: el análisis estático, que examina el código sin ejecutarlo, y el fuzzing, que bombardea tu programa con millones de entradas para encontrar las que lo rompen.
- Análisis estático: encontrar bugs sin ejecutar.
- Valgrind para memoria en tiempo de ejecución.
- Fuzzing: descubrir entradas que rompen.
- Cómo se complementan las tres técnicas.
Análisis estático
Un analizador estático lee tu código y razona sobre todos los caminos posibles, señalando bugs potenciales sin ejecutar nada:
# el analizador integrado en GCC
gcc -std=c23 -fanalyzer -Wall prog.c
# clang-tidy: cientos de checks (bugs, estilo, modernización)
clang-tidy prog.c -- -std=c23
# cppcheck: analizador dedicado, fácil de integrar
cppcheck --enable=all prog.c
Detectan cosas que el compilador normal no: dobles frees en ciertos caminos, punteros nulos condicionales, fugas en ramas de error, uso de memoria liberada. Como examinan todos los caminos (no solo los que tus tests ejecutan), encuentran bugs latentes.
Valgrind
Valgrind (su herramienta Memcheck) ejecuta tu programa en una máquina virtual que vigila cada acceso a memoria. Más lento que ASan pero no requiere recompilar y detecta cosas distintas:
valgrind --leak-check=full ./prog
# informa de accesos inválidos, memoria no inicializada y fugas exactas
Fuzzing: el que encuentra lo que no imaginaste
El fuzzing alimenta tu programa con entradas generadas automáticamente —millones, guiadas por cobertura de código— buscando las que provocan un crash o un fallo de sanitizer. Descubre casos límite que ningún humano escribiría como test:
// objetivo de libFuzzer: recibe datos arbitrarios y los procesa
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
parsear(data, size); // tu función bajo prueba
return 0;
}
clang -std=c23 -fsanitize=fuzzer,address parser_fuzz.c -o fuzzer
./fuzzer # genera entradas sin parar hasta encontrar un crash
Esta es la técnica que más humilla y más protege. Tú escribes tests para los casos que se te ocurren; el fuzzer prueba los que no se te ocurren: una cadena de 4 GB, bytes nulos en medio, un número negativo donde esperabas positivo, entradas maliciosamente formadas. Combinado con ASan, cada entrada que dispara un error de memoria queda registrada con el input exacto que lo provoca. Proyectos críticos —navegadores, librerías de parsing, criptografía— fuzzean sin parar (Google ejecuta OSS-Fuzz sobre miles de proyectos 24/7) y encuentran miles de vulnerabilidades que ninguna revisión humana habría visto. Si escribes código que procesa entrada no confiable —un parser, un decodificador, un formato de red—, el fuzzing no es opcional: es la diferencia entre “creo que es seguro” y “lo he bombardeado con millones de entradas y aguanta”.
Las tres técnicas se complementan
Estático
Sin ejecutar. Cubre todos los caminos, encuentra bugs latentes. Puede dar falsos positivos.
Sanitizers + tests
Ejecutando tus casos. Preciso y sin falsos positivos, pero solo los caminos que tus tests recorren.
Fuzzing
Ejecutando entradas automáticas. Encuentra los caminos que no imaginaste. Necesita tiempo de cómputo.
- Pasa
gcc -fanalyzerycppcheck --enable=allsobre un programa tuyo y lee los avisos. - Ejecuta un programa con fugas bajo
valgrind --leak-check=full. - Escribe un pequeño
LLVMFuzzerTestOneInputpara una función de parsing y lánzalo con-fsanitize=fuzzer,address. - Deja el fuzzer corriendo unos minutos: ¿encuentra alguna entrada que no habías previsto?