Czy snprintf () ZAWSZE kończy się null?

84

Czy snprintf zawsze przerywa bufor docelowy?

Innymi słowy, czy to wystarczy:

czy też musisz to robić, jeśli jakiś czas jest wystarczająco długi?

Interesuje mnie zarówno to, co mówi standard, jak i co może zrobić niektóre popularne libc, co nie jest standardowym zachowaniem.

Prof. Falken
źródło
Czy w drugim przykładzie masz na myśli zakończenie somestr lub dst?
Hudson
@chux, Martin Ba uwzględnił to w zaakceptowanej odpowiedzi. :)
Prof. Falken
@chux Myślę, że to było dobre, Twój komentarz bardzo wyraźnie wyjaśnił, że jeśli dest i 0 long, nic nie jest napisane. Każdy komentarz traktuję jako potencjalną wymówkę do rozmowy z innymi kolesiami. :)
Prof. Falken
@Prof. Falken Zgadzam się, że komentarz był w porządku i wyraźny, ale był zbędny w odpowiedziach - po prostu przegapiłem to w mojej recenzji.
chux - Przywróć Monikę
stackoverflow.com/a/8712996/193892 Visual Studio obsługuje teraz snprintf ()
Prof. Falken

Odpowiedzi:

73

Jak ustalają inne odpowiedzi: Powinien :

snprintf... Zapisuje wyniki w buforze ciągu znaków. (...) zostanie zakończone znakiem null, chyba że buf_size wynosi zero.

Więc wszystko, co musisz uważać, to aby nie przekazać do niego bufora o rozmiarze zerowym, ponieważ (oczywiście) nie może on zapisać zera „nigdzie”.


Jednak uważaj , że biblioteki Microsoftu nie posiada funkcję o nazwie snprintflecz historycznie tylko miał funkcję o nazwie _snprintf(uwaga wiodącym podkreślenia), które nie dołączania null obciążeniowy. Oto dokumenty (VS 2012, ~~ VS 2013):

http://msdn.microsoft.com/en-us/library/2ts7cx93%28v=vs.110%29.aspx

Wartość zwracana

Niech len będzie długością sformatowanego ciągu danych (bez kończącej wartości null). len i count są w bajtach dla _snprintf, szerokie znaki dla _snwprintf.

  • Jeśli len <count, to len znaki są przechowywane w buforze, dołączany jest terminator null i zwracana jest wartość len.

  • Jeśli len = count, to len znaki są przechowywane w buforze, nie jest dodawany żaden terminator null i zwracana jest wartość len.

  • Jeśli len> count, to liczba znaków jest przechowywana w buforze, nie jest dołączany żaden terminator null i zwracana jest wartość ujemna.

(...)

Visual Studio 2015 (VC14) najwyraźniej wprowadził zgodną snprintffunkcji, ale jeden z wiodących Legacy podkreślenia i non zerowej kończącego zachowań nadal istnieje:

snprintfFunkcja obcina wyjście kiedy len jest większa niż lub równa ilość, umieszczając zerowej-terminator buffer[count-1]. (...)

Dla wszystkich funkcji innych niż snprintf, jeśli len = count, len znaki są przechowywane w buforze, nie jest dodawany żaden terminator null , (...)

Martin Ba
źródło
24
Co, na imię Aslana, myśleli inżynierowie Microsoftu, kiedy wprowadzili, _snprintfktóry po cichu usuwa kluczową funkcję bezpieczeństwasnprintf i pozwala, aby ciąg nie był zakończony zerem ?!
Colin D Bennett
2
@ColinDBennett - to jest dziwne i bardzo irytujące i nie mam pojęcia, czy ktoś w ogóle pomyślał :-)
Martin Ba
2
@MartinBa tak przepraszam, to co testowałem template <size_t size> int _snprintf_s(char (&buffer)[size], size_t count, const char *format [, argument] ...);i powinienem też wspomnieć, że dzieje się to tylko z flagą kompilacji / GS (Security Check). Ta funkcja zna rozmiar, liczbę i długość.
sekmet64
3
Uważaj, mingw64 używał (używa?) Implementacji Microsoft _snprintf jako „normalnego” snprintf, chyba że określono inaczej nvd.nist.gov/vuln/detail/CVE-2018-1000101
domenukk
2
@Sajjon To wprawdzie głupi (i być może całkowicie oryginalny) okrzyk irytacji ( idioms.thefreedictionary.com/in+the+name+of+God ), być może trochę jak mielona przysięga ( en.wikipedia.org/wiki/Minced_oath ). Innym przykładem może być „Co na imię Zeusa…?!” ( forum.wordreference.com/threads/in-the-name-of-zeus.2132965 )
Colin D Bennett
19

Zgodnie ze stroną podręcznika snprintf (3).

Funkcje snprintf()i vsnprintf()zapisują co najwyżej sizebajtów (łącznie z końcowym bajtem zerowym („\ 0”)) do str.

Więc tak, nie trzeba przerywać, jeśli rozmiar> = 1.

piotr
źródło
3
I dzięki Bogu za to; to jedyny rozsądny projekt. Cały sens sprawdzonych wersji tych funkcji polega na tym, aby być bezpiecznym i byłoby okropnie, gdybyś musiał ręcznie wykonywać wszystkie malarkey.
Kerrek SB
1
Zalecałbym przetestowanie go na platformach, których używasz, zanim na tym polegasz. Nawet jeśli powinien zapisać bajt zerowy, wiem, że napotkałem implementacje, które tego nie zrobiły (mogło to być z MinGW, który używał starszego środowiska uruchomieniowego MS).
Dmitri
10

Zgodnie ze standardem C, chyba że rozmiar bufora wynosi 0 vsnprintf()i snprintf()null kończy jego wyjście.

snprintf()Funkcją jest równoważna sprintf()z dodatkiem n argumentu, który stwierdza, rozmiar bufora określone przez s. Jeśli n wynosi zero, nic nie zostanie zapisane, a s może być pustym wskaźnikiem. W przeciwnym razie bajty wyjściowe poza n-pierwszym powinny zostać odrzucone zamiast zapisywania ich w tablicy, a bajt zerowy jest zapisywany na końcu bajtów faktycznie zapisanych w tablicy.

Tak więc, jeśli chcesz wiedzieć, jak duży bufor ma zostać przydzielony, użyj rozmiaru zerowego, a następnie możesz użyć wskaźnika zerowego jako miejsca docelowego. Zauważ, że linkowałem do stron POSIX, ale te wyraźnie mówią, że nie ma żadnej rozbieżności między Standardem C a POSIX, jeśli dotyczą tego samego zagadnienia:

Funkcjonalność opisana na tej stronie referencyjnej jest zgodna z normą ISO C. Jakikolwiek konflikt między opisanymi tu wymaganiami a normą ISO C jest niezamierzony. Ten tom POSIX.1-2008 odnosi się do standardu ISO C.

Uważaj na wersję Microsoft vsnprintf(). Zdecydowanie zachowuje się inaczej niż standardowa wersja C, gdy nie ma wystarczającej ilości miejsca w buforze (zwraca -1, gdzie standardowa funkcja zwraca wymaganą długość). Nie jest do końca jasne, że wersja Microsoft null kończy swoje wyjście w przypadku wystąpienia błędu, podczas gdy standardowa wersja C.

Zwróć także uwagę na odpowiedzi na pytanie Czy używasz bezpiecznych funkcji TR 24731? (zobacz MSDN dla wersji Microsoft vsprintf_s()) i rozwiązanie Mac dla bezpiecznych alternatyw dla niebezpiecznych funkcji biblioteki C?

Jonathan Leffler
źródło
oh, zły, nigdy o tym nie myślałem. Z drugiej strony ... :)
Prof. Falken
ach, myślę, że MS vsprintf () ugryzło mnie i podniosłem to - 1 nawyk
Prof. Falken
4

Niektóre starsze wersje SunOS robiły dziwne rzeczy z snprintf i mogły nie przerywać wyjścia NUL i zwracać wartości, które nie pasowały do ​​tego, co robili wszyscy inni, ale wszystko, co zostało wydane w ciągu ostatnich 10 lat, robiło to, co C99 mówi.

Sztuka
źródło
Zauważyłem, że XP został wydany nieco ponad 10 lat temu. :-)
Prof. Falken
W tym roku był przestarzały. :)
Prof. Falken
4

Niejednoznaczność zaczyna się od samego standardu C. Zarówno C99, jak i C11 mają identyczny opis snprintffunkcji. Oto opis z C99:

7.19.6.5 snprintfFunkcja
Streszczenie
1 #include <stdio.h> int snprintf(char * restrict s, size_t n, const char * restrict format, ...);
Opis
2 snprintfFunkcja jest równoważna fprintf, z tym wyjątkiem, że dane wyjściowe są zapisywane w tablicy (określonej przez argument s), a nie w strumieniu. Jeśli nwynosi zero, nic nie jest zapisywane i smoże być pustym wskaźnikiem. W przeciwnym razie znaki wyjściowe poza n-1st są odrzucane, a nie zapisywane w tablicy, a znak null jest zapisywany na końcu znaków faktycznie zapisanych w tablicy. Jeśli kopiowanie odbywa się między nakładającymi się obiektami, zachowanie jest niezdefiniowane.
Zwraca
3 snprintfFunkcja zwraca liczbę znaków, które zostałyby zapisanenbyły wystarczająco duże, nie licząc kończącego znaku null lub wartości ujemnej, jeśli wystąpił błąd kodowania. W związku z tym dane wyjściowe zakończone znakiem null zostały całkowicie zapisane wtedy i tylko wtedy, gdy zwrócona wartość jest nieujemna i mniejsza niż n.

Z jednej strony zdanie

W przeciwnym razie znaki wyjściowe poza n-1st są odrzucane, a nie zapisywane w tablicy, a znak null jest zapisywany na końcu znaków faktycznie zapisanych w tablicy

mówi, że
jeśli ( swskazuje na tablicę składającą się z 3 znaków i) nwynosi 3, to zostaną zapisane 2 znaki, a znaki poza drugim są odrzucane ; wtedy znak null jest zapisywany po tych 2 (a znak null będzie trzecim zapisanym znakiem) .

I to, jak sądzę, odpowiada na pierwotne pytanie.
ODPOWIEDŹ:
Jeśli kopiowanie odbywa się między nakładającymi się obiektami, zachowanie jest niezdefiniowane.
Jeśli nwynosi 0, nic nie jest zapisywane na wyjściu, w
przeciwnym razie, jeśli nie napotkano błędów kodowania, wyjście jest ZAWSZE zakończone znakiem null ( niezależnie od tego, czy dane wyjściowe mieszczą się w tablicy wyjściowej, czy nie ; jeśli nie, to niektóre znaki są odrzucane, tak że wynik tablica nigdy nie jest przepełniona), w
przeciwnym razie (jeśli wystąpią błędy kodowania) dane wyjściowe mogą pozostać niezerowe .

Z drugiej strony
Ostatnie zdanie

Zatem wyjście zakończone znakiem null zostało całkowicie zapisane wtedy i tylko wtedy, gdy zwrócona wartość jest nieujemna i mniejsza niż n

daje niejednoznaczność (lub mój angielski nie jest wystarczająco dobry). Mogę zinterpretować to zdanie na co najmniej dwa sposoby:
1. Wyjście jest zakończone wartością zerową wtedy i tylko wtedy, gdy zwrócona wartość jest nieujemna i mniejsza niżn (co oznacza, że ​​jeśli zwrócona wartość jest nie mniejsza niż n, tj. Dane wyjściowe (w tym kończący znak null) nie mieści się w tablicy, wtedy dane wyjściowe nie są zakończone znakiem null ).
2. Dane wyjściowe są kompletne (żadne znaki nie zostały odrzucone) wtedy i tylko wtedy, gdy zwrócona wartość jest nieujemna i mniejsza niżn .


Uważam, że powyższa interpretacja 1 jest sprzeczna z ODPOWIEDZI, powoduje nieporozumienia i długie dyskusje. Dlatego ostatnie zdanie opisujące snprintffunkcję wymaga zmiany w celu usunięcia wszelkich niejasności (co daje podstawy do napisania Propozycji na Standard języka C).
Uważam, że przykład niejednoznacznego sformułowania można znaleźć na stronie http://en.cppreference.com/w/c/io/fprintf (zobacz 4)), dzięki @ "Martin Ba" za link.

Zobacz także pytanie „ snprintf: Czy są jakieś propozycje / plany C Standardu zmiany opisu tej funkcji? ”.

Robin Kuzmin
źródło
4
Twoja interpretacja 1 nie wydaje mi się w ogóle wiarygodna. Parsuję to zdanie jako „Wyjście (które, nawiasem mówiąc, zakończone jest znakiem zerowym) zostało w całości napisane, jeśli ...”, co rozumiem tylko jako # 2.
zwolniony
1
Negacja zdania „wyjście zakończone znakiem null zostało całkowicie zapisane” jest „wyjściem zakończonym znakiem null” nie zostało całkowicie zapisane”. Nic więcej. Samo zdanie zanegowane nie oznacza, że cokolwiek zostało napisane (obejmuje to niekompletne wyjście zakończone zerem, niepełne wyjście zakończone zerem lub bezbarwne zielone pomysły). Inne miejsce w standardzie mówi, co dokładnie jest napisane, gdy wynik jest niekompletny, i to miejsce stwierdza, że ​​wyjście jest zakończone wartością null, chyba że jest puste (n == 0).
n. zaimki m.