Pourquoi la fonction gets est-elle si dangereuse qu'elle ne devrait pas être utilisée?

229

Lorsque j'essaie de compiler du code C qui utilise la gets()fonction avec GCC, j'obtiens cet avertissement:

(.text + 0x34): avertissement: la fonction `gets 'est dangereuse et ne doit pas être utilisée.

Je me souviens que cela a quelque chose à voir avec la protection et la sécurité de la pile, mais je ne sais pas exactement pourquoi.

Comment puis-je supprimer cet avertissement et pourquoi existe-t-il un tel avertissement concernant l'utilisation gets()?

Si gets()c'est si dangereux, alors pourquoi ne pouvons-nous pas le supprimer?

vinit dhatrak
la source

Réponses:

179

Pour utiliser en getstoute sécurité, vous devez savoir exactement combien de caractères vous lirez, afin de pouvoir créer un tampon suffisamment grand. Vous saurez seulement que si vous savez exactement quelles données vous lirez.

Au lieu d'utiliser gets, vous voulez utiliser fgets, qui a la signature

char* fgets(char *string, int length, FILE * stream);

( fgets, s'il lit une ligne entière, laissera le '\n'dans la chaîne; vous devrez y faire face.)

Il est resté une partie officielle du langage jusqu'à la norme ISO C de 1999, mais il a été officiellement supprimé par la norme de 2011. La plupart des implémentations C le supportent toujours, mais au moins gcc émet un avertissement pour tout code qui l'utilise.

Thomas Owens
la source
79
Ce n'est en fait pas gcc qui avertit, c'est la glibc qui contient un pragma ou un attribut sur gets()lequel le compilateur émet un avertissement lorsqu'il est utilisé.
fuz
@fuz en fait, ce n'est même pas seulement le compilateur qui prévient: l'avertissement cité dans l'OP a été imprimé par l'éditeur de liens!
Ruslan
163

Pourquoi est gets()dangereux

Le premier ver Internet (le Morris Internet Worm ) s'est échappé il y a environ 30 ans (1988-11-02), et il a utilisé gets()un débordement de tampon comme l'une de ses méthodes de propagation d'un système à l'autre. Le problème de base est que la fonction ne sait pas quelle est la taille du tampon, donc elle continue de lire jusqu'à ce qu'elle trouve une nouvelle ligne ou rencontre EOF, et peut déborder les limites du tampon qui lui a été donné.

Vous devez oublier que vous avez déjà entendu dire que cela gets()existait.

La norme C11 ISO / IEC 9899: 2011 a été éliminée en gets()tant que fonction standard, qui est A Good Thing ™ (elle était formellement marquée comme «obsolète» et «obsolète» dans ISO / IEC 9899: 1999 / Cor.3: 2007 - Rectificatif technique 3 pour C99, puis supprimé dans C11). Malheureusement, il restera dans les bibliothèques pendant de nombreuses années (ce qui signifie «décennies») pour des raisons de rétrocompatibilité. Si cela ne tenait qu'à moi, la mise en œuvre de gets()deviendrait:

char *gets(char *buffer)
{
    assert(buffer != 0);
    abort();
    return 0;
}

Étant donné que votre code se bloquera de toute façon, tôt ou tard, il est préférable de régler le problème plus tôt que tard. Je serais prêt à ajouter un message d'erreur:

fputs("obsolete and dangerous function gets() called\n", stderr);

Les versions modernes du système de compilation Linux génèrent des avertissements si vous établissez un lien gets()- et également pour certaines autres fonctions qui ont également des problèmes de sécurité ( mktemp(),…).

Alternatives à gets()

fgets ()

Comme tout le monde l' a déjà dit, l'alternative canonique gets()est en fgets()spécifiant stdinque le flux de fichiers.

char buffer[BUFSIZ];

while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
    ...process line of data...
}

Ce que personne d'autre n'a encore mentionné, c'est qu'il gets()n'inclut pas la nouvelle ligne mais le fgets()fait. Ainsi, vous devrez peut-être utiliser un wrapper fgets()qui supprime la nouvelle ligne:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        size_t len = strlen(buffer);
        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        return buffer;
    }
    return 0;
}

Ou mieux:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        buffer[strcspn(buffer, "\n")] = '\0';
        return buffer;
    }
    return 0;
}

De plus, comme le souligne caf dans un commentaire et paxdiablo le montre dans sa réponse, fgets()vous pourriez avoir des données sur une ligne. Mon code wrapper laisse ces données à lire la prochaine fois; vous pouvez facilement le modifier pour engloutir le reste de la ligne de données si vous préférez:

        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        else
        {
             int ch;
             while ((ch = getc(fp)) != EOF && ch != '\n')
                 ;
        }

Le problème résiduel est de savoir comment signaler les trois états de résultat différents - EOF ou erreur, ligne lue et non tronquée, et ligne partielle lue mais les données ont été tronquées.

Ce problème ne se pose pas gets()car il ne sait pas où se termine votre tampon et piétine joyeusement au-delà de la fin, faisant des ravages sur votre disposition de mémoire magnifiquement entretenue, gâchant souvent la pile de retour (un débordement de pile ) si le tampon est alloué sur la pile, ou le piétinement sur les informations de contrôle si le tampon est alloué dynamiquement, ou la copie de données sur d'autres précieuses variables globales (ou module) si le tampon est alloué statiquement. Rien de tout cela n'est une bonne idée - ils incarnent l'expression «comportement indéfini».


Il existe également le TR 24731-1 (rapport technique du comité de normalisation C) qui offre des alternatives plus sûres à une variété de fonctions, notamment gets():

§6.5.4.1 La gets_sfonction

Synopsis

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

Contraintes d'exécution

sne doit pas être un pointeur nul. nne doit pas être égal à zéro ni supérieur à RSIZE_MAX. Un caractère de nouvelle ligne, une fin de fichier ou une erreur de lecture doit se produire dans la lecture des n-1caractères de stdin. 25)

3 S'il y a une violation de contrainte d'exécution, s[0]est défini sur le caractère nul et les caractères sont lus et ignorés stdinjusqu'à ce qu'un caractère de nouvelle ligne soit lu, ou à la fin du fichier ou qu'une erreur de lecture se produise.

La description

4 La gets_sfonction lit au plus un de moins que le nombre de caractères spécifié par n dans le flux pointé par stdin, dans le tableau pointé par s. Aucun caractère supplémentaire n'est lu après un caractère de nouvelle ligne (qui est supprimé) ou après la fin du fichier. Le caractère de nouvelle ligne supprimé ne compte pas dans le nombre de caractères lus. Un caractère nul est écrit immédiatement après le dernier caractère lu dans le tableau.

5 Si la fin du fichier est rencontrée et qu'aucun caractère n'a été lu dans le tableau, ou si une erreur de lecture se produit pendant l'opération, le s[0]paramètre est défini sur le caractère nul et les autres éléments de sprennent des valeurs non spécifiées.

Pratique recommandée

6 La fgetsfonction permet aux programmes correctement écrits de traiter en toute sécurité les lignes d'entrée trop longtemps pour être stockées dans le tableau de résultats. En général, cela nécessite que les appelants soient fgetsattentifs à la présence ou à l'absence d'un caractère de nouvelle ligne dans le tableau de résultats. Envisagez d'utiliser fgets(avec tout traitement nécessaire basé sur des caractères de nouvelle ligne) au lieu de gets_s.

25) La gets_sfonction, contrairement à gets, en fait une violation de contrainte d'exécution pour qu'une ligne d'entrée déborde le tampon pour le stocker. Contrairement à fgets, gets_smaintient une relation un à un entre les lignes d'entrée et les appels réussis vers gets_s. Les programmes qui utilisent getss'attendent à une telle relation.

Les compilateurs Microsoft Visual Studio implémentent une approximation de la norme TR 24731-1, mais il existe des différences entre les signatures implémentées par Microsoft et celles du TR.

La norme C11, ISO / IEC 9899-2011, inclut TR24731 dans l'annexe K en tant que partie facultative de la bibliothèque. Malheureusement, il est rarement implémenté sur des systèmes de type Unix.


getline() - POSIX

POSIX 2008 fournit également une alternative sûre aux gets()appels getline(). Il alloue de l'espace de manière dynamique à la ligne, vous avez donc besoin de la libérer. Il supprime donc la limitation de la longueur de ligne. Il renvoie également la longueur des données lues, ou -1(et non EOF!), Ce qui signifie que les octets nuls dans l'entrée peuvent être traités de manière fiable. Il existe également une variante «choisissez votre propre délimiteur à caractère unique» appelée getdelim(); cela peut être utile si vous traitez la sortie d' find -print0où les extrémités des noms de fichiers sont marquées avec un caractère ASCII NUL '\0', par exemple.

Jonathan Leffler
la source
8
Il convient également de le souligner fgets()et votre fgets_wrapper()version laissera la partie de fin d'une ligne trop longue dans le tampon d'entrée, pour être lue par la prochaine fonction d'entrée. Dans de nombreux cas, vous souhaiterez lire et supprimer ces caractères.
caf
5
Je me demande pourquoi ils n'ont pas ajouté d'alternative fgets () qui permet d'utiliser ses fonctionnalités sans avoir à faire un appel idiot. Par exemple, une variante fgets qui a renvoyé le nombre d'octets lus dans la chaîne permettrait au code de voir facilement si le dernier octet lu était une nouvelle ligne. Si le comportement de passage d'un pointeur nul pour le tampon était défini comme «lire et supprimer jusqu'à n-1 octets jusqu'à la nouvelle ligne», cela permettrait au code de supprimer facilement la queue des lignes surdimensionnées.
supercat
2
@supercat: Oui, je suis d'accord - c'est dommage. L'approche la plus proche est probablement POSIX getline()et son parent getdelim(), qui renvoient la longueur de la «ligne» lue par les commandes, en allouant l'espace nécessaire pour pouvoir stocker la ligne entière. Même cela peut poser des problèmes si vous vous retrouvez avec un fichier JSON à une seule ligne de plusieurs gigaoctets; pouvez-vous vous permettre toute cette mémoire? (Et pendant que nous y sommes, nous pouvons avoir strcpy()et les strcat()variantes qui renvoient un pointeur sur l'octet nul à la fin etc.?)
Jonathan Leffler
4
@supercat: l'autre problème fgets()est que si le fichier contient un octet nul, vous ne pouvez pas dire combien de données il y a après l'octet nul jusqu'à la fin de la ligne (ou EOF). strlen()peut uniquement signaler jusqu'à l'octet nul dans les données; après cela, c'est une conjecture et donc presque certainement une erreur.
Jonathan Leffler
7
"oublie que tu as déjà entendu que ça gets()existait." Quand je fais cela, je le rencontre à nouveau et je reviens ici. Piratez-vous stackoverflow pour obtenir des votes positifs?
candied_orange
21

Parce getsqu'il ne fait aucune vérification en récupérant des octets de stdin et en les plaçant quelque part. Un exemple simple:

char array1[] = "12345";
char array2[] = "67890";

gets(array1);

Maintenant, tout d'abord, vous êtes autorisé à entrer le nombre de caractères que vous voulez, getscela ne vous importera pas. Deuxièmement, les octets sur la taille du tableau dans lequel vous les placez (dans ce cas array1) écraseront tout ce qu'ils trouveront en mémoire car getsils les écriront. Dans l'exemple précédent, cela signifie que si vous saisissez "abcdefghijklmnopqrts"peut-être, de manière imprévisible, il sera également écrasé array2ou autre.

La fonction n'est pas sûre car elle suppose une entrée cohérente. NE L'UTILISEZ JAMAIS!

Jack
la source
3
Ce qui rend getscarrément inutilisable, c'est qu'il n'a pas de paramètre de longueur / nombre de tableaux qu'il prend; s'il avait été là, ce serait juste une autre fonction standard C ordinaire.
legends2k
@ legends2k: Je suis curieux de savoir à quoi getsservait l' usage et pourquoi aucune variante standard de fgets n'a été aussi pratique pour les cas d'utilisation où la nouvelle ligne n'est pas souhaitée dans le cadre de l'entrée?
supercat
1
@supercat a getsété, comme son nom l'indique, conçu pour obtenir une chaîne stdin, mais la raison pour ne pas avoir de paramètre de taille peut provenir de l'esprit de C : Trust the programmer. Cette fonction a été supprimée en C11 et le remplacement donné gets_sprend la taille du tampon d'entrée. Je n'ai cependant aucune idée de la fgetspartie.
legends2k
@ legends2k: Le seul contexte que je peux voir dans lequel getspourrait être excusable serait si l'on utilisait un système d'E / S à mémoire tampon matérielle qui était physiquement incapable de soumettre une ligne sur une certaine longueur, et la durée de vie prévue du programme était plus courte que la durée de vie du matériel. Dans ce cas, si le matériel est incapable de soumettre des lignes de plus de 127 octets, cela peut être justifié getsdans un tampon de 128 octets, bien que je pense que les avantages de pouvoir spécifier un tampon plus court lorsque vous attendez une entrée plus petite justifieraient plus que Coût.
supercat
@ legends2k: En fait, ce qui aurait pu être idéal aurait été d'avoir un "pointeur de chaîne" identifier un octet qui sélectionnerait parmi quelques formats de chaîne / tampon / info-tampon différents, avec une valeur d'octet de préfixe indiquant une structure qui contenait l'octet de préfixe [plus le remplissage], plus la taille du tampon, la taille utilisée et l'adresse du texte réel. Un tel modèle permettrait au code de passer une sous-chaîne arbitraire (pas seulement la queue) d'une autre chaîne sans avoir à copier quoi que ce soit, et permettrait aux méthodes comme getset strcatd'accepter en toute sécurité autant que cela conviendrait.
supercat
16

Vous ne devez pas l'utiliser getscar il n'a aucun moyen d'arrêter un débordement de tampon. Si l'utilisateur tape plus de données que ne peut en contenir votre tampon, vous vous retrouverez très probablement avec une corruption ou pire.

En fait, l'ISO a en fait pris la décision de retirer gets de la norme C (à partir de C11, bien qu'elle soit déconseillée en C99) qui, étant donné le taux élevé de compatibilité ascendante, devrait être une indication de la gravité de cette fonction.

La bonne chose à faire est d'utiliser la fgetsfonction avec le stdindescripteur de fichier car vous pouvez limiter les caractères lus par l'utilisateur.

Mais cela a aussi ses problèmes tels que:

  • les caractères supplémentaires saisis par l'utilisateur seront récupérés la prochaine fois.
  • il n'y a pas de notification rapide que l'utilisateur a entré trop de données.

À cette fin, presque tous les codeurs C à un moment donné de leur carrière écriront également un wrapper plus utile fgets. Voici la mienne:

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

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

    // Get line with buffer overrun protection.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }
    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.
    if (buff[strlen(buff)-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[strlen(buff)-1] = '\0';
    return OK;
}

avec du code de test:

// Test program for getLine().

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

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        printf ("No input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long\n");
        return 1;
    }

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

    return 0;
}

Il offre les mêmes protections que fgetsdans la mesure où il empêche les débordements de tampon mais il informe également l'appelant de ce qui s'est passé et efface les caractères en excès afin qu'ils n'affectent pas votre prochaine opération d'entrée.

N'hésitez pas à l'utiliser comme vous le souhaitez, je le libère sous la licence "faites ce que vous voulez bien" :-)

paxdiablo
la source
En fait, la norme C99 d'origine n'a pas déprécié explicitement gets()ni dans la section 7.19.7.7 où elle est définie, ni dans la section 7.26.9 Orientations futures de la bibliothèque et la sous-section pour <stdio.h>. Il n'y a même pas de note de bas de page indiquant qu'il est dangereux. (Cela dit, je vois "C'est obsolète dans ISO / IEC 9899: 1999 / Cor.3: 2007 (E))" dans la réponse de Yu Hao .) Mais C11 l'a supprimé de la norme - et pas avant l'heure!
Jonathan Leffler
int getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL)cache la size_tà la intconversion de sz. sz > INT_MAX || sz < 2attraperait des valeurs étranges de sz.
chux
if (buff[strlen(buff)-1] != '\n') {est un exploit de pirate, car le premier caractère saisi par l'utilisateur malveillant pourrait être un caractère nul intégré rendant buff[strlen(buff)-1]UB. while (((ch = getchar())...a des problèmes si un utilisateur entre un caractère nul.
chux
12

fgets .

Pour lire à partir du stdin:

char string[512];

fgets(string, sizeof(string), stdin); /* no buffer overflows here, you're safe! */
Thiago Silveira
la source
6

Vous ne pouvez pas supprimer les fonctions de l'API sans casser l'API. Si vous le faites, de nombreuses applications ne seront plus compilées ou exécutées du tout.

C'est la raison pour laquelle une référence donne:

La lecture d'une ligne qui déborde du tableau pointé par s entraîne un comportement indéfini. L'utilisation de fgets () est recommandée.

Gerd Klima
la source
4

J'ai lu récemment, dans un article USENET àcomp.lang.c , qui gets()est en train d'être supprimé de la norme. WOOHOO

Vous serez heureux de savoir que le comité vient de voter (à l'unanimité, en fin de compte) pour supprimer gets () du projet également.

pmg
la source
3
Il est excellent qu'il soit retiré de la norme. Cependant, la plupart des implémentations le fourniront comme une «extension désormais non standard» pendant au moins les 20 prochaines années, en raison de la compatibilité descendante.
Jonathan Leffler
1
Oui, c'est vrai, mais lorsque vous compilez avec gcc -std=c2012 -pedantic ...gets () ne passera pas. (Je viens de composer le -stdparamètre)
pmg
4

Dans C11 (ISO / IEC 9899: 201x), gets()a été supprimé. (Il est déconseillé dans ISO / IEC 9899: 1999 / Cor.3: 2007 (E))

De plus fgets(), C11 présente une nouvelle alternative sûre gets_s():

C11 K.3.5.4.1 La gets_sfonction

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

Cependant, dans la section Pratique recommandée , fgets()est toujours préférable.

La fgetsfonction permet aux programmes correctement écrits de traiter en toute sécurité les lignes d'entrée trop longtemps pour être stockées dans le tableau de résultats. En général, cela nécessite que les appelants soient fgetsattentifs à la présence ou à l'absence d'un caractère de nouvelle ligne dans le tableau de résultats. Envisagez d'utiliser fgets(avec tout traitement nécessaire basé sur des caractères de nouvelle ligne) au lieu de gets_s.

Yu Hao
la source
3

gets()est dangereux car il est possible pour l'utilisateur de planter le programme en tapant trop dans l'invite. Il ne peut pas détecter la fin de la mémoire disponible, donc si vous allouez une quantité de mémoire trop petite à cet effet, cela peut provoquer une erreur de segmentation et un crash. Parfois, il semble très improbable qu'un utilisateur tape 1000 lettres dans une invite destinée au nom d'une personne, mais en tant que programmeurs, nous devons rendre nos programmes à l'épreuve des balles. (cela peut également représenter un risque pour la sécurité si un utilisateur peut planter un programme système en envoyant trop de données).

fgets() vous permet de spécifier le nombre de caractères à retirer du tampon d'entrée standard, afin qu'ils ne dépassent pas la variable.

Aradhana Mohanty
la source
Notez que le vrai danger n'est pas de pouvoir planter votre programme, mais de lui faire exécuter du code arbitraire . (En général, exploiter un comportement indéfini .)
Tanz87
2

Je voudrais adresser une invitation sérieuse à tous les responsables de bibliothèques C qui sont toujours inclus getsdans leurs bibliothèques "juste au cas où quelqu'un en dépendrait encore": veuillez remplacer votre implémentation par l'équivalent de

char *gets(char *str)
{
    strcpy(str, "Never use gets!");
    return str;
}

Cela vous aidera à vous assurer que personne n'en dépend encore. Je vous remercie.

Sommet Steve
la source
2

La fonction C gets est dangereuse et a été une erreur très coûteuse. Tony Hoare le souligne pour une mention spécifique dans son discours "Null References: The Billion Dollar Mistake":

http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

Toute l'heure mérite d'être regardée, mais pour ses commentaires, la vue à partir de 30 minutes avec le détail obtient une critique d'environ 39 minutes.

J'espère que cela vous mettra en appétit pour toute la discussion, ce qui attire l'attention sur la façon dont nous avons besoin de preuves de correction plus formelles dans les langues et sur la façon dont les concepteurs de langues devraient être blâmés pour les erreurs dans leurs langues, pas sur le programmeur. Cela semble avoir été la raison douteuse pour les concepteurs de mauvais langages de rejeter la faute sur les programmeurs sous le couvert de la «liberté du programmeur».

user3717661
la source