Nachteile von scanf

Lesezeit: 15 Minuten

Benutzeravatar von karthi_ms
karthi_ms

Ich möchte die Nachteile wissen scanf().

Auf vielen Seiten habe ich das mit gelesen scanf kann zu Pufferüberläufen führen. Was ist der Grund dafür? Gibt es noch andere Nachteile mit scanf?

AnT steht mit Russlands Benutzer-Avatar
AnT steht zu Russland

Die meisten Antworten scheinen sich bisher auf das Problem des String-Pufferüberlaufs zu konzentrieren. In Wirklichkeit die Formatbezeichner, die verwendet werden können scanf Funktionen unterstützen explizit Feldbreite -Einstellung, die die maximale Größe der Eingabe begrenzt und einen Pufferüberlauf verhindert. Dies macht die populären Anschuldigungen des String-Pufferüberlaufs zu Gefahren in scanf praktisch grundlos. Behaupten, dass scanf ist irgendwie analog zu gets in der Hinsicht ist völlig falsch. Es gibt einen großen qualitativen Unterschied zwischen scanf und gets: scanf stellt dem Benutzer Funktionen zur Verhinderung von String-Pufferüberläufen zur Verfügung, während gets nicht.

Man kann argumentieren, dass diese scanf Funktionen sind schwierig zu verwenden, da die Feldbreite in die Formatzeichenfolge eingebettet werden muss (es gibt keine Möglichkeit, sie durch ein variadisches Argument zu übergeben, wie dies in möglich ist printf). Das stimmt tatsächlich. scanf ist in dieser Hinsicht in der Tat ziemlich schlecht konzipiert. Aber das behauptet doch jeder scanf bezüglich der String-Buffer-Overflow-Sicherheit irgendwie hoffnungslos kaputt ist, sind völlig falsch und werden normalerweise von faulen Programmierern gemacht.

Das eigentliche Problem mit scanf hat eine ganz andere Natur, obwohl es auch darum geht Überlauf. Wann scanf -Funktion zum Konvertieren von Dezimaldarstellungen von Zahlen in Werte arithmetischer Typen verwendet wird, bietet sie keinen Schutz vor arithmetischem Überlauf. Wenn es zu einem Überlauf kommt, scanf erzeugt undefiniertes Verhalten. Aus diesem Grund ist die einzig richtige Möglichkeit, die Konvertierung in der C-Standardbibliothek durchzuführen, Funktionen von strto... Familie.

Also, um das oben Gesagte zusammenzufassen, das Problem mit scanf ist, dass es schwierig (wenn auch möglich) ist, String-Puffer richtig und sicher zu verwenden. Und es ist unmöglich, es sicher für arithmetische Eingaben zu verwenden. Letzteres ist das eigentliche Problem. Ersteres ist nur eine Unannehmlichkeit.

PS Das Obige soll sich auf die gesamte Familie beziehen scanf Funktionen (einschließlich auch fscanf und sscanf). Mit scanf Das offensichtliche Problem besteht insbesondere darin, dass die Idee, eine streng formatierte Funktion zum Lesen zu verwenden, potenziell besteht interaktiv Eingabe ist eher fragwürdig.

  • Ich muss nur darauf hinweisen, dass Sie arithmetische Eingaben nicht sicher lesen können, sondern dass Sie sie nicht korrekt ausführen können und robust für Dirty-Input. Für mich gibt es einen großen Unterschied zwischen dem Absturz meines Programms und/oder dem Öffnen des Betriebssystems für einen Angriff und dem einfachen Erhalten einiger falscher Werte, wenn Benutzer absichtlich Unheil anstellen. Was kümmert es mich, wenn sie 1431337.4044194872987 eingeben und stattdessen 4,0 erhalten? So oder so haben sie 4.0 eingegeben. (Manchmal mag es wichtig sein, aber wie oft?)

    Benutzer645280

    3. Januar 2014 um 14:48 Uhr


  • Dritter Absatz: scanf liest gerne einen Wert von> 2 ^ 32, wenn er in der Zeichenfolge gefunden wird, in eine 32-Bit-Ganzzahl und verursacht undefiniertes Verhalten?

    – 2501

    20. Januar 2016 um 14:52 Uhr

  • @2501: Ja, genau. Zumindest passiert das laut Sprachstandard.

    – AnT steht zu Russland

    20. Januar 2016 um 15:38 Uhr


  • “Behaupten, dass scanf ist irgendwie analog zu bekommt in der Hinsicht ist völlig falsch.” Ich verstehe es, scanf wenigstens tut Sie können die maximale Feldgröße angeben, aber die ideologische Verwendung von %s hat sicherlich die gleichen Probleme wie gets, und wie bei vielen anderen gefährlichen, aber nützlichen Werkzeugen in C sind sie alle leicht zu missbrauchen. Eben strtoul hat seine Gefahren, also anstatt vorzuschlagen, dass die Leute aufhören, es zu benutzen Teile von C, können wir nicht einfach dazu übergehen, den Leuten vorzuschlagen, dass sie aufhören sollten alles C?

    – autistisch

    11. Juli 2018 um 23:49 Uhr

Benutzeravatar von paxdiablo
paxdiablo

Die Probleme mit scanf sind (mindestens):

  • verwenden %s um eine Zeichenfolge vom Benutzer zu erhalten, was zu der Möglichkeit führt, dass die Zeichenfolge länger als Ihr Puffer ist und einen Überlauf verursacht.
  • die Möglichkeit, dass ein fehlgeschlagener Scan Ihren Dateizeiger an einem unbestimmten Ort hinterlässt.

Ich benutze sehr viel lieber fgets ganze Zeilen einzulesen, um die gelesene Datenmenge zu begrenzen. Wenn Sie einen 1K-Puffer haben und eine Zeile mit hineinlesen fgets Sie können erkennen, ob die Zeile zu lang war, weil das Zeilenumbruchzeichen fehlt (trotz der letzten Zeile einer Datei ohne Zeilenumbruch).

Dann können Sie sich beim Benutzer beschweren oder dem Rest der Zeile mehr Platz zuweisen (kontinuierlich, wenn nötig, bis Sie genug Platz haben). In beiden Fällen besteht kein Risiko eines Pufferüberlaufs.

Sobald Sie die Zeile gelesen haben, können Sie kennt dass Sie in der nächsten Zeile positioniert sind, sodass es dort kein Problem gibt. Sie können dann sscanf Ihre Zeichenfolge nach Herzenslust, ohne den Dateizeiger zum erneuten Lesen speichern und wiederherstellen zu müssen.

Hier ist ein Codeausschnitt, den ich häufig verwende, um sicherzustellen, dass kein Pufferüberlauf auftritt, wenn ich den Benutzer nach Informationen frage.

Es könnte leicht angepasst werden, um bei Bedarf eine andere Datei als die Standardeingabe zu verwenden, und Sie könnten es auch einen eigenen Puffer zuweisen lassen (und ihn weiter erhöhen, bis er groß genug ist), bevor Sie diesen an den Aufrufer zurückgeben (obwohl der Aufrufer dann verantwortlich wäre). für die Befreiung natürlich).

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

    // Catch possibility of `\0` in the input stream.

    size_t len = strlen(buff);
    if (len < 1)
        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[len - 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[len - 1] = '\0';
    return OK;
}

Und ein Testtreiber dafür:

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

Abschließend ein Testlauf, um es in Aktion zu zeigen:

$ printf "\0" | ./tstprg     # Singular NUL in input stream.
Enter string>
No input

$ ./tstprg < /dev/null       # EOF in input stream.
Enter string>
No input

$ ./tstprg                   # A one-character string.
Enter string> a
OK [a]

$ ./tstprg                   # Longer string but still able to fit.
Enter string> hello
OK [hello]

$ ./tstprg                   # Too long for buffer.
Enter string> hello there
Input too long [hello the]

$ ./tstprg                   # Test limit of buffer.
Enter string> 123456789
OK [123456789]

$ ./tstprg                   # Test just over limit.
Enter string> 1234567890
Input too long [123456789]

  • if (fgets (buff, sz, stdin) == NULL) return NO_INPUT; Warum hast du verwendet NO_INPUT als Rückgabewert? fgets kehrt zurück NULL nur auf Fehler.

    – Fabio Carello

    29. März 2013 um 17:19 Uhr


  • @Fabio, nicht ganz. Es gibt auch null zurück, wenn der Stream geschlossen wird, bevor eine Eingabe erfolgt ist. Das ist der Fall, der hier gefangen wird. Machen Sie nicht den Fehler, dass NO_INPUT eine leere Eingabe bedeutet (drücken Sie ENTER vor allem anderen) – Letzteres gibt Ihnen eine leere Zeichenfolge ohne NO_INPUT-Fehlercode.

    – paxdiablo

    29. März 2013 um 23:21 Uhr


  • Der neueste POSIX-Standard erlaubt char *buf; scanf("%ms", &buf); die Ihnen genügend Platz zuweist malloc (daher muss es später freigegeben werden), was dazu beitragen würde, Pufferüberläufe zu verhindern.

    – Traumlax

    3. Oktober 2014 um 1:14 Uhr

  • Was passiert, wenn wir anrufen getLine mit 1 als die sz Parameter? if (buff[strlen(buff)-1] != '\n') da tritt das problem auf. Vielleicht if (!sz) { return TOO_LONG; } if (buff[sz = strcspn(buff, "\n")] == '\n' || getchar() == '\n') { buff[sz] = '\0'; return OK; } unsigned char c; while (fread(&c, 1, 1, stdin) == 1 && c != '\n'); return TOO_LONG; was in der Tat Gewohnheit überlaufen, wenn Sie passieren sz <= 1 und hat den zusätzlichen Vorteil, das zu entfernen '\n' für Sie ohne Overhead, obwohl es bemerkt werden sollte, dass Ihr Code könnte durch strategischen Einsatz verbessert werden scanf

    – autistisch

    11. Juli 2018 um 23:21 Uhr

  • Das ist ein guter Fang, @chux, ich habe eine zusätzliche Prüfung dafür hinzugefügt, um es als “keine Eingabe” zu behandeln. Der Test wurde mit durchgeführt printf "\0" | exeName um das ursprüngliche Problem zu überprüfen und zu beheben. Ich schätze, ich habe noch nie mit einem so verrückten Eingabeszenario nachgesehen (aber ich verdammt gut sollte haben). Danke für die Warnung.

    – paxdiablo

    1. September 2020 um 2:00 Uhr


Benutzeravatar von jamesdlin
jamesdlin

Aus der comp.lang.c-FAQ: Warum sagt jeder, dass man scanf nicht verwenden soll? Was sollte ich stattdessen verwenden?

scanf hat eine Reihe von Problemen – siehe Fragen 12.17, 12.18aund 12.19. Auch seine %s Format hat das gleiche Problem, dass gets() hat (siehe Frage 23.12) – es ist schwer zu garantieren, dass der Empfangspuffer nicht überläuft. [footnote]

Allgemeiner, scanf ist für eine relativ strukturierte, formatierte Eingabe konzipiert (der Name leitet sich tatsächlich von „scan formatted“ ab). Wenn Sie darauf achten, wird es Ihnen sagen, ob es erfolgreich war oder fehlgeschlagen ist, aber es kann Ihnen nur ungefähr sagen, wo es fehlgeschlagen ist, und überhaupt nicht, wie oder warum. Sie haben nur sehr wenige Möglichkeiten, Fehler zu beheben.

