Jak zapobiec wywoływaniu przez scanf przepełnienia bufora w C?

83

Używam tego kodu:

Jaki byłby najlepszy sposób, aby zapobiec możliwemu przepełnieniu buforu, aby można było przekazywać ciągi o losowej długości?

Wiem, że mogę ograniczyć ciąg wejściowy, wywołując na przykład:

Ale wolałbym mieć możliwość przetwarzania wszystkiego, co wprowadzi użytkownik. Czy nie można tego zrobić bezpiecznie za pomocą scanf i powinienem używać fgets?

goe
źródło

Odpowiedzi:

66

W swojej książce The Practice of Programming (która jest warta przeczytania), Kernighan i Pike omawiają ten problem i rozwiązują go, używając snprintf()do stworzenia łańcucha z odpowiednim rozmiarem bufora do przekazania do scanf()rodziny funkcji. W efekcie:

Uwaga, to nadal ogranicza dane wejściowe do rozmiaru podanego jako „bufor”. Jeśli potrzebujesz więcej miejsca, musisz dokonać alokacji pamięci lub użyć niestandardowej funkcji bibliotecznej, która alokuje pamięć za Ciebie.


Należy zauważyć, że POSIX 2008 (2013) wersja scanf()rodziny funkcji obsługuje modyfikator formatu m(o charakterze przydział alokacji) dla wejść strunowych ( %s, %c, %[). Zamiast pobierać char *argument, pobiera char **argument i przydziela niezbędną przestrzeń dla odczytywanej wartości:

Jeśli sscanf()funkcja nie spełnia wszystkich specyfikacji konwersji, cała pamięć, którą zaalokowała na potrzeby %mskonwersji podobnych do tych , jest zwalniana, zanim funkcja zwróci.

Jonathan Leffler
źródło
@Sam: Tak, powinno być buflen-1- dziękuję. Następnie musisz się martwić o niepodpisany niedomiar (zawijanie do dość dużej liczby), stąd iftest. Byłbym bardzo kuszony, aby zastąpić to znakiem assert()lub zarchiwizować go assert()przed tym, ifktóry uruchomi się podczas programowania, jeśli ktoś jest na tyle nieostrożny, aby podać 0 jako rozmiar. Nie przejrzałem dokładnie dokumentacji, co %0soznacza sscanf()- test może być lepszy, jak if (buflen < 2).
Jonathan Leffler,
Więc snprintfzapisuje niektóre dane do bufora ciągów i sscanfodczytuje z utworzonego ciągu. Gdzie dokładnie to zastępuje scanfw tym, że czyta ze standardowego wejścia?
krb686
Dość mylące jest również to, że używasz słowa „format” jako ciągu wynikowego i przekazujesz w ten sposób „format” jako pierwszy argument, snprintfale nie jest to faktyczny parametr formatu.
krb686
@ krb686: Ten kod jest napisany w taki sposób, że dane do skanowania znajdują się w parametrze datai dlatego sscanf()są odpowiednie. Jeśli zamiast tego chcesz czytać ze standardowego wejścia, usuń dataparametr i wywołaj scanf()zamiast tego. Jeśli chodzi o wybór nazwy formatzmiennej, która staje się ciągiem formatującym w wywołaniu sscanf(), możesz zmienić jej nazwę, jeśli chcesz, ale jej nazwa nie jest niedokładna. Nie jestem pewien, która alternatywa ma sens; by in_formatuczynić go być jaśniejsze? Nie planuję zmieniać tego w tym kodzie; możesz, jeśli wykorzystasz ten pomysł we własnym kodzie.
Jonathan Leffler
1
@mabraham: Jest to nadal prawdą w systemie macOS Sierra 10.12.5 (do 06.06.2017) - system scanf()macOS nie jest udokumentowany jako pomocniczy %ms, chociaż byłby użyteczny.
Jonathan Leffler
31

Jeśli używasz gcc, możesz użyć specyfikatora rozszerzenia GNU, aby funkcja ascanf () przydzieliła pamięć do przechowywania danych wejściowych:

Edycja: Jak zauważył Jonathan, powinieneś zapoznać się ze scanfstronami podręcznika, ponieważ specyfikator może być inny ( %m) i może być konieczne włączenie pewnych definicji podczas kompilacji.

John Ledbetter
źródło
8
To bardziej problem używania glibc (biblioteki GNU C) niż używania kompilatora GNU C.
Jonathan Leffler,
3
Zauważ, że standard POSIX 2008 zapewnia mmodyfikator do tego samego zadania. Zobacz scanf(). Musisz sprawdzić, czy używane systemy obsługują ten modyfikator.
Jonathan Leffler
4
GNU (przynajmniej w wersji Ubuntu 13.10) obsługuje %ms. Notacja %ajest synonimem %f(na wyjściu żąda szesnastkowych danych zmiennoprzecinkowych). Strona scanf()podręcznika GNU mówi: _ Nie jest dostępna, jeśli program jest skompilowany z gcc -std=c99lub gcc -D_ISOC99_SOURCE (chyba że _GNU_SOURCEjest również określony), w którym to przypadku ajest interpretowany jako specyfikator liczb zmiennoprzecinkowych (patrz wyżej) ._
Jonathan Leffler
8

W większości przypadków połączenie fgetsi sscanfspełnia swoje zadanie. Inną rzeczą byłoby napisanie własnego parsera, jeśli dane wejściowe są dobrze sformatowane. Zwróć też uwagę, że twój drugi przykład wymaga pewnych modyfikacji, aby mógł być bezpiecznie używany:

Powyższe powoduje odrzucenie strumienia wejściowego do góry, ale nie obejmuje \nznaku nowej linii ( ). Będziesz musiał dodać a, getchar()aby to wykorzystać. Sprawdź również, czy osiągnąłeś koniec transmisji:

i to wszystko.

dirkgently
źródło
2
Czy mógłbyś umieścić feofkod w szerszym kontekście? Pytam, ponieważ ta funkcja jest często używana nieprawidłowo.
Roland Illig
1
arraymusi byćchar array[LENGTH+1];
jxh
4

Bezpośrednie użycie scanf(3)i jego warianty stwarza szereg problemów. Zwykle użytkownicy i nieinteraktywne przypadki użycia są definiowane za pomocą wierszy danych wejściowych. Rzadko można spotkać przypadek, w którym, jeśli nie zostanie znaleziona wystarczająca liczba obiektów, więcej linii rozwiąże problem, ale jest to domyślny tryb scanf. (Jeśli użytkownik nie wiedział, jak wpisać liczbę w pierwszej linii, druga i trzecia linia prawdopodobnie nie pomogą.)

Przynajmniej jeśli fgets(3)wiesz, ile wierszy wejściowych będzie potrzebnych Twój program i nie będziesz mieć żadnych przepełnień bufora ...

DigitalRoss
źródło
1

Ograniczenie długości wejścia jest zdecydowanie łatwiejsze. Możesz zaakceptować dowolnie długie dane wejściowe, używając pętli, czytając po jednym kawałku, ponownie przydzielając miejsce dla ciągu w razie potrzeby ...

Ale to dużo pracy, więc większość programistów C po prostu odcina dane wejściowe na dowolnej długości. Przypuszczam, że już to wiesz, ale użycie fgets () nie pozwoli ci zaakceptować dowolnej ilości tekstu - nadal będziesz musiał ustawić limit.

Mark Bessey
źródło
Więc czy ktoś wie, jak to zrobić za pomocą scanfa?
goe
3
Używanie fgets w pętli pozwala na akceptowanie dowolnych ilości tekstu - po prostu zachowując realloc()bufor.
bdonlan,
1

Stworzenie funkcji, która przydziela potrzebną pamięć dla twojego łańcucha, nie wymaga wiele pracy. To mała funkcja c, którą napisałem jakiś czas temu, zawsze używam jej do czytania w łańcuchach.

Zwróci odczytany ciąg lub jeśli wystąpi błąd pamięci NULL. Ale pamiętaj, że musisz zwolnić () swój ciąg i zawsze sprawdzać, czy jest zwracana.


źródło
sizeof (char)jest z definicji 1. Nie potrzebujesz tego tutaj.
RastaJedi
Zwykle dobrą praktyką jest utrzymywanie alokacji / zwalniania wskaźnika na tym samym poziomie, co oznacza, że ​​funkcja nie powinna samodzielnie przydzielać pamięci, ponieważ wywołujący musi ją zwolnić. Większość standardowych bibliotek / POSIX funkcji stosować się do tej zasady, albo przez powrocie ciąg statyczny (jak strerror(3)) lub oczekiwać wstępnie przydzielone ciąg przekazany w (jak ( strerror_r(3)- lub scanf(3)) ...
Michael Beer