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

Lorsque j’essaie de comstackr du code C qui utilise la fonction gets() avec GCC,

j’ai compris

avertissement :

(.text + 0x34): warning: la fonction `get ‘est dangereuse et ne doit pas être utilisée.

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

Est-ce que quelqu’un peut m’aider à supprimer cet avertissement et expliquer pourquoi il y a un tel avertissement concernant l’utilisation de gets() ?

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

Pour utiliser en toute sécurité, vous devez savoir exactement combien de caractères vous allez lire, de sorte que vous puissiez rendre votre tampon suffisamment grand. Vous ne le saurez que si vous savez exactement quelles données vous allez lire.

Au lieu d’utiliser fgets , vous voulez utiliser des fgets , qui ont la signature

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

( fgets , s’il lit une ligne entière, laissera le '\n' dans la chaîne; vous devrez gérer cela.)

Il est resté une partie officielle de la langue 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.

Pourquoi gets() dangereux

Le premier ver internet (le Morris Internet Worm ) s’est échappé il y a environ 30 ans (1988-11-02), et il utilisait get gets() et un buffer overflow comme méthode de propagation d’un système à un autre. Le problème de base est que la fonction ne sait pas quelle est la taille du tampon, donc elle continue à 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 devriez oublier que vous avez déjà entendu que gets() existait.

La norme C11 ISO / IEC 9899: 2011 a éliminé gets() tant que fonction standard, ce qui est A Good Thing ™ (elle a été officiellement marquée comme «obsolète» et «déconseillée» dans ISO / IEC 9899: 1999 / Cor.3: 2007). – Corrigendum technique 3 pour C99, puis retiré en C11). Malheureusement, il restra dans les bibliothèques pendant de nombreuses années (c’est-à-dire des décennies) pour des raisons de rétrocompatibilité. Si cela ne tenait qu’à moi, l’implémentation de gets() deviendrait:

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

Étant donné que votre code tombera en panne de toute façon, tôt ou tard, il est préférable de régler le problème plus rapidement. Je serais prêt à append 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 liez gets() – et aussi pour certaines autres fonctions qui ont également des problèmes de sécurité ( mktemp() ,…).

Alternatives à gets()

fgets ()

Comme tout le monde l’a dit, l’alternative canonique à fgets() est fgets() spécifiant stdin comme stream 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 que gets() n’inclut pas le fgets() mais fgets() fait. Donc, vous devrez peut-être utiliser un wrapper autour de 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; } 

En outre, comme le souligne caf dans un commentaire et que paxdiablo montre dans sa réponse, avec 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 rest 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 – EOF ou erreur, lecture de ligne et non tronquée, et lecture de ligne partielle, mais les données ont été tronquées.

Ce problème ne se pose pas avec gets() car il ne sait pas où votre tampon se termine et piétine joyeusement au-delà de la fin, causant des ravages sur la disposition de votre mémoire magnifiquement tendue. est alloué sur la stack, ou piétine les informations de contrôle si le tampon est alloué dynamicment, ou copie les données sur d’autres variables globales (ou modules) précieuses si le tampon est alloué de manière statique. Aucune de ces idées n’est une bonne idée – elles incarnent l’expression «comportement indéfini».


Il existe également le TR 24731-1 (rapport technique du Comité de la norme C), qui offre des alternatives plus sûres à diverses fonctions, y compris gets() :

§6.5.4.1 La fonction de gets_s

Synopsis

 #define __STDC_WANT_LIB_EXT1__ 1 #include  char *gets_s(char *s, rsize_t n); 

Contraintes d’exécution

s ne doit pas être un pointeur nul. n ne doit ni être égal à zéro ni être supérieur à RSIZE_MAX. Un caractère de nouvelle ligne, une fin de fichier ou une erreur de lecture doivent apparaître dans les caractères n-1 de stdin . 25)

3 S’il y a une violation de contrainte à l’exécution, s[0] est défini sur le caractère nul et les caractères sont lus et supprimés de stdin jusqu’à ce qu’un caractère de nouvelle ligne soit lu, ou une fin de fichier ou une erreur de lecture.

La description

4 La fonction gets_s lit au plus un de moins que le nombre de caractères spécifié par n partir du stream pointé par stdin , dans le tableau désigné par s . Aucun caractère supplémentaire n’est lu après un caractère de nouvelle ligne (qui est ignoré) ou après la fin du fichier. Le caractère de nouvelle ligne ignoré 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, alors s[0] est défini sur le caractère nul et les autres éléments sur s prennent des valeurs non spécifiées .

Pratique recommandée

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

25) gets_s fonction gets_s fait une violation de contrainte à l’exécution pour qu’une ligne d’entrée déborde du tampon pour la stocker. Contrairement à fgets , gets_s entretient une relation gets_s entre les lignes d’entrée et les appels réussis à gets_s . Les programmes utilisés utilisent une telle relation.

Les compilateurs Microsoft Visual Studio implémentent une approximation à 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 à getline() appelée getline() . Il alloue de l’espace pour la ligne de manière dynamic, vous devez donc le libérer. Il supprime donc la limitation de 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 gérés de manière fiable. Il existe également une variante “choisir votre propre délimiteur à un seul caractère” appelée getdelim() ; Cela peut être utile si vous find -print0 le résultat de find -print0 où les extrémités des noms de fichiers sont marquées d’un caractère ASCII NUL '\0' , par exemple.

Parce que gets ne fait aucune sorte de vérification en obtenant 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é à saisir le nombre de caractères que vous souhaitez, mais cela ne vous intéressera pas. Deuxièmement, les octets de plus que la taille du tableau dans lequel vous les placez (dans ce cas, array1 ) array1 tout ce qu’ils trouvent dans la mémoire car array1 les écrira. Dans l’exemple précédent, cela signifie que si vous entrez "abcdefghijklmnopqrts" peut-être qu’imprévisiblement, il écrasera aussi array2 ou autre.

La fonction est dangereuse car elle suppose des entrées cohérentes. N’UTILISEZ JAMAIS!

Vous ne devriez pas utiliser gets car 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 probablement avec une corruption ou pire.

En fait, ISO a en fait pris la décision de supprimer gets du standard C (à partir de C11, même si elle était obsolète en C99) qui, compte tenu de leur compatibilité ascendante, devrait indiquer à quel point cette fonction était mauvaise.

La bonne chose à faire est d’utiliser la fonction fgets avec le handle de fichier stdin 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 indiquant que l’utilisateur a saisi trop de données.

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

 #include  #include  #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 ssortingng back to caller. buff[strlen(buff)-1] = '\0'; return OK; } 

avec un code de test:

 // Test program for getLine(). int main (void) { int rc; char buff[10]; rc = getLine ("Enter ssortingng> ", 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 fournit les mêmes protections que les fgets en ce sens qu’il empêche les dépassements de tampon mais avertit é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 de saisie.

N’hésitez pas à l’utiliser comme vous le souhaitez, je le cède sous la licence “fais ce que tu veux bien” 🙂

fgets .

Pour lire depuis le stdin:

 char ssortingng[512]; fgets(ssortingng, sizeof(ssortingng), stdin); /* no buffer overflows here, you're safe! */ 

Vous ne pouvez pas supprimer les fonctions de l’API sans casser l’API. Si vous le souhaitez, de nombreuses applications ne seraient plus compilées ni exécutées du tout.

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

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

J’ai lu récemment, dans une publication USENET sur comp.lang.c , que la comp.lang.c gets() est supprimée du standard. WOOHOO

Vous serez heureux d’apprendre que le comité vient de voter (à l’unanimité, comme il se trouve) pour supprimer get () du projet.

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

En plus de fgets() , C11 introduit une nouvelle alternative sûre gets_s() :

C11 K.3.5.4.1 La fonction de gets_s

 #define __STDC_WANT_LIB_EXT1__ 1 #include  char *gets_s(char *s, rsize_t n); 

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

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

Je souhaiterais adresser une invitation sérieuse à tous les responsables de la bibliothèque C qui incluent toujours des gets dans 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 aidera à s’assurer que personne ne dépend toujours de lui. Je vous remercie.

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 pour cela, cela peut provoquer un dysfonctionnement et un blocage. 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 à toute épreuve. (cela peut également constituer un risque de sécurité si un utilisateur peut bloquer un programme système en envoyant trop de données).

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

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

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

Toute l’heure vaut la peine d’être regardée, mais pour ses commentaires, la critique dure environ 30 minutes.

J’espère que cela vous mettra en appétit pour toute la conférence, ce qui attire notre attention sur la nécessité de preuves de correction plus formelles dans les langues et sur la manière dont les concepteurs de langage doivent être blâmés pour leurs erreurs linguistiques. 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».