web-dev-qa-db-fra.com

Inconvénients de scanf

Je veux connaître les inconvénients de scanf().

Dans de nombreux sites, j'ai lu que l'utilisation de scanf pouvait provoquer des débordements de tampon. Quelle est la raison pour ça? Y a-t-il d'autres inconvénients avec scanf?

56
karthi_ms

Les problèmes avec scanf sont (au minimum):

  • en utilisant %s pour obtenir une chaîne de l'utilisateur, ce qui entraîne la possibilité que la chaîne soit plus longue que votre tampon, provoquant un débordement.
  • la possibilité d'un échec de l'analyse laissant votre pointeur de fichier dans un emplacement indéterminé.

Je préfère de loin utiliser fgets pour lire des lignes entières afin que vous puissiez limiter la quantité de données lues. Si vous avez un tampon 1K et que vous y lisez une ligne avec fgets vous pouvez dire si la ligne était trop longue par le fait qu'il n'y a pas de caractère de fin de ligne (dernière ligne d'un fichier sans une nouvelle ligne malgré ).

Ensuite, vous pouvez vous plaindre à l'utilisateur ou allouer plus d'espace pour le reste de la ligne (en continu si nécessaire jusqu'à ce que vous ayez suffisamment d'espace). Dans les deux cas, il n'y a aucun risque de débordement de tampon.

Une fois que vous avez lu la ligne, vous savez que vous êtes positionné à la ligne suivante, il n'y a donc aucun problème. Vous pouvez alors sscanf votre chaîne au contenu de votre cœur sans avoir à enregistrer et restaurer le pointeur de fichier pour une relecture.

Voici un extrait de code que j'utilise fréquemment pour éviter tout débordement de tampon lors de la demande d'informations à l'utilisateur.

Il pourrait être facilement ajusté pour utiliser un fichier autre que l'entrée standard si nécessaire et vous pourriez également lui allouer son propre tampon (et continuer à l'augmenter jusqu'à ce qu'il soit assez grand) avant de le rendre à l'appelant (bien que l'appelant soit alors responsable pour le libérer, bien sûr).

#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;

    // 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.
    size_t lastPos = strlen(buff) - 1;
    if (buff[lastPos] != '\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[lastPos] = '\0';
    return OK;
}

Et, un pilote de test pour cela:

// 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;
}

Enfin, un test pour le montrer en action:

$ ./tstprg
Enter string>[CTRL-D]
No input

$ ./tstprg
Enter string> a
OK [a]

$ ./tstprg
Enter string> hello
OK [hello]

$ ./tstprg
Enter string> hello there
Input too long [hello the]

$ ./tstprg
Enter string> i am pax
OK [i am pax]
52
paxdiablo

Jusqu'à présent, la plupart des réponses semblent se concentrer sur le problème de dépassement de tampon de chaîne. En réalité, les spécificateurs de format qui peuvent être utilisés avec les fonctions scanf prennent en charge le paramètre explicite largeur de champ, qui limite la taille maximale de l'entrée et empêche le débordement de la mémoire tampon. Cela rend les accusations populaires de dangers de débordement de tampon de chaîne présentes dans scanf pratiquement sans fondement. Affirmer que scanf est en quelque sorte analogue à gets à cet égard est complètement incorrect. Il y a une différence qualitative majeure entre scanf et gets: scanf fournit à l'utilisateur des fonctionnalités empêchant le débordement du tampon de chaîne, tandis que gets ne le fait pas .

