web-dev-qa-db-fra.com

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

Lorsque j'essaie de compiler du code C utilisant la fonction gets() avec GCC, je reçois cet avertissement:

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

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

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

Si gets() est si dangereux, pourquoi ne pouvons-nous pas l'enlever?

202
vinit dhatrak

Pour pouvoir utiliser gets en toute sécurité, vous devez savoir exactement combien de caractères vous allez lire, afin que votre mémoire tampon soit suffisamment grande. Vous ne saurez que si vous savez exactement quelles données vous allez lire.

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 vous en occuper.)

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 en C le supportent toujours, mais au moins gcc envoie un avertissement pour tout code qui l'utilise.

159
Thomas Owens

Pourquoi gets() est-il dangereux?

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

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

La norme C11 ISO/IEC 9899: 2011 a éliminé gets() en tant que fonction standard, A Good Thing ™ (elle était officiellement désignée comme 'obsolescente' et 'déconseillée' dans ISO/IEC 9899: 1999/Cor.3: 2007 - Technical Corrigendum 3 pour C99, puis retiré en C11). Malheureusement, il restera dans les bibliothèques pendant de nombreuses années (c'est-à-dire "décennies") pour des raisons de compatibilité ascendante. Si cela dépendait de moi, la mise en oeuvre de gets() deviendrait:

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

Étant donné que votre code plantera de toute façon, tôt ou tard, il est préférable de régler le problème le plus tôt possible. 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 associez gets() - ainsi que d'autres fonctions ayant également des problèmes de sécurité (mktemp(),…).

Alternatives à gets()

fgets ()

Comme tout le monde l'a dit, l'alternative canonique à gets() est fgets() en spécifiant stdin comme 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 que gets() n’inclut pas la nouvelle ligne, mais fgets(). 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 caf indique dans un commentaire et paxdiablo apparaît dans sa réponse, avec fgets() vous pourriez avoir des données laissé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 lue partielle mais les données ont été tronquées.

Ce problème ne se pose pas avec gets() car il ne sait pas où se trouve votre tampon et piétine joyeusement au-delà de celui-ci, semant le chaos sur votre structure de mémoire magnifiquement soignée, bousillant souvent la pile de retour (a Stack Overflow =) si le tampon est alloué sur la pile, ou piétiner les informations de contrôle si le tampon est alloué de manière dynamique, ou copier des données sur d’autres précieuses variables globales (ou de module) si le tampon est alloué de manière statique. Aucune d'entre elles n'est une bonne idée - elles résument l'expression "comportement indéfini".


Il existe également le TR 24731-1 (rapport technique du comité sur les normes C) qui offre des solutions de remplacement plus sûres pour diverses fonctions, notamment gets():

§6.5.4.1 La fonction gets_s

Synopsis

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

Runtime-contraintes

s ne doit pas être un pointeur nul. n ne doit être ni égal à zéro ni supérieur à RSIZE_MAX. Une erreur de caractère de nouvelle ligne, de fin de fichier ou de lecture doit survenir lors de la lecture de n-1 caractères de stdin.25)

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

La description

4 La fonction gets_s lit au plus un nombre inférieur au 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 vous rencontrez une fin de fichier 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 null et les autres éléments de s prend 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 à stocker dans le tableau de résultats. En général, cela nécessite que les appelants de fgets fassent attention à la présence ou à l'absence d'un caractère de nouvelle ligne dans le tableau de résultats. Pensez à utiliser fgets (avec tout traitement nécessaire basé sur les caractères de nouvelle ligne) au lieu de gets_s.

25) La fonction gets_s, à la différence de gets, en fait une violation de contrainte d'exécution pour qu'une ligne d'entrée déborde du tampon afin de le stocker. Contrairement à fgets, gets_s maintient une relation un-à-un entre les lignes d'entrée et les appels réussis à gets_s. Les programmes qui utilisent gets s'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 de la TR.

La norme C11, ISO/IEC 9899-2011, inclut TR24731 dans l’Annexe K en tant que partie optionnelle 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 à gets() appelé getline() . Il alloue de manière dynamique l’espace à la ligne, vous devez donc 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 des octets nuls dans l'entrée peuvent être géré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 utilisez 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.

140
Jonathan Leffler

Parce que gets ne fait aucune sorte de vérification en obtenant des octets de stdin et en les mettant 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, gets ne s'en souciera pas. Deuxièmement, les octets qui dépassent la taille du tableau dans lequel vous les avez placés (dans ce cas, array1) écraseront tout ce qu’ils trouveront en mémoire car gets les écrira. Dans l'exemple précédent, cela signifie que si vous saisissez "abcdefghijklmnopqrts", de manière imprévisible, il écrasera également array2 ou peu importe.

La fonction est dangereuse car elle suppose une entrée cohérente. NE JAMAIS UTILISER IT!

21
Jack

Vous ne devez pas utiliser gets car il n’a aucun moyen d’arrêter un débordement de mémoire tampon. Si l'utilisateur saisit plus de données que ne peut en contenir votre mémoire tampon, vous risquez probablement de subir une corruption ou pire.

En fait, l’ISO a en fait franchi le pas de suppressiongets de la norme C (à partir de C11, bien qu’elle ait été dépréciée en C99), ce qui, vu l’importance de la compatibilité avec les versions antérieures, devrait être une indication de la gravité de cette fonction.

La bonne chose à faire est d'utiliser la fonction fgets avec le descripteur de fichier stdin, car vous pouvez limiter le nombre de caractères lus par l'utilisateur.

Mais cela a aussi ses problèmes tels que:

  • les caractères supplémentaires entrés 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 de leur carrière écriront également un wrapper plus utile autour de 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 un 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 fgets en ce sens qu'il empêche les débordements de mémoire tampon, mais 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 publie ici sous la licence "faites ce que vous voulez bien" :-)

16
paxdiablo

fgets .

Pour lire du stdin:

char string[512];

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

Vous ne pouvez pas supprimer les fonctions de l'API sans casser l'API. Si vous le souhaitez, de nombreuses applications ne compileraient plus ou ne fonctionneraient plus du tout.

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

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

6
Gerd Klima

Dans C11 (ISO/IEC 9899: 201x), 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écurisée gets_s():

C11 K.3.5.4.1 La fonction gets_s

#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éré.

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

4
Yu Hao

J'ai lu récemment, dans un poste USENET sur comp.lang.c , que gets() est supprimé de la norme. WOOHOO

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

4
pmg

gets() est dangereux car il est possible pour l'utilisateur de bloquer le programme en tapant trop dans l'invite. Il ne peut pas détecter la fin de la mémoire disponible. Par conséquent, si vous allouez une quantité de mémoire trop petite, vous risquez une défaillance et un crash. Parfois, il semble très improbable qu'un utilisateur saisisse 1000 lettres dans une invite désignée sous le nom d'une personne, mais en tant que programmeurs, nous devons rendre nos programmes à toute épreuve. (Cela peut également être 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 sortis du tampon d'entrée standard afin qu'ils ne surchargent pas la variable.

3
Aradhana Mohanty

Je souhaite adresser une invitation sérieuse à tous les responsables de la maintenance de la bibliothèque C qui incluent toujours gets dans leurs bibliothèques "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.

3
Steve Summit

La fonction C obtient est dangereuse et a été une erreur très coûteuse. Tony Hoare le mentionne avec une mention spécifique dans son discours "Références nulles: L'erreur d'un milliard de dollars":

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 à partir de 30 minutes reçoit les critiques autour de 39 minutes.

J'espère que cela ouvrira l'appétit pour tout le discours, ce qui attirera l'attention sur la nécessité de disposer de preuves de correction plus formelles dans les langues et sur la façon dont les concepteurs devraient être tenus pour responsables des erreurs commises dans leur langue, et non du programmeur. Cela semble avoir été la raison la plus douteuse pour laquelle les concepteurs de mauvaises langues poussent le blâme aux programmeurs sous le prétexte de "liberté de programmation".

2
user3717661