Sanitizers: Detección de errores en tiempo de ejecución

Los sanitizers son herramientas de instrumentación del compilador que inyectan código de chequeo adicional durante la fase de compilación para detectar errores lógicos y de memoria en tiempo de ejecución. A diferencia de los debuggers tradicionales, que requieren una ejecución manual paso a paso, los sanitizers analizan el flujo de ejecución de forma automática, detectando anomalías en el momento exacto en que ocurren.

Funcionan mediante la instrumentación de código: el compilador modifica las instrucciones originales para añadir comprobaciones antes de operaciones críticas, como un acceso a memoria o una operación aritmética. Por ejemplo, antes de cada carga o almacenamiento en memoria, el compilador inserta una comprobación que consulta una estructura de datos interna llamada shadow memory (memoria sombra) para verificar si la dirección es válida o está “envenenada” (poisoned).

Debes utilizarlos durante el desarrollo y en tus tuberías de CI/CD (Integración Continua) para asegurar la calidad del código. Sin embargo, nunca deben usarse en entornos de producción, ya que introducen un overhead significativo tanto en el tiempo de ejecución como en el uso de memoria, y pueden alterar el determinismo de los errores de sincronización. Si los aplicas incorrectamente o intentas mezclarlos de forma incompatible, lo que romperás será la estabilidad de tu test suite: intentar compilar un binario con AddressSanitizer (ASan) y ThreadSanitizer (TSan) simultáneamente resultará en errores de enlazado, ya que ambos intentan controlar el mismo modelo de memoria y las llamadas a malloc/free.

El conjunto principal incluye:
* AddressSanitizer (ASan): Detecta errores de memoria como out-of-bounds (en stack, heap o global), use-after-free (UAF), double-free y stack overflow.
* UndefinedBehaviorSanitizer (UBSan): Detecta comportamiento indefinido según el estándar, como signed integer overflow, desalineación de punteros (misaligned access) o desreferencia de punteros nulos.
* ThreadSanitizer (TSan): Identifica data races entre hilos. Es fundamental para encontrar errores de sincronización que no siempre provocan un crash pero corrompen el estado del programa.
* MemorySanitizer (MSan): (Exclusivo de Clang) Detecta lecturas de memoria que no han sido inicializadas.

#include <iostream>
#include <thread>
#include <vector>
#include <climits>

// Trigger UBSan: Signed integer overflow
// El estándar prohíbe que el desbordamiento de un entero con signo sea predecible.
void trigger_ubsan() {
    int max_val = std::numeric_limits<int>::max();
    std::cout << "Intentando desbordar: " << max_val << "\n";
    max_val += 1; // UB: Signed integer overflow
    std::cout << "Valor después del desbordamiento: " << max_val << "\n";
}

// Trigger ASan: Heap buffer overflow
// Accedemos a una posición de memoria fuera de los límites del bloque asignado.
void trigger_asan() {
    int* arr = new int[5]{1, 2, 3, 4, 5};
    std::cout << "Accediendo a índice 10 (fuera de bounds)...\n";
    // ASan detectará esto mediante la consulta de la shadow memory.
    std::cout << "Valor en índice 10: " << arr[10] << "\n"; 
    delete[] arr;
}

// Trigger TSan: Data race
// Dos hilos acceden a la misma posición de memoria simultáneamente y al menos uno es una escritura.
int shared_resource = 0;
void trigger_tsan() {
    std::thread t1([]() { shared_resource++; });
    std::thread t2([]() { shared_resource++; });
    t1.join();
    t2.join();
    std::cout << "Valor final tras race: " << shared_resource << "\n";
}

int main(int argc, char** argv) {
    if (argc < 2) {
        std::cerr << "Uso: " << argv[0] << " <1|2|3>\n";
        std::cerr << "1: UBSan, 2: ASan, 3: TSan\n";
        return 1;
    }

    int mode = std::stoi(argv[1]);

    if (mode == 1) {
        trigger_ubsan();
    } else if (mode == 2) {
        trigger_asan();
    } else if (mode == 3) {
        trigger_tsan();
    }

    return 0;
}

Para compilar este ejemplo con las distintas capacidades, necesitas generar binarios distintos, ya que los sanitizers de memoria y de hilos son mutuamente excluyentes:
* Para UBSan: g++ -std=c++20 -fsanitize=undefined -g example.cpp -o example_ubsan
* Para ASan: g++ -std=c++20 -fsanitize=address -fno-omit-frame-pointer -g example.cpp -o example_asan
* Para TSan: g++ -std=c++20 -fsanitize=thread -g example.cpp -o example_tsan -lpthread

Desglose del análisis

En trigger_ubsan, el compilador ha inyectado una comprobación antes de la operación max_val += 1. Al ejecutarse, el runtime de UBSan detecta que el resultado excedería el INT_MAX y emitirá un error informando exactamente en qué línea ocurrió la violación del estándar.

En trigger_asan, la llamada a new int[5] reserva memoria en el heap. ASan marca los bytes inmediatamente adyacentes a ese bloque como “venenosos” (redzones). Cuando intentamos acceder a arr[10], el código instrumentado detecta que esa dirección pertenece a una zona prohibida y detiene la ejecución inmediatamente, proporcionando un log detallado del estado de la pila (stack trace) de la asignación original.

En trigger_tsan, la variable shared_resource es modificada por dos hilos sin ningún mecanismo de sincronización (como std::mutex o std::atomic). TSan mantiene un registro de los accesos a la memoria y las relaciones “happens-before”. Al detectar que dos hilos acceden a la misma dirección y no hay una barrera de memoria que los separe, identifica una data race, permitiéndonos ver qué hilos colisionaron.

El error frecuente

Un error común es compilar con -fsanitize=address pero olvidar incluir -fno-omit-frame-pointer en sistemas basados en x86. Sin este flag, el compilador optimiza la gestión de la pila de manera que las trazas de error (stack traces) que proporcionan los sanitizers pierdan la información de los marcos de las funciones. Esto resulta en informes de error casi inútiles que te dicen que hubo un error de memoria, pero no te dicen la función ni la línea exacta que lo provocó, obligándote a usar un debugger manual para reconstruir el flujo.

8

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio