· 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.
| Familia | Qué se rompe | Fallos |
|---|---|---|
| Espacio | Se usa un casillero fuera de lo reservado | Desbordamiento de búfer, lectura fuera de límites |
| Tiempo | Se usa un casillero que ya no es tuyo | Uso después de liberar, doble liberación, fuga de memoria |
| Valor | Se confía en algo que no existe | Puntero nulo |
| Concurrencia | Dos hilos tocan lo mismo a la vez | Carrera 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
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.
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.
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.
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 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.
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.
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
| Fallo | C y C++ | Java, C#, Go, Python | Rust seguro |
|---|---|---|---|
| Desbordamiento de búfer | Posible | Error en ejecución | Pánico en ejecución |
| Lectura fuera de límites | Posible | Error en ejecución | Pánico en ejecución |
| Uso después de liberar | Posible | Evitado por el recolector | No compila |
| Doble liberación | Posible | No aplica | No compila |
| Puntero nulo | Posible | Error en ejecución | No compila |
| Carrera de datos | Posible | Posible | No compila |
| Fuga de memoria | Posible | Posible | Difí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
- 2025 CWE Top 25 Most Dangerous Software WeaknessesMITRE, 2025
- The Heartbleed BugSynopsys, 2014
- Shared-State ConcurrencyThe Rust Programming Language
- Reference Cycles Can Leak MemoryThe Rust Programming Language
- Cloudflare outage on November 18, 2025Cloudflare, 2025