Dlaczego funkcja gets jest tak niebezpieczna, że ​​nie należy jej używać?

229

Gdy próbuję skompilować kod C korzystający z gets()funkcji za pomocą GCC, pojawia się następujące ostrzeżenie:

(.text + 0x34): ostrzeżenie: funkcja „gets” jest niebezpieczna i nie należy jej używać.

Pamiętam, że ma to coś wspólnego z ochroną stosu i bezpieczeństwem, ale nie jestem pewien, dlaczego.

Jak mogę usunąć to ostrzeżenie i dlaczego pojawia się takie ostrzeżenie dotyczące używania gets()?

Jeśli gets()jest tak niebezpieczny, dlaczego nie możemy go usunąć?

vinit dhatrak
źródło

Odpowiedzi:

179

Aby getsbezpiecznie korzystać , musisz dokładnie wiedzieć, ile znaków będziesz czytać, abyś mógł odpowiednio zwiększyć swój bufor. Będziesz wiedział tylko, że jeśli dokładnie wiesz, jakie dane będziesz czytać.

Zamiast używać gets, chcesz użyć fgets, który ma podpis

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

( fgetsjeśli odczytuje całą linię, pozostawi '\n'ciąg znaków; będziesz musiał sobie z tym poradzić.)

Pozostał oficjalną częścią języka aż do standardu ISO C z 1999 roku, ale został oficjalnie usunięty przez standard z 2011 roku. Większość implementacji C nadal go obsługuje, ale przynajmniej gcc wydaje ostrzeżenie dla każdego kodu, który go używa.

Thomas Owens
źródło
79
Tak naprawdę to nie gcc ostrzega, to glibc zawiera pragmę lub atrybut, gets()który powoduje, że kompilator emituje ostrzeżenie, gdy jest używane.
fuz
@ fuz w rzeczywistości ostrzega nie tylko kompilator: ostrzeżenie cytowane w OP zostało wydrukowane przez linker!
Ruslan
163

Dlaczego jest gets()niebezpieczny

Pierwszy robak internetowy ( Morris Internet Worm ) uciekł około 30 lat temu (1988-11-02) i wykorzystał gets()przepełnienie bufora jako jedną ze swoich metod rozprzestrzeniania się z systemu do systemu. Podstawowym problemem jest to, że funkcja nie wie, jak duży jest bufor, więc kontynuuje czytanie, dopóki nie znajdzie nowego wiersza lub nie napotka EOF, i może przekroczyć granice podanego bufora.

Powinieneś zapomnieć, że kiedykolwiek słyszałeś o gets()istnieniu.

Norma C11 ISO / IEC 9899: 2011 została wyeliminowana gets()jako funkcja standardowa, którą jest A Good Thing ™ (została formalnie oznaczona jako „przestarzała” i „przestarzała” w ISO / IEC 9899: 1999 / Cor.3: 2007 - Corrigendum techniczne 3 dla C99, a następnie usunięty w C11). Niestety, pozostanie w bibliotekach przez wiele lat (co oznacza „dekady”) ze względu na kompatybilność wsteczną. Gdyby to zależało ode mnie, wdrożenie gets()byłoby:

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

Biorąc pod uwagę, że Twój kod i tak się zawiesi, prędzej czy później, lepiej jest usunąć problem wcześniej niż później. Byłbym przygotowany na dodanie komunikatu o błędzie:

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

Nowoczesne wersje systemu kompilacji Linux generują ostrzeżenia, jeśli łączysz gets()- a także dla niektórych innych funkcji, które również mają problemy z bezpieczeństwem ( mktemp(),…).

Alternatywy dla gets()

fgets ()

Jak wszyscy inni mówili, kanoniczną alternatywą gets()jest fgets()określenie stdinjako strumień plików.

char buffer[BUFSIZ];

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

Nikt jeszcze nie wspomniał, że gets()nie zawiera nowej linii, ale ją fgets()zawiera. Może być konieczne użycie opakowania, fgets()które usuwa nowy wiersz:

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

Albo lepiej:

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

Ponadto, jak wskazuje caf w komentarzu, a paxdiablo pokazuje w swojej odpowiedzi, a fgets()ty możesz mieć dane w linii. Mój kod opakowania pozostawia te dane do odczytania następnym razem; możesz go łatwo zmodyfikować, aby pochłonąć resztę wiersza danych, jeśli wolisz:

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

Pozostały problem polega na tym, jak zgłosić trzy różne stany wynikowe - EOF lub błąd, odczyt linii i nie obcięty oraz częściowy odczyt linii, ale dane zostały obcięte.

Ten problem nie występuje, gets()ponieważ nie wie, gdzie kończy się bufor, i wesoło tratuje za nim, siejąc spustoszenie w pięknie utrzymanym układzie pamięci, często psując stos zwrotny ( przepełnienie stosu ), jeśli bufor jest przydzielony na stos lub deptanie informacji kontrolnych, jeśli bufor jest dynamicznie przydzielany, lub kopiowanie danych przez inne cenne zmienne globalne (lub modułowe), jeśli bufor jest przydzielany statycznie. Żadne z nich nie jest dobrym pomysłem - są one uosobieniem wyrażenia „niezdefiniowane zachowanie”.


Istnieje również TR 24731-1 (Raport techniczny komitetu standardowego C), który zapewnia bezpieczniejsze alternatywy dla różnych funkcji, w tym gets():

§6.5.4.1 gets_sFunkcja

Streszczenie

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

Ograniczenia w czasie wykonywania

snie będzie wskaźnikiem zerowym. nnie będzie równy zero ani większy niż RSIZE_MAX. Znak odczytu nowej linii, błąd końca pliku lub błąd odczytu wystąpią podczas odczytu n-1znaków z stdin. 25)

3 W przypadku naruszenia ograniczenia środowiska wykonawczego s[0]jest ustawiany na znak zerowy, a znaki są odczytywane i odrzucane stdindo momentu odczytania znaku nowej linii lub wystąpienia błędu końca pliku lub błędu odczytu.

Opis

4 gets_sFunkcja wczytuje najwyżej jeden mniej niż liczbę znaków określoną przez n ze wskazanego strumienia do stdintablicy wskazanej przez s. Żadne dodatkowe znaki nie są odczytywane po znaku nowej linii (który jest odrzucany) lub po końcu pliku. Odrzucony znak nowej linii nie jest wliczany do liczby odczytanych znaków. Znak zerowy jest zapisywany natychmiast po ostatnim znaku odczytanym do tablicy.

5 Jeśli napotkany zostanie koniec pliku i nie zostaną wczytane żadne znaki do tablicy lub jeśli podczas operacji wystąpi błąd odczytu, wówczas s[0]ustawiany jest znak pusty, a pozostałe elementy sprzyjmują nieokreślone wartości.

Zalecana praktyka

6 fgetsFunkcja pozwala poprawnie napisanym programom bezpiecznie przetwarzać wiersze wejściowe zbyt długo, aby zapisać je w tablicy wyników. Zasadniczo wymaga to, aby osoby wywołujące fgetszwracały uwagę na obecność lub brak znaku nowej linii w tablicy wyników. Rozważ użycie fgets(wraz z niezbędnym przetwarzaniem opartym na znakach nowej linii) zamiast gets_s.

25) W gets_sprzeciwieństwie do tej funkcji, getsnaruszenie linii czasu wykonywania dla linii wejściowej przepełnia bufor, aby go zapisać. W przeciwieństwie fgets, gets_sutrzymuje relację jeden do jednego między liniami wejściowymi i udanych połączeń do gets_s. Programy, które używają, getsoczekują takiej relacji.

Kompilatory Microsoft Visual Studio implementują zbliżenie do standardu TR 24731-1, ale istnieją różnice między podpisami zaimplementowanymi przez Microsoft a podpisami w TR.

Norma C11, ISO / IEC 9899-2011, zawiera TR24731 w załączniku K jako opcjonalną część biblioteki. Niestety rzadko jest implementowany w systemach uniksopodobnych.


getline() - POSIX

POSIX 2008 zapewnia również bezpieczną alternatywę dla gets()wywoływanych getline(). Dynamicznie przydziela miejsce dla linii, więc musisz go zwolnić. Usuwa zatem ograniczenie długości linii. Zwraca również długość odczytanych danych -1( lub nie EOF!), Co oznacza, że ​​bajty zerowe na wejściu mogą być obsługiwane niezawodnie. Istnieje również wariant „wybierz własny separator jednoznakowy” getdelim(); może to być przydatne, jeśli masz do czynienia z danymi wyjściowymi, na find -print0których końce nazw plików są oznaczone '\0'na przykład znakiem NUL ASCII .

Jonathan Leffler
źródło
8
Warto również zauważyć, że fgets()i twoja fgets_wrapper()wersja pozostawi końcową część zbyt długiej linii w buforze wejściowym, do odczytania przez następną funkcję wejściową. W wielu przypadkach będziesz chciał przeczytać i odrzucić te postacie.
caf
5
Zastanawiam się, dlaczego nie dodali alternatywy fgets (), która pozwala korzystać z jej funkcji bez konieczności wykonywania głupiego wywołania strlen. Na przykład wariant fgets, który zwrócił liczbę bajtów odczytanych w ciągu, ułatwiłby kodowi sprawdzenie, czy ostatni odczytany bajt jest znakiem nowej linii. Jeśli zachowanie przekazywania wskaźnika zerowego dla bufora zostało zdefiniowane jako „odczytaj i odrzuć do n-1 bajtów do następnej nowej linii”, pozwoliłoby to kodowi na łatwe odrzucenie końca linii o zbyt dużej długości.
supercat
2
@ superupat: Tak, zgadzam się - szkoda. Najbliższym podejściem do tego jest prawdopodobnie POSIX getline()i jego krewny getdelim(), które zwracają długość „linii” czytanej przez komendy, przydzielając miejsce zgodnie z wymaganiami, aby móc zapisać całą linię. Nawet to może powodować problemy, jeśli skończy się na jednowierszowym pliku JSON o rozmiarze wielu gigabajtów; możesz sobie pozwolić na całą tę pamięć? (A skoro już to robimy, czy możemy mieć strcpy()i strcat()warianty, które zwracają wskaźnik do bajtu zerowego na końcu? Itd.)
Jonathan Leffler
4
@ superupat: drugim problemem fgets()jest to, że jeśli plik zawiera bajt zerowy, nie można powiedzieć, ile danych jest po bajcie zerowym do końca linii (lub EOF). strlen()może zgłaszać tylko do bajtu zerowego w danych; potem jest to zgadywanie i dlatego prawie na pewno jest błędne.
Jonathan Leffler
7
„zapomnij, że kiedykolwiek słyszałeś o gets()istnieniu”. Kiedy to robię, znów na nie wpadam i wracam tutaj. Czy hakujesz stackoverflow, aby uzyskać upvotes?
candied_orange
21

Ponieważ getsnie wykonuje żadnej kontroli podczas pobierania bajtów ze standardowego wejścia i umieszczania ich gdzieś. Prosty przykład:

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

gets(array1);

Teraz przede wszystkim możesz wpisać, ile znaków chcesz, getsnie przejmuj się tym. Po drugie, bajty ponad rozmiar tablicy, w której je umieściłeś (w tym przypadku array1) zastąpią wszystko, co znajdą w pamięci, ponieważ getsje zapiszą. W poprzednim przykładzie oznacza to, że jeśli wpiszesz "abcdefghijklmnopqrts"być może, nieprzewidzianie, nadpisze również array2lub cokolwiek innego.

Ta funkcja jest niebezpieczna, ponieważ zakłada spójne dane wejściowe. NIGDY NIE UŻYWAJ!

Jacek
źródło
3
Tym, co sprawia, że ​​jest getscałkowicie bezużyteczny, jest to, że nie ma parametru długości / liczby tablic, który bierze; Gdyby tam był, byłaby to zwykła standardowa funkcja C.
legends2k
@ legends2k: Jestem ciekawy, do czego przeznaczone było użycie getsi dlaczego nie stworzono żadnego standardowego wariantu fgets jako wygodnego dla przypadków użycia, w których nowa linia nie jest pożądana jako część danych wejściowych?
supercat
1
@supercat gets, jak sama nazwa wskazuje, został zaprojektowany, aby uzyskać ciąg znaków stdin, jednak uzasadnienie braku parametru rozmiaru mogło wynikać z ducha C : Zaufaj programistom. Ta funkcja została usunięta w C11, a podana zamiana gets_sprzyjmuje rozmiar bufora wejściowego. Nie mam jednak pojęcia o tej fgetsczęści.
legends2k
@ legends2k: Jedyny kontekst, w którym mogę getssię usprawiedliwić, to sytuacja, w której używa się systemu We / Wy buforowanego linią sprzętową, który fizycznie nie byłby w stanie przesłać linii na pewną długość i zamierzonego czasu życia programu był krótszy niż okres użytkowania sprzętu. W takim przypadku, jeśli sprzęt nie jest w stanie przesłać wierszy o długości przekraczającej 127 bajtów, uzasadnione może być umieszczenie getsw buforze 128-bajtowym, choć moim zdaniem korzyści wynikające z możliwości określenia krótszego bufora, gdy oczekiwane są mniejsze dane wejściowe, byłyby bardziej niż uzasadnione koszt.
supercat
@ legends2k: W rzeczywistości idealnym rozwiązaniem byłoby posiadanie „wskaźnika ciągu” identyfikującego bajt, który wybrałby spośród kilku różnych formatów informacji o łańcuchach / buforach / buforach, z jedną wartością bajtu przedrostka wskazującą strukturę, która zawierała bajt prefiksu [plus dopełnienie] plus rozmiar bufora, użyty rozmiar i adres rzeczywistego tekstu. Taki wzorzec umożliwiłby kodowi przekazywanie dowolnego podłańcucha (nie tylko ogona) innego łańcucha bez konieczności kopiowania czegokolwiek, i pozwoliłby metodom takim jak getsi strcatbezpiecznie zaakceptować tyle, ile będzie pasować.
supercat
16

Nie powinieneś używać, getsponieważ nie ma sposobu na powstrzymanie przepełnienia bufora. Jeśli użytkownik wpisze więcej danych, niż może zmieścić się w buforze, najprawdopodobniej skończy się to uszkodzeniem lub gorzej.

W rzeczywistości ISO podjęło krok polegający na usunięciu gets ze standardu C (od C11, chociaż był przestarzały w C99), co, biorąc pod uwagę, jak wysoko oceniają kompatybilność wsteczną, powinno wskazywać na to, jak zła była ta funkcja.

Prawidłową czynnością jest użycie fgetsfunkcji z stdinuchwytem pliku, ponieważ można ograniczyć liczbę znaków odczytywanych przez użytkownika.

Ale ma to również swoje problemy, takie jak:

  • dodatkowe znaki wprowadzone przez użytkownika zostaną zebrane następnym razem.
  • nie ma szybkiego powiadomienia, że ​​użytkownik wprowadził zbyt dużo danych.

W tym celu prawie każdy koder C w pewnym momencie swojej kariery napisze bardziej przydatne opakowanie fgets. To moje:

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

z pewnym kodem testowym:

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

Zapewnia takie same zabezpieczenia, jak fgetsto, że zapobiega przepełnieniu bufora, ale również powiadamia osobę dzwoniącą o tym, co się stało, i usuwa nadmiar znaków, aby nie wpływały na następną operację wprowadzania.

Używaj go tak, jak chcesz, niniejszym udostępniam na licencji „rób co chcesz, do cholery” :-)

paxdiablo
źródło
W rzeczywistości oryginalny standard C99 nie gets()zastąpił jawnie ani w sekcji 7.19.7.7, w której jest zdefiniowany, ani w sekcji 7.26.9 Przyszłe kierunki bibliotek i podrozdział dla <stdio.h>. Nie ma nawet przypisu, że jest niebezpieczny. (Mimo, że widzę „To przestarzałe w normie ISO / IEC 9899: 1999 / Cor.3: 2007 (E))” w odpowiedzi przez Yu Hao ) Ale C11 nie usunąć go z norma - a nie przed czasem.!
Jonathan Leffler,
int getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL)ukrywa size_tsię intkonwersji sz. sz > INT_MAX || sz < 2złapałby dziwne wartości sz.
chux - Przywróć Monikę
if (buff[strlen(buff)-1] != '\n') {jest exploitem hakera, ponieważ pierwszy wprowadzony znak złego użytkownika może być osadzonym znakiem buff[strlen(buff)-1]UB o wartości zerowej . while (((ch = getchar())...ma problemy, jeśli użytkownik wprowadzi znak zerowy.
chux - Przywróć Monikę
12

fgets .

Aby przeczytać ze standardowego wejścia:

char string[512];

fgets(string, sizeof(string), stdin); /* no buffer overflows here, you're safe! */
Thiago Silveira
źródło
6

Nie można usunąć funkcji API bez zerwania interfejsu API. Jeśli tak, wiele aplikacji nie będzie się w ogóle kompilować ani uruchamiać.

To jest powód, dla którego jedno odniesienie daje:

Odczytanie linii, która przepełnia tablicę wskazywaną przez s, powoduje niezdefiniowane zachowanie. Zalecane jest użycie fgets ().

Gerd Klima
źródło
4

Niedawno przeczytałem w poście USENETcomp.lang.c , który gets()jest usuwany ze standardu. WOOHOO

Z przyjemnością dowiesz się, że komitet właśnie głosował (jednogłośnie, jak się okazuje), aby usunąć również get () z projektu.

pmg
źródło
3
Wspaniale jest, że jest usuwany ze standardu. Jednak większość wdrożeń zapewni go jako „teraz niestandardowe rozszerzenie” na co najmniej następne 20 lat, ze względu na kompatybilność wsteczną.
Jonathan Leffler,
1
Tak, racja, ale podczas kompilacji za pomocą gcc -std=c2012 -pedantic ...gets () nie przejdzie. (Właśnie nadrobiłem -stdparametr)
pmg
4

W C11 (ISO / IEC 9899: 201x) gets()został usunięty. (Jest przestarzałe w ISO / IEC 9899: 1999 / Cor.3: 2007 (E))

Oprócz fgets()C11 wprowadza nową bezpieczną alternatywę gets_s():

C11 K.3.5.4.1 gets_sFunkcja

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

Jednak w Zalecaną praktyką sekcji fgets()jest nadal korzystne.

Ta fgetsfunkcja pozwala poprawnie napisanym programom bezpiecznie przetwarzać wiersze wejściowe zbyt długo, aby zapisać je w tablicy wyników. Zasadniczo wymaga to, aby osoby wywołujące fgetszwracały uwagę na obecność lub brak znaku nowej linii w tablicy wyników. Rozważ użycie fgets(wraz z niezbędnym przetwarzaniem opartym na znakach nowej linii) zamiast gets_s.

Yu Hao
źródło
3

gets()jest niebezpieczne, ponieważ użytkownik może zawiesić program, wpisując zbyt wiele w monicie. Nie może wykryć końca dostępnej pamięci, więc jeśli przydzielisz zbyt małą ilość pamięci do tego celu, może to spowodować awarię seg i awarię. Czasami wydaje się bardzo mało prawdopodobne, aby użytkownik wpisał 1000 liter w pytaniu przeznaczonym na nazwisko osoby, ale jako programiści musimy uczynić nasze programy kuloodpornymi. (może to również stanowić zagrożenie bezpieczeństwa, jeśli użytkownik może zawiesić program systemowy, wysyłając zbyt dużo danych).

fgets() pozwala określić, ile znaków jest pobieranych ze standardowego bufora wejściowego, aby nie przekroczyły zmiennej.

Aradhana Mohanty
źródło
Zauważ, że prawdziwym niebezpieczeństwem nie jest możliwość zawieszenia programu, ale możliwość uruchomienia dowolnego kodu . (Ogólnie rzecz biorąc, wykorzystywanie nieokreślonego zachowania .)
Tanz87
2

Chciałbym poważnie zaprosić wszystkich opiekunów bibliotek C, którzy nadal włączają się getsdo swoich bibliotek „na wypadek, gdyby ktoś nadal polegał na tym”: Proszę zastąpić swoją implementację odpowiednikiem

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

Pomoże to upewnić się, że nikt nie jest zależny od tego. Dziękuję Ci.

Steve Summit
źródło
2

Funkcja C dostaje się jest niebezpieczna i była bardzo kosztownym błędem. Tony Hoare wyróżnia go na szczególną uwagę w swoim przemówieniu „Null References: The Billion Dollar Mistake”:

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

Warto obejrzeć całą godzinę, ale jego komentarze są wyświetlane od 30 minut, a konkretna krytyka około 39 minut.

Mam nadzieję, że to pobudza apetyt na całą rozmowę, która zwraca uwagę na to, jak potrzebujemy bardziej formalnych dowodów poprawności w językach i jak winić projektantów języków za błędy w ich językach, a nie programistę. Wydaje się, że był to cały wątpliwy powód dla projektantów złych języków, by zrzucić winę na programistów pod postacią „wolności programisty”.

użytkownik3717661
źródło