On peut dire que ces fonctionnalités scanf sont difficiles à utiliser, car la largeur du champ doit être incorporée dans la chaîne de format (il n'y a aucun moyen de la passer à travers un argument variadic, comme cela peut être fait dans printf). C'est en fait vrai. scanf est en effet assez mal conçu à cet égard. Mais néanmoins, toute affirmation selon laquelle scanf est d'une manière ou d'une autre désespérément brisée en ce qui concerne la sécurité du débordement du tampon de chaîne est complètement fausse et généralement faite par des programmeurs paresseux.

Le vrai problème avec scanf a une nature complètement différente, même s'il s'agit aussi de débordement. Lorsque la fonction scanf est utilisée pour convertir des représentations décimales de nombres en valeurs de types arithmétiques, elle n'offre aucune protection contre les dépassements arithmétiques. Si un débordement se produit, scanf produit un comportement indéfini. Pour cette raison, la seule manière appropriée d'effectuer la conversion dans la bibliothèque standard C est les fonctions de strto... famille.

Donc, pour résumer ce qui précède, le problème avec scanf est qu'il est difficile (bien que possible) d'utiliser correctement et en toute sécurité avec les tampons de chaîne. Et il est impossible d'utiliser en toute sécurité pour la saisie arithmétique. Ce dernier est le vrai problème. Le premier n'est qu'un inconvénient.

P.S. Ce qui précède est destiné à concerner toute la famille des fonctions scanf (y compris aussi fscanf et sscanf). Avec scanf spécifiquement, le problème évident est que l'idée même d'utiliser une fonction strictement formatée pour lire potentiellement l'entrée interactive est plutôt discutable.

53
AnT

De la FAQ comp.lang.c: Pourquoi tout le monde dit de ne pas utiliser scanf? Que dois-je utiliser à la place?

scanf a un certain nombre de problèmes - voir les questions 12.17 , 12.18a et 12.19 . De plus, son format %s A le même problème que gets() (voir question 12.2 ) - il est difficile de garantir que le tampon de réception ne débordera pas. [note]

Plus généralement, scanf est conçu pour une entrée formatée et relativement structurée (son nom est en fait dérivé de "scan formaté"). Si vous faites attention, il vous dira s'il a réussi ou échoué, mais il ne peut vous dire qu'environ où il a échoué et pas du tout comment ni pourquoi. Vous avez très peu d'occasions de faire une récupération d'erreur.

Pourtant, l'entrée utilisateur interactive est l'entrée la moins structurée qui soit. Une interface utilisateur bien conçue permettra à l'utilisateur de taper à peu près n'importe quoi - pas seulement des lettres ou des signes de ponctuation lorsque des chiffres étaient attendus, mais aussi plus ou moins de caractères que prévu, ou aucun caractère du tout ( ie , juste la touche RETOUR), ou EOF prématuré, ou quoi que ce soit. Il est presque impossible de traiter avec élégance tous ces problèmes potentiels lorsque vous utilisez scanf; il est beaucoup plus facile de lire des lignes entières (avec fgets ou similaire), puis de les interpréter, en utilisant sscanf ou d'autres techniques. (Des fonctions comme strtol, strtok et atoi sont souvent utiles; voir aussi les questions 12.16 et 13.6 .) Si vous utilisez une variante scanf, assurez-vous de vérifier la valeur de retour pour vous assurer que le nombre attendu d'éléments a été trouvé. De plus, si vous utilisez %s, Assurez-vous de vous prémunir contre le dépassement de tampon.

Notez, en passant, que les critiques de scanf ne sont pas nécessairement des mises en examen de fscanf et sscanf. scanf lit à partir de stdin, qui est généralement un clavier interactif et est donc le moins contraint, ce qui entraîne le plus de problèmes. Lorsqu'un fichier de données a un format connu, en revanche, il peut être approprié de le lire avec fscanf. Il est parfaitement approprié d'analyser les chaînes avec sscanf (tant que la valeur de retour est vérifiée), car il est si facile de reprendre le contrôle, de redémarrer l'analyse, de supprimer l'entrée si elle ne correspond pas, etc.

Liens supplémentaires:

Références: K & R2 Sec. 7.4 p. 159

12
jamesdlin

Oui, tu as raison. Il y a une faille de sécurité majeure dans la famille scanf (scanf, sscanf, fscanf .. etc) surtout lors de la lecture d'une chaîne, car ils ne le font pas tenir compte de la longueur du tampon (dans lequel ils lisent).

Exemple:

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

il est clair que le tampon buf peut contenir MAX 3 car. Mais le sscanf essaiera de mettre "abcdef" en elle provoquant un débordement de tampon.

5
codaddict

Il est très difficile de faire en sorte que scanf fasse ce que vous voulez. Bien sûr, vous pouvez, mais des choses comme scanf("%s", buf); sont aussi dangereuses que gets(buf);, comme tout le monde l'a dit.

Par exemple, ce que paxdiablo fait dans sa fonction de lecture peut être fait avec quelque chose comme:

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

Ce qui précède lira une ligne, stockera les 10 premiers caractères non-retour à la ligne dans buf, puis supprimera tout jusqu'à (et y compris) un retour à la ligne. Ainsi, la fonction de paxdiablo pourrait être écrite en utilisant scanf de la manière suivante:

#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;
}

