Perché la funzione gets è così pericolosa che non dovrebbe essere usata?


229

Quando provo a compilare il codice C che utilizza la gets()funzione con GCC, ricevo questo avviso:

(.text + 0x34): avviso: la funzione `get 'è pericolosa e non deve essere utilizzata.

Ricordo che questo ha a che fare con la protezione dello stack e la sicurezza, ma non sono sicuro del perché.

Come posso rimuovere questo avviso e perché esiste un tale avviso sull'uso gets()?

Se gets()è così pericoloso, allora perché non possiamo rimuoverlo?



Risposte:


179

Per getspoterlo usare in sicurezza, devi sapere esattamente quanti caratteri leggerai, in modo da rendere il tuo buffer abbastanza grande. Lo saprai solo se sai esattamente quali dati leggerai.

Invece di utilizzare gets, si desidera utilizzare fgets, che ha la firma

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

( fgetsse legge un'intera riga, lascerà il carattere '\n'nella stringa; dovrai occupartene.)

Rimase una parte ufficiale della lingua fino allo standard ISO C del 1999, ma fu ufficialmente rimosso dallo standard 2011. La maggior parte delle implementazioni C lo supporta ancora, ma almeno gcc emette un avviso per qualsiasi codice che lo utilizza.


79
In realtà non è gcc che avverte, è il glibc che contiene un pragma o un attributo gets()che fa sì che il compilatore emetta un avviso quando viene usato.
fuz,

@fuz in realtà, non è nemmeno solo il compilatore che avverte: l'avviso citato nell'OP è stato stampato dal linker!
Ruslan,

163

Perché è gets()pericoloso

Il primo worm Internet ( Morris Internet Worm ) è fuggito circa 30 anni fa (1988-11-02) e ha usato gets()e un buffer overflow come uno dei suoi metodi di propagazione da un sistema all'altro. Il problema di base è che la funzione non sa quanto è grande il buffer, quindi continua a leggere fino a quando non trova una nuova riga o incontra EOF, e può traboccare i limiti del buffer che gli è stato dato.

Dovresti dimenticare di aver mai sentito dire che gets()esisteva.

Lo standard C11 ISO / IEC 9899: 2011 eliminato gets()come funzione standard, che è A Good Thing ™ (è stato formalmente contrassegnato come "obsoleto" e "deprecato" in ISO / IEC 9899: 1999 / Cor.3: 2007 - Rettifica tecnica 3 per C99, quindi rimosso in C11). Purtroppo, rimarrà nelle biblioteche per molti anni (che significa "decenni") per motivi di compatibilità con le versioni precedenti. Se dipendesse da me, l'implementazione di gets()diventerebbe:

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

Dato che il tuo codice andrà in crash comunque, prima o poi, è meglio eliminare il problema prima che dopo. Sarei pronto ad aggiungere un messaggio di errore:

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

Le versioni moderne del sistema di compilazione Linux generano avvisi se si collega gets()- e anche per alcune altre funzioni che hanno anche problemi di sicurezza ( mktemp(), ...).

Alternative a gets()

fgets ()

Come tutti gli altri hanno detto, l'alternativa canonica a gets()sta fgets()specificando stdincome flusso di file.

char buffer[BUFSIZ];

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

Ciò che nessun altro ancora menzionato è che gets()non include la newline ma lo fgets()fa. Pertanto, potrebbe essere necessario utilizzare un wrapper per fgets()eliminare la nuova riga:

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

O meglio:

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

Inoltre, come sottolinea caf in un commento e paxdiablo mostra nella sua risposta, fgets()potresti avere dei dati lasciati su una riga. Il mio codice wrapper lascia che i dati vengano letti la prossima volta; puoi prontamente modificarlo per inghiottire il resto della linea di dati se preferisci:

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

Il problema residuo è come riportare i tre diversi stati di risultato: EOF o errore, riga letta e non troncata e riga parziale letta ma i dati sono stati troncati.

Questo problema non si pone gets()perché non sa dove finisce il buffer e calpesta allegramente oltre la fine, provocando il caos sul layout della memoria ben curato, spesso incasinando lo stack di ritorno (uno Stack Overflow ) se il buffer è allocato su lo stack, o calpestando le informazioni di controllo se il buffer è allocato dinamicamente, o copiando i dati su altre preziose variabili globali (o modulo) se il buffer è allocato staticamente. Nessuna di queste è una buona idea: incarnano la frase "comportamento indefinito".


Esiste anche il TR 24731-1 (Rapporto tecnico del Comitato Standard C) che offre alternative più sicure a una varietà di funzioni, tra cui gets():

§6.5.4.1 La gets_sfunzione

Sinossi

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

Runtime-vincoli

snon deve essere un puntatore nullo. nnon deve essere uguale a zero né maggiore di RSIZE_MAX. Un carattere di nuova riga, fine del file o errore di lettura deve verificarsi nella lettura dei n-1caratteri da stdin. 25)

3 Se si verifica una violazione del vincolo di runtime, s[0]viene impostato sul carattere null e i caratteri vengono letti e scartati stdinfino alla lettura di un carattere di nuova riga o alla fine del file o si verifica un errore di lettura.

Descrizione

4 La gets_sfunzione legge al massimo uno in meno del numero di caratteri specificato n dal flusso puntato da stdin, nella matrice puntata da s. Nessun carattere aggiuntivo viene letto dopo un carattere di nuova riga (che viene scartato) o dopo la fine del file. Il carattere di nuova riga scartato non conta per il numero di caratteri letti. Un carattere null viene scritto immediatamente dopo l'ultimo carattere letto nell'array.

5 Se viene rilevata la fine del file e non sono stati letti caratteri nell'array o se si verifica un errore di lettura durante l'operazione, s[0]viene impostato sul carattere null e gli altri elementi di saccettano valori non specificati.

Pratica consigliata

6 La fgetsfunzione consente ai programmi scritti correttamente di elaborare in modo sicuro linee di input troppo lunghe per essere memorizzate nell'array dei risultati. In generale ciò richiede che i chiamanti fgetsprestino attenzione alla presenza o all'assenza di un carattere di nuova riga nella matrice dei risultati. Prendi in considerazione l'utilizzo fgets(insieme a qualsiasi elaborazione necessaria basata su caratteri di nuova riga) anziché gets_s.

25) La gets_sfunzione, a differenza gets, la rende una violazione del vincolo di runtime per una linea di input che trabocca il buffer per memorizzarlo. Diversamente fgets, gets_smantiene una relazione uno a uno tra le linee di input e le chiamate riuscite a gets_s. I programmi che usano si getsaspettano una simile relazione.

I compilatori di Microsoft Visual Studio implementano un'approssimazione allo standard TR 24731-1, ma ci sono differenze tra le firme implementate da Microsoft e quelle nella TR.

Lo standard C11, ISO / IEC 9899-2011, include TR24731 nell'allegato K come parte opzionale della libreria. Sfortunatamente, è raramente implementato su sistemi simili a Unix.


getline() - POSIX

POSIX 2008 offre anche un'alternativa sicura a gets()chiamata getline(). Alloca lo spazio per la linea in modo dinamico, quindi alla fine devi liberarla. Rimuove quindi la limitazione sulla lunghezza della linea. Restituisce anche la lunghezza dei dati letti o -1(e non EOF!), Il che significa che i byte null nell'input possono essere gestiti in modo affidabile. C'è anche una variante 'scegli il tuo delimitatore a carattere singolo' chiamata getdelim(); questo può essere utile se hai a che fare con l'output da find -print0cui le estremità dei nomi dei file sono contrassegnate con un '\0'carattere ASCII NUL , ad esempio.


8
Vale anche la pena sottolineare che fgets()e la tua fgets_wrapper()versione lascerà la porzione finale di una linea troppo lunga nel buffer di input, da leggere dalla successiva funzione di input. In molti casi, vorrai leggere e scartare questi personaggi.
Caf

5
Mi chiedo perché non abbiano aggiunto un'alternativa a fgets () che consente di utilizzare la sua funzionalità senza dover fare una stupida chiamata strlen. Ad esempio, una variante di fgets che ha restituito il numero di byte letti nella stringa renderebbe più semplice per il codice vedere se l'ultimo byte letto era una nuova riga. Se il comportamento di passare un puntatore nullo per il buffer fosse definito come "leggi e scarta fino a n-1 byte fino alla nuova riga successiva", ciò consentirebbe al codice di scartare facilmente la coda delle righe di lunghezza eccessiva.
supercat

2
@supercat: Sì, sono d'accordo - è un peccato. L'approccio più vicino a ciò è probabilmente POSIX getline()e il suo relativo getdelim(), che restituiscono la lunghezza della 'linea' letta dai comandi, allocando lo spazio necessario per poter memorizzare l'intera linea. Anche questo può causare problemi se si finisce con un file JSON a riga singola di dimensioni multiple di gigabyte; puoi permetterti tutto quel ricordo? (E mentre ci siamo, possiamo avere strcpy()e strcat()varianti che restituiscono un puntatore al byte null alla fine? Ecc.)
Jonathan Leffler

4
@supercat: l'altro problema fgets()è che se il file contiene un byte null, non si può dire quanti dati ci sono dopo il byte null fino alla fine della riga (o EOF). strlen()può riportare solo fino al byte null nei dati; dopodiché si tratta di congetture e quindi quasi certamente sbagliate.
Jonathan Leffler,

7
"dimentica che hai mai sentito che gets()esistesse." Quando lo faccio, ci incontro di nuovo e torno qui. Stai hackerando StackOverflow per ottenere voti positivi?
candied_orange

21

Perché getsnon fa alcun tipo di controllo mentre ottiene byte da stdin e li mette da qualche parte. Un semplice esempio:

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

gets(array1);

Ora, prima di tutto ti è permesso di inserire quanti personaggi vuoi, getsnon ti interessa. In secondo luogo, i byte oltre le dimensioni dell'array in cui vengono inseriti (in questo caso array1) sovrascriveranno tutto ciò che trovano in memoria perché getsli scriveranno. Nell'esempio precedente questo significa che se si inserisce "abcdefghijklmnopqrts"forse, imprevedibilmente, sovrascriverà anche array2o altro.

La funzione non è sicura perché presuppone un input coerente. MAI USARLO!


3
Ciò che rende getsassolutamente inutilizzabile è che non ha un parametro lunghezza / conteggio array che richiede; se fosse stato lì, sarebbe stata solo un'altra normale funzione C standard.
legends2k

@ legends2k: sono curioso di sapere a cosa servisse l'uso previsto getse perché nessuna variante di budget standard è stata resa conveniente per i casi d'uso in cui la newline non è desiderata come parte dell'input?
supercat

1
@supercat è getsstato, come suggerisce il nome, progettato per ottenere una stringa stdin, tuttavia la logica per non avere un parametro size potrebbe essere stata dallo spirito di C : Fidati del programmatore. Questa funzione è stata rimossa in C11 e la sostituzione fornita gets_sassume la dimensione del buffer di input. Non ho idea della fgetsparte però.
legends2k

@ legends2k: L'unico contesto che posso vedere in cui getspotrebbe essere scusabile sarebbe se si stesse usando un sistema di I / O con buffer in linea hardware che era fisicamente incapace di presentare una linea per una certa lunghezza e la durata prevista del programma è stato più breve della durata dell'hardware. In tal caso, se l'hardware non è in grado di inoltrare linee di lunghezza superiore a 127 byte, potrebbe essere giustificabile getsun buffer da 128 byte, sebbene riterrei che i vantaggi di poter specificare un buffer più corto quando si prevede un input più piccolo giustificerebbero più che costo.
supercat

@ legends2k: In realtà, ciò che sarebbe stato ideale sarebbe stato avere un "puntatore di stringa" che identifichi un byte che selezionerebbe tra diversi formati stringa / buffer / buffer-informazioni, con un valore di byte prefisso che indica una struttura che conteneva il byte prefisso [più padding], più la dimensione del buffer, la dimensione utilizzata e l'indirizzo del testo effettivo. Un tale schema consentirebbe al codice di passare una sottostringa arbitraria (non solo la coda) di un'altra stringa senza dover copiare nulla e consentirebbe metodi come getse strcataccettare in modo sicuro quanto si adatterà.
supercat

16

Non si dovrebbe usare getspoiché non ha modo di arrestare un overflow del buffer. Se l'utente digita più dati di quanti ne possano contenere nel buffer, molto probabilmente finirai con la corruzione o peggio.

In effetti, ISO ha effettivamente fatto il passo della rimozione gets dallo standard C (a partire da C11, sebbene fosse deprecato in C99) che, dato quanto valutano la compatibilità con le versioni precedenti, dovrebbe essere un'indicazione di quanto male fosse quella funzione.

La cosa corretta da fare è usare la fgetsfunzione con l' stdinhandle del file poiché puoi limitare i caratteri letti dall'utente.

Ma questo ha anche i suoi problemi come:

  • i caratteri extra inseriti dall'utente verranno raccolti la volta successiva.
  • non c'è una notifica rapida che l'utente ha inserito troppi dati.

A tal fine, quasi tutti i programmatori C ad un certo punto della loro carriera scriveranno anche un wrapper più utile fgets. Ecco il mio:

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

con un po 'di codice di prova:

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

Fornisce le stesse protezioni fgetsin quanto previene gli overflow del buffer ma notifica al chiamante cosa è successo e cancella i caratteri in eccesso in modo che non influiscano sulla successiva operazione di input.

Sentiti libero di usarlo come desideri, con la presente rilascialo sotto la licenza "fai quello che maledettamente bene vuoi" :-)


In realtà, lo standard C99 originale non si è deprezzato esplicitamente gets()né nella sezione 7.19.7.7 dove è definito né nella sezione 7.26.9 Istruzioni future della libreria e sottosezione per <stdio.h>. Non c'è nemmeno una nota in calce sul fatto che sia pericoloso. (Detto questo, vedo "È deprecato in ISO / IEC 9899: 1999 / Cor.3: 2007 (E))" nella risposta di Yu Hao .) Ma C11 lo ha rimosso dallo standard - e non prima!
Jonathan Leffler,

int getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL)nasconde la size_ta intconversione di sz. sz > INT_MAX || sz < 2catturerebbe strani valori di sz.
chux - Ripristina Monica il

if (buff[strlen(buff)-1] != '\n') {è un exploit hacker poiché il primo personaggio inserito dall'utente malvagio potrebbe essere un carattere null incorporato che rende buff[strlen(buff)-1]UB. while (((ch = getchar())...ha problemi se un utente inserisce un carattere null.
chux - Ripristina Monica il

12

budget .

Per leggere dallo stdin:

char string[512];

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

6

Non è possibile rimuovere le funzioni API senza interrompere l'API. In tal caso, molte applicazioni non verrebbero più compilate o eseguite.

Questo è il motivo che fornisce un riferimento :

La lettura di una riga che trabocca dall'array indicato da s comporta un comportamento indefinito. Si consiglia l'uso di fgets ().


4

Ho letto di recente, in un post di USENETcomp.lang.c , che gets()viene rimosso dallo standard. WOOHOO

Sarai felice di sapere che la commissione ha appena votato (all'unanimità, a quanto pare) per rimuovere anche get () dal progetto.


3
È eccellente che venga rimosso dallo standard. Tuttavia, la maggior parte delle implementazioni lo fornirà come "estensione non standard" per almeno i prossimi 20 anni, a causa della retrocompatibilità.
Jonathan Leffler

1
Sì, giusto, ma quando si compila con gcc -std=c2012 -pedantic ...gets () non ci riuscirà. (Ho appena -std
inventato

4

In C11 (ISO / IEC 9899: 201x), gets()è stato rimosso. (È deprecato in ISO / IEC 9899: 1999 / Cor.3: 2007 (E))

Inoltre fgets(), C11 introduce una nuova alternativa sicura gets_s():

C11 K.3.5.4.1 La gets_sfunzione

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

Tuttavia, nella sezione Pratica raccomandata , fgets()è ancora preferito.

La fgetsfunzione consente ai programmi scritti correttamente di elaborare in modo sicuro linee di input troppo lunghe per essere archiviate nella matrice dei risultati. In generale ciò richiede che i chiamanti fgetsprestino attenzione alla presenza o all'assenza di un carattere di nuova riga nella matrice dei risultati. Prendi in considerazione l'utilizzo fgets(insieme a qualsiasi elaborazione necessaria basata su caratteri di nuova riga) anziché gets_s.


3

gets()è pericoloso perché è possibile che l'utente si blocchi il programma digitando troppo nel prompt. Non è in grado di rilevare la fine della memoria disponibile, quindi se si alloca una quantità di memoria troppo piccola allo scopo, può causare un errore e un arresto anomalo del seg. A volte sembra molto improbabile che un utente digiti 1000 lettere in un prompt pensato per il nome di una persona, ma come programmatori, dobbiamo rendere i nostri programmi a prova di proiettile. (può anche essere un rischio per la sicurezza se un utente può arrestare in modo anomalo un programma di sistema inviando troppi dati).

fgets() ti consente di specificare quanti caratteri vengono estratti dal buffer di input standard, in modo che non superino la variabile.


Nota che il vero pericolo non è riuscire a mandare in crash il tuo programma, ma riuscire a far funzionare codice arbitrario . (In generale, sfruttando comportamenti indefiniti .)
Tanz87

2

Vorrei estendere un serio invito a tutti i manutentori delle librerie C che stanno ancora includendo getsnelle loro librerie "nel caso in cui qualcuno sia ancora dipendente da esso": Sostituisci la tua implementazione con l'equivalente di

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

Ciò contribuirà a garantire che nessuno sia ancora dipendente da esso. Grazie.


2

La funzione C è pericolosa ed è stato un errore molto costoso. Tony Hoare lo individua per una menzione specifica nel suo discorso "Null References: The Billion Dollar Mistake":

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

L'intera ora vale la pena guardare, ma per i suoi commenti vista da 30 minuti in poi con lo specifico ottiene critiche circa 39 minuti.

Spero che questo stimoli il tuo appetito per l'intero discorso, che attiri l'attenzione su come abbiamo bisogno di prove di correttezza più formali nelle lingue e su come i progettisti linguistici dovrebbero essere incolpati degli errori nelle loro lingue, non del programmatore. Questa sembra essere stata tutta la dubbia ragione per cui i progettisti di linguaggi volgari hanno dato la colpa ai programmatori nelle vesti di "libertà del programmatore".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.