Wady scanf

Chcę poznać wady scanf().

W wielu miejscach czytałem, że użycie scanf może powodować przepełnienie bufora. Jaki jest tego powód? Czy są jakieś inne wady scanf?

Author: Cool Guy, 2010-03-12

9 answers

Problemy ze scanf są (co najmniej):

  • użycie %s, aby uzyskać łańcuch znaków od użytkownika, co prowadzi do możliwości, że łańcuch może być dłuższy niż twój bufor, powodując przepełnienie.
  • możliwość nieudanego skanowania pozostawiając wskaźnik pliku w nieokreślonej lokalizacji.

Bardzo wolę używać fgets do odczytu całych linii, aby ograniczyć ilość odczytywanych danych. Jeśli masz bufor 1K i wczytujesz do niego linijkę fgets to może stwierdzić, czy linia była zbyt długa przez fakt, że nie ma znaku kończącego znak nowej linii (niezależnie od ostatniej linii pliku bez nowej linii).

Następnie możesz złożyć skargę do użytkownika lub przydzielić więcej miejsca na resztę linii (w razie potrzeby stale, dopóki nie masz wystarczająco dużo miejsca). W obu przypadkach nie ma ryzyka przepełnienia bufora.

Po przeczytaniu linii, wiesz że jesteś ustawiony w następnej linii, więc nie ma problemu. Możesz wtedy sscanf ciąg do zawartości serca bez konieczności zapisywania i przywracania wskaźnika pliku do ponownego odczytu.

Oto fragment kodu, którego często używam, aby upewnić się, że nie przepełnia się bufor pytając użytkownika o informacje.

Można go łatwo dostosować, aby użyć pliku innego niż standardowe wejście, jeśli to konieczne, a także można go przypisać własny bufor (i zwiększać go, dopóki nie będzie wystarczająco duży) przed oddaniem go z powrotem do wywołującego (chociaż wywołujący byłby wtedy odpowiedzialny za jego uwolnienie, oczywiście).

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

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

    // Size zero or one cannot store enough, so don't even
    // try - we need space for at least newline and terminator.
    if (sz < 2)
        return SMALL_BUFF;

    // Output prompt.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }

    // Get line with buffer overrun protection.
    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.
    size_t lastPos = strlen(buff) - 1;
    if (buff[lastPos] != '\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[lastPos] = '\0';
    return OK;
}

I, kierowca testowy dla niego:

// Test program for getLine().

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

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        // Extra NL since my system doesn't output that on EOF.
        printf ("\nNo input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long [%s]\n", buff);
        return 1;
    }

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

    return 0;
}
W końcu test pokazujący go w akcji:
$ ./tstprg
Enter string>[CTRL-D]
No input

$ ./tstprg
Enter string> a
OK [a]

$ ./tstprg
Enter string> hello
OK [hello]

$ ./tstprg
Enter string> hello there
Input too long [hello the]

$ ./tstprg
Enter string> i am pax
OK [i am pax]
 49
Author: paxdiablo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-12 01:08:13

Większość odpowiedzi do tej pory wydaje się skupiać na problemie przepełnienia bufora łańcuchów. W rzeczywistości, specyfikatory formatów, które mogą być używane z funkcjami scanf, obsługują jawne ustawienie szerokości pola , które ogranicza maksymalny rozmiar wejścia i zapobiega przepełnieniu bufora. To sprawia, że popularne oskarżenia o przepełnienie bufora łańcuchów obecne w scanf są praktycznie bezpodstawne. Twierdzenie, że {[0] } jest w jakiś sposób analogiczne do gets pod tym względem jest całkowicie błędne. Jest major różnica jakościowa między scanf a gets: scanf nie jest to jednak możliwe, ponieważ nie jest to możliwe.]}

Można argumentować, że te scanf funkcje są trudne w użyciu, ponieważ szerokość pola musi być osadzona w łańcuchu formatu (nie ma sposobu, aby przekazać go przez zmienny argument, jak to można zrobić w printf). To prawda. {[0] } jest rzeczywiście dość słabo zaprojektowany pod tym względem. Niemniej jednak wszelkie twierdzenia, że {[0] } jest jakoś beznadziejnie złamane w odniesieniu do bezpieczeństwa string-buffer-overflow są całkowicie fałszywe i zwykle wykonane przez leniwych programistów.

Prawdziwy problem z scanf ma zupełnie inną naturę, choć dotyczy również przepełnienia. Gdy funkcja scanf jest używana do konwersji dziesiętnych reprezentacji liczb na wartości typów arytmetycznych, nie zapewnia ochrony przed przepełnieniem arytmetycznym. Jeśli wystąpi przepełnienie, scanf wytworzy nieokreślone zachowanie. Z tego powodu jedynym poprawnym sposobem wykonania konwersji w bibliotece standardowej C są funkcje z rodziny strto....

Tak więc, podsumowując powyższe, problem z scanf polega na tym, że jest to trudne (aczkolwiek możliwe) do prawidłowego i bezpiecznego użycia z buforami łańcuchowymi. I nie można bezpiecznie używać do wprowadzania arytmetycznego. Ten ostatni jest prawdziwym problemem. Ta pierwsza jest tylko niedogodnością.

P. S. powyższe ma dotyczyć całej rodziny scanf funkcji (w tym również fscanf i sscanf). W przypadku scanf oczywistym problemem jest to, że sam pomysł użycia ściśle sformatowanej funkcji do odczytu potencjalnie interaktywnego danych wejściowych jest raczej wątpliwy.

 49
Author: AnT,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-11-29 14:39:46

Z komp.lang.C FAQ: dlaczego wszyscy mówią, aby nie używać scanf? Czego powinienem użyć zamiast tego?

scanf ma wiele problemów-zobacz pytania 12.17, 12.18 a , oraz 12.19. Również jego format %s ma ten sam problem co gets() (Zobacz pytanie 12.23)-trudno jest zagwarantować, że bufor odbiorczy nie przepełni się. [przypis]

Bardziej ogólnie, {[0] } jest przeznaczony do stosunkowo structured, formatted input (jego nazwa w rzeczywistości pochodzi od "scan formatted"). Jeśli zwrócisz uwagę, powie Ci, czy się udało, czy nie, ale może powiedzieć tylko w przybliżeniu, gdzie się nie powiodło, a wcale nie jak i dlaczego. Masz bardzo mało możliwości odzyskania błędów.

Interaktywne wejście użytkownika jest jednak najmniej ustrukturyzowanym wejściem. Dobrze zaprojektowany interfejs użytkownika pozwoli na możliwość pisania przez użytkownika niemal wszystkiego-nie tylko liter czy interpunkcja, gdy oczekiwano cyfr, ale także więcej lub mniej znaków niż oczekiwano, lub w ogóle żadnych znaków ( tj., tylko klawisz RETURN), lub przedwczesne EOF, lub cokolwiek innego. Jest prawie niemożliwe, aby z gracją poradzić sobie z tymi wszystkimi potencjalnymi problemami podczas używania scanf; o wiele łatwiej jest odczytać całe wiersze( z fgets lub tym podobne), a następnie zinterpretować je, używając sscanf lub innych technik. (Funkcje takie jak strtol, strtok, i atoi są często przydatne; Zobacz też pytania 12.16 oraz 13.6.) Jeśli używasz dowolnego wariantu scanf, sprawdź wartość zwracaną, aby upewnić się, że znaleziono oczekiwaną liczbę elementów. Ponadto, jeśli używasz %s, pamiętaj, aby chronić przed przepełnieniem bufora.

Zauważ przy okazji, że krytyka scanf niekoniecznie jest oskarżeniem fscanf i sscanf. scanf czyta z stdin, który jest zwykle interaktywną klawiaturą i dlatego jest najmniej ograniczony, co prowadzi do większości problemów. Gdy plik danych ma znany format, z drugiej strony może być właściwe odczytanie go za pomocą fscanf. Jest całkowicie właściwe parsowanie łańcuchów za pomocą sscanf (o ile zwracana wartość jest zaznaczona), ponieważ tak łatwo jest odzyskać kontrolę, ponownie uruchomić skanowanie, odrzucić dane wejściowe, jeśli nie pasują, itd.

Dodatkowe linki:

[[19]}Bibliografia: K & R2 sek. 7.4 str. 159
 12
Author: jamesdlin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-01-05 13:50:28

Tak, masz rację. W rodzinie scanf istnieje poważna wada bezpieczeństwa(scanf,sscanf, fscanf..itd) esp podczas czytania łańcucha, ponieważ nie biorą pod uwagę długości bufora (do którego są odczytywane).

Przykład:

char buf[3];
sscanf("abcdef","%s",buf);

Oczywiście bufor buf może pomieścić znak max 3. Ale sscanf spróbuje umieścić "abcdef" w nim powodując przepełnienie bufora.

 5
Author: codaddict,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-12 03:25:55

Bardzo trudno jest zrobić to, co chcesz. Oczywiście, że możesz, ale rzeczy takie jak scanf("%s", buf); są równie niebezpieczne jak gets(buf);, Jak wszyscy mówili.

Jako przykład, co paxdiablo robi w swojej funkcji do czytania, można zrobić za pomocą czegoś takiego:

scanf("%10[^\n]%*[^\n]", buf));
getchar();

Powyższe spowoduje odczytanie linii, zapisanie pierwszych 10 znaków nie-nowej linii w buf, a następnie odrzucenie wszystkiego do (włącznie) nowej linii. Zatem funkcję paxdiablo można zapisać używając scanf następującego sposób:

#include <stdio.h>

enum read_status {
    OK,
    NO_INPUT,
    TOO_LONG
};

static int get_line(const char *prompt, char *buf, size_t sz)
{
    char fmt[40];
    int i;
    int nscanned;

    printf("%s", prompt);
    fflush(stdout);

    sprintf(fmt, "%%%zu[^\n]%%*[^\n]%%n", sz-1);
    /* read at most sz-1 characters on, discarding the rest */
    i = scanf(fmt, buf, &nscanned);
    if (i > 0) {
        getchar();
        if (nscanned >= sz) {
            return TOO_LONG;
        } else {
            return OK;
        }
    } else {
        return NO_INPUT;
    }
}

int main(void)
{
    char buf[10+1];
    int rc;

    while ((rc = get_line("Enter string> ", buf, sizeof buf)) != NO_INPUT) {
        if (rc == TOO_LONG) {
            printf("Input too long: ");
        }
        printf("->%s<-\n", buf);
    }
    return 0;
}

Jednym z innych problemów z scanf jest jego zachowanie w przypadku przepełnienia. Na przykład, podczas czytania int:

int i;
scanf("%d", &i);

Powyższego nie można bezpiecznie używać w przypadku przepełnienia. Nawet w pierwszym przypadku czytanie ciągu znaków jest o wiele prostsze do zrobienia z fgets, a nie z scanf.

 5
Author: Alok Singhal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-12 06:39:42

Problemy, które mam z *scanf() rodziną:

  • możliwość przepełnienia bufora ze specyfikatorami % s i % [konwersji. Tak, możesz określić maksymalną szerokość pola, ale w przeciwieństwie do printf(), nie możesz uczynić go argumentem w wywołaniu scanf(); musi być zakodowany na twardo w specyfikatorze konwersji.
  • potencjał przepełnienia arytmetycznego z %d, % i itd.
  • ograniczona zdolność do wykrywania i odrzucania źle uformowanych danych wejściowych. Na przykład, "12w4" nie jest poprawną liczbą całkowitą, ale scanf("%d", &value); będzie pomyślnie przekonwertowano i przypisano 12 do value, pozostawiając "w4" w strumieniu wejściowym, aby zablokować przyszły odczyt. W idealnym przypadku cały łańcuch wejściowy powinien zostać odrzucony, ale scanf() nie daje łatwego mechanizmu, aby to zrobić.

Jeśli wiesz, że Twoje dane wejściowe zawsze będą dobrze uformowane z ciągami o stałej długości i wartościami liczbowymi, które nie flirtują z przepełnieniem, to scanf() jest świetnym narzędziem. Jeśli masz do czynienia z interaktywnym wejściem lub wejściem, które nie jest gwarantowane dobrze uformowany, a następnie użyj czegoś innego.

 3
Author: John Bode,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-12 15:18:24

Wiele odpowiedzi omawia potencjalne problemy z przepełnieniem podczas używania scanf("%s", buf), ale najnowsza Specyfikacja POSIX rozwiązuje ten problem, dostarczając znak m assign-allocation, który może być używany w specyfikacjach formatów dla c, s, i [ formaty. Pozwala to scanf przydzielić tyle pamięci, ile jest to konieczne za pomocą malloc (więc musi zostać uwolniona później za pomocą free).

Przykład jego użycia:

char *buf;
scanf("%ms", &buf); // with 'm', scanf expects a pointer to pointer to char.

// use buf

free(buf);

Zobacz tutaj . Wady takiego podejścia jest to stosunkowo nowy dodatek do specyfikacji POSIX i w ogóle nie jest określony w specyfikacji C, więc na razie pozostaje raczej nieortable.

 3
Author: dreamlax,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-10-03 01:20:04

Jest jeden duży problem z scanf-podobnymi funkcjami-brakiemjakiegokolwiek bezpieczeństwa typu. Oznacza to, że możesz to kodować:

int i;
scanf("%10s", &i);

Cholera, nawet to jest "w porządku":

scanf("%10s", i);

Jest gorzej niż printf-podobne funkcje, ponieważ scanf oczekuje wskaźnika, więc awarie są bardziej prawdopodobne.

Oczywiście, istnieją pewne Kontrolery określające format, ale te nie są idealne i dobrze, nie są częścią języka lub biblioteki standardowej.

 3
Author: vladimir veljkovic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-13 14:48:10

Zaletą scanf jest to, że gdy nauczysz się używać tego narzędzia, jak zawsze powinieneś to robić w C, ma ono niezwykle użyteczne zastosowania. możesz dowiedzieć się, jak używać scanf i przyjaciół, czytając i rozumiejąc podręcznik. Jeśli nie możesz przejść przez ten podręcznik bez poważnych problemów ze zrozumieniem, prawdopodobnie oznacza to, że nie znasz dobrze języka C.


scanf i przyjaciele cierpieli z powodu niefortunnych wyborów projektowych, które utrudniały (i czasami niemożliwe) do poprawnego użycia bez czytania dokumentacji, jak pokazały inne odpowiedzi. Niestety, występuje to w całym C, więc jeśli miałbym odradzać używanie scanf, to prawdopodobnie odradzałbym używanie C.]}

Jedną z największych wad wydaje się być wyłącznie reputacja, którą zdobywa wśród niewtajemniczonych; podobnie jak w przypadku wielu przydatnych funkcji C powinniśmy być dobrze poinformowani, zanim go użyjemy. Kluczem jest uświadomienie sobie, że podobnie jak w przypadku reszty C, to wydaje się zwięzłe i idiomatyczne, ale to może być subtelnie mylące. Jest to wszechobecne w C; początkującym łatwo jest napisać kod, który ich zdaniem ma sens i może nawet zadziałać dla nich na początku, ale nie ma sensu i może zawieść katastrofalnie.

Na przykład niewtajemniczeni zwykle oczekują, że delegat %s spowoduje odczytanie linii i chociaż może to wydawać się intuicyjne, niekoniecznie jest to prawdą. Właściwsze jest opisanie pola odczytanego jako a word . Czytanie instrukcji jest zdecydowanie zalecane dla każdej funkcji.

Jaka byłaby odpowiedź na to pytanie bez wspominania o braku bezpieczeństwa i ryzyku przepełnienia bufora? Jak już omówiliśmy, C nie jest bezpiecznym językiem i pozwoli nam na skrócenie czasu, ewentualnie zastosowanie optymalizacji kosztem poprawności lub bardziej prawdopodobne, ponieważ jesteśmy leniwymi programistami. Tak więc, gdy wiemy, że system nigdy nie otrzyma łańcucha większego niż stała liczba bajtów, otrzymujemy możliwość deklarowania tablicy o takim rozmiarze i sprawdzania granic. Nie postrzegam tego jako upadku w dół; jest to opcja. Ponownie, czytanie instrukcji jest zdecydowanie zalecane i ujawniłoby tę opcję dla nas.

Leniwi programiści nie są jedynymi użądlonymi scanf. Często zdarza się, że ludzie próbują odczytać float lub double wartości używając na przykład %d. Zwykle mylą się w przekonaniu, że implementacja wykona jakąś konwersję za scen, co miałoby sens, ponieważ podobne konwersje zdarzają się w całej reszcie języka, ale tutaj tak nie jest. Jak powiedziałem wcześniej, scanf i przyjaciele (i rzeczywiście reszta C) są zwodnicze; wydają się zwięzłe i idiomatyczne, ale nie są.

Niedoświadczeni programiści nie są zmuszeni do rozważenia sukcesu operacji . Załóżmy, że użytkownik wprowadzi coś całkowicie nie-numerycznego, gdy powiemy scanf, aby odczytał i przekonwertował ciąg cyfr dziesiętnych za pomocą %d. Jedynym sposobem na przechwycenie takich błędnych danych jest sprawdzenie zwracanej wartości, a jak często zawracamy sobie głowę sprawdzaniem zwracanej wartości?

Kiedy przyjaciele nie przeczytają tego, co im kazano czytać, strumień zostanie pozostawiony w niezwykłym stanie.]} - W przypadku fgets, jeśli nie ma wystarczającej ilości miejsca do przechowywania pełnego wiersza, reszta nieprzeczytanego wiersza może być błędnie traktowana tak, jakby była to nowa linia, Jeśli nie jest. - W przypadku scanf i znajomych konwersja nie powiodła się, jak opisano powyżej, błędne dane pozostają nieprzeczytane w strumieniu i mogą być błędnie traktowane tak, jakby były częścią innego pola.

Nie jest łatwiej używać scanf i przyjaciół niż używać fgets. Jeśli sprawdzamy sukces, szukając '\n', gdy używamy fgets lub sprawdzając wartość zwracaną, gdy używamy scanf i przyjaciół, i okaże się, że przeczytaliśmy niekompletną linię używając fgets lub nie przeczytaliśmy jej pole używając scanf, wtedy mamy do czynienia z tą samą rzeczywistością: prawdopodobnie odrzucimy dane wejściowe (Zwykle do następnej linii nowej linii włącznie)! Yuuuuuuuu!

Niestety, scanf jedno i drugie sprawia, że odrzucenie danych wejściowych w ten sposób jest trudne (nieintuicyjne) i łatwe (mało naciśnięć klawiszy). W obliczu tej rzeczywistości odrzucania wkładu użytkownika, niektórzy próbowali scanf("%*[^\n]%*c");, nie zdając sobie sprawy, że delegat %*[^\n] zawiedzie, gdy napotka tylko nową linię, a więc nowa linia nadal pozostanie w strumieniu.

Lekka adaptacja, rozdzielając dwa delegaty formatu i widzimy tutaj pewien sukces: scanf("%*[^\n]"); getchar();. Spróbuj to zrobić z tak małą liczbą naciśnięć klawiszy za pomocą innego narzędzia;) [32]}

 3
Author: autistic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-11 22:28:05