Regla 0x400Ah: Prohibición de operar sobre flujos de archivo tras haber invocado fclose() (use-after-close)
Archivos y E/S (0x40XX)
0x400Ah: Prohibición de operar sobre flujos de archivo tras haber invocado fclose() (use-after-close)¶
Enunciado normativo¶
NO DEBE usarse un
FILE *después de haber invocadofclosesobre él. Inmediatamente después de cerrar, el puntero DEBE asignarse aNULLcuando siga estando en alcance.
fclose no es “pausar” el flujo: lo destruye. El puntero queda colgando y toda
operación posterior es inválida.
¿Por qué existe esta regla?¶
El problema¶
fclose libera la estructura interna FILE asociada al flujo y desasocia el
descriptor. A partir de ese momento, el valor del puntero deja de referirse a un
objeto válido: es un puntero colgante (dangling pointer). Pasar ese puntero a
cualquier función de stdio (fread, fgets, fseek, feof, un segundo
fclose) es comportamiento indefinido.
La biblioteca no puede detectar el error de forma confiable: la memoria liberada
puede reutilizarse para otra cosa y el puntero puede apuntar a datos que ya no
tienen nada que ver con el flujo. En algunas plataformas la operación parece
funcionar y en otras corrompe el estado del runtime; incluso puede provocar una
violación de segmento dentro de la propia libc.
El segundo fclose merece mención aparte: se conoce como double free de
flujos y corrompe las estructuras del asignador de stdio, con abortos
inmediatos o daños diferidos. Anular el puntero después de cerrar convierte
cualquier uso accidental en un fallo predecible y diagnosticable, porque
fclose(NULL) es un error detectable y la desreferencia es evidente.
Consecuencias de violarla¶
| Tipo de consecuencia | Efecto concreto |
|---|---|
| Comportamiento indefinido | La operación sobre la estructura liberada produce resultados impredecibles. |
| Corrupción del runtime | Un double fclose daña las estructuras internas de stdio. |
| Violación de segmento | La libc desreferencia memoria ya liberada y el proceso cae por SIGSEGV. |
| Bug dependiente de plataforma | El mismo código “funciona” en un equipo y falla en otro. |
Fundamento en el estándar y en la cátedra¶
ISO/IEC 9899:2011 §7.21.5.1 define fclose como desasociación del flujo: una
vez cerrado, el valor del puntero no puede usarse para acceder al objeto. La
cátedra prohíbe el use-after-close por la misma razón que prohíbe usar memoria
liberada (0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes): es el análogo de acceso a punteros colgantes en la
gestión de flujos.
Alcance y excepciones¶
Aplica a todo uso de un FILE * tras fclose, incluidos los flujos estándar
si el programa los cierra. También aplica a copias del puntero: si dos variables
apuntan al mismo flujo y una lo cierra, la otra queda colgante.
No hay excepción: salvo volver a abrir explícitamente con fopen, no existe
un uso válido del mismo valor después de cerrar.
Ejemplos exhaustivos¶
❌ Contraejemplo 1 — Leer después de cerrar¶
FILE *f = fopen("log.txt", "r");
fclose(f);
fread(buf, 1, 10, f);Por qué falla: fread recibe un puntero a una estructura FILE ya liberada.
La operación es comportamiento indefinido; puede leer datos ajenos o provocar
una caída, según cómo haya quedado la memoria del asignador.
❌ Contraejemplo 2 — Doble cierre y uso posterior¶
FILE *f = fopen("salida.txt", "w");
fclose(f);
fprintf(f, "fin\n");
fclose(f);Por qué falla: el fprintf sobre el flujo cerrado y el segundo fclose operan
sobre un objeto destruido. El doble cierre suele abortar el proceso al corromper
las estructuras de stdio, y el alcance del daño es impredecible.
✅ Ejemplo conforme 1 — Cerrar y anular el puntero¶
FILE *f = fopen("log.txt", "r");
if (f == NULL) {
return -1;
}
size_t leidos = fread(buf, 1, 10, f);
fclose(f);
f = NULL;Tras el cierre, f se anula. Cualquier uso accidental posterior será un
NULL detectable en lugar de un acceso a memoria liberada; el conteo de
fread se conserva para verificación (0x4002h: Validá los retornos de las operaciones de lectura y escritura de archivos).
✅ Ejemplo conforme 2 — Una sola ventana de uso y un único cierre¶
int procesar(const char *ruta)
{
FILE *f = fopen(ruta, "r");
if (f == NULL) {
return -1;
}
int estado = 0;
char linea[128];
while (fgets(linea, sizeof(linea), f) != NULL && estado == 0) {
estado = analizar(linea);
}
fclose(f);
return estado;
}Todas las operaciones ocurren antes del cierre y existe un único fclose en el
camino de salida; no queda ninguna ventana en la que el puntero pueda usarse
después (ver 0x4004h: Mantené la simetría de recursos al abrir y cerrar archivos en el mismo nivel de abstracción).
⚠️ Casos límite¶
Punteros alias: si dos variables guardan el mismo
FILE *, cerrar por una invalida la otra; no copiar el puntero sin transferir la propiedad.stdin/stdout/stderr: si se cierran, no deben volver a usarse enprintf/scanf; el programa puede perder toda su salida.Doble cierre: no es una variante inocua del cierre; es un defecto grave análogo al double free de memoria (0x3014h: Prohibición de doble liberación de memoria (double free) sobre el mismo puntero).
Reutilizar la variable: si se necesita otro archivo, se vuelve a llamar a
fopeny se obtiene un puntero nuevo, no se reutiliza el cerrado.
Cómo detectarla¶
| Herramienta | Comando | Señal |
|---|---|---|
gaff | gaff check archivo.c | Regla 0x400Ah: uso de FILE * tras fclose o doble cierre. |
gcc | gcc -Wall -Wextra -std=c11 archivo.c | -Wuse-after-free con optimizaciones en versiones recientes. |
valgrind | valgrind --track-fds=yes ./programa | Acceso a descriptor ya cerrado. |
| Revisión manual | — | Todo fclose debe ir seguido de = NULL si el puntero sigue vivo. |
Checklist de autocontrol¶
¿Todas las operaciones sobre el flujo ocurren antes de
fclose?¿Asigné
NULLal puntero después de cerrarlo?¿Evité cerrar dos veces el mismo flujo?
¿Revisé que no existan copias del puntero que queden colgando?
Reglas relacionadas¶
0x4001h: Manejá correctamente la apertura y cierre de archivos — el ciclo de vida del flujo empieza en
fopeny termina enfclose.0x4004h: Mantené la simetría de recursos al abrir y cerrar archivos en el mismo nivel de abstracción — el dueño del recurso es quien cierra.
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — anular el puntero tras liberar evita el uso colgante.
0x3014h: Prohibición de doble liberación de memoria (double free) sobre el mismo puntero — la doble liberación de memoria es el defecto análogo.