Un des autres problèmes avec scanf est son comportement en cas de débordement. Par exemple, lors de la lecture d'un int:

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

ce qui précède ne peut pas être utilisé en toute sécurité en cas de débordement. Même dans le premier cas, la lecture d'une chaîne est beaucoup plus simple à faire avec fgets plutôt qu'avec scanf.

5
Alok Singhal

De nombreuses réponses ici discutent des problèmes potentiels de débordement de l'utilisation de scanf("%s", buf), mais la dernière spécification POSIX résout plus ou moins ce problème en fournissant un caractère d'allocation d'affectation m qui peut être utilisé au format spécificateurs pour c, s et [ formats. Cela permettra à scanf d'allouer autant de mémoire que nécessaire avec malloc (elle doit donc être libérée ultérieurement avec free).

Un exemple de son utilisation:

char *buf;
scanf("%ms", &buf); // with 'm', scanf expects a pointer to pointer to char.

// use buf

free(buf);

Voir ici . L'inconvénient de cette approche est qu'elle est un ajout relativement récent à la spécification POSIX et qu'elle n'est pas spécifiée du tout dans la spécification C, donc elle reste plutôt non transférable pour l'instant.

3
dreamlax

Il y a un gros problème avec les fonctions de type scanf - le manque de sécurité de type any. Autrement dit, vous pouvez coder ceci:

int i;
scanf("%10s", &i);

Enfer, même cela est "bien":

scanf("%10s", i);

C'est pire que les fonctions de type printf, car scanf attend un pointeur, donc les plantages sont plus probables.

Bien sûr, il existe des vérificateurs de spécificateurs de format, mais ceux-ci ne sont pas parfaits et bien, ils ne font pas partie du langage ou de la bibliothèque standard.

3
Vladimir Veljkovic

L'avantage de scanf est une fois que vous apprenez comment utiliser l'outil, comme vous devriez toujours le faire en C, il a des cas d'utilisation extrêmement utiles. Vous pouvez apprendre à utiliser scanf et vos amis en lisant et en comprenant le manuel . Si vous ne pouvez pas parcourir ce manuel sans de graves problèmes de compréhension, cela indiquerait probablement que vous ne connaissez pas très bien C.


scanf et ses amis ont souffert de choix de conception malheureux qui ont rendu difficile (et parfois impossible) une utilisation correcte sans lire la documentation, comme d'autres les réponses ont montré. Cela se produit malheureusement tout au long de C, donc si je déconseillais d'utiliser scanf, je déconseillerais probablement d'utiliser C.

L'un des plus gros inconvénients semble être purement la réputation qu'il a gagnée parmi les non-initiés ; comme pour de nombreuses fonctionnalités utiles de C, nous devons être bien informés avant de l'utiliser. La clé est de réaliser que, comme avec le reste de C, cela semble succinct et idiomatique, mais cela peut être subtilement trompeur. Ceci est omniprésent en C; il est facile pour les débutants d'écrire du code qui, selon eux, a du sens et pourrait même fonctionner pour eux au début, mais n'a pas de sens et peut échouer de manière catastrophique.

Par exemple, les non-initiés s'attendent généralement à ce que le délégué %s Provoque la lecture de une ligne, et même si cela peut sembler intuitif, ce n'est pas nécessairement vrai. Il est plus approprié de décrire le champ lu comme un mot. La lecture du manuel est fortement conseillée pour chaque fonction.

Quelle serait la réponse à cette question sans mentionner son manque de sécurité et le risque de débordements de tampon? Comme nous l'avons déjà vu, C n'est pas un langage sûr, et nous permettra de réduire les coins, éventuellement d'appliquer une optimisation au détriment de l'exactitude ou plus probablement parce que nous sommes des programmeurs paresseux. Ainsi, lorsque nous savons que le système ne recevra jamais une chaîne supérieure à un nombre fixe d'octets, nous avons la possibilité de déclarer un tableau de cette taille et de renoncer à la vérification des limites. Je ne vois pas vraiment cela comme une chute; c'est une option. Encore une fois, la lecture du manuel est fortement conseillée et nous dévoilerait cette option.

Les programmeurs paresseux ne sont pas les seuls à être piqués par scanf. Il n'est pas rare de voir des gens essayer de lire les valeurs de float ou double en utilisant %d, Par exemple. Ils se trompent généralement en pensant que l'implémentation effectuera une sorte de conversion en arrière-plan, ce qui aurait du sens car des conversions similaires se produisent dans le reste du langage, mais ce n'est pas le cas ici. Comme je l'ai dit plus tôt, scanf et ses amis (et en effet le reste de C) sont trompeurs; ils semblent succincts et idiomatiques, mais ils ne le sont pas.

Les programmeurs inexpérimentés ne sont pas obligés de considérer le succès de l'opération . Supposons que l'utilisateur entre quelque chose de entièrement non numérique lorsque nous avons dit à scanf de lire et de convertir une séquence de chiffres décimaux en utilisant %d. La seule façon dont nous pouvons intercepter ces données erronées est de vérifier la valeur de retour, et à quelle fréquence prenons-nous la peine de vérifier la valeur de retour?

Tout comme fgets, lorsque scanf et des amis ne lisent pas ce qu'on leur dit de lire, le flux sera laissé dans un état inhabituel; - Dans le cas de fgets, s'il n'y a pas suffisamment d'espace pour stocker une ligne complète, le reste de la ligne non lue peut être traité à tort comme s'il s'agissait d'une nouvelle ligne quand ce n'est pas le cas. - Dans le cas de scanf et amis, une conversion a échoué comme indiqué ci-dessus, les données erronées sont laissées non lues sur le flux et peuvent être traitées par erreur comme si elles faisaient partie d'un champ différent.

Il n'est pas plus facile d'utiliser scanf et ses amis que d'utiliser fgets. Si nous vérifions le succès en recherchant un '\n' Lorsque nous utilisons fgets ou en inspectant la valeur de retour lorsque nous utilisons scanf et amis, et nous constatons que nous ' J'ai lu une ligne incomplète en utilisant fgets ou j'ai échoué à lire un champ en utilisant scanf, alors nous sommes confrontés à la même réalité: nous sommes susceptibles de rejeter l'entrée (généralement jusqu'à et y compris la prochaine nouvelle ligne)! Yuuuuuuck!

Malheureusement, scanf à la fois, il est difficile (non intuitif) et facile (moins de frappes) de supprimer les entrées de cette manière. Face à cette réalité de rejet des entrées utilisateurs, certains ont essayé scanf("%*[^\n]%*c");, ne réalisant pas que le délégué %*[^\n] échouera lorsqu'il ne rencontrera rien d'autre qu'un retour à la ligne, et donc le retour à la ligne sera toujours laissé sur le flux.

Une légère adaptation, en séparant les deux délégués de format et nous voyons un certain succès ici: scanf("%*[^\n]"); getchar();. Essayez de faire cela avec si peu de touches en utilisant un autre outil;)

3
autistic

Problèmes rencontrés avec la famille *scanf():

  • Potentiel de dépassement de tampon avec% s et% [spécificateurs de conversion. Oui, vous pouvez spécifier une largeur de champ maximale, mais contrairement à printf(), vous ne pouvez pas en faire un argument dans l'appel scanf(); il doit être codé en dur dans le spécificateur de conversion.
  • Potentiel de dépassement arithmétique avec% d,% i, etc.
  • Capacité limitée à détecter et rejeter les entrées mal formées. Par exemple, "12w4" n'est pas un entier valide, mais scanf("%d", &value); réussira à convertir et à affecter 12 à value, laissant le "w4" coincé dans le flux d'entrée pour gâcher une lecture future. Idéalement, la chaîne d'entrée entière devrait être rejetée, mais scanf() ne vous donne pas un mécanisme simple pour le faire.

Si vous savez que votre entrée sera toujours bien formée avec des chaînes de longueur fixe et des valeurs numériques qui ne flirteront pas avec le débordement, alors scanf() est un excellent outil. Si vous avez affaire à une entrée interactive ou à une entrée qui n'est pas garantie d'être bien formée, utilisez autre chose.

3
John Bode