1. Por qué son los peores errores
C en C, el programador gestiona la memoria manualmente. El asignador (malloc/free) es rápido porque no verifica cada acceso: confía en que el programador respete los límites y la vida de los bloques. Cuando no es así, el comportamiento es indefinido: el programa puede funcionar, corromper datos silenciosamente o fallar en una máquina distinta.
Estas son las propiedades que hacen que los errores de memoria sean tan difíciles:
- No deterministas: el mismo programa puede funcionar en una ejecución y fallar en otra, según el contenido de la memoria liberada.
- Alejados de la causa: el fallo aparece lejos del error, en un código que no tiene la culpa.
- Silenciosos: no siempre hay un crash; a veces solo se corrompen datos.
- Dependientes de la plataforma: lo que funciona en tu máquina puede fallar en la del usuario.
Por eso las herramientas de detección (Valgrind, AddressSanitizer) son indispensables: convierten errores silenciosos en mensajes claros con la línea exacta del código.
2. Fuga de memoria (memory leak)
Una fuga ocurre cuando se pide memoria con malloc/calloc/realloc y nunca se libera, aunque ya no se use. El heap crece sin límite hasta agotar la RAM.
#include <stdlib.h>
void procesar(void)
{
char *buf = malloc(1024);
/* usar buf ... */
/* ¡falta free(buf)! */
}
int main(void)
{
for (int i = 0; i < 1000000; i++) procesar();
return 0;
}
En este ejemplo, cada llamada a procesar pierde 1 KiB. Tras un millón de llamadas, se han perdido ~1 GiB. El programa puede seguir "funcionando" mientras haya RAM, pero consume cada vez más.
Las fugas son especialmente graves en servidores y demonios que corren durante días o semanas.
3. Uso después de free (use-after-free)
Se accede a un bloque después de haberlo liberado. El bloque puede haberse reutilizado para otro propósito, por lo que se leen o escriben datos de otro objeto.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char *p = malloc(16);
strcpy(p, "hola");
free(p);
printf("%s\n", p); /* uso después de free */
p[0] = 'H'; /* escritura después de free */
return 0;
}
El programa puede imprimir "hola" (porque la memoria no se borra) o basura, o fallar. Escribir después de free puede corromper el heap si el bloque ya se reutilizó.
4. Doble free
Se libera el mismo bloque dos veces. El asignador mantiene los bloques libres en listas; liberar dos veces corrompe esas listas, lo que puede causar fallos posteriores o ser explotable para ataques.
#include <stdlib.h>
int main(void)
{
char *p = malloc(16);
free(p);
free(p); /* doble free */
return 0;
}
glibc suele detectar el doble free y abortar con "double free or corruption", pero no siempre: depende del estado del heap.
5. Desbordamiento de búfer (buffer overflow)
Se escribe más allá del final (o antes del principio) de un bloque. Corrompe los metadatos del heap o los datos adyacentes.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char *p = malloc(8);
strcpy(p, "esto es mucho más largo que 8 bytes"); /* desbordamiento */
printf("%s\n", p);
free(p);
return 0;
}
El desbordamiento de búfer es una de las vulnerabilidades más explotadas de la historia: permite sobrescribir la dirección de retorno o los metadatos del heap para ejecutar código arbitrario.
6. Memoria no inicializada
Se lee memoria que nunca se escribió. Con malloc, el contenido es basura; con variables locales no inicializadas, también. El programa usa valores arbitrarios, que pueden ser secretos de otras partes del programa (fuga de información).
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int x; /* no inicializada */
char *p = malloc(16); /* no inicializada */
printf("x = %d\n", x);
printf("p[0] = %d\n", p[0]);
return 0;
}
7. Punteros colgantes y salvajes
- Puntero colgante (dangling): apunta a memoria liberada. Es el resultado de un use-after-free si se sigue usando el puntero.
- Puntero salvaje (wild): no inicializado o con un valor arbitrario. Acceder a él es comportamiento indefinido.
- Puntero NULL: no es un error por sí mismo; acceder a él (desreferenciar) provoca SIGSEGV, que es una falla controlada y detectable.
La buena práctica es inicializar los punteros a NULL y ponerlos a NULL después de free.
8. Experimentos que reproducen cada error
Compila y ejecuta cada programa para ver el comportamiento (indefinido, pero útil para entender):
$ gcc -std=c17 -Wall -Wextra -O1 -o fuga fuga.c && ./fuga
(puede consumir mucha RAM; Ctrl+C para parar)
$ gcc -std=c17 -Wall -Wextra -O1 -o uaf uaf.c && ./uaf
hola
Segmentation fault
$ gcc -std=c17 -Wall -Wextra -O1 -o doublefree doublefree.c && ./doublefree
double free or corruption (out)
$ gcc -std=c17 -Wall -Wextra -O1 -o overflow overflow.c && ./overflow
esto es mucho más largo que 8 bytes
free(): invalid next size (normal)
$ gcc -std=c17 -Wall -Wextra -O1 -o uninit uninit.c && ./uninit
x = 32767
p[0] = 0
Los resultados varían entre ejecuciones y máquinas. Esa no determinación es precisamente el problema.
9. Detección con Valgrind
Valgrind es una herramienta que ejecuta el programa en una máquina virtual y detecta errores de memoria y fugas. Se usa así:
$ valgrind --leak-check=full --show-leak-kinds=all ./uaf
==1234== Invalid read of size 1
==1234== at 0x...: main (uaf.c:9)
==1234== Address 0x... is 0 bytes inside a block of size 16 free'd
==1234== at 0x...: free (vg_replace_malloc.c:...)
==1234== by 0x...: main (uaf.c:8)
==1234==
==1234== HEAP SUMMARY:
==1234== in use at exit: 0 bytes in 0 blocks
==1234== total heap usage: 1 allocs, 1 frees, 16 bytes allocated
==1234==
==1234== All heap blocks were freed -- no leaks are possible
Valgrind reporta el tipo de error, la línea exacta, la traza de llamadas y el estado del heap al salir. Es lento (10-50x más lento) pero muy completo.
--leak-check=full: detecta fugas con detalle.--show-leak-kinds=all: muestra todos los tipos de fuga.--track-origins=yes: rastrea el origen de la memoria no inicializada.
10. Detección con AddressSanitizer
AddressSanitizer (ASan) es un sanitizer del compilador que instrumenta el código para detectar errores de memoria con bajo overhead (~2x). Se activa al compilar:
$ gcc -std=c17 -g -fsanitize=address -o uaf-asan uaf.c
$ ./uaf-asan
=================================================================
==1234==ERROR: AddressSanitizer: heap-use-after-free on address 0x... at pc 0x...
READ of size 1 at 0x... thread T0
#0 0x... in main uaf.c:9
#1 0x... in __libc_start_main
0x... is located 0 bytes inside of 16-byte region [0x...,0x...)
freed by thread T0 here:
#0 0x... in free
#1 0x... in main uaf.c:8
previously allocated by thread T0 here:
#0 0x... in malloc
#1 0x... in main uaf.c:7
=================================================================
ASan muestra la traza de asignación, liberación y uso, con números de línea. Es más rápido que Valgrind y se integra en el ciclo de desarrollo.
-fsanitize=address: detección de errores de memoria.-fsanitize=leak: detección de fugas (LSan, incluido en ASan).-g: incluye información de depuración para las líneas.
11. Otras herramientas
| Herramienta | Qué detecta | Cómo se activa |
|---|---|---|
| MemorySanitizer (MSan) | Lecturas de memoria no inicializada. | -fsanitize=memory |
| UndefinedBehaviorSanitizer (UBSan) | Comportamientos indefinidos (desbordamientos de enteros, etc.). | -fsanitize=undefined |
| ThreadSanitizer (TSan) | Carreras de datos entre hilos. | -fsanitize=thread |
| GDB | Inspección interactiva, breakpoints, backtrace. | gdb ./prog |
Estos sanitizers no se pueden combinar entre sí (ASan + TSan no), pero se pueden usar por separado en distintas compilaciones.
12. Probar con un simulador
Este simulador presenta un escenario de código y te pide clasificar el error. Selecciona el tipo de error que crees que contiene el fragmento.
Simulador · clasificar errores de memoria
13. Mirar en Linux
$ valgrind --leak-check=full ./prog
$ gcc -fsanitize=address -g prog.c -o prog-asan && ./prog-asan
$ gcc -fsanitize=memory -g prog.c -o prog-msan && ./prog-msan
$ gcc -fsanitize=undefined -g prog.c -o prog-ubsan && ./prog-ubsan
$ gdb ./prog
- Valgrind: completo pero lento.
- ASan: rápido y detallado; el más usado en desarrollo.
- MSan: para memoria no inicializada.
- UBSan: para comportamientos indefinidos.
- GDB: para inspección interactiva cuando hay un crash.
14. Resumen
- Los errores de memoria son indefinidos, no deterministas y silenciosos.
- Los cuatro clásicos: fuga, uso después de free, doble free y desbordamiento de búfer.
- Otros: memoria no inicializada, punteros colgantes y salvajes.
- Valgrind detecta todos con detalle, pero es lento.
- AddressSanitizer es rápido (~2x) y da trazas precisas; es el más usado en desarrollo.
- MSan, UBSan y TSan cubren casos específicos.
- La buena práctica: inicializar punteros a NULL y ponerlos a NULL tras free.
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué es una fuga de memoria?
Pedir memoria y no liberarla cuando ya no se usa, haciendo que el heap crezca sin límite.
2. ¿Qué es un use-after-free?
Acceder a un bloque después de haberlo liberado. El bloque puede haberse reutilizado, causando corrupción o fugas de información.
3. ¿Por qué el doble free es peligroso?
Corrompe las listas de bloques libres del asignador, lo que puede causar fallos posteriores o ser explotable para ataques.
4. ¿Qué es un desbordamiento de búfer?
Escribir más allá del final (o antes del principio) de un bloque, corrompiendo metadatos o datos adyacentes.
5. ¿Qué herramienta detecta memoria no inicializada?
MemorySanitizer (MSan) con -fsanitize=memory, y Valgrind con --track-origins=yes.
6. ¿Qué ventaja tiene AddressSanitizer sobre Valgrind?
Es mucho más rápido (~2x vs 10-50x) y se integra en el ciclo de compilación, con trazas precisas.
7. ¿Qué es un puntero colgante?
Un puntero que apunta a memoria ya liberada. Usarlo es un use-after-free.
8. ¿Por qué hay que poner un puntero a NULL tras free?
Para evitar que se use accidentalmente (use-after-free) y para que free(NULL) sea seguro si se libera de nuevo.
Para practicar
- Escribe un programa con una fuga deliberada y ejecútalo con Valgrind (--leak-check=full). ¿Qué reporta?
- Compila un programa con use-after-free con -fsanitize=address y observa la traza.
- Escribe un programa con un desbordamiento de búfer en el heap y ejecútalo con ASan.
- Crea un programa con memoria no inicializada y ejecútalo con MSan.
- Compara el tiempo de ejecución de un programa con y sin ASan para 1.000.000 de asignaciones.
- Usa GDB para inspeccionar un programa que crashea por doble free. ¿Qué muestra bt?
- Corrige los cuatro programas de error (fuga, uaf, doublefree, overflow) y verifica con Valgrind que ya no hay errores.