Почему функция get так опасна, что ее нельзя использовать?

229

Когда я пытаюсь скомпилировать код C, который использует gets()функцию с GCC, я получаю это предупреждение:

(.text + 0x34): предупреждение: функция `gets 'опасна и не должна использоваться.

Я помню, что это как-то связано с защитой стека и безопасностью, но я точно не знаю, почему.

Как я могу удалить это предупреждение и почему есть такое предупреждение об использовании gets()?

Если gets()это так опасно, то почему мы не можем удалить это?

Винит Дхатрак
источник

Ответы:

179

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

Вместо того gets, чтобы использовать , вы хотите использовать fgets, который имеет подпись

char* fgets(char *string, int length, FILE * stream);

( fgetsесли он читает всю строку, оставит '\n'в строке; вам придется иметь дело с этим.)

Он оставался официальной частью языка до стандарта ISO C 1999 года, но был официально удален стандартом 2011 года. Большинство реализаций C по-прежнему поддерживают его, но по крайней мере gcc выдает предупреждение для любого кода, который его использует.

Томас Оуэнс
источник
79
На самом деле предупреждает не gcc, а glibc, который содержит прагму или атрибут, gets()который заставляет компилятор выдавать предупреждение при использовании.
fuz
На самом деле @fuz предупреждает не только компилятор: предупреждение, указанное в OP, было напечатано компоновщиком!
Руслан
163

Почему gets()опасно

Первый интернет-червь ( Morris Internet Worm ) сбежал около 30 лет назад (1988-11-02), и он использовал gets()и переполнение буфера в качестве одного из своих методов распространения от системы к системе. Основная проблема заключается в том, что функция не знает, насколько большой буфер, поэтому она продолжает чтение, пока не найдет новую строку или не встретит EOF, и может переполнить границы заданного буфера.

Вы должны забыть, что когда-либо слышали о gets()существовании.

Стандарт C11 ISO / IEC 9899: 2011 исключен gets()как стандартная функция, которая называется «Good Thing ™» (она была официально помечена как «устаревшая» и «устарела» в ISO / IEC 9899: 1999 / Cor.3: 2007 - Техническое исправление 3 для C99, а затем удалены в C11). К сожалению, из-за обратной совместимости он будет храниться в библиотеках в течение многих лет (что означает «десятилетия»). Если бы это было до меня, реализация gets()стала бы:

char *gets(char *buffer)
{
    assert(buffer != 0);
    abort();
    return 0;
}

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

fputs("obsolete and dangerous function gets() called\n", stderr);

Современные версии системы компиляции Linux генерируют предупреждения, если вы ссылаетесь, gets()а также для некоторых других функций, которые также имеют проблемы с безопасностью ( mktemp(),…).

Альтернативы gets()

fgets ()

Как и все остальные, канонической альтернативой gets()является fgets()указание stdinпотока файлов.

char buffer[BUFSIZ];

while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
    ...process line of data...
}

Что еще никто не упомянул, так это то, что gets()он не включает перевод строки, но fgets()включает. Таким образом, вам может понадобиться обертка, fgets()которая удаляет символ новой строки:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        size_t len = strlen(buffer);
        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        return buffer;
    }
    return 0;
}

Или лучше:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        buffer[strcspn(buffer, "\n")] = '\0';
        return buffer;
    }
    return 0;
}

Кроме того, как отмечает caf в комментарии, а paxdiablo показывает в своем ответе, fgets()вы можете оставить данные в строке. Мой код оболочки оставляет эти данные для чтения в следующий раз; вы можете легко изменить его, чтобы поглотить остальную часть строки данных, если вы предпочитаете:

        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        else
        {
             int ch;
             while ((ch = getc(fp)) != EOF && ch != '\n')
                 ;
        }

Остаточная проблема заключается в том, как сообщать о трех различных состояниях результата - EOF или ошибке, чтение строки не усечено, а частично прочитано, но данные были усечены.

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


Существует также TR 24731-1 (Технический отчет от Стандартного комитета C), который предоставляет более безопасные альтернативы различным функциям, включая gets():

§6.5.4.1 gets_sФункция

конспект

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

Время воспроизведения-ограничение

sне должен быть нулевым указателем. nне должен быть ни равен нулю, ни быть больше, чем RSIZE_MAX. Символ чтения новой строки, конец файла или ошибка чтения должны возникать при чтении n-1символов из stdin. 25)

3 В случае нарушения ограничения времени выполнения s[0]устанавливается нулевой символ, и символы считываются и отбрасываются stdinдо тех пор, пока не будет прочитан символ новой строки, не произойдет конец файла или ошибка чтения.

Описание

4 gets_sФункция считывает не более чем на одно число меньше числа символов, указанного в указанном n потоке stdin, в массив, указанный в s. Никакие дополнительные символы не читаются после символа новой строки (который отбрасывается) или после конца файла. Выброшенный символ новой строки не учитывается в количестве прочитанных символов. Нулевой символ записывается сразу после последнего прочитанного символа в массив.

5 Если обнаружен конец файла, и в массив не было прочитано ни одного символа, или если во время операции возникла ошибка чтения, то s[0]устанавливается нулевой символ, а остальные элементы sпринимают неопределенные значения.

Рекомендуемая практика

6 fgetsФункция позволяет правильно написанным программам безопасно обрабатывать входные строки, слишком длинные для хранения в массиве результатов. В общем случае это требует, чтобы вызывающие абоненты fgetsобращали внимание на наличие или отсутствие символа новой строки в массиве результатов. Попробуйте использовать fgets(наряду с любой необходимой обработкой, основанной на символах новой строки) вместо gets_s.

25)gets_s функция, в отличие от gets, делает это нарушение выполнения-ограничение для линии входа к переполнению буфера для его хранения. В отличие от этого fgets, gets_sподдерживает непосредственное отношение между строками ввода и успешными вызовами gets_s. Программы, которые используют, getsожидают таких отношений.

Компиляторы Microsoft Visual Studio реализуют приближение к стандарту TR 24731-1, но существуют различия между сигнатурами, реализованными Microsoft, и сигнатурами в TR.

Стандарт C11, ISO / IEC 9899-2011, включает TR24731 в Приложении K в качестве необязательной части библиотеки. К сожалению, он редко реализуется в Unix-подобных системах.


getline() - POSIX

POSIX 2008 также предоставляет безопасную альтернативу gets()вызываемым getline(). Он динамически распределяет место для линии, поэтому вам, в конечном итоге, придется ее освободить. Таким образом, он снимает ограничение на длину строки. Он также возвращает длину данных, которые были прочитаны, или -1(и не EOF!), Что означает, что нулевые байты на входе могут быть надежно обработаны. Существует также вариант «выберите свой собственный односимвольный разделитель» getdelim(); это может быть полезно, если вы имеете дело с выводом, из find -print0которого, например, концы имен файлов помечены '\0'символом ASCII NUL .

Джонатан Леффлер
источник
8
Стоит также отметить, что fgets()ваша fgets_wrapper()версия оставит завершающую часть слишком длинной строки во входном буфере для чтения следующей входной функцией. Во многих случаях вы захотите прочитать и выбросить эти символы.
Кафе
5
Интересно, почему они не добавили альтернативу fgets (), которая позволяет использовать ее функциональность без необходимости делать глупый вызов strlen. Например, вариант fgets, который возвращает количество байтов, прочитанных в строку, облегчил бы коду, чтобы видеть, был ли последний прочитанный байт новой строкой. Если бы поведение передачи нулевого указателя для буфера было определено как «чтение и удаление до n-1 байтов до следующей новой строки», это позволило бы коду легко отбросить хвост длинных строк.
суперкат
2
@supercat: Да, я согласен - очень жаль. Ближайшим подходом к этому, вероятно, является POSIX getline()и его родственник getdelim(), которые возвращают длину «строки», читаемой командами, выделяя пространство, необходимое для сохранения всей строки. Даже это может вызвать проблемы, если в итоге вы получите однострочный файл JSON размером несколько гигабайт; Вы можете позволить себе всю эту память? (И пока мы на этом, можем ли мы иметь strcpy()и strcat()варианты, которые возвращают указатель на нулевой байт в конце? И т.д.)
Джонатан Леффлер
4
@supercat: другая проблема в fgets()том, что если файл содержит нулевой байт, вы не можете сказать, сколько данных есть после нулевого байта до конца строки (или EOF). strlen()может сообщать только до нулевого байта в данных; после этого это догадки и, следовательно, почти наверняка неправильно.
Джонатан Леффлер
7
«забудь, что ты когда-либо слышал, что gets()существовало». Когда я делаю это, я снова сталкиваюсь с этим и возвращаюсь сюда. Вы взламываете stackoverflow для получения голосов?
candied_orange
21

Потому что getsне делает каких - либо проверки при получении байт из стандартного ввода и положить их куда - то. Простой пример:

char array1[] = "12345";
char array2[] = "67890";

gets(array1);

Теперь, во-первых, вы можете ввести, сколько символов вы хотите, getsне будет заботиться об этом. Во-вторых, байты по размеру массива, в который вы их помещаете (в данном случае array1), будут перезаписывать все, что они находят в памяти, потому что getsбудут записывать их. В предыдущем примере это означает, что если вы введете, "abcdefghijklmnopqrts"возможно, непредсказуемо, он будет также перезаписан array2или что-то еще.

Эта функция небезопасна, поскольку предполагает согласованный ввод. НИКОГДА НЕ ИСПОЛЬЗУЙТЕ ЕГО!

разъем
источник
3
Что делает getsего непригодным для использования, так это то, что он не имеет параметра длины / числа массива, который он принимает; если бы это было там, это была бы просто другая обычная функция языка C.
legends2k
@ legends2k: Мне любопытно, для чего предназначалось использование gets, и почему ни один стандартный вариант fgets не был сделан таким удобным для случаев использования, когда перевод строки не желателен как часть ввода?
суперкат
1
@supercat gets, как следует из названия, был разработан для получения строки stdin, однако причина отсутствия параметра размера могла быть в духе C : Доверьтесь программисту. Эта функция была удалена в C11, и данная замена gets_sпринимает размер входного буфера. Я понятия не имею о fgetsчасти, хотя.
legends2k
@ legends2k: Единственный контекст, в котором я могу видеть, который getsможет быть оправдан, был бы, если бы кто-то использовал систему ввода-вывода с аппаратной буферизацией, которая физически была неспособна передать строку определенной длины и предполагаемый срок жизни программы. был короче, чем срок службы оборудования. В этом случае, если аппаратное обеспечение не способно передавать строки длиной более 127 байт, это может быть оправдано getsв 128-байтовом буфере, хотя я думаю, что преимущества возможности указать более короткий буфер при ожидании меньшего ввода более чем оправдывают Стоимость.
суперкат
@ legends2k: На самом деле, то, что могло бы быть идеальным, было бы иметь «указатель строки», идентифицирующий байт, который выбирал бы среди нескольких различных форматов строки / буфера / информации буфера, с одним значением байта префикса, указывающим структуру, которая содержала байт префикса [плюс отступ], плюс размер буфера, используемый размер и адрес фактического текста. Такой шаблон позволил бы коду передавать произвольную подстроку (а не только хвост) другой строки без необходимости что-либо копировать, и позволял бы таким методам getsи strcatбезопасно принимать столько, сколько потребуется.
Суперкат
16

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

Фактически, ISO фактически предприняли шаг к удалению gets из стандарта C (начиная с C11, хотя он был признан устаревшим в C99), который, учитывая, насколько высоко они оценивают обратную совместимость, должен указывать на то, насколько плохой была эта функция.

Правильнее всего использовать fgetsфункцию с stdinдескриптором файла, поскольку вы можете ограничить количество символов, читаемых пользователем.

Но это также имеет свои проблемы, такие как:

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

С этой целью почти каждый кодер C в какой-то момент своей карьеры также напишет более полезную оболочку fgets. Вот мой:

#include <stdio.h>
#include <string.h>

#define OK       0
#define NO_INPUT 1
#define TOO_LONG 2
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Get line with buffer overrun protection.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }
    if (fgets (buff, sz, stdin) == NULL)
        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[strlen(buff)-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[strlen(buff)-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) {
        printf ("No input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long\n");
        return 1;
    }

    printf ("OK [%s]\n", buff);

    return 0;
}

Он обеспечивает ту же защиту, что и fgetsв случае предотвращения переполнения буфера, но также уведомляет вызывающую сторону о том, что произошло, и удаляет лишние символы, чтобы они не влияли на вашу следующую операцию ввода.

Не стесняйтесь использовать его по своему усмотрению, поэтому я выпускаю его под лицензией «делай, что хочешь, черт побери» :-)

paxdiablo
источник
Фактически, исходный стандарт C99 явно не устарел gets()ни в разделе 7.19.7.7, где он определен, ни в разделе 7.26.9 Будущие указания библиотеки и подраздел для <stdio.h>. Нет даже сноски, что это опасно. (Сказав это, я вижу «Это не рекомендуется в ISO / IEC 9899: 1999 / Cor.3: 2007 (E))» в ответ по Ю. Хао ) Но C11 же удалить его от стандартного - и не раньше времени.!
Джонатан Леффлер
int getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL)скрывает size_tв intпреобразовании sz. sz > INT_MAX || sz < 2поймает странные значения sz.
chux - Восстановить Монику
if (buff[strlen(buff)-1] != '\n') {это хакерский эксплойт, поскольку первый введенный символ злого пользователя может быть встроенным нулевым символом, отображающим buff[strlen(buff)-1]UB. while (((ch = getchar())...имеет проблемы, если пользователь вводит нулевой символ.
chux - Восстановить Монику
12

fgets .

Читать со стандартного ввода:

char string[512];

fgets(string, sizeof(string), stdin); /* no buffer overflows here, you're safe! */
Тиаго Сильвейра
источник
6

Вы не можете удалить функции API, не нарушая API. При желании многие приложения больше не будут компилироваться или запускаться вообще.

Это причина того, что одна ссылка дает:

Чтение строки, переполняющей массив, на который указывает s, приводит к неопределенному поведению. Рекомендуется использовать fgets ().

Герд Клима
источник
4

Недавно я прочитал в посте USENETcomp.lang.c , который gets()удаляется из Стандарта. WooHoo

Вы будете рады узнать, что комитет только что проголосовал (как оказалось, единогласно), чтобы убрать get () из черновика.

PMG
источник
3
Прекрасно, что это исключается из стандарта. Тем не менее, большинство реализаций будет предоставлять его как «теперь нестандартное расширение» в течение как минимум следующих 20 лет из-за обратной совместимости.
Джонатан Леффлер
1
Да, верно, но когда вы скомпилируете с gcc -std=c2012 -pedantic ...помощью get (), вы не получите. (Я только что составил -stdпараметр)
pmg
4

В C11 (ISO / IEC 9899: 201x) gets()был удален. (Это устарело в ISO / IEC 9899: 1999 / Cor.3: 2007 (E))

В дополнение к fgets(), C11 представляет новую безопасную альтернативу gets_s():

C11 K.3.5.4.1 gets_sФункция

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

Тем не менее, в разделе Рекомендуемая практика , fgets()все еще является предпочтительным.

fgetsФункция позволяет правильно написанные программы для безопасного ввода технологических линий слишком долго хранить в массиве результатов. В общем случае это требует, чтобы вызывающие абоненты fgetsобращали внимание на наличие или отсутствие символа новой строки в массиве результатов. Попробуйте использовать fgets(наряду с любой необходимой обработкой, основанной на символах новой строки) вместо gets_s.

Ю Хао
источник
3

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

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

Арадхана Моханти
источник
Обратите внимание, что реальная опасность заключается не в том, чтобы вывести из строя вашу программу, а в том, чтобы заставить ее выполнять произвольный код . (В общем, использование неопределенного поведения .)
Tanz87
2

Я хотел бы направить искреннее приглашение всем сопровождающим библиотеки C, которые все еще включены getsв их библиотеки «на тот случай, если кто-то все еще зависит от этого»: пожалуйста, замените вашу реализацию эквивалентом

char *gets(char *str)
{
    strcpy(str, "Never use gets!");
    return str;
}

Это поможет убедиться, что никто не зависит от этого. Спасибо.

Стив Саммит
источник
2

Функция C get опасна и была очень дорогой ошибкой. Тони Хоар особо отмечает это в своем выступлении «Нулевые ссылки: ошибка в миллиард долларов»:

http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

Целый час стоит посмотреть, но за просмотр его комментариев от 30 минут с конкретной критикой около 39 минут.

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

user3717661
источник