Недостатки сканирования

Я хочу знать недостатки scanf().

На многих сайтах я читал, что использование scanf может вызвать переполнение буфера. Что является причиной этого? Есть ли другие недостатки у scanf?


person karthi_ms    schedule 12.03.2010    source источник
comment
См. также Руководство для начинающих от scanf().   -  person Jonathan Leffler    schedule 19.07.2017


Ответы (9)


Проблемы с scanf (как минимум):

  • использование %s для получения строки от пользователя, что приводит к возможности того, что строка может быть длиннее вашего буфера, вызывая переполнение.
  • возможность неудачного сканирования оставить указатель вашего файла в неопределенном месте.

Я очень предпочитаю использовать fgets для чтения целых строк, чтобы вы могли ограничить объем считываемых данных. Если у вас есть буфер размером 1 КБ, и вы читаете в него строку с помощью fgets, вы можете определить, была ли строка слишком длинной, по тому факту, что в ней нет завершающего символа новой строки (несмотря на последнюю строку файла без новой строки).

Затем вы можете пожаловаться пользователю или выделить больше места для остальной части строки (при необходимости непрерывно, пока у вас не будет достаточно места). В любом случае нет риска переполнения буфера.

Как только вы прочитали строку, вы знаете, что находитесь на следующей строке, так что здесь нет проблем. После этого вы можете sscanf продублировать вашу строку в свое удовольствие, не сохраняя и не восстанавливая указатель файла для повторного чтения.

Вот фрагмент кода, который я часто использую, чтобы предотвратить переполнение буфера при запросе информации у пользователя.

Его можно легко настроить для использования файла, отличного от стандартного ввода, если это необходимо, и вы также можете выделить ему собственный буфер (и продолжать увеличивать его, пока он не станет достаточно большим), прежде чем вернуть его вызывающему (хотя вызывающий тогда будет нести ответственность за освобождение, конечно).

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

И тест-драйвер для него:

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

Наконец, тестовый запуск, чтобы показать его в действии:

$ 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]
person paxdiablo    schedule 12.03.2010
comment
if (fgets (buff, sz, stdin) == NULL) return NO_INPUT; Почему вы использовали NO_INPUT в качестве возвращаемого значения? fgets возвращает NULL только в случае ошибки. - person Fabio Carello; 29.03.2013
comment
@ Фабио, не совсем. Он также возвращает null, если поток закрывается до того, как был сделан какой-либо ввод. Это тот случай, который пойман здесь. Не делайте ошибку, что NO_INPUT означает пустой ввод (нажатие ENTER перед чем-либо еще) - последнее дает вам пустую строку без кода ошибки NO_INPUT. - person paxdiablo; 30.03.2013
comment
Последний стандарт POSIX позволяет использовать char *buf; scanf("%ms", &buf);, который выделит достаточно места для вас с помощью malloc (поэтому его необходимо освободить позже), что поможет предотвратить переполнение буфера. - person dreamlax; 03.10.2014
comment
Что произойдет, если мы вызовем getLine с 1 в качестве параметра sz? if (buff[strlen(buff)-1] != '\n') вот где возникает проблема. Возможно, 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;, который действительно не переполнится, когда вы пройдете sz <= 1, и имеет дополнительное преимущество удаления '\n' для вас с нулевыми накладными расходами, хотя следует отметить, что ваш код может быть улучшен за счет стратегического использования из scanf... - person autistic; 12.07.2018
comment
@autistic, это хороший момент, спасибо за это, я должен признать, что никогда не пробовал это с размером буфера, равным единице, просто потому, что это не может вернуть никакой полезной информации. Поэтому я просто решил поймать это быстро и сделать это условием ошибки. - person paxdiablo; 12.07.2018
comment
size_t lastPos = strlen(buff) - 1; if (buff[lastPos] != '\n') { можно использовать как UB, если сначала ввести нулевой символ. - person chux - Reinstate Monica; 31.08.2020
comment
Это хороший улов, @chux, я добавил дополнительную проверку, чтобы рассматривать это как отсутствие ввода. Тестирование было проведено с помощью printf "\0" | exeName для проверки исходной проблемы и ее устранения. Думаю, я никогда не проверял с таким безумным сценарием ввода (но, черт возьми, должен был это сделать). Спасибо за внимание. - person paxdiablo; 01.09.2020

Большинство ответов до сих пор, похоже, сосредоточены на проблеме переполнения строкового буфера. На самом деле спецификаторы формата, которые можно использовать с функциями scanf, поддерживают явную настройку ширины поля, которая ограничивает максимальный размер ввода и предотвращает переполнение буфера. Это делает популярные обвинения в опасности переполнения строкового буфера, присутствующие в scanf, практически беспочвенными. Утверждение, что scanf чем-то похоже на gets в этом отношении, совершенно неверно. Между scanf и gets есть большая качественная разница: scanf предоставляет пользователю функции предотвращения переполнения строкового буфера, а gets — нет.

Можно возразить, что эти возможности scanf сложны в использовании, так как ширина поля должна быть встроена в строку формата (нет возможности передать ее через аргумент с переменным числом аргументов, как это можно сделать в printf). Это действительно так. scanf действительно довольно плохо спроектирован в этом отношении. Но, тем не менее, любые заявления о том, что scanf каким-то образом безнадежно нарушен в отношении безопасности переполнения строкового буфера, являются полностью фиктивными и обычно исходят от ленивых программистов.

Настоящая проблема с scanf имеет совершенно другую природу, хотя она тоже связана с переполнением. Когда функция scanf используется для преобразования десятичных представлений чисел в значения арифметических типов, она не обеспечивает защиты от арифметического переполнения. Если происходит переполнение, scanf приводит к неопределенному поведению. По этой причине единственный правильный способ выполнить преобразование в стандартной библиотеке C — это функции из семейства strto....

Итак, подытоживая вышесказанное, проблема с scanf заключается в том, что его сложно (хотя и возможно) правильно и безопасно использовать со строковыми буферами. И нельзя смело использовать для арифметического ввода. Последнее является настоящей проблемой. Первое просто неудобство.

P.S. Вышеупомянутое предназначено для всего семейства функций scanf (включая также fscanf и sscanf). В частности, с scanf очевидная проблема заключается в том, что сама идея использования строго отформатированной функции для чтения потенциально интерактивного ввода довольно сомнительна.

person AnT    schedule 12.03.2010
comment
Я просто должен отметить, что дело не в том, что вы не можете безопасно читать арифметический ввод, а в том, что вы не можете сделать это правильно и надежно для грязного ввода. Для меня есть огромная разница между сбоем моей программы и/или открытием ОС для атаки и просто получением нескольких неправильных значений, когда пользователи пытаются целенаправленно навредить. Какое мне дело до того, что они набрали 1431337.4044194872987 и вместо этого получили 4.0? В любом случае они вошли в 4.0. (Иногда это может иметь значение, но как часто?) - person ; 03.01.2014
comment
Третий абзац: scanf с радостью прочитает значение ›2^32, если оно встречается в строке, в 32-битное целое число и вызовет неопределенное поведение? - person 2501; 20.01.2016
comment
@2501: Да, точно. По крайней мере, так происходит в соответствии со стандартом языка. - person AnT; 20.01.2016
comment
Утверждение, что scanf чем-то похоже на gets в этом отношении, совершенно неверно. Я понимаю, scanf по крайней мере действительно позволяет указать максимальный размер поля, но идеологическое использование of %s, безусловно, имеет те же проблемы, что и gets, и, как и многие другие опасные, но полезные инструменты в C, ими легко злоупотреблять. Даже у strtoul есть свои опасности, так что вместо того, чтобы предлагать людям прекратить использовать части языка C, не можем ли мы сразу предложить людям прекратить использование всего языка C? - person autistic; 12.07.2018

Из часто задаваемых вопросов comp.lang.c: Почему все говорят не использовать scanf? Что я должен использовать вместо этого?

scanf имеет ряд проблем — см. вопросы 12.17, 12.18a и 12.19. Кроме того, его формат %s имеет ту же проблему, что и gets() (см. вопрос 12.23) — это трудно гарантировать, что принимающий буфер не переполнится. [сноска]

В более общем смысле, scanf предназначен для относительно структурированного, форматированного ввода (на самом деле его название происходит от «отформатированного сканирования»). Если вы обратите внимание, то оно скажет вам, удалось это или не удалось, но может сказать вам лишь приблизительно, где это не удалось, а вовсе не как и почему. У вас очень мало возможностей для исправления ошибок.

Тем не менее, интерактивный пользовательский ввод — это наименее структурированный ввод. Хорошо продуманный пользовательский интерфейс позволяет пользователю вводить что угодно — не только буквы или знаки препинания, когда ожидалось ввести цифры, но и больше или меньше символов, чем ожидалось, или вообще не вводить символы (т.е. /em>, просто клавиша RETURN), или преждевременный EOF, или что-то еще. Почти невозможно изящно справиться со всеми этими потенциальными проблемами при использовании scanf; гораздо проще читать целые строки (с fgets и т.п.), а затем интерпретировать их, используя либо sscanf, либо какие-либо другие приемы. (Часто бывают полезны такие функции, как strtol, strtok и atoi; см. также вопросы 12.16 и 13.6.) Если вы используете какой-либо вариант scanf, обязательно проверьте возвращаемое значение, чтобы уверен, что ожидаемое количество элементов было найдено. Кроме того, если вы используете %s, обязательно предохраняйтесь от переполнения буфера.

Обратите внимание, кстати, что критика scanf не обязательно является обвинением fscanf и sscanf. scanf считывается с stdin, которая обычно представляет собой интерактивную клавиатуру и, следовательно, наименее ограничена, что приводит к наибольшему количеству проблем. С другой стороны, когда файл данных имеет известный формат, может быть уместно прочитать его с помощью fscanf. Совершенно уместно анализировать строки с помощью sscanf (пока возвращаемое значение проверяется), потому что так легко восстановить контроль, перезапустить сканирование, отбросить ввод, если он не соответствует, и т. д.

Дополнительные ссылки:

Ссылки: K&R2 Sec. 7,4 р. 159

person jamesdlin    schedule 12.03.2010

Очень трудно заставить scanf делать то, что вы хотите. Конечно, можно, но такие вещи, как scanf("%s", buf);, так же опасны, как и gets(buf);, как все говорят.

Например, то, что делает paxdiablo в своей функции чтения, можно сделать с помощью чего-то вроде:

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

Вышеупомянутое будет читать строку, сохранять первые 10 символов, отличных от новой строки, в buf, а затем отбрасывать все до (и включая) новой строки. Итак, функция paxdiablo может быть записана с использованием scanf следующим образом:

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

Еще одна проблема с scanf — его поведение в случае переполнения. Например, при чтении int:

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

вышеуказанное нельзя безопасно использовать в случае переполнения. Даже в первом случае чтение строки гораздо проще сделать с помощью fgets, чем с scanf.

person Alok Singhal    schedule 12.03.2010

Да, ты прав. Существует серьезный недостаток безопасности в scanf family(scanf,sscanf, fscanf..etc) esp при чтении строки, потому что они не учитывают длину буфера (в который они считываются).

Пример:

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

ясно, что буфер buf может содержать MAX 3 символов. Но sscanf попытается поместить в него "abcdef", что вызовет переполнение буфера.

person codaddict    schedule 12.03.2010
comment
Вы можете указать %10s в качестве спецификатора формата, и он будет считывать в буфер не более 10 символов. - person dreamlax; 12.03.2010
comment
Конечно, можно безопасно использовать API. Также можно использовать динамит, чтобы безопасно убрать грязь из вашего сада. Но я бы тоже не рекомендовал, тем более, что есть более безопасные альтернативы. - person ReinstateMonica Larry Osterman; 12.03.2010
comment
Мой папа использовал гелигнит для расчистки деревьев на ферме. Вы просто должны понимать свои инструменты и знать опасности. - person paxdiablo; 12.03.2010
comment
Этот буфер может содержать только 2 символа, так как вам нужно зарезервировать один для нулевого терминатора. - person Arthur Kalliokoski; 12.03.2010
comment
@codaddict: тот факт, что кто-то не использует ширину поля с scanf, является проблемой с этим кем-то, а не с scanf. Совершенно не имеет отношения к рассматриваемому вопросу. В конце концов, это C, а не Java. - person AnT; 12.03.2010
comment
Проблема в том, что ширина поля в scanf() должна быть жестко запрограммирована в спецификаторе преобразования; с printf() вы можете использовать * в спецификаторе преобразования и передать длину в качестве аргумента. Но так как * означает что-то другое в scanf(), это не работает, поэтому вам в основном нужно генерировать новый формат для каждого чтения, как это делает Алок в своем примере. Это просто добавляет больше работы и беспорядка; можно также использовать fgets() и покончить с этим. - person John Bode; 12.03.2010

Проблемы, которые у меня есть с семьей *scanf():

  • Возможность переполнения буфера со спецификаторами преобразования %s и %[. Да, вы можете указать максимальную ширину поля, но, в отличие от printf(), вы не можете сделать его аргументом в вызове scanf(); это должно быть жестко закодировано в спецификаторе преобразования.
  • Возможно арифметическое переполнение с помощью %d, %i и т. д.
  • Ограниченная способность обнаруживать и отклонять плохо сформированные входные данные. Например, «12w4» не является допустимым целым числом, но scanf("%d", &value); успешно преобразует и присвоит 12 value, оставив «w4» застрявшим во входном потоке, чтобы испортить будущее чтение. В идеале вся входная строка должна быть отклонена, но scanf() не дает вам простого механизма для этого.

Если вы знаете, что ваш ввод всегда будет правильно сформирован со строками фиксированной длины и числовыми значениями, которые не заигрывают с переполнением, то scanf() — отличный инструмент. Если вы имеете дело с интерактивным вводом или вводом, который не гарантируется правильно сформированным, используйте что-то другое.

person John Bode    schedule 12.03.2010
comment
Какие другие разумные альтернативы существуют для безопасного чтения строк фиксированной длины и числовых значений? - person Rajkumar S; 23.03.2012

Во многих ответах здесь обсуждаются потенциальные проблемы переполнения при использовании scanf("%s", buf), но последняя спецификация POSIX более или менее решает эту проблему, предоставляя символ назначения-распределения m, который можно использовать в спецификаторах формата для форматов c, s и [. Это позволит scanf выделить столько памяти, сколько необходимо с помощью malloc (поэтому ее необходимо освободить позже с помощью free).

Пример его использования:

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

// use buf

free(buf);

См. здесь. Недостатком этого подхода является то, что это относительно недавнее дополнение к спецификации POSIX и вообще не указано в спецификации C, поэтому на данный момент он остается непереносимым.

person dreamlax    schedule 03.10.2014

У scanf-подобных функций есть одна большая проблема — отсутствие безопасности типов any. То есть вы можете закодировать это:

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

Черт, даже это "хорошо":

scanf("%10s", i);

Это хуже, чем printf-подобные функции, потому что scanf ожидает указатель, поэтому сбои более вероятны.

Конечно, есть некоторые средства проверки спецификаторов формата, но они не идеальны и не являются частью языка или стандартной библиотеки.

person Vladimir Veljkovic    schedule 13.10.2015
comment
Это скорее исторический вопрос, поскольку большинство современных компиляторов проверяют, соответствует ли тип аргументов тому, что указано в строке формата, и выдают предупреждения, если это не так. Тем не менее, я уверен, что есть еще много людей, которые этого не делают. - person Graeme; 03.10.2018

Преимущество scanf заключается в том, что после того, как вы научитесь использовать этот инструмент, как вы всегда должны делать в C, у него есть чрезвычайно полезные варианты использования. Вы можете научиться использовать scanf и друзей, прочитав и поняв руководство. Если вы не можете пройти это руководство без серьезных проблем с пониманием, это, вероятно, указывает на то, что вы не очень хорошо знаете C.


scanf и друзья пострадали от неудачного выбора дизайна, из-за которого было трудно (а иногда и невозможно) правильно использовать без чтения документации, как показали другие ответы. К сожалению, это происходит во всем C, поэтому, если бы я советовал не использовать scanf, я бы, вероятно, посоветовал не использовать C.

Одним из самых больших недостатков, по-видимому, является репутация, которую он заработал среди непосвященных; как и со многими полезными функциями C, мы должны быть хорошо информированы, прежде чем использовать его. Ключ в том, чтобы понять, что, как и в остальной части C, он кажется кратким и идиоматичным, но это может слегка ввести в заблуждение. Это распространено в C; новичкам легко написать код, который, по их мнению, имеет смысл и может даже работать на них изначально, но не имеет смысла и может привести к катастрофическим сбоям.

Например, непосвященные обычно ожидают, что делегат %s вызовет чтение строки, и хотя это может показаться интуитивно понятным, это не обязательно так. Поле лучше описать как слово. Настоятельно рекомендуется прочитать руководство для каждой функции.

Каким был бы ответ на этот вопрос, если бы не упоминание о его небезопасности и риске переполнения буфера? Как мы уже говорили, C не является безопасным языком и позволит нам срезать углы, возможно, применить оптимизацию за счет корректности или, что более вероятно, потому что мы ленивые программисты. Таким образом, когда мы знаем, что система никогда не получит строку больше фиксированного числа байтов, нам предоставляется возможность объявить массив такого размера и отказаться от проверки границ. Я действительно не вижу в этом падения; это вариант. Опять же, настоятельно рекомендуется прочитать руководство, которое откроет нам эту возможность.

Не только ленивые программисты страдают от scanf. Нередко можно увидеть, как люди пытаются прочитать значения float или double, используя, например, %d. Обычно они ошибаются, полагая, что реализация будет выполнять какое-то преобразование за кулисами, что имеет смысл, поскольку подобные преобразования происходят во всем остальном языке, но здесь это не так. Как я уже говорил ранее, scanf и друзья (и, конечно же, остальные члены C) обманчивы; они кажутся краткими и идиоматическими, но это не так.

Неопытные программисты не обязаны учитывать успех операции. Предположим, пользователь вводит что-то совершенно нечисловое, когда мы сказали scanf прочитать и преобразовать последовательность десятичных цифр, используя %d. Единственный способ перехватить такие ошибочные данные — это проверить возвращаемое значение, а как часто мы утруждаем себя проверкой возвращаемого значения?

Подобно fgets, когда scanf и друзья не могут прочитать то, что им велят прочитать, поток останется в необычном состоянии; – В случае fgets, если не хватает места для сохранения полной строки, то оставшаяся часть строки, оставшаяся непрочитанной, может быть ошибочно воспринята как новая строка, когда это не так. - В случае scanf и друзей, преобразование завершилось неудачно, как описано выше, ошибочные данные остаются непрочитанными в потоке и могут быть ошибочно обработаны, как если бы они были частью другого поля.

Использовать scanf и друзей не проще, чем fgets. Если мы проверим успех, ища '\n', когда мы используем fgets, или проверим возвращаемое значение, когда мы используем scanf и других, и обнаружим, что мы прочитали неполную строку, используя fgets, или не смогли прочитать поле, используя scanf , то мы сталкиваемся с той же реальностью: мы, вероятно, отбрасываем ввод (обычно до следующей новой строки включительно)! Юууууук!

К сожалению, scanf одновременно усложняет (неинтуитивно) и упрощает (наименьшее количество нажатий клавиш) отбрасывание ввода таким образом. Столкнувшись с этой реальностью отказа от пользовательского ввода, некоторые пытались использовать scanf("%*[^\n]%*c");, не понимая, что делегат %*[^\n] потерпит неудачу, если не встретит ничего, кроме новой строки, и, следовательно, новая строка все равно останется в потоке.

Небольшая адаптация путем разделения двух делегатов формата, и мы видим здесь некоторый успех: scanf("%*[^\n]"); getchar();. Попробуйте сделать это с таким количеством нажатий клавиш, используя какой-нибудь другой инструмент;)

person autistic    schedule 22.03.2016