Un buffer overflow (desbordamiento de búfer) ocurre cuando intentas escribir más bytes en un bloque de memoria de los que este puede contener. En C, esto sucede porque el lenguaje no verifica los límites de los arreglos de forma automática durante la ejecución; el procesador simplemente sigue la dirección de memoria y escribe lo que le pidas, sin importar que se salga de los límites asignados.
Este comportamiento se debe a que C está diseñado para la máxima eficiencia y control del hardware. El runtime no lleva un conteo de cuántos elementos tiene un arreglo para evitar la sobrecarga de instrucciones en cada acceso. Sin embargo, esta potencia tiene un coste: si usas funciones que no conocen el tamaño del destino —como la ya eliminada de C11 gets() o el peligroso scanf("%s", buf)— estás dejando la integridad de tu programa en manos del azar.
Debes usar estas técnicas de seguridad solo cuando trabajes con protocolos de red o archivos donde el tamaño sea estrictamente conocido y controlado por ti. En cualquier otra situación, un desbordamiento puede causar un segmentation fault (si el sistema detecta la violación de memoria) o, mucho peor, permitir un ataque de stack smashing. En un ataque de este tipo, un atacante sobreescribe la dirección de retorno en el stack, desviando el flujo de ejecución hacia código malicioso.
Para evitarlo, la regla de oro es: nunca uses una función que no acepte el tamaño máximo del buffer como argumento.
#include <stdio.h>
#include <string.h>
#define MAX_NAME_LEN 32
#define MAX_GREET_LEN 64
int main(void) {
char name[MAX_NAME_LEN];
char greeting[MAX_GREET_LEN];
printf("Introduce tu nombre: ");
/* fgets es la forma segura de leer líneas. Limita la lectura al tamaño del buffer
y garantiza que el buffer termine con '\0' (siempre que el tamaño sea > 0). */
if (fgets(name, sizeof(name), stdin) == NULL) {
return 1;
}
/* fgets suele incluir el salto de línea '\n'. Usamos strcspn para
encontrar su posición y reemplazarlo por el terminador nulo. */
name[strcspn(name, "\n")] = '\0';
/* snprintf es la alternativa segura a sprintf. El tercer argumento
garantiza que nunca escribiremos más de lo que cabe en 'greeting'. */
int written = snprintf(greeting, sizeof(greeting), "Hola, %s!", name);
/* snprintf devuelve el número de bytes que HABRÍAN sido escritos.
Si el valor es >= sizeof(greeting), significa que hubo truncamiento. */
if (written >= (int)sizeof(greeting)) {
fprintf(stderr, "Error: El mensaje de saludo fue demasiado largo y se truncó.\n");
} else {
printf("%s\n", greeting);
}
return 0;
}
Análisis del código
Fíjate en cómo el control del flujo depende totalmente de la información de tamaño. En fgets(name, sizeof(name), stdin), le estamos pasando explícitamente al sistema operativo el límite físico de nuestra variable name. Si el usuario escribe 100 caracteres, fgets solo tomará 31 y dejará el resto en el búfer de entrada, protegiendo la integridad de nuestra memoria.
Al limpiar el salto de línea con name[strcspn(name, "\n")] = '\0', estamos manipulando la cadena de forma segura porque strcspn nos devuelve el índice exacto, evitando que busquemos un carácter que podría no estar ahí si la entrada fue demasiado larga.
La parte más crítica es snprintf. A diferencia de sprintf, que podría desbordar greeting si el nombre es muy largo, snprintf corta la escritura al llegar a sizeof(greeting) - 1 y añade el terminador nulo. Además, el valor de retorno de snprintf es una herramienta de depuración esencial: nos permite detectar si la información se ha perdido por un truncamiento, algo que strcpy o strcat jamás podrían informarnos.
Si compilas este programa con -fsanitize=address (una herramienta de AddressSanitizer), el compilador insertará chequeos en tiempo de ejecución que detendrán el programa e indicarán la línea exacta si alguna vez intentas acceder a un byte fuera de los límites.
El error frecuente
Un error clásico es confiar en strncpy para evitar desbordamientos. Muchos programadores creen que usar strncpy(dest, src, sizeof(dest)) es seguro, pero tiene una trampa de diseño: si la longitud de src es igual o mayor que el tamaño especificado, strncpy no añadirá el carácter terminador \0 al final de dest.
char buf[10];
strncpy(buf, "esto_es_demasiado_largo", sizeof(buf));
// buf NO tiene '\0' al final.
// La siguiente línea leerá memoria adyacente hasta encontrar un cero por pura suerte.
printf("%s\n", buf);
Esto causa un “over-read”, donde funciones como printf o strlen seguirán leyendo memoria ilegal hasta que encuentren un byte nulo, provocando un comportamiento indefinido o un crash. Si necesitas copiar con un límite, usa snprintf o asegúrate manualmente de null-terminar el buffer.
N° 58