Un depurador te dice qué ocurre cuando ejecutas el programa paso a paso. Los sanitizers instrumentan el código en tiempo de compilación para detectar errores que no siempre producen un crash inmediato: acceso a memoria liberada, lectura de un byte más allá del final de un buffer, uso de variable no inicializada, o dos hilos modificando la misma variable sin sincronización.
Esos errores pueden existir durante meses en el código sin manifestarse, hasta que lo hacen en producción con datos específicos. Los sanitizers los sacan a la luz en desarrollo.
AddressSanitizer (ASan)
ASan detecta errores de memoria: desbordamientos de heap y stack, use-after-free, use-after-return, double-free y leaks.
# Compilar con ASan g++ -fsanitize=address -fno-omit-frame-pointer -g -O1 programa.cpp -o programa clang++ -fsanitize=address -fno-omit-frame-pointer -g -O1 programa.cpp -o programa
El flag -fno-omit-frame-pointer es importante para que los stack traces sean útiles. -O1 en lugar de -O0 da mejor detección sin eliminar demasiado contexto.
Ejemplo: heap buffer overflow
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3};
return v[5]; // acceso fuera de rango: UB sin ASan, error con ASan
}
ASan genera un informe con la dirección exacta del acceso, el tamaño del buffer y el stack trace de dónde ocurrió el acceso y dónde se asignó la memoria. Con -g aparecen los números de línea.
Ejemplo: use-after-free
int* crear() {
int* p = new int(42);
delete p;
return p; // peligro: p apunta a memoria liberada
}
int main() {
int* p = crear();
return *p; // use-after-free: ASan lo detecta
}
Variables de entorno de ASan
# Activar detección de leaks (por defecto en Linux) ASAN_OPTIONS=detect_leaks=1 ./programa # Ver el informe incluso si el programa termina normalmente ASAN_OPTIONS=leak_check_at_exit=1 ./programa # Parar en el primer error (útil con gdb) ASAN_OPTIONS=abort_on_error=1 ./programa
UBSan: comportamiento indefinido
UBSan detecta comportamiento indefinido (UB): desbordamiento de enteros con signo, desreferencia de puntero nulo, shift fuera de rango, conversiones de puntero inválidas y muchos más.
g++ -fsanitize=undefined -g programa.cpp -o programa
Ejemplos de UB que UBSan detecta
#include <climits>
int main() {
int x = INT_MAX;
int y = x + 1; // overflow de entero con signo: UB
// UBSan: runtime error: signed integer overflow
int* p = nullptr;
*p = 42; // desreferencia de nullptr: UB
// UBSan: runtime error: null pointer dereference
int arr[3] = {1, 2, 3};
int v = arr[5]; // out of bounds: UB (diferente de ASan, que lo detecta en heap)
}
Seleccionar checks específicos
# Solo overflow de enteros -fsanitize=signed-integer-overflow,unsigned-integer-overflow # Solo null pointer -fsanitize=null # Todos los checks de UBSan juntos -fsanitize=undefined # Hacer que UBSan aborte en lugar de imprimir y continuar -fsanitize=undefined -fno-sanitize-recover=all
ThreadSanitizer (TSan)
TSan detecta data races: dos hilos acceden a la misma variable, al menos uno de ellos escribe, y no hay sincronización entre los accesos.
g++ -fsanitize=thread -g -O1 programa.cpp -o programa -lpthread
TSan no es compatible con ASan. Solo uno de los dos puede estar activo a la vez.
Ejemplo: data race
#include <thread>
int contador = 0;
void incrementar() {
for (int i = 0; i < 100000; ++i)
++contador; // data race: sin mutex
}
int main() {
std::thread t1(incrementar);
std::thread t2(incrementar);
t1.join();
t2.join();
// El resultado es impredecible, TSan lo detecta
}
TSan produce un informe indicando qué hilos accedieron a la variable, en qué línea, y el stack trace de cada acceso. Sin TSan, el bug puede no manifestarse durante días o aparecer solo bajo carga alta.
ASan + UBSan juntos
g++ -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 programa.cpp -o programa
Se pueden combinar ASan y UBSan sin problema. TSan es el que no se puede mezclar con los otros dos.
Integración con CMake
# CMakeLists.txt
option(ENABLE_ASAN "Activar AddressSanitizer" OFF)
option(ENABLE_UBSAN "Activar UBSan" OFF)
option(ENABLE_TSAN "Activar ThreadSanitizer" OFF)
function(target_enable_sanitizers target)
if(ENABLE_ASAN)
target_compile_options(${target} PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(${target} PRIVATE -fsanitize=address)
endif()
if(ENABLE_UBSAN)
target_compile_options(${target} PRIVATE -fsanitize=undefined -fno-sanitize-recover=all)
target_link_options(${target} PRIVATE -fsanitize=undefined)
endif()
if(ENABLE_TSAN)
target_compile_options(${target} PRIVATE -fsanitize=thread)
target_link_options(${target} PRIVATE -fsanitize=thread)
endif()
endfunction()
add_executable(mi_app src/main.cpp)
target_enable_sanitizers(mi_app)
# Activar desde línea de comandos o preset: cmake -DENABLE_ASAN=ON -DENABLE_UBSAN=ON --preset debug cmake --build --preset debug
Rendimiento y uso en CI
Los sanitizers añaden overhead significativo: ASan ralentiza la ejecución entre 2x y 10x. Por eso no se usan en builds de producción, sino en tests automáticos en CI. La estrategia habitual es tener un preset de CI que active ASan+UBSan y ejecutar la suite de tests completa con él.
El artículo sobre CMake moderno tiene más detalles sobre cómo estructurar los presets para desarrollo, release y CI. Y si te interesa cómo Rust aborda la seguridad de memoria sin runtime checks, la guía de Rust 2024 edition lo explica desde el lado del compilador.
Imagen: Pexels / Godfrey Atima
