Comportamiento Indefinido: Los tres errores más peligrosos

El Comportamiento Indefinido (Undefined Behavior o UB) es un estado en el que el estándar C no especifica qué debe hacer la máquina abstracta. Cuando el código entra en este estado, el compilador queda liberado de todas las restricciones: el programa puede funcionar correctamente de forma casual, colapsar con un segmentation fault, o —lo más peligroso— comportarse de una manera completamente distinta a la lógica del programador. Esto sucede porque el compilador opera bajo un modelo de optimización basado en la lógica matemática pura; si una operación es matemáticamente imposible según las reglas del estándar (como un desbordamiento de enteros con signo), el compilador simplemente asume que ese escenario jamás ocurrirá.

Este fenómeno es el motor que permite las optimizaciones más agresivas, pero también el que convierte errores lógicos en vulnerabilidades de seguridad. Ocurre habitualmente cuando manejas aritmética de enteros con signo sin validar límites, accedes a arrays con índices calculados erróneamente, o declaras variables locales sin asignarles un valor inicial. El resultado es impredecible: desde que el compilador elimine una validación de seguridad crítica por considerarla “código muerto”, hasta la corrupción de la pila (stack) o la ejecución de instrucciones arbitrarias mediante el secuestro del puntero de retorno.

#include <stdio.h>
#include <limits.h>

/* Estructura para demostrar cómo el acceso fuera de límites
   puede corromper miembros adyacentes en memoria. */
typedef struct {
    int datos[5];
    int estado;
} Contexto;

void demostrar_overflow(int n) {
    /* El desbordamiento de enteros con signo es UB. 
       Como el estándar dicta que n + 1 no puede ser menor que n 
       en aritmética con signo, el compilador puede eliminar 
       este bloque 'if' por completo en optimizaciones (-O2, -O3). */
    if (n + 1 < n) {
        printf("Error: Overflow detectado (probablemente invisible con -O3)\n");
    } else {
        printf("Resultado seguro: %d\n", n + 1);
    }
}

int main(void) {
    // 1. Signed Overflow (Desbordamiento de enteros con signo)
    int max_val = INT_MAX;
    demostrar_overflow(max_val);

    // 2. Out-of-bounds (Acceso fuera de límites)
    Contexto ctx = { .estado = 0 };
    
    /* El bucle escribe en el índice 5. Dado que el array 'datos' 
       tiene tamaño 5 (índices 0-4), el índice 5 sobreescribe 
       directamente la variable 'estado' en la estructura. */
    for (int i = 0; i <= 5; i++) {
        ctx.datos[i] = i * 10;
    }
    printf("Estado de 'ctx.estado' tras corrupción: %d\n", ctx.estado);

    // 3. Variable no inicializada
    int variable_suerte;
    /* El valor de 'variable_suerte' es indeterminado. 
       No es necesariamente cero; es el residuo de lo que hubiera 
       en la pila en esa dirección de memoria. */
    if (variable_suerte > 0) {
        printf("La suerte es positiva\n");
    } else {
        printf("La suerte es negativa o cero\n");
    }

    return 0;
}

Desglose del ejemplo

En la función demostrar_overflow, el parámetro n recibe INT_MAX. Bajo una compilación normal, podrías esperar que el programa detecte el desbordamiento, pero el compilador moderno ve la expresión n + 1 < n y, aplicando las reglas de la máquina abstracta de C, determina que esa condición es siempre falsa. En consecuencia, el bloque de error es eliminado por el optimizador, y el programa imprimirá un valor incorrecto o simplemente ignorará la lógica de seguridad.

En el caso de la estructura Contexto, hemos creado un error de desbordamiento en un bucle for mediante un error de tipo off-by-one (i <= 5). Debido al layout de memoria de una estructura, los elementos de ctx.datos están contiguos a ctx.estado. Al escribir en ctx.datos[5], estamos accediendo a la dirección de memoria que pertenece a ctx.estado, corrompiendo su valor sin que el procesador levante una excepción de inmediato.

Finalmente, la variable variable_suerte en main carece de inicialización. En sistemas de bajo nivel, esto significa que el programa leerá “basura” de la pila. Dependiendo de los valores que funciones llamadas previamente hayan dejado en los registros o en la memoria, el flujo del if será totalmente errático y no reproducible en todas las máquinas.

El error frecuente

Uno de los errores más sutiles es intentar usar el desbordamiento como una forma de detección de errores:

/* ERROR: Lógica de detección de desbordamiento defectuosa */
if (x + 1 < x) {
    // Tratar el error
}

Si compilas con -O2 o -O3, el compilador asume que la aritmética de enteros con signo es matemáticamente perfecta. Para el optimizador, x + 1 siempre es mayor que x, por lo que la condición es siempre falsa. El compilador eliminará todo el bloque de error, dejando la vulnerabilidad activa pero invisible. Para detectar estos errores durante el desarrollo, es imprescindible utilizar AddressSanitizer (-fsanitize=address) para problemas de memoria, y UndefinedBehaviorSanitizer (-fsanitize=undefined) para desbordamientos de enteros.

76

Dejar un comentario

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

Scroll al inicio