Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Regla 0x5006h: Preferí fgets sobre gets y scanf para leer cadenas

Compilacion, preprocesador y seguridad (0x50XX)

Universidad Nacional de Río Negro

0x5006h: Preferí fgets sobre gets y scanf para leer cadenas

Enunciado normativo

DEBE leerse texto de entrada con fgets indicando el tamaño del buffer. NO DEBEN usarse gets ni scanf("%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 consecuenciaEfecto concreto
Comportamiento indefinidogets y scanf("%s") desbordan la pila con entradas largas.
Compilacióngets no existe en C11; scanf sin ancho dispara -Wformat sólo en algunos compiladores.
Bug silenciososcanf("%s") deja el resto de la línea en el buffer y rompe la lectura siguiente.
CompatibilidadCó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

Cómo detectarla

HerramientaComandoSeñal
gaffgaff check archivo.cReporta 0x5006h ante gets o scanf("%s", ...) sin ancho.
gcc / clanggcc -std=c11 -Wall -Wextra -Werror -pedantic archivo.cimplicit declaration of function 'gets' y error de enlace.

Checklist de autocontrol

Reglas relacionadas

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 corrompida

Checklist de verificación

Reglas relacionadas