Desventajas de scanf

Resuelto karthi_ms asked hace 14 años • 9 respuestas

Quiero saber las desventajas de scanf().

En muchos sitios, he leído que el uso scanfpuede provocar desbordamientos del búfer. ¿Cuál es la razón para esto? ¿ Existen otros inconvenientes con scanf?

karthi_ms avatar Mar 12 '10 10:03 karthi_ms
Aceptado

La mayoría de las respuestas hasta ahora parecen centrarse en el problema del desbordamiento del búfer de cadenas. En realidad, los especificadores de formato que se pueden usar con scanffunciones admiten la configuración explícita del ancho de campo , que limita el tamaño máximo de la entrada y evita el desbordamiento del búfer. Esto hace que las acusaciones populares sobre los peligros de desbordamiento del buffer de cadena presentes sean scanfprácticamente infundadas. Afirmar que scanfde alguna manera es análogo a getsin the respect es completamente incorrecto. Existe una diferencia cualitativa importante entre scanfy gets: scanfproporciona al usuario funciones para prevenir el desbordamiento del búfer de cadena, mientras que getsno.

Se puede argumentar que estas scanfcaracterísticas son difíciles de usar, ya que el ancho del campo debe estar incrustado en la cadena de formato (no hay manera de pasarlo a través de un argumento variado, como se puede hacer en printf). Eso es realmente cierto. scanfde hecho está bastante mal diseñado en ese sentido. Sin embargo, cualquier afirmación que scanfde alguna manera esté irremediablemente incumplida con respecto a la seguridad del desbordamiento del búfer de cadenas es completamente falsa y generalmente la hacen programadores perezosos.

El verdadero problema scanftiene una naturaleza completamente diferente, aunque también se trata de desbordamiento . Cuando scanfla función se utiliza para convertir representaciones decimales de números en valores de tipos aritméticos, no proporciona protección contra el desbordamiento aritmético. Si ocurre un desbordamiento, scanfproduce un comportamiento indefinido. Por esta razón, la única forma adecuada de realizar la conversión en la biblioteca estándar de C son las funciones de strto...la familia.

Entonces, para resumir lo anterior, el problema scanfes que es difícil (aunque posible) usarlo de manera adecuada y segura con buffers de cadena. Y es imposible utilizarlo de forma segura para entradas aritméticas. Este último es el verdadero problema. Lo primero es sólo un inconveniente.

PD: Lo anterior pretende referirse a toda la familia de scanffunciones (incluidas también fscanfy sscanf). Específicamente scanf, el problema obvio es que la idea misma de usar una función con formato estricto para leer entradas potencialmente interactivas es bastante cuestionable.

AnT stands with Russia avatar Mar 12 '2010 06:03 AnT stands with Russia

Los problemas con scanf son (como mínimo):

  • usando %spara obtener una cadena del usuario, lo que genera la posibilidad de que la cadena sea más larga que su búfer, provocando un desbordamiento.
  • la posibilidad de que un análisis fallido deje el puntero del archivo en una ubicación indeterminada.

Prefiero usar fgetspara leer líneas completas para poder limitar la cantidad de datos leídos. Si tiene un búfer de 1K y lee una línea, fgetspuede saber si la línea era demasiado larga por el hecho de que no hay un carácter de nueva línea final (a pesar de la última línea de un archivo sin una nueva línea).

Luego puedes quejarte con el usuario o asignar más espacio para el resto de la línea (de forma continua si es necesario hasta que tengas suficiente espacio). En cualquier caso, no hay riesgo de desbordamiento del búfer.

Una vez que hayas leído la línea, sabrás que estás ubicado en la siguiente línea, por lo que no hay ningún problema allí. Luego puede configurar sscanfsu cadena al contenido de su corazón sin tener que guardar y restaurar el puntero del archivo para volver a leerlo.

Aquí hay un fragmento de código que uso con frecuencia para garantizar que no se desborde el búfer cuando le pido información al usuario.

Se podría ajustar fácilmente para usar un archivo que no sea la entrada estándar si es necesario y también podría hacer que asigne su propio búfer (y seguir incrementándolo hasta que sea lo suficientemente grande) antes de devolvérselo a la persona que llama (aunque la persona que llama entonces sería responsable). por liberarlo, por supuesto).

#include <stdio.h>
#include <string.h>

#define OK         0
#define NO_INPUT   1
#define TOO_LONG   2
#define SMALL_BUFF 3
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Size zero or one cannot store enough, so don't even
    // try - we need space for at least newline and terminator.

    if (sz < 2)
        return SMALL_BUFF;

    // Output prompt.

    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }

    // Get line with buffer overrun protection.

    if (fgets (buff, sz, stdin) == NULL)
        return NO_INPUT;

    // Catch possibility of `\0` in the input stream.

    size_t len = strlen(buff);
    if (len < 1)
        return NO_INPUT;

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.

    if (buff[len - 1] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return (extra == 1) ? TOO_LONG : OK;
    }

    // Otherwise remove newline and give string back to caller.
    buff[len - 1] = '\0';
    return OK;
}

Y un piloto de pruebas para ello:

// Test program for getLine().

int main (void) {
    int rc;
    char buff[10];

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        // Extra NL since my system doesn't output that on EOF.
        printf ("\nNo input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long [%s]\n", buff);
        return 1;
    }

    printf ("OK [%s]\n", buff);

    return 0;
}

Finalmente, una prueba para mostrarlo en acción:

$ printf "\0" | ./tstprg     # Singular NUL in input stream.
Enter string>
No input

$ ./tstprg < /dev/null       # EOF in input stream.
Enter string>
No input

$ ./tstprg                   # A one-character string.
Enter string> a
OK [a]

$ ./tstprg                   # Longer string but still able to fit.
Enter string> hello
OK [hello]

$ ./tstprg                   # Too long for buffer.
Enter string> hello there
Input too long [hello the]

$ ./tstprg                   # Test limit of buffer.
Enter string> 123456789
OK [123456789]

$ ./tstprg                   # Test just over limit.
Enter string> 1234567890
Input too long [123456789]
paxdiablo avatar Mar 12 '2010 03:03 paxdiablo

De las preguntas frecuentes de comp.lang.c: ¿ Por qué todo el mundo dice que no se use scanf? ¿Qué debería usar en su lugar?

scanftiene varios problemas; consulte las preguntas 12.17 , 12.18a y 12.19 . Además, su %sformato tiene el mismo problema que gets()tiene (consulte la pregunta 12.23 ): es difícil garantizar que el búfer de recepción no se desborde. [nota]

De manera más general, scanfestá diseñado para entradas formateadas y relativamente estructuradas (de hecho, su nombre deriva de “escaneo formateado”). Si prestas atención, te dirá si tuvo éxito o fracasó, pero sólo puede decirte aproximadamente dónde falló, y no cómo ni por qué. Tiene muy pocas oportunidades de realizar alguna recuperación de errores.

Sin embargo, la entrada interactiva del usuario es la entrada menos estructurada que existe. Una interfaz de usuario bien diseñada permitirá al usuario escribir casi cualquier cosa, no sólo letras o signos de puntuación cuando se esperaban dígitos, sino también más o menos caracteres de los esperados, o ningún carácter (es decir, solo el botón RETURN ). clave), o EOF prematuro, o cualquier cosa. Es casi imposible lidiar con todos estos problemas potenciales al usar scanf; Es mucho más fácil leer líneas completas (con fgetso similares) y luego interpretarlas, ya sea usando sscanfotras técnicas. (Funciones como strtol, strtoky atoisuelen ser útiles; consulte también las preguntas 12.16 y 13.6 .) Si utiliza alguna scanfvariante, asegúrese de verificar el valor de retorno para asegurarse de que se encontró la cantidad esperada de elementos. Además, si usa %s, asegúrese de protegerse contra el desbordamiento del búfer.

Tenga en cuenta, por cierto, que las críticas a scanfno son necesariamente acusaciones a fscanfy sscanf. scanflee desde stdin, que suele ser un teclado interactivo y, por lo tanto, es el menos restringido y genera la mayor cantidad de problemas. Por otro lado, cuando un archivo de datos tiene un formato conocido, puede ser apropiado leerlo con fscanf. Es perfectamente apropiado analizar cadenas sscanf(siempre que se verifique el valor de retorno), porque es muy fácil recuperar el control, reiniciar el análisis, descartar la entrada si no coincide, etc.

Enlaces adicionales:

  • explicación más larga de Chris Torek
  • explicación más larga por parte de un servidor

Referencias: K&R2 Sec. 7,4p. 159

jamesdlin avatar Mar 12 '2010 06:03 jamesdlin

Es muy difícil llegar scanfa hacer lo que quieres. Claro que puedes, pero cosas como scanf("%s", buf);son tan peligrosas gets(buf);como todo el mundo ha dicho.

Como ejemplo, lo que está haciendo paxdiablo en su función de leer se puede hacer con algo como:

scanf("%10[^\n]%*[^\n]", buf));
getchar();

Lo anterior leerá una línea, almacenará los primeros 10 caracteres que no sean de nueva línea bufy luego descartará todo hasta (incluido) una nueva línea. Entonces, la función de paxdiablo podría escribirse de scanfla siguiente manera:

#include <stdio.h>

enum read_status {
    OK,
    NO_INPUT,
    TOO_LONG
};

static int get_line(const char *prompt, char *buf, size_t sz)
{
    char fmt[40];
    int i;
    int nscanned;

    printf("%s", prompt);
    fflush(stdout);

    sprintf(fmt, "%%%zu[^\n]%%*[^\n]%%n", sz-1);
    /* read at most sz-1 characters on, discarding the rest */
    i = scanf(fmt, buf, &nscanned);
    if (i > 0) {
        getchar();
        if (nscanned >= sz) {
            return TOO_LONG;
        } else {
            return OK;
        }
    } else {
        return NO_INPUT;
    }
}

int main(void)
{
    char buf[10+1];
    int rc;

    while ((rc = get_line("Enter string> ", buf, sizeof buf)) != NO_INPUT) {
        if (rc == TOO_LONG) {
            printf("Input too long: ");
        }
        printf("->%s<-\n", buf);
    }
    return 0;
}

Uno de los otros problemas scanfes su comportamiento en caso de desbordamiento. Por ejemplo, al leer un int:

int i;
scanf("%d", &i);

lo anterior no se puede utilizar de forma segura en caso de desbordamiento. Incluso en el primer caso, leer una cadena es mucho más sencillo que hacerlo fgetscon scanf.

Alok Singhal avatar Mar 12 '2010 06:03 Alok Singhal

Sí, tiene usted razón. Hay una falla de seguridad importante en scanffamily( scanf, sscanf, fscanf..etc), especialmente cuando se lee una cadena, porque no tienen en cuenta la longitud del buffer (en el que están leyendo).

Ejemplo:

char buf[3];
sscanf("abcdef","%s",buf);

Claramente, el búfer bufpuede contener 3caracteres MAX. Pero sscanfintentará ingresarlo "abcdef"provocando un desbordamiento del búfer.

codaddict avatar Mar 12 '2010 03:03 codaddict