Dennoch ist die interaktive Benutzereingabe die am wenigsten strukturierte Eingabe, die es gibt. Eine gut gestaltete Benutzeroberfläche ermöglicht es dem Benutzer, fast alles einzugeben – nicht nur Buchstaben oder Satzzeichen, wenn Ziffern erwartet wurden, sondern auch mehr oder weniger Zeichen als erwartet oder überhaupt keine Zeichen (dh, nur die RETURN-Taste), oder vorzeitiges EOF oder irgendetwas. Es ist fast unmöglich, mit all diesen potenziellen Problemen bei der Verwendung elegant umzugehen scanf; Es ist viel einfacher, ganze Zeilen zu lesen (mit fgets oder dergleichen), interpretieren Sie sie dann entweder mit sscanf oder einige andere Techniken. (Funktionen wie strtol, strtokund atoi sind oft nützlich; siehe auch Fragen 12.16 und 13.6.) Wenn Sie welche verwenden scanf Variante überprüfen Sie unbedingt den Rückgabewert, um sicherzustellen, dass die erwartete Anzahl von Elementen gefunden wurde. Auch wenn Sie verwenden %sstellen Sie sicher, dass Sie sich vor einem Pufferüberlauf schützen.

Beachten Sie übrigens, dass Kritik an scanf sind nicht unbedingt Anklagen gegen fscanf und sscanf. scanf liest aus stdin, die normalerweise eine interaktive Tastatur ist und daher am wenigsten eingeschränkt ist und zu den meisten Problemen führt. Wenn eine Datendatei andererseits ein bekanntes Format hat, kann es angebracht sein, sie damit zu lesen fscanf. Es ist perfekt geeignet, um Zeichenfolgen mit zu analysieren sscanf (solange der Rückgabewert geprüft wird), weil es so einfach ist, die Kontrolle wiederzuerlangen, den Scan neu zu starten, die Eingabe zu verwerfen, wenn sie nicht übereinstimmt, usw.

Weiterführende Links:

Referenzen: K&R2 Sek. 7,4 p. 159

Es ist sehr schwer zu bekommen scanf um das zu tun, was du willst. Sicher, das kannst du, aber Dinge wie scanf("%s", buf); sind genauso gefährlich wie gets(buf);wie alle gesagt haben.

Als Beispiel kann das, was Paxdiablo in seiner Funktion zum Lesen tut, mit etwas wie:

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

Das Obige liest eine Zeile und speichert die ersten 10 Nicht-Zeilenumbruchzeichen darin buf, und verwerfen Sie dann alles bis (einschließlich) einem Zeilenumbruch. Die Funktion von paxdiablo könnte also mit geschrieben werden scanf folgender Weg:

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

Eines der anderen Probleme mit scanf ist sein Verhalten bei Überlauf. Zum Beispiel beim Lesen einer int:

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

Die oben genannten können im Falle eines Überlaufs nicht sicher verwendet werden. Selbst im ersten Fall ist das Lesen eines Strings viel einfacher zu handhaben fgets eher als mit scanf.

Ja, du hast recht. Da ist eine große Sicherheitslücke drin scanf Familie(scanf,sscanf, fscanf..etc), insbesondere beim Lesen eines Strings, da sie die Länge des Puffers (in den sie lesen) nicht berücksichtigen.

Beispiel:

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

eindeutig der Puffer buf kann MAX halten 3 verkohlen. Aber die sscanf werde versuchen zu setzen "abcdef" hinein, was zu einem Pufferüberlauf führt.

  • Sie können “%10s” als Formatbezeichner angeben und es werden nicht mehr als 10 Zeichen in den Puffer eingelesen.

    – Traumlax

    12. März 2010 um 3:29 Uhr

  • Sicher – es ist möglich, die API sicher zu verwenden. Es ist auch möglich, Dynamit zu verwenden, um Schmutz sicher aus Ihrem Garten zu entfernen. Aber ich würde es auch nicht empfehlen, zumal es sicherere Alternativen gibt.

    – Larry Ostermann

    12. März 2010 um 3:34 Uhr

  • Mein Vater benutzte früher Gelignite, um Bäume auf der Farm zu fällen. Sie müssen nur Ihre Werkzeuge verstehen und die Gefahren kennen.

    – paxdiablo

    12. März 2010 um 3:42 Uhr

  • @codaddict: Die Tatsache, dass jemand die Feldbreite nicht mit verwendet scanf ist das problem mit dem jemand, nicht mit scanf. Es ist völlig irrelevant für das fragliche Thema. Das ist schließlich C, nicht Java.

    – AnT steht zu Russland

    12. März 2010 um 7:44 Uhr

  • Das Problem ist, dass die Feldbreite in scanf() muss im Konvertierungsbezeichner fest codiert sein; mit printf()können Sie verwenden * im Konvertierungsbezeichner und übergeben Sie die Länge als Argument. Aber seit * bedeutet etwas anderes in scanf(), das funktioniert nicht, also muss man im Grunde für jeden Lesevorgang ein neues Format generieren, wie es Alok in seinem Beispiel tut. Es fügt nur mehr Arbeit und Unordnung hinzu; könnte man auch verwenden fgets() und fertig damit.

    – Johannes Bode

    12. März 2010 um 15:23 Uhr

Benutzeravatar von John Bode
Johannes Bode

Probleme habe ich mit der *scanf() Familie:

  • Potenzial für Pufferüberlauf mit %s und %[ conversion specifiers. Yes, you can specify a maximum field width, but unlike with printf(), you can’t make it an argument in the scanf() call; it must be hardcoded in the conversion specifier.
  • Potential for arithmetic overflow with %d, %i, etc.
  • Limited ability to detect and reject badly formed input. For example, “12w4” is not a valid integer, but scanf("%d", &value); will successfully convert and assign 12 to value, leaving the “w4” stuck in the input stream to foul up a future read. Ideally the entire input string should be rejected, but scanf() doesn’t give you an easy mechanism to do that.

If you know your input is always going to be well-formed with fixed-length strings and numerical values that don’t flirt with overflow, then scanf() is a great tool. If you’re dealing with interactive input or input that isn’t guaranteed to be well-formed, then use something else.

  • You can provide “%10s” as the format specifier and it will read no more than 10 characters into the buffer.

    – dreamlax

    Mar 12, 2010 at 3:29

  • Sure – it’s possible to use the API safely. It’s also possible to use dynamite to clear dirt out of your garden safely. But I wouldn’t recommend either, especially since there are safer alternatives.

    – Larry Osterman

    Mar 12, 2010 at 3:34

  • My dad used to use gelignite for clearing down trees on the farm. You just have to understand your tools and know the dangers.

    – paxdiablo

    Mar 12, 2010 at 3:42

  • @codaddict: The fact that someone doesn’t use field width with scanf is the problem with that someone, not with scanf. It is completely irrelevant to the issue in question. This is C after all, not Java.

    – AnT stands with Russia

    Mar 12, 2010 at 7:44

  • The problem is that the field width in scanf() must be hardcoded in the conversion specifier; with printf(), you can use * in the conversion specifier and pass the length as an argument. But since * means something different in scanf(), that doesn’t work, so you basically have to generate a new format for each read like Alok does in his example. It just adds more work and clutter; might as well use fgets() and be done with it.

    – John Bode

    Mar 12, 2010 at 15:23

dreamlax's user avatar
dreamlax

Many answers here discuss the potential overflow issues of using scanf("%s", buf), but the latest POSIX specification more-or-less resolves this issue by providing an m assignment-allocation character that can be used in format specifiers for c, s, and [ formats. This will allow scanf to allocate as much memory as necessary with malloc (so it must be freed later with free).

An example of its use:

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

// use buf

free(buf);

See here. Disadvantages to this approach is that it is a relatively recent addition to the POSIX specification and it is not specified in the C specification at all, so it remains rather unportable for now.

1418300cookie-checkNachteile von scanf

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy