Regla 0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes
Memoria, punteros y tipos (0x30XX)
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes¶
Enunciado normativo¶
Por cada asignación dinámica DEBE existir exactamente una liberación con
free, y DEBE asignarseNULLal puntero inmediatamente después de liberarlo. NO DEBE liberarse memoria que no provino del asignador.
¿Por qué existe esta regla?¶
El problema¶
free(ptr) devuelve el bloque al heap, pero no modifica la variable ptr, que
queda apuntando a memoria cuya vida terminó: un dangling pointer. Toda lectura
o escritura posterior es comportamiento indefinido (C11 §6.5.3.2), porque el
bloque pudo reutilizarse. Anular el puntero convierte el uso posterior en un
fallo determinista y fácil de rastrear: free(NULL) es un no-op válido.
Consecuencias de violarla¶
| Tipo de consecuencia | Efecto concreto |
|---|---|
| Comportamiento indefinido | Use-after-free sobre el bloque liberado. |
| Doble liberación | El segundo free corrompe las estructuras del asignador y aborta. |
| Fuga de memoria | No liberar deja bloques inalcanzables hasta el fin del proceso. |
Fundamento en el estándar y en la cátedra¶
C11 §7.22.3.3 establece que sólo puede liberarse un puntero devuelto por las
funciones de asignación (o NULL). La cátedra adopta el par “liberar y anular”
como una unidad: no se piensa en una sin la otra. Es la contraparte simétrica de
0x3001h: Siempre verificá la asignación exitosa de memoria dinámica.
Alcance y excepciones¶
Aplica a todo recurso obtenido con malloc, calloc, realloc o strdup.
No se debe llamar a free sobre variables estáticas ni automáticas, ni
sobre punteros interiores (&arreglo[3]).
Ejemplos exhaustivos¶
❌ Contraejemplo 1 — Uso después de liberar¶
char *mensaje = malloc(64);
copiar(mensaje, "hola");
free(mensaje);
printf("%s\n", mensaje);Por qué falla: el printf lee un bloque que ya no pertenece al programa; si se
reutilizó, la salida es basura impredecible. Anular mensaje habría hecho que el
acceso falle de forma visible.
❌ Contraejemplo 2 — Liberar dirección de pila o interior de un bloque¶
int datos[10];
free(datos);Por qué falla: datos es un arreglo automático en la pila, no un bloque del
heap. Pasar su dirección a free es comportamiento indefinido y, en glibc,
aborta con free(): invalid pointer. El mismo error ocurre con
free(&datos[3]), porque no es la dirección que devolvió malloc.
✅ Ejemplo conforme 1 — Liberar y anular¶
char *mensaje = malloc(64);
copiar(mensaje, "hola");
free(mensaje);
mensaje = NULL;La anulación cuesta una línea y elimina toda una clase de errores posteriores: documenta que el recurso ya no está disponible.
✅ Ejemplo conforme 2 — Constructor y destructor simétricos del TAD¶
lista_t *lista_crear(void)
{
lista_t *lista = malloc(sizeof(*lista));
if (lista == NULL)
{
return NULL;
}
lista->cabeza = NULL;
lista->cantidad = 0;
return lista;
}
void lista_destruir(lista_t *lista)
{
if (lista == NULL)
{
return;
}
liberar_nodos(lista->cabeza);
free(lista);
}El módulo encapsula el ciclo de vida completo: quien crea con lista_crear
destruye con lista_destruir, y el cliente anula su variable. Esta simetría es
la que exige 0x4004h: Mantené la simetría de recursos al abrir y cerrar archivos en el mismo nivel de abstracción para archivos.
⚠️ Casos límite¶
free(NULL): está definido como no-op; no hace falta chequear antes.Alias: si dos punteros apuntan al mismo bloque, liberarlo una vez y anular sólo uno deja al otro colgando; anulá todos los alias.
realloc: el bloque original no debe liberarse por separado;reallocya lo gestiona (ver 0x3015h: Reallocación segura: no sobreescribir el puntero original directamente).
Cómo detectarla¶
| Herramienta | Comando | Señal |
|---|---|---|
gaff | gaff check archivo.c | free(ptr) sin ptr = NULL inmediato; reserva sin free asociado. |
gcc / clang | gcc -Wall -Wextra -std=c11 -fanalyzer ... | use after 'free' en caminos simples. |
valgrind | valgrind --leak-check=full ./programa | definitely lost, invalid free, use of uninitialised value. |
Checklist de autocontrol¶
¿Cada
malloc/calloc/realloctiene sufree?¿Anulé el puntero inmediatamente después de liberarlo?
¿Todos los alias quedaron anulados o fuera de alcance?
¿Me aseguré de no liberar memoria estática, de pila ni punteros interiores?
Reglas relacionadas¶
0x3001h: Siempre verificá la asignación exitosa de memoria dinámica — la verificación de la reserva es la contraparte simétrica.
0x300Fh: Liberá la memoria en el orden inverso a su asignación — el orden inverso de liberación evita fugas anidadas.
0x3014h: Prohibición de doble liberación de memoria (double free) sobre el mismo puntero — anular el puntero es la defensa natural contra el doble
free.0x301Eh: Asigná NULL al puntero tras liberar un recurso opaco en el ámbito del cliente — caso particular del TAD opaco, donde el destructor no puede anular.
Antipatrón: Puntero colgante sin asignar NULL tras free()¶
Síntoma en el código del estudiante¶
La variable se libera, pero conserva la dirección anterior. Después del free
siguen apareciendo usos del mismo identificador:
free(ptr);
/* ... más instrucciones ... */
ptr->dato = 5; /* use-after-free */
free(ptr); /* double free */El síntoma clásico es no ver nunca un ptr = NULL; acompañando a la
liberación.
Diagnóstico¶
Mecanismo del defecto¶
free(ptr) devuelve el bloque al asignador, pero no modifica la variable:
el parámetro se recibe por valor, así que la copia local de free se descarta
y ptr sigue conteniendo la misma dirección. Esa dirección ahora apunta a
memoria que el asignador considera libre.
Use-after-free: si el bloque se reutiliza para otra reserva, escribir o leer mediante
ptrcorrompe datos ajenos sin aviso.Double free: un segundo
free(ptr)sobre el mismo bloque viola el invariante interno del asignador (listas de bloques libres) y suele terminar enSIGABRTdesdeglibc.
Consecuencia observable¶
Corrupción silenciosa de datos cuando el bloque fue reciclado.
malloc(): double free detectedofree(): double free detected in tcache 2cuando se libera dos veces.Comportamiento indefinido formal: usar un puntero a memoria liberada ya no es válido (§6.2.4p2: el valor del puntero queda indeterminado tras
free).
Fundamento en el estándar C11¶
C11 §7.22.3.3p2: el valor de un puntero que refería a un objeto liberado pasa a ser indeterminado; desreferenciarlo es comportamiento indefinido.
C11 §7.22.3.3p1:
free(NULL)no hace nada, por lo que asignarNULLes seguro y barato.La cátedra exige
ptr = NULLcomo convención defensiva: convierte un futurouse-after-freesilencioso en una desreferencia nula explícita y detectable.
Corrección idiomática¶
❌ Código con el antipatrón¶
void liberar(nodo_t *n)
{
free(n);
}✅ Código refactorizado¶
void liberar(nodo_t **n)
{
if (*n != NULL) {
free(*n);
*n = NULL;
}
}Anular el puntero del llamador requiere pasar su dirección; si se pasa por valor, la anulación queda en una copia local y no sirve.
❌ Contraejemplo adicional — doble liberación indirecta¶
free(a);
free(b); /* b era un alias de a: double free */✅ Refactorización con anulación simétrica¶
free(a);
if (b == a) {
b = NULL;
}
a = NULL;⚠️ Casos límite¶
Asignar
NULLno arregla un puntero que ya fue usado después delfree: previene el siguiente error, no repara el anterior.Los alias (copias del mismo puntero) conservan el valor viejo; anular una variable no anula las demás. Por eso la regla 0x3005h desaconseja la indirección y la duplicación innecesaria de referencias.
En una estructura, anular solo el campo liberado, no el nodo entero.
Errores típicos al compilar o ejecutar¶
$ ./programa
free(): double free detected in tcache 2
Aborted (core dumped)
# Con AddressSanitizer:
ERROR: AddressSanitizer: heap-use-after-free on address 0x...
READ of size 4 at 0x... thread T0Checklist de verificación¶
¿Todo
free(p)está seguido dep = NULL;?¿Los alias del puntero quedaron anulados o dejaron de usarse?
¿La función que libera anula el puntero del llamador (paso por
**)?¿Evité liberar dos veces el mismo bloque por caminos alternativos?
Reglas relacionadas¶
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — liberar y anular el puntero.
0x3014h: Prohibición de doble liberación de memoria (double free) sobre el mismo puntero — prohibición explícita de
double free.0x3005h: Minimizá el uso de múltiples niveles de indirección (punteros a punteros) — menos alias, menos punteros colgantes.
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — liberar lo que nunca vino del heap.
Antipatrón: Retorno de puntero a variable local (Dangling Stack Pointer)¶
Síntoma en el código del estudiante¶
Una función declara una variable (o un arreglo) local y retorna su dirección:
int *elegir(void)
{
int local = 42;
return &local;
}El síntoma es el & sobre un identificador cuyo alcance termina al cerrar la
función. El error aparece incluso en funciones que “parecen” andar.
Diagnóstico¶
Mecanismo del defecto¶
Las variables automáticas viven en el marco de pila (stack frame) de la función. Ese marco se crea al entrar y se destruye al retornar: el puntero de pila se restaura y el espacio queda disponible para el siguiente llamado.
El return &local entrega una dirección que ya no pertenece a ningún objeto
vivo. Al desreferenciarla desde el llamador, cualquier función intermedia puede
haber reutilizado esos bytes. El valor que se lee es basura; el valor que se
escribe pisa el marco de otra función.
Consecuencia observable¶
“Funciona” a veces: el dato todavía está en la pila y se lee bien por casualidad, hasta que una llamada lo sobrescribe.
Caídas intermitentes o corrupción de variables del llamador.
Con optimizaciones (
-O2) el compilador puede advertir con-Wreturn-local-addry el comportamiento cambia respecto de-O0.
Fundamento en el estándar C11¶
C11 §6.2.4p2: la vida de un objeto automático termina al salir del bloque que lo declara; el puntero deja de ser válido.
C11 §6.2.4p2 y §6.5.3.2: desreferenciar un puntero a un objeto cuya vida terminó es comportamiento indefinido.
La cátedra lo prohíbe de forma explícita en 0x200Bh: Prohibición de retornar la dirección de una variable local de stack; este antipatrón documenta el defecto asociado a la gestión de memoria.
Corrección idiomática¶
❌ Código con el antipatrón¶
char *nombre(void)
{
char buffer[32] = "apunte";
return buffer;
}✅ Código refactorizado — reserva en el heap¶
char *nombre(void)
{
char *buffer = malloc(32 * sizeof(*buffer));
if (buffer == NULL) {
return NULL;
}
strcpy(buffer, "apunte");
return buffer;
}El llamador se vuelve dueño del bloque y debe liberarlo (regla 0x3006h).
✅ Alternativa — buffer provisto por el llamador¶
void nombre(char *destino, size_t capacidad)
{
snprintf(destino, capacidad, "%s", "apunte");
}Esta variante evita la reserva y deja la propiedad en el llamador.
⚠️ Casos límite¶
Retornar punteros a datos
statices válido (vida de programa), pero rompe la reentrancia y la cátedra desaconseja el estado global (regla 0x2004h).Retornar el valor de un arreglo pasado como parámetro es válido: no es local.
Un
structretornado por valor se copia y es seguro; el problema es retornar su dirección.
Errores típicos al compilar o ejecutar¶
$ gcc -Wall -Wextra -std=c11 programa.c
programa.c: In function 'nombre':
programa.c:4:12: warning: function returns address of local variable [-Wreturn-local-addr]
4 | return buffer;
$ ./programa
Segmentation fault (core dumped)Checklist de verificación¶
¿Algún
returndevuelve&variablelocal o el nombre de un arreglo local?¿La memoria que retorno vive más que la función (heap o
static)?¿Si reservé en el heap documenté quién libera (regla 0x3006h)?
¿El compilador con
-Wall -Wextrano emite-Wreturn-local-addr?
Reglas relacionadas¶
0x200Bh: Prohibición de retornar la dirección de una variable local de stack — prohibición directa de retornar direcciones de pila.
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — anular punteros y gestionar la liberación.
0x3006h: Documentá la propiedad de los recursos al utilizar punteros — documentar la propiedad del recurso retornado.
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — misma causa vía parámetro de salida.
Antipatrón: Invocación a free() sobre memoria estática o variables automáticas de pila¶
Síntoma en el código del estudiante¶
Se llama a free con la dirección de una variable común o de un arreglo
estático:
int x = 42;
free(&x);o bien:
static char nombre[32] = "apunte";
free(nombre);El puntero no proviene de malloc, calloc ni realloc.
Diagnóstico¶
Mecanismo del defecto¶
free administra exclusivamente bloques entregados por el heap a través de
los asignadores dinámicos. La variable x vive en la pila y nombre en el
segmento de datos; ninguna está registrada en las estructuras internas del
asignador.
Al llamar free(&x), glibc busca los metadatos del bloque antes de la
dirección indicada. Como no hay cabecera válida, detecta la inconsistencia y
aborta con SIGABRT. Algunos asignadores, o versiones viejas, corrompen sus
listas internas y dejan el heap en un estado inconsistente: a partir de ahí,
cualquier reserva o liberación posterior falla.
Consecuencia observable¶
free(): invalid pointery aborto inmediato (el caso amable).Corrupción del heap y fallos posteriores difíciles de asociar a esta línea.
Comportamiento indefinido por el estándar:
freeexige que el argumento provenga de una asignación dinámica o sea nulo.
Fundamento en el estándar C11¶
C11 §7.22.3.3p2: el argumento de
freedebe provenir demalloc,callocorealloc(o ser nulo, único caso definido); cualquier otro valor es UB.C11 §6.2.4: la vida de un objeto automático termina al salir del bloque; no tiene sentido “liberarlo”.
La regla 0x3002h establece la simetría: solo se libera lo que se reservó.
Corrección idiomática¶
❌ Código con el antipatrón¶
void procesar(void)
{
int x = 42;
free(&x);
}✅ Código refactorizado — reservar para poder liberar¶
void procesar(void)
{
int *p = malloc(sizeof(*p));
if (p == NULL) {
return;
}
*p = 42;
free(p);
p = NULL;
}Si el dato puede vivir en la pila, simplemente no se libera: sale de alcance solo.
✅ Estructura mixta correcta¶
struct config {
char *nombre;
int valor;
};
void destruir(struct config *c)
{
free(c->nombre); /* solo el campo dinámico */
c->nombre = NULL;
/* c y c->valor son locales o parte de otro objeto: no se liberan */
}⚠️ Casos límite¶
free(NULL)es válido y no hace nada (regla 0x3008h): no confundir con liberar una variable local.Un arreglo dentro de una
structreservada en el heap no se libera por separado: se libera lastructcompleta.
Errores típicos al compilar o ejecutar¶
$ ./programa
free(): invalid pointer
Aborted (core dumped)
# Con AddressSanitizer:
ERROR: AddressSanitizer: attempting free on address which was not
malloc()-ed: 0x... in thread T0Checklist de verificación¶
¿Todo
free(p)recibe un puntero que vino demalloc,callocorealloc?¿No libero direcciones de variables locales (
&x) ni de arreglos estáticos?¿En estructuras libero solo los campos dinámicos?
¿La propiedad del recurso está documentada (regla 0x3006h)?
Reglas relacionadas¶
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — simetría reserva/liberación.
0x3006h: Documentá la propiedad de los recursos al utilizar punteros — documentar la propiedad del recurso.
0x3001h: Siempre verificá la asignación exitosa de memoria dinámica — verificar el retorno antes de usar el bloque.
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — el error siguiente tras liberar.
Antipatrón: Asignación de punteros a arreglos locales en parámetros de salida¶
Síntoma en el código del estudiante¶
La función “devuelve” un arreglo escribiéndolo en un parámetro de salida, pero el arreglo es local:
void obtener(int **res)
{
int arr[5] = {0};
*res = arr;
}El llamador recibe una dirección que apunta al marco de pila ya destruido.
Diagnóstico¶
Mecanismo del defecto¶
arr es una variable automática: vive en el marco de pila de obtener. La
asignación *res = arr guarda en el llamador la dirección del primer elemento,
pero esa dirección solo es válida mientras obtener esté activa. Al retornar,
el puntero de pila se restaura y el marco queda reutilizable.
Igual que en 0x3002h, el puntero resultante es colgante: apunta a
memoria que ya no pertenece a ningún objeto vivo. Cualquier función que se llame
después puede sobrescribir esos bytes, y la lectura desde el llamador devuelve
basura o pisa datos ajenos.
Consecuencia observable¶
El arreglo “parece” tener los datos correctos justo después de la llamada y luego se corrompe solo.
Fallos intermitentes que dependen del orden de llamadas.
El compilador puede advertir con
-Wreturn-local-addrsolo en el caso dereturndirecto; aquí la fuga de dirección a través de**es más difícil de detectar estáticamente.
Fundamento en el estándar C11¶
C11 §6.2.4p2: la vida de
arrtermina al salir del bloque; el puntero deja de referir un objeto.C11 §6.5.3.2: acceder al objeto a través de un puntero cuya vida terminó es comportamiento indefinido.
C11 §6.7.6.3p7: el parámetro
int **resno extiende la vida de lo apuntado; solo copia una dirección.La regla 0x3002h exige que la memoria sobreviva a quien la produce y se libere de forma simétrica.
Corrección idiomática¶
❌ Código con el antipatrón¶
void obtener(int **res)
{
int arr[5] = {0};
*res = arr;
}✅ Código refactorizado — el llamador provee el buffer¶
void obtener(int *destino, size_t n)
{
for (size_t i = 0; i < n; i++) {
destino[i] = 0;
}
}El llamador declara el arreglo y sigue siendo su dueño; no hay puntero colgante.
✅ Alternativa — reserva dinámica con propiedad explícita¶
int *obtener(size_t n)
{
int *arr = calloc(n, sizeof(*arr));
if (arr == NULL) {
return NULL;
}
return arr; /* el llamador debe free()ar */
}La memoria del heap vive más allá de la función; la propiedad se documenta (regla 0x3006h).
⚠️ Casos límite¶
Copiar el contenido con
memcpyhacia un buffer del llamador es válido si el buffer tiene capacidad suficiente.Un arreglo
staticlocal sí sobrevive, pero rompe la reentrancia y la cátedra desaconseja el estado global (regla 0x2004h).El mismo error se comete con arreglos de estructuras y con buffers de caracteres.
Errores típicos al compilar o ejecutar¶
$ gcc -Wall -Wextra -std=c11 programa.c
# puede no advertir nada por el paso por **
$ ./programa
# datos correctos al principio, basura tras llamar a otra función
0 0 0 0 0
-1073743 32766 0 0 0Checklist de verificación¶
¿La memoria que entrego por un parámetro de salida vive más que la función?
¿Si es un arreglo, es del llamador o del heap?
¿Documenté quién libera el recurso (regla 0x3006h)?
¿Evité
staticlocal como parche?
Reglas relacionadas¶
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — gestión y anulación de memoria.
0x200Bh: Prohibición de retornar la dirección de una variable local de stack — prohibición de retornar direcciones de pila.
0x3006h: Documentá la propiedad de los recursos al utilizar punteros — propiedad del recurso.
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — misma raíz por
return.
Antipatrón: Modificación directa del puntero base asignado por malloc()¶
Síntoma en el código del estudiante¶
El puntero devuelto por malloc se avanza con ++ y luego se pasa a free:
char *p = malloc(100);
while (*p) {
p++;
}
free(p);p ya no apunta al inicio del bloque cuando llega al free.
Diagnóstico¶
Mecanismo del defecto¶
malloc entrega la dirección de inicio del bloque, que es la única que el
asignador reconoce. La aritmética de punteros modifica la variable p: cada
p++ la corre un elemento. Al llegar al free, el valor es
inicio + desplazamiento.
free exige exactamente el puntero original. Al recibir una dirección
desplazada, no encuentra la cabecera del bloque donde la busca, interpreta
basura como metadatos y corrompe el heap. El resultado típico es
free(): invalid pointer o, peor, corrupción silenciosa detectada mucho
después.
Además, la dirección del inicio queda irrecuperable: aunque se quisiera liberar bien, ya no se sabe dónde empezaba el bloque. Es una pérdida de la referencia base.
Consecuencia observable¶
free(): invalid pointery aborto.Corrupción de las estructuras internas del asignador.
Fugas si se elige no liberar para evitar el aborto.
Comportamiento indefinido:
freeexige el puntero devuelto por el asignador (§7.22.3.3p2).
Fundamento en el estándar C11¶
C11 §7.22.3.3p2: el argumento de
freedebe ser un puntero devuelto pormalloc,callocorealloc, o el puntero nulo. Un puntero desplazado no cumple la condición.C11 §6.5.6: la aritmética de punteros produce un valor distinto del original; modifica la variable, por lo que se pierde la referencia base.
La regla 0x3002h exige conservar la referencia liberable; la cátedra desaconseja además el movimiento innecesario de punteros.
Corrección idiomática¶
❌ Código con el antipatrón¶
char *p = malloc(100);
while (*p != '\0') {
p++; /* mueve la base */
}
free(p); /* dirección desplazada: free inválido */✅ Código refactorizado — puntero auxiliar¶
char *p = malloc(100);
if (p == NULL) {
return -1;
}
char *iter = p;
while (*iter != '\0') {
iter++;
}
free(p); /* se libera la base, no el iterador */
p = NULL;El puntero base p nunca se mueve; el iterador es una copia descartable.
✅ Alternativa — indexación por corchetes¶
char *p = malloc(100);
if (p == NULL) {
return -1;
}
size_t i = 0;
while (p[i] != '\0') {
i++;
}
free(p);
p = NULL;p[i] equivale a *(p + i) sin modificar p.
⚠️ Casos límite¶
Pasar un subpuntero a funciones de lectura es válido; el problema aparece solo al liberar una dirección desplazada.
realloctambién exige la base original; mover el puntero rompe la reasignación (ver0x3015h).
Errores típicos al compilar o ejecutar¶
$ ./programa
free(): invalid pointer
Aborted (core dumped)
# Con AddressSanitizer:
ERROR: AddressSanitizer: attempting free on address which was not
malloc()-ed: 0x... in thread T0Checklist de verificación¶
¿Conservé sin modificar el puntero devuelto por
malloc?¿Uso un iterador aparte o indexación para recorrer?
¿El
freerecibe siempre la dirección base?¿Anulé la base después de liberarla?
Reglas relacionadas¶
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — liberar la base y anularla.
0x3015h: Reallocación segura: no sobreescribir el puntero original directamente — conservar el puntero original también en
realloc.0x3005h: Minimizá el uso de múltiples niveles de indirección (punteros a punteros) — simplificar la indirección.
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — otro
freecon dirección inválida.