· 17 min de lectura · Por Grover Cordova

Los 7 fallos de memoria más comunes, ilustrados

Desbordamiento de búfer, uso después de liberar, doble liberación, puntero nulo y más: qué son, cómo se ven en C y cómo los trata Rust.

Imagina la memoria como una fila larga de casilleros numerados. Tu programa pide unos cuantos, guarda cosas en ellos y los devuelve cuando termina. Casi todo lo que puede salir mal se resume en tres preguntas: dónde escribes, cuándo usas lo que tienes y quién más lo está tocando.

FamiliaQué se rompeFallos
EspacioSe usa un casillero fuera de lo reservadoDesbordamiento de búfer, lectura fuera de límites
TiempoSe usa un casillero que ya no es tuyoUso después de liberar, doble liberación, fuga de memoria
ValorSe confía en algo que no existePuntero nulo
ConcurrenciaDos hilos tocan lo mismo a la vezCarrera de datos

No son rarezas de laboratorio. En el CWE Top 25 de 2025, la escritura fuera de límites ocupa el puesto 5, el uso después de liberar el 7 y la lectura fuera de límites el 8. La cifra del 70 % de Microsoft habla de esta misma familia.

1. Desbordamiento de búfer (buffer overflow)

Analogía: guardas tanto en tu casillero que lo que sobra invade el del vecino.

Ocurre cuando se escribe más de lo que cabe en el espacio reservado. C no comprueba el tamaño por ti:

char nombre[8];
int es_admin = 0;
strcpy(nombre, entrada); // copia sin mirar cuánto cabe
Antes: dos variables, una junto a otraes_admin = 0nombre: 8 espaciosDespués de copiar «María Gracia», 12 letrasMaría·Gracialo reservadoes_admin ya no es 0
Lo que sobra no desaparece: cae sobre lo que estaba al lado. Ilustración simplificada; la disposición real depende del compilador.

Si quien envía entrada elige bien los bytes, puede cambiar datos del programa e incluso desviar su ejecución. Por eso este fallo acompaña a la seguridad informática desde hace décadas.

En Rust, cada acceso por índice comprueba los límites. Si te pasas, el programa se detiene de forma controlada, sin pisar nada:

fn main() {
    let notas = vec![12, 15, 18];
    let i = notas.len();
    println!("{}", notas[i]); // pánico: index out of bounds
}

Detenerse es mejor que corromper datos, pero en un servidor sigue siendo una caída: la interrupción de Cloudflare de noviembre de 2025 fue justamente un pánico en código Rust. Por eso conviene preguntar antes de acceder:

fn main() {
    let notas = vec![12, 15, 18];
    match notas.get(3) {
        Some(nota) => println!("{nota}"),
        None => println!("Esa posición no existe"),
    }
}

2. Lectura fuera de límites (out-of-bounds read)

Analogía: pides ver tu casillero, y te muestran también los tres siguientes.

Aquí no se daña nada, y justamente por eso es peligroso: el programa sigue funcionando mientras entrega lo que no debería.

Petición honesta: «devuélveme 4 letras»holacontraseña de otra personarecibe: holaPetición maliciosa: «devuélveme 12»holacontraseña de otra personarecibe: hola y la contraseña
El programa confía en la cifra que le dan, y entrega lo que haya al lado. Nada falla ni deja rastro evidente.

El caso más famoso es Heartbleed (CVE-2014-0160), en OpenSSL. Un atacante podía indicar una longitud mayor que la real y recibir el contenido de la memoria del servidor, incluidas claves y contraseñas.

En Rust se aplica la misma comprobación de límites del caso anterior: un rebanado o un índice fuera de rango no devuelve memoria ajena.

3. Uso después de liberar (use-after-free)

Analogía: devuelves la llave del casillero, pero conservas una copia. Otra persona ya guardó allí sus cosas.

Es uno de los fallos más explotados, porque quien ataca puede intentar controlar qué ocupa esa memoria.

1. Reservasstu datos es válido2. Liberasslibres sigue apuntando3. Otro la reutilizasdato ajenos lee datos ajenos
El puntero parece válido, pero la memoria ya pertenece a otra parte del programa.

Este código en C compila, aunque es incorrecto:

char *s = malloc(16);
free(s);
s[0] = 'x'; // uso después de liberar

En Rust el compilador comprueba que ninguna referencia viva más que el dato. Es el trabajo de los tiempos de vida:

fn main() {
    let r;
    {
        let s = String::from("hola");
        r = &s;
    } // `s` se libera aquí
    println!("{r}"); // error[E0597]: `s` does not live long enough
}

El error aparece antes de ejecutar el programa.

4. Doble liberación (double free)

Analogía: dos personas devuelven la misma llave. El registro de casilleros queda con una entrada falsa, y más tarde el mismo casillero se asigna a dos personas.

1. Dos punterosabbloqueun solo bloque2. free(a)abdevueltob no se enteró3. free(b)abdevuelto dos vecesel gestor se corrompe
Dos variables creen ser responsables del mismo bloque, y ambas lo devuelven.
char *a = malloc(16);
char *b = a;
free(a);
free(b); // el mismo bloque, por segunda vez

En Rust cada valor tiene un único dueño, así que solo uno puede liberarlo. Intentarlo dos veces no compila:

fn main() {
    let s = String::from("hola");
    drop(s);
    drop(s); // error[E0382]: use of moved value: `s`
}

5. Puntero nulo (null pointer dereference)

Analogía: te dan una llave con un número de casillero que no existe.

Su creador, Tony Hoare, lo llamó su "error de mil millones de dólares". El problema de fondo es que cualquier referencia podría no apuntar a nada, y nada en el tipo lo avisa. En C suele terminar en una caída. En Java aparece como NullPointerException.

En C: la ausencia no se veEn Rust: la ausencia es un tipoclienteNULL: aquí no hay nadaleerlo detiene el programaOptionSomeNoneel compilador exige los dos casos
El problema no es que algo falte, sino que nada avise de que puede faltar.

En Rust las referencias seguras nunca son nulas. Lo que puede faltar se declara con Option, y el compilador impide usarlo sin revisar:

fn buscar(id: u32) -> Option<String> {
    if id == 1 { Some(String::from("Ana")) } else { None }
}

fn main() {
    match buscar(7) {
        Some(nombre) => println!("{}", nombre.len()),
        None => println!("Cliente no encontrado"),
    }
}

Lo explicamos paso a paso en la guía de Option y Result.

6. Carrera de datos (data race)

Analogía: dos cajeros consultan el mismo saldo a la vez, y cada uno anota su propio resultado. Una de las dos operaciones desaparece.

Paso 1Paso 2Paso 3Paso 4Hilo Alee 100escribe 110Hilo Blee 100escribe 80saldo10010011080Debió quedar 90: el +10 se perdió
Ninguno de los dos hilos hizo algo incorrecto por sí solo. El error está en el momento.

Es el fallo más difícil de reproducir: puede aparecer una vez cada millón de ejecuciones, y desaparecer justo cuando lo depuras.

En Rust este código no compila, porque el hilo podría tocar saldo mientras el programa principal también lo usa:

use std::thread;

fn main() {
    let mut saldo = 100;
    let hilo = thread::spawn(|| saldo += 10); // error[E0373]
    saldo -= 20;
    hilo.join().unwrap();
}

La versión correcta hace explícito cómo se comparte el dato. Sin tomar el candado, ni siquiera se puede acceder al valor:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let saldo = Arc::new(Mutex::new(100));
    let copia = Arc::clone(&saldo);

    let hilo = thread::spawn(move || *copia.lock().unwrap() += 10);
    *saldo.lock().unwrap() -= 20;

    hilo.join().unwrap();
    println!("{}", saldo.lock().unwrap()); // siempre 90
}

Rust evita las carreras de datos, pero no evita los interbloqueos ni otros errores de lógica en la concurrencia.

7. Fuga de memoria (memory leak)

Analogía: pides casilleros y nunca los devuelves. Nadie resulta afectado hoy, hasta que no quedan casilleros.

límite de memoriatiempocada ciclo olvida devolver un poco
Nada se corrompe. El servicio solo crece, hasta que el sistema lo detiene. Ilustración, no medición.

No es una vulnerabilidad de memoria en sentido estricto, pero tumba servicios. Y aquí Rust no promete tanto: el Libro lo dice con claridad, las fugas son seguras en memoria y evitarlas por completo no es una de sus garantías. Son difíciles de provocar por accidente, porque la memoria se libera sola al salir de alcance, pero un ciclo de Rc lo consigue:

use std::cell::RefCell;
use std::rc::Rc;

struct Nodo {
    siguiente: RefCell<Option<Rc<Nodo>>>,
}

fn main() {
    let a = Rc::new(Nodo { siguiente: RefCell::new(None) });
    let b = Rc::new(Nodo { siguiente: RefCell::new(Some(Rc::clone(&a))) });
    *a.siguiente.borrow_mut() = Some(Rc::clone(&b));
    // `a` y `b` se sostienen entre sí: nunca se liberan
}

Los lenguajes con recolector de basura también pueden fugar memoria, si una lista o una caché conserva referencias que ya nadie necesita.

Comparación por lenguaje

FalloC y C++Java, C#, Go, PythonRust seguro
Desbordamiento de búferPosibleError en ejecuciónPánico en ejecución
Lectura fuera de límitesPosibleError en ejecuciónPánico en ejecución
Uso después de liberarPosibleEvitado por el recolectorNo compila
Doble liberaciónPosibleNo aplicaNo compila
Puntero nuloPosibleError en ejecuciónNo compila
Carrera de datosPosiblePosibleNo compila
Fuga de memoriaPosiblePosibleDifícil, pero posible

"Rust seguro" importa: el código marcado como unsafe queda bajo tu responsabilidad, y los errores de lógica o de autorización siguen siendo posibles en cualquier lenguaje.

Java, C#, Go y Python resuelven buena parte de esta tabla con un recolector de basura, a cambio de trabajo en tiempo de ejecución. Ese costo tiene sus propias consecuencias, y las explicamos en Recolector de basura vs. Rust.

Fuentes

  1. 2025 CWE Top 25 Most Dangerous Software WeaknessesMITRE, 2025
  2. The Heartbleed BugSynopsys, 2014
  3. Shared-State ConcurrencyThe Rust Programming Language
  4. Reference Cycles Can Leak MemoryThe Rust Programming Language
  5. Cloudflare outage on November 18, 2025Cloudflare, 2025

Construye mejor software.

Seguridad, eficiencia y oficio, con fuentes y en español.