Regla 0x5006h: Preferí fgets sobre gets y scanf para leer cadenas
Compilacion, preprocesador y seguridad (0x50XX)
0x5006h: Preferí fgets sobre gets y scanf para leer cadenas¶
Enunciado normativo¶
DEBE leerse texto de entrada con
fgetsindicando el tamaño del buffer. NO DEBEN usarsegetsniscanf("%s", ...)sin ancho máximo para leer cadenas.
¿Por qué existe esta regla?¶
El problema¶
gets no recibe el tamaño del destino: lee hasta el salto de línea o el fin de
archivo y escribe todo lo leído; una línea larga desborda cualquier buffer. Por
eso se eliminó del estándar C11. scanf("%s", buf) agrava el defecto: tampoco
acota y además corta en el primer espacio, así que no sirve para leer una línea
completa. fgets, en cambio, recibe el tamaño máximo, nunca escribe más de
n - 1 caracteres y garantiza el terminador.
Consecuencias de violarla¶
| Tipo de consecuencia | Efecto concreto |
|---|---|
| Comportamiento indefinido | gets y scanf("%s") desbordan la pila con entradas largas. |
| Compilación | gets no existe en C11; scanf sin ancho dispara -Wformat sólo en algunos compiladores. |
| Bug silencioso | scanf("%s") deja el resto de la línea en el buffer y rompe la lectura siguiente. |
| Compatibilidad | Código con gets no enlaza en glibc moderno ni compila en macOS. |
Fundamento en el estándar y en la cátedra¶
ISO/IEC 9899:2011 §7.21.7.2 define fgets y ya no incluye gets, removida
por la enmienda técnica que dio origen a C11. §7.21.6.2 describe scanf y el
uso de un ancho de campo (%99s) como única forma acotada. La cátedra adopta
fgets como entrada canónica porque el sandbox de corrección prueba con líneas
de longitud arbitraria.
Alcance y excepciones¶
Aplica a la lectura de cadenas de texto. Para números, scanf("%d", &x) es
aceptable verificando su retorno. No aplica a un «token» sin espacios leído
con ancho (scanf("%99s", buf)): es seguro, pero fgets sigue siendo preferible
para líneas completas. fgets conserva el '\n': hay que quitarlo.
Ejemplos exhaustivos¶
❌ Contraejemplo 1 — gets sin límite¶
#include <stdio.h>
void leer_nombre(void)
{
char nombre[32];
gets(nombre);
}Por qué falla: una entrada de 100 caracteres escribe 100 bytes en un arreglo de
32; pisa el marco de pila y puede ejecutar código. Además, gets ni siquiera
existe al compilar en C11.
❌ Contraejemplo 2 — scanf sin ancho y resto de línea olvidado¶
#include <stdio.h>
void leer_nombre(void)
{
char nombre[32];
scanf("%s", nombre);
}Por qué falla: sin ancho, %s no conoce el tamaño de nombre y desborda. Si la
entrada es Ana Maria, sólo guarda Ana y deja Maria pendiente, corrompiendo
la lectura siguiente.
✅ Ejemplo conforme 1 — fgets con eliminación del salto¶
#include <stdio.h>
#include <string.h>
void leer_nombre(char *nombre, size_t tam)
{
if (fgets(nombre, tam, stdin) == NULL) {
return;
}
nombre[strcspn(nombre, "\n")] = '\0';
}fgets escribe a lo sumo tam - 1 caracteres, agrega '\0' y devuelve NULL
en error o fin de archivo. strcspn recorta el salto sin suponer que existe;
pasar tam evita el error de sizeof sobre un puntero.
✅ Ejemplo conforme 2 — scanf con ancho para un token¶
#include <stdio.h>
int leer_comando(char *comando, size_t tam)
{
if (scanf("%15s", comando) != 1) {
return -1;
}
return 0;
}Cuando se necesita un token sin espacios, el ancho 15 deja lugar para el
terminador y el valor de retorno confirma que la conversión ocurrió. Aun así,
para líneas completas la elección correcta sigue siendo fgets.
⚠️ Casos límite¶
sizeofsobre puntero: dentro devoid f(char *buf),sizeof(buf)da 8; pasá el tamaño como parámetro.Línea más larga que el buffer:
fgetscorta y deja el resto para la próxima llamada; hay que consumirlo o detectarlo.Fin de archivo:
fgetsdevuelveNULLsin modificar el buffer; siempre verificá el retorno.
Cómo detectarla¶
| Herramienta | Comando | Señal |
|---|---|---|
gaff | gaff check archivo.c | Reporta 0x5006h ante gets o scanf("%s", ...) sin ancho. |
gcc / clang | gcc -std=c11 -Wall -Wextra -Werror -pedantic archivo.c | implicit declaration of function 'gets' y error de enlace. |
Checklist de autocontrol¶
¿Leí con
fgets(buffer, sizeof(buffer), stdin)?¿Verifiqué que
fgetsno devolvieraNULL?¿Recorté el
'\n'antes de usar la cadena?¿Evité
getsy, si uséscanf, declaré el ancho y chequié el retorno?
Reglas relacionadas¶
0x5008h: Prohibición de funciones obsoletas o inseguras (gets, atoi) —
getses la función obsoleta prohibida por excelencia.0x5004h: Todas las operaciones con cadenas deben ser seguras —
fgetsacota el destino, igual quesnprintf.
Antipatrón: Lectura de cadenas con scanf() sin límite de ancho en buffer fijo¶
Síntoma en el código del estudiante¶
Se lee una cadena con scanf("%s", buf) o fscanf(f, "%s", buf) sobre un
arreglo de tamaño fijo, sin indicar un ancho máximo. A veces el arreglo es
pequeño (10 o 20 bytes) y el dato proviene del usuario o de un archivo externo.
Diagnóstico¶
Mecanismo del defecto¶
El especificador %s de scanf lee caracteres hasta encontrar un espacio en
blanco, sin límite superior. La función no conoce el tamaño del arreglo destino:
solo recibe el puntero. Si la entrada es más larga que el búfer, escribe más
allá del final, pisando variables contiguas en la pila (u otras zonas del
programa).
Este es el desbordamiento de búfer (buffer overflow) canónico. No es un error de compilación: el compilador no puede saber cuántos caracteres tendrá la entrada. El daño depende de qué haya después del arreglo en memoria: puede corromper otra variable, el marco de pila, la dirección de retorno o provocar la caída del proceso. Un atacante puede aprovecharlo para redirigir el flujo de ejecución.
Consecuencia observable¶
Con entradas cortas todo funciona y el defecto permanece latente. Con una entrada larga, el programa produce “stack smashing detected” (cuando el canario de pila de GCC detecta la corrupción) o una violación de segmento. En el mejor caso, una variable vecina cambia de valor sin explicación.
Fundamento en el estándar C11¶
ISO/IEC 9899:2011 §7.21.6.2 define fscanf y su conversión %s: si no se
especifica un ancho de campo, la función lee tantos caracteres no blancos como
encuentre. El estándar no limita la escritura al tamaño del objeto apuntado,
porque no lo conoce; la responsabilidad de acotar es del programador. El ancho
debe reservar un byte para el terminador nulo, como indica la conversión de
cadenas.
Corrección idiomática¶
❌ Código con el antipatrón¶
char buf[10];
scanf("%s", buf);Por qué es incorrecto: una entrada de 10 o más caracteres escribe fuera de
buf. El terminador nulo agrega un byte más, de modo que incluso 9 caracteres
más el '\0' ya alcanzan el límite exacto.
✅ Código refactorizado¶
char buf[10];
if (scanf("%9s", buf) != 1) {
return -1;
}
char linea[64];
if (fgets(linea, sizeof(linea), stdin) == NULL) {
return -1;
}El ancho %9s reserva los 9 caracteres útiles y deja lugar para el '\0'. Para
leer una línea completa —que es lo habitual— fgets acota por construcción
porque recibe el tamaño del búfer (0x5006h: Preferí fgets sobre gets y scanf para leer cadenas).
Errores típicos al compilar o ejecutar¶
No hay advertencia de compilación en el caso general. El fallo aparece en ejecución con una entrada larga.
$ ./saludar
Ingrese su nombre: un_nombre_demasiado_largo
*** stack smashing detected ***: terminated
Aborted (core dumped)Con suerte, el daño es silencioso y corrompe otra variable.
$ ./configurar
Ingrese usuario: administrador_de_sistemas
usuario = administrador_de_sistemas
permisos = 1919905620 <-- variable vecina corrompidaChecklist de verificación¶
¿Acoté el ancho de todo
%satamanio_del_buffer - 1?¿Verifiqué el retorno de
scanf/fscanf?¿Prefiero
fgetspara leer cadenas y líneas?¿Compilé con
-Wall -Wextray-D_FORTIFY_SOURCE=2?
Reglas relacionadas¶
0x400Ah: Prohibición de operar sobre flujos de archivo tras haber invocado fclose() (use-after-close) — regla asociada sobre el estado del flujo de E/S.
0x5006h: Preferí fgets sobre gets y scanf para leer cadenas — preferir
fgetssobregetsyscanfpara cadenas.0x5004h: Todas las operaciones con cadenas deben ser seguras — todas las operaciones con cadenas deben ser seguras.
0x5008h: Prohibición de funciones obsoletas o inseguras (gets, atoi) — prohibición de funciones obsoletas o inseguras.