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

81

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 se puedan 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?

goe
fuente

Respuestas:

64

En su libro The Practice of Programming (que vale la pena leer), Kernighan y Pike discuten este problema y lo resuelven utilizando 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 aún 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) de la scanf()familia de funciones admite un modificador de formato m(un carácter de asignación asignación) para las entradas de cadena ( %s, %c, %[). En lugar de tomar un char *argumento, toma un char **argumento y asigna el espacio necesario para el valor que lee:

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

Si la sscanf()función no satisface todas las especificaciones de conversión, toda la memoria que asignó para %msconversiones similares se libera antes de que la función regrese.

Jonathan Leffler
fuente
@Sam: Sí, debería ser buflen-1- Gracias. Luego debe preocuparse por el subdesbordamiento sin firmar (envolviendo a un número bastante grande), de ahí la ifprueba. Me sentiría muy tentado a reemplazar eso con an assert(), o respaldarlo con un assert()antes de ifque se active durante el desarrollo si alguien es lo suficientemente descuidado como para pasar 0 como tamaño. No he revisado cuidadosamente la documentación para saber qué %0ssignifica sscanf(): la prueba podría ser mejor como if (buflen < 2).
Jonathan Leffler
Entonces snprintfescribe algunos datos en un búfer de cadena y sscanflee de esa cadena creada. ¿Dónde exactamente reemplaza esto scanfen el sentido de que se lee en stdin?
krb686
También es bastante confuso que use la palabra "formato" para su cadena de resultado y, por lo tanto, pase "formato" como primer argumento, snprintfpero no es el parámetro de formato real.
krb686
@ krb686: este código está escrito para que los datos que se escaneen estén en el parámetro datay, por sscanf()lo tanto, sean apropiados. Si desea leer desde la entrada estándar, elimine el dataparámetro y llame en su scanf()lugar. En cuanto a la elección del nombre formatde la variable que se convierte en la cadena de formato en la llamada a sscanf(), tiene derecho a cambiarle el nombre si lo desea, pero su nombre no es inexacto. No estoy seguro de qué alternativa tiene sentido; ¿Lo dejaría in_formatmás claro? No planeo cambiarlo en este código; puede hacerlo si usa esta idea en su propio código.
Jonathan Leffler
1
@mabraham: Todavía es cierto en macOS Sierra 10.12.5 (hasta 2017-06-06); el scanf()en macOS no está documentado como compatible %ms, aunque sería útil.
Jonathan Leffler
30

Si está usando gcc, puede usar el aespecificador de extensión GNU para que scanf () asigne memoria para que usted mantenga la entrada:

int main()
{
  char *str = NULL;

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

Editar: como señaló Jonathan, debe consultar las scanfpáginas de manual, ya que el especificador puede ser diferente ( %m) y es posible que deba habilitar ciertas definiciones al compilar.

John Ledbetter
fuente
8
Eso es más una cuestión de usar glibc (la biblioteca GNU C) que usar el compilador GNU C.
Jonathan Leffler
3
Y tenga en cuenta que el estándar POSIX 2008 proporciona el mmodificador para hacer el mismo trabajo. Ver scanf(). Deberá comprobar si los sistemas que utiliza admiten este modificador.
Jonathan Leffler
4
GNU (como se encuentra en Ubuntu 13.10, en cualquier caso) es compatible %ms. La notación %aes sinónimo de %f(en la salida, solicita datos de coma flotante hexadecimal). La página de manual de GNU para scanf()dice: _ No está disponible si el programa está compilado con gcc -std=c99o gcc -D_ISOC99_SOURCE (a menos _GNU_SOURCEque también se especifique), en cuyo caso ase interpreta como un especificador para números de punto flotante (ver arriba) ._
Jonathan Leffler
8

La mayoría de las veces una combinación de fgetsy sscanfhace el trabajo. La otra cosa sería escribir su propio analizador, si la entrada está bien formateada. También tenga en cuenta que su segundo ejemplo necesita un poco de modificación para usarse de manera segura:

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

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

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

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

y eso es todo.

dirkgently
fuente
2
¿Podrías poner el feofcódigo en un contexto más amplio? Estoy preguntando porque esa función a menudo se usa mal.
Roland Illig
1
arraynecesita serchar array[LENGTH+1];
jxh
4

El uso directo scanf(3)y sus variantes plantea una serie de problemas. Normalmente, 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 ayudarán).

Al menos si fgets(3)sabe cuántas líneas de entrada necesitará su programa y no tendrá desbordamientos de búfer ...

DigitalRoss
fuente
1

Limitar la longitud de la entrada es definitivamente más fácil. Puede aceptar una entrada arbitrariamente larga utilizando un bucle, leyendo un poco 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 el uso de fgets () no le permitirá aceptar cantidades arbitrarias de texto; todavía tendrá que establecer un límite.

Mark Bessey
fuente
Entonces, ¿alguien sabe cómo hacer eso con scanf?
Ve el
3
El uso de fgets en un bucle puede permitirle aceptar cantidades arbitrarias de texto, simplemente manteniendo realloc()su búfer.
bdonlan
1

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 en cadenas.

Devolverá la cadena leída o si ocurre 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;
}

fuente
sizeof (char)es por definición 1. No lo necesitas aquí.
RastaJedi
Por lo general, es una buena práctica mantener la asignación / liberación de punteros al mismo nivel, lo que significa que su función no debe asignar memoria por sí sola, ya que la persona que llama tiene que liberarla. La mayoría de las funciones estándar de biblioteca / posix se adhieren a este principio al devolver una cadena estática (como strerror(3)) o esperar que se pase una cadena preasignada (como ( strerror_r(3)- o scanf(3)) ...
Michael Beer