¿Cómo evitar que scanf cause un desbordamiento de búfer en C?

7 minutos de lectura

avatar de usuario
vamos

Yo uso este código:

while ( scanf("%s", buf) == 1 ){

¿Cuál sería la mejor manera de evitar un posible desbordamiento del búfer para que pueda pasar cadenas de longitudes aleatorias?

Sé que puedo limitar la cadena de entrada llamando, por ejemplo:

while ( scanf("%20s", buf) == 1 ){

Pero preferiría poder procesar cualquier entrada del usuario. ¿O no se puede hacer esto de manera segura usando scanf y debería usar fgets?

avatar de usuario
jonathan leffler

En su libro La práctica de la programación (que vale la pena leer), Kernighan y Pike discuten este problema y lo resuelven usando snprintf() para crear la cadena con el tamaño de búfer correcto para pasar a la scanf() familia de funciones. En efecto:

int scanner(const char *data, char *buffer, size_t buflen)
{
    char format[32];
    if (buflen == 0)
        return 0;
    snprintf(format, sizeof(format), "%%%ds", (int)(buflen-1));
    return sscanf(data, format, buffer);
}

Tenga en cuenta que esto todavía limita la entrada al tamaño proporcionado como ‘búfer’. Si necesita más espacio, debe realizar la asignación de memoria o utilizar una función de biblioteca no estándar que realice la asignación de memoria por usted.


Tenga en cuenta que la versión POSIX 2008 (2013) del scanf() familia de funciones admite un modificador de formato m (un carácter de asignación-asignación) para entradas de cadena (%s, %c, %[). Instead of taking a char * argument, it takes a char ** argument, and it allocates the necessary space for the value it reads:

char *buffer = 0;
if (sscanf(data, "%ms", &buffer) == 1)
{
    printf("String is: <<%s>>\n", buffer);
    free(buffer);
}

If the sscanf() function fails to satisfy all the conversion specifications, then all the memory it allocated for %ms-like conversions is freed before the function returns.

  • @Sam: Yes, it should be buflen-1 — Thank You. You then have to worry about unsigned underflow (wrapping to a rather large number), hence the if test. I’d be sorely tempted to replace that with an assert(), or back it up with an assert() before the if that fires during development if anyone is careless enough to pass 0 as the size. I’ve not carefully reviewed the documentation for what %0s means to sscanf() — the test might be better as if (buflen < 2).

    – Jonathan Leffler

    Sep 11, 2013 at 16:35

  • So snprintf writes some data to a string buffer, and sscanf reads from that created string. Where exactly does this replace scanf in that it reads from stdin?

    – krb686

    Apr 26, 2015 at 14:18

  • It’s also quite confusing that you use the word “format” for your result string and thus pass in “format” as the first argument to snprintf yet it is not the actual format parameter.

    – krb686

    Apr 26, 2015 at 14:24

  • @krb686: This code is written so that the data to be scanned is in the parameter data and hence sscanf() is appropriate. If you want to read from standard input instead, drop the data parameter and call scanf() instead. As to the choice of the name format for the variable that becomes the format string in the call to sscanf(), you are entitled to rename it if you wish, but its name is not inaccurate. I am not sure what alternative makes sense; would in_format make it any clearer? I am not planning to change it in this code; you may if you use this idea in your own code.

    – Jonathan Leffler

    Apr 26, 2015 at 15:28

  • @mabraham: It is still true under macOS Sierra 10.12.5 (up to 2017-06-06) — the scanf() on macOS is not documented as supporting %ms, useful though it would be.

    – Jonathan Leffler

    Jun 6, 2017 at 19:36


user avatar
John Ledbetter

If you are using gcc, you can use the GNU-extension a specifier to have scanf() allocate memory for you to hold the input:

int main()
{
  char *str = NULL;

  scanf ("%as", &str);
  if (str) {
      printf("\"%s\"\n", str);
      free(str);
  }
  return 0;
}

Edit: As Jonathan pointed out, you should consult the scanf man pages as the specifier might be different (%m) and you might need to enable certain defines when compiling.

  • That’s more an issue of using glibc (the GNU C Library) than of using the GNU C Compiler.

    – Jonathan Leffler

    Oct 25, 2009 at 20:24

  • And note that the POSIX 2008 standard provides the m modifier to do the same job. See scanf(). You’ll need to check whether the systems you use do support this modifier.

    – Jonathan Leffler

    Feb 22, 2014 at 21:15

  • GNU (as found on Ubuntu 13.10, at any rate) supports %ms. The notation %a is a synonym for %f (on output, it requests hexadecimal floating point data). The GNU man page for scanf() says: _ It is not available if the program is compiled with gcc -std=c99 or gcc -D_ISOC99_SOURCE (unless _GNU_SOURCE is also specified), in which case the a is interpreted as a specifier for floating-point numbers (see above)._

    – Jonathan Leffler

    Feb 22, 2014 at 21:16

Most of the time a combination of fgets and sscanf does the job. The other thing would be to write your own parser, if the input is well formatted. Also note your second example needs a bit of modification to be used safely:

#define LENGTH          42
#define str(x)          # x
#define xstr(x)         str(x)

/* ... */ 
int nc = scanf("%"xstr(LENGTH)"[^\n]%*[^\n]", matriz); 

Lo anterior descarta el flujo de entrada hasta, pero sin incluir, la nueva línea (\n) personaje. Deberá agregar un getchar() para consumir esto. También verifique si llegó al final de la transmisión:

if (!feof(stdin)) { ...

y eso es todo.

  • ¿Podrías poner el feof código en un contexto más grande? Lo pregunto ya que esa función a menudo se usa mal.

    – Roland Illig

    24/10/2016 a las 21:52

  • array necesita ser char array[LENGTH+1];

    – jxh

    19 de marzo de 2019 a las 22:58

  • Voto negativo por presentar el infame !feof patrón sin ningún contexto ni explicación y sin arreglarlo durante 5 años.

    – Roland Illig

    23 de diciembre de 2021 a las 11:17


usando directamente scanf(3) y sus variantes plantea una serie de problemas. Por lo general, los usuarios y los casos de uso no interactivos se definen en términos de líneas de entrada. Es raro ver un caso en el que, si no se encuentran suficientes objetos, más líneas resolverán el problema, pero ese es el modo predeterminado para scanf. (Si un usuario no sabía cómo ingresar un número en la primera línea, una segunda y una tercera línea probablemente no ayuden).

al menos si tu fgets(3) sabe cuántas líneas de entrada necesitará su programa, y ​​no tendrá ningún desbordamiento de búfer…

Limitar la longitud de la entrada es definitivamente más fácil. Podría aceptar una entrada arbitrariamente larga usando un bucle, leyendo un bit a la vez, reasignando espacio para la cadena según sea necesario…

Pero eso es mucho trabajo, por lo que la mayoría de los programadores de C simplemente cortan la entrada en una longitud arbitraria. Supongo que ya lo sabe, pero usar fgets() no le permitirá aceptar cantidades arbitrarias de texto; aún necesitará establecer un límite.

  • Entonces, ¿alguien sabe cómo hacer eso con scanf entonces?

    – vaya

    25 de octubre de 2009 a las 17:17

  • El uso de fgets en un bucle puede permitirle aceptar cantidades arbitrarias de texto, solo mantenga realloc()ing su búfer.

    – bdonlan

    25 de octubre de 2009 a las 18:53

No es mucho trabajo hacer una función que asigne la memoria necesaria para su cadena. Esa es una pequeña función c que escribí hace algún tiempo, siempre la uso para leer cadenas.

Devolverá la cadena de lectura o si se produce un error de memoria NULL. Pero tenga en cuenta que debe liberar () su cadena y siempre verificar su valor de retorno.

#define BUFFER 32

char *readString()
{
    char *str = malloc(sizeof(char) * BUFFER), *err;
    int pos;
    for(pos = 0; str != NULL && (str[pos] = getchar()) != '\n'; pos++)
    {
        if(pos % BUFFER == BUFFER - 1)
        {
            if((err = realloc(str, sizeof(char) * (BUFFER + pos + 1))) == NULL)
                free(str);
            str = err;
        }
    }
    if(str != NULL)
        str[pos] = '\0';
    return str;
}

  • Entonces, ¿alguien sabe cómo hacer eso con scanf entonces?

    – vaya

    25 de octubre de 2009 a las 17:17

  • El uso de fgets en un bucle puede permitirle aceptar cantidades arbitrarias de texto, solo mantenga realloc()ing su búfer.

    – bdonlan

    25 de octubre de 2009 a las 18:53

¿Ha sido útil esta solución?