Wie kann verhindert werden, dass scanf in C einen Pufferüberlauf verursacht?

81

Ich benutze diesen Code:

while ( scanf("%s", buf) == 1 ){

Was wäre der beste Weg, um einen möglichen Pufferüberlauf zu verhindern, damit Zeichenfolgen mit zufälligen Längen übergeben werden können?

Ich weiß, dass ich die Eingabezeichenfolge einschränken kann, indem ich zum Beispiel Folgendes aufrufe:

while ( scanf("%20s", buf) == 1 ){

Aber ich würde es vorziehen, alle Benutzereingaben verarbeiten zu können. Oder kann dies nicht sicher mit scanf gemacht werden und ich sollte fgets verwenden?

goe
quelle

Antworten:

64

In ihrem Buch The Practice of Programming (das es wert ist, gelesen zu werden) diskutieren Kernighan und Pike dieses Problem und lösen es, indem sie snprintf()den String mit der richtigen Puffergröße für die Übergabe an die scanf()Funktionsfamilie erstellen . In der Tat:

int scanner(const char *data, char *buffer, size_t buflen)
{
    char format[32];
    if (buflen == 0)
        return 0;
    snprintf(format, sizeof(format), "%%%ds", (int)(buflen-1));
    return sscanf(data, format, buffer);
}

Beachten Sie, dass dies die Eingabe weiterhin auf die als "Puffer" angegebene Größe beschränkt. Wenn Sie mehr Speicherplatz benötigen, müssen Sie die Speicherzuweisung durchführen oder eine nicht standardmäßige Bibliotheksfunktion verwenden, die die Speicherzuweisung für Sie übernimmt.


Beachten Sie, dass die POSIX 2008 (2013) Version der scanf()Familie von Funktionen ein Format Modifikator unterstützt m(eine Zuweisung Zuweisungszeichen) für String - Eingänge ( %s, %c, %[). Anstatt ein char *Argument zu verwenden, wird ein char **Argument verwendet und der erforderliche Speicherplatz für den gelesenen Wert zugewiesen:

char *buffer = 0;
if (sscanf(data, "%ms", &buffer) == 1)
{
    printf("String is: <<%s>>\n", buffer);
    free(buffer);
}

Wenn die sscanf()Funktion nicht alle Konvertierungsspezifikationen erfüllt, wird der gesamte Speicher, den sie für %msähnliche Konvertierungen zugewiesen hat, freigegeben, bevor die Funktion zurückkehrt.

Jonathan Leffler
quelle
@ Sam: Ja, es sollte sein buflen-1- Danke. Sie müssen sich dann um einen nicht signierten Unterlauf (Umhüllung auf eine ziemlich große Anzahl) kümmern, daher der ifTest. Ich wäre sehr versucht, dies durch ein zu ersetzen assert()oder es durch ein assert()vor dem ifwährend der Entwicklung ausgelöstes zu sichern, wenn jemand nachlässig genug ist, um 0 als Größe zu übergeben. Ich habe die Dokumentation nicht sorgfältig geprüft, was dies %0sbedeutet sscanf()- der Test könnte besser sein als if (buflen < 2).
Jonathan Leffler
So snprintfschreibt einige Daten in einem String - Puffer und sscanfliest von diesem erstellten Zeichenfolge. Wo genau ersetzt dies, scanfindem es von stdin liest?
krb686
Es ist auch ziemlich verwirrend, dass Sie das Wort "Format" für Ihre Ergebniszeichenfolge verwenden und daher "Format" als erstes Argument übergeben, snprintfaber es ist nicht der eigentliche Formatparameter.
krb686
@ krb686: Dieser Code ist so geschrieben, dass sich die zu scannenden Daten im Parameter befinden dataund daher sscanf()angemessen sind. Wenn Sie stattdessen von der Standardeingabe lesen möchten, löschen Sie den dataParameter und rufen Sie scanf()stattdessen auf. Bei der Auswahl des Namens formatfür die Variable, die die Formatzeichenfolge im Aufruf von wird sscanf(), können Sie diese bei Bedarf umbenennen, der Name ist jedoch nicht ungenau. Ich bin mir nicht sicher, welche Alternative Sinn macht. würde in_formates klarer machen? Ich habe nicht vor, es in diesem Code zu ändern. Sie können, wenn Sie diese Idee in Ihrem eigenen Code verwenden.
Jonathan Leffler
1
@mabraham: Es ist immer noch wahr unter macOS Sierra 10.12.5 (bis 2017-06-06) - das scanf()auf macOS ist nicht als unterstützend dokumentiert %ms, obwohl es nützlich wäre.
Jonathan Leffler
30

Wenn Sie gcc verwenden, können Sie den GNU-Erweiterungsspezifizierer verwenden a, damit scanf () Speicher zuweist, damit Sie die Eingabe halten können:

int main()
{
  char *str = NULL;

  scanf ("%as", &str);
  if (str) {
      printf("\"%s\"\n", str);
      free(str);
  }
  return 0;
}

Bearbeiten: Wie Jonathan betonte, sollten Sie die scanfManpages konsultieren, da der Bezeichner möglicherweise anders ist ( %m) und Sie möglicherweise bestimmte Definitionen beim Kompilieren aktivieren müssen.

John Ledbetter
quelle
8
Dies ist eher ein Problem bei der Verwendung von glibc (der GNU C-Bibliothek) als bei der Verwendung des GNU C-Compilers.
Jonathan Leffler
3
Beachten Sie auch, dass der POSIX 2008-Standard den mModifikator bereitstellt , um denselben Job auszuführen. Siehe scanf(). Sie müssen überprüfen, ob die von Ihnen verwendeten Systeme diesen Modifikator unterstützen.
Jonathan Leffler
4
GNU (wie unter Ubuntu 13.10 jedenfalls zu finden) unterstützt %ms. Die Notation %aist ein Synonym für %f(bei der Ausgabe werden hexadezimale Gleitkommadaten angefordert). Die GNU-Manpage für scanf()lautet: _ Es ist nicht verfügbar, wenn das Programm mit gcc -std=c99oder gcc -D_ISOC99_SOURCE kompiliert wurde (sofern nicht anders _GNU_SOURCEangegeben). In diesem Fall awird das als Bezeichner für Gleitkommazahlen interpretiert (siehe oben) ._
Jonathan Leffler
8

Meistens eine Kombination aus fgetsund sscanferledigt den Job. Die andere Sache wäre, einen eigenen Parser zu schreiben, wenn die Eingabe gut formatiert ist. Beachten Sie auch, dass Ihr zweites Beispiel einige Änderungen erfordert, um sicher verwendet zu werden:

#define LENGTH          42
#define str(x)          # x
#define xstr(x)         str(x)

/* ... */ 
int nc = scanf("%"xstr(LENGTH)"[^\n]%*[^\n]", array); 

Mit dem obigen Befehl wird der Eingabestream bis zum Zeichen newline ( \n) verworfen, jedoch nicht . Sie müssen ein hinzufügen getchar(), um dies zu verbrauchen. Überprüfen Sie auch, ob Sie das Ende des Streams erreicht haben:

if (!feof(stdin)) { ...

und das war's auch schon.

dirkgently
quelle
2
Könnten Sie den feofCode in einen größeren Kontext stellen? Ich frage, da diese Funktion oft falsch verwendet wird.
Roland Illig
1
arraymuss seinchar array[LENGTH+1];
jxh
4

Die direkte Verwendung scanf(3)und ihre Varianten werfen eine Reihe von Problemen auf. In der Regel werden Benutzer und nicht interaktive Anwendungsfälle in Form von Eingabezeilen definiert. Es kommt selten vor, dass mehr Zeilen das Problem lösen, wenn nicht genügend Objekte gefunden werden. Dies ist jedoch der Standardmodus für scanf. (Wenn ein Benutzer nicht wusste, dass er eine Nummer in die erste Zeile eingeben soll, helfen eine zweite und eine dritte Zeile wahrscheinlich nicht.)

Zumindest wenn Sie fgets(3)wissen, wie viele Eingabezeilen Ihr Programm benötigt und Sie keine Pufferüberläufe haben ...

DigitalRoss
quelle
1

Das Begrenzen der Länge der Eingabe ist definitiv einfacher. Sie können eine beliebig lange Eingabe akzeptieren, indem Sie eine Schleife verwenden, jeweils ein Stück einlesen und den Speicherplatz für die Zeichenfolge nach Bedarf neu zuweisen ...

Aber das ist eine Menge Arbeit, so dass die meisten C-Programmierer die Eingabe einfach in beliebiger Länge abschneiden. Ich nehme an, Sie wissen das bereits, aber mit fgets () können Sie keine beliebigen Textmengen akzeptieren - Sie müssen immer noch ein Limit festlegen.

Mark Bessey
quelle
Weiß dann jemand, wie man das mit scanf macht?
Goe
3
Wenn Sie fgets in einer Schleife verwenden, können Sie beliebige Textmengen akzeptieren - behalten realloc()Sie einfach Ihren Puffer.
Bdonlan
1

Es ist nicht so viel Arbeit, eine Funktion zu erstellen, die den benötigten Speicher für Ihre Zeichenfolge zuweist. Das ist eine kleine C-Funktion, die ich vor einiger Zeit geschrieben habe. Ich benutze sie immer, um Strings einzulesen.

Es wird die gelesene Zeichenfolge zurückgegeben oder wenn ein Speicherfehler auftritt, NULL. Beachten Sie jedoch, dass Sie Ihre Zeichenfolge freigeben () und immer auf den Rückgabewert prüfen müssen.

#define BUFFER 32

char *readString()
{
    char *str = malloc(sizeof(char) * BUFFER), *err;
    int pos;
    for(pos = 0; str != NULL && (str[pos] = getchar()) != '\n'; pos++)
    {
        if(pos % BUFFER == BUFFER - 1)
        {
            if((err = realloc(str, sizeof(char) * (BUFFER + pos + 1))) == NULL)
                free(str);
            str = err;
        }
    }
    if(str != NULL)
        str[pos] = '\0';
    return str;
}

quelle
sizeof (char)ist per Definition 1. Du brauchst es hier nicht.
RastaJedi
Normalerweise empfiehlt es sich, die Zuweisung / Freigabe von Zeigern auf demselben Niveau zu halten, was bedeutet, dass Ihre Funktion keinen eigenen Speicher zuweisen sollte, da der Aufrufer ihn dann freigeben muss. Die meisten Standardfunktionen der Bibliothek / Posix halten sich an dieses Prinzip, indem sie entweder eine statische Zeichenfolge (wie strerror(3)) zurückgeben oder eine vorab zugewiesene Zeichenfolge erwarten (wie ( strerror_r(3)- oder scanf(3)) ...
Michael Beer