Чому функція get настільки небезпечна, що її не слід використовувати?


229

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

(.text + 0x34): попередження: функція `get 'є небезпечною і не повинна використовуватися.

Я пам’ятаю, це має щось спільне із захистом та безпекою стеків, але я не впевнений, чому саме.

Як я можу видалити це попередження і чому існує таке попередження про використання 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 насправді, попереджає навіть не лише компілятор: попередження, яке цитується в ОП, було надруковано посиланням!
Руслан

163

Чому це gets()небезпечно

Перший Інтернет-черв'як ( Інтернет-черв'як Морріса ) втік близько 30 років тому (1988-11-02), і він використовував gets()і переповнення буфера як один із його методів поширення з системи в систему. Основна проблема полягає в тому, що функція не знає, наскільки великий буфер, тому він продовжує читати, поки не знайде новий рядок або не зустріне EOF, і може переповнити межі буфера, який йому було надано.

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

Стандарт C11 ISO / IEC 9899: 2011 виключений gets()як стандартна функція, яка є «Гарною річчю ™» (вона офіційно була позначена як «застаріла» та «застаріла» в 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;
}

Крім того, як в коментарі зазначає кафе , а у своїй відповіді показує 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 (Технічний звіт Комітету зі стандартів С), який надає більш безпечні альтернативи для різних функцій, включаючи 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', наприклад, символом NUL ASCII .


8
Варто також зазначити, що fgets()і ваша fgets_wrapper()версія залишить кінцеву частину задовго рядка у вхідному буфері, щоб прочитати наступну функцію введення. У багатьох випадках вам потрібно буде прочитати та відкинути ці символи.
caf

5
Цікаво, чому вони не додали альтернативу fgets (), яка дозволяє користуватися її функціональністю без необхідності робити нерозумний стринг-дзвінок. Наприклад, варіант fgets, який повертає кількість байтів, прочитаних у рядку, полегшить код, щоб зрозуміти, чи був останній прочитаний байт новим рядком. Якщо поведінку передачі нульового вказівника для буфера було визначено як "прочитати та відкинути до n-1 байт до наступного нового рядка", це дозволило б коду легко відкинути хвіст ліній надмірної довжини.
supercat

2
@supercat: Так, я згоден - шкода. Найближчим підходом до цього, ймовірно, є POSIX getline()та його відносні getdelim(), які дійсно повертають довжину 'рядка', прочитаного командами, виділяючи простір, необхідний для збереження цілого рядка. Навіть це може спричинити проблеми, якщо ви отримаєте однорядковий файл JSON, який має розмір декількох гігабайт; ти можеш дозволити собі всю цю пам’ять? (І поки ми на цьому, чи можемо ми мати варіанти strcpy()та strcat()варіанти, які в кінці повертають вказівник на нульовий байт? І т. Д.)
Джонатан Леффлер

4
@supercat: інша проблема fgets()полягає в тому, що якщо файл містить нульовий байт, ви не можете сказати, скільки є даних після нульового байта до кінця рядка (або EOF). strlen()може повідомляти лише до нульового байта в даних; після цього - здогадки і тому майже напевно неправильні.
Джонатан Леффлер

7
"забудь, ти коли-небудь чув, що gets()існувало". Коли я це роблю, я знову стикаюся з цим і повертаюся сюди. Ви хакуєте stackoverflow, щоб отримати оновлення?
candied_orange

21

Тому getsщо не робить будь-якої перевірки, отримуючи байти від stdin і кладучи їх кудись. Простий приклад:

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

gets(array1);

Тепер, перш за все, вам дозволяється вводити скільки символів ви хочете, getsце не хвилюватиметься. По-друге, байти за розміром масиву, в який ви їх помістили (в даному випадку array1) перезаписують все, що вони знайдуть в пам'яті, тому що getsзаписуть їх. У попередньому прикладі це означає, що якщо ви введете, "abcdefghijklmnopqrts"можливо, непередбачувано, він буде також перезаписаний array2або що завгодно.

Ця функція небезпечна, оскільки передбачає послідовне введення даних. НІКОЛИ НЕ ВИКОРИСТУЙТЕ!


3
Що getsявно робить непридатним - це те, що він не має параметра довжини / підрахунку масиву, який він потребує; якби він був там, це була б просто інша звичайна функція C.
legends2k

@ legends2k: Мені цікаво, для чого було призначено використання gets, і чому жоден стандартний варіант fgets не був настільки зручним для випадків використання, коли новий рядок не бажаний як частина введення?
supercat

1
@supercat gets, як випливає з назви, було розроблено для отримання рядка stdin, однак обґрунтування відсутності параметра розміру, можливо, виходило з духу C : Довіряйте програмісту. Ця функція була видалена в C11, і дана заміна gets_sзаймає розмір вхідного буфера. Я не маю жодного уявлення про цю fgetsчастину.
legends2k

@ legends2k: Єдиний контекст, який я можу побачити, getsможе бути виправданим, якби користувався системою вбудованого вводу-виводу, захищеною лінійкою, яка фізично не була здатна подати рядок певної довжини та передбачуваний термін служби програми був коротшим, ніж термін експлуатації обладнання. У такому випадку, якщо апаратне забезпечення не в змозі подати рядки довжиною понад 127 байт, це може бути виправданим getsперетворенням в 128-байтний буфер, хоча я думаю, що переваги можливості вказувати коротший буфер, коли очікується менший вхід, буде більше, ніж виправдовує вартість.
supercat

@ legends2k: Насправді, те, що може бути ідеальним, було б мати "вказівник рядка" ідентифікувати байт, який вибирав би декілька різних форматів рядка / буфера / буфера-інформації, з одним значенням байта префікса, що вказує на структуру, яка містить байт префікса [плюс padding], плюс розмір буфера, використаний розмір та адреса фактичного тексту. Такий зразок дозволив би коду пропустити довільну підрядку (не лише хвіст) іншої рядки, не вимагаючи нічого копіювати, і дозволив би такі методи, як getsі strcatбезпечно приймати стільки, скільки підходить.
supercat

16

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

Насправді, ІСО фактично взяли стадію видалення gets зі стандарту C (від С11, хоча це було застарілим в 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що і тим, що запобігає переповненню буфера, але також повідомляє абонента про те, що сталося, і очищає зайві символи, щоб вони не впливали на наступну операцію введення.

Не соромтесь користуватися ним, як хочете, я відпускаю його під ліцензією "роби те, що чорт ти хочеш", ліцензію :-)


Власне, оригінальний стандарт 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


6

Ви не можете видалити функції API, не порушивши API. Якщо ви хочете, багато додатків більше не збираються і не запускаються.

Саме тому одна посилання дає:

Читання рядка, що переповнює масив, на який вказує s, призводить до невизначеної поведінки. Використання fgets () рекомендується.


4

Нещодавно я читав у публікації USENETcomp.lang.c , яку gets()вилучають зі стандарту. WOOHOO

Ви будете раді дізнатись, що комітет щойно проголосував (одноголосно, як виявляється) за видалення отримує також () з проекту.


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 отримує небезпеку і була дуже дорогою помилкою. Тоні Хоаре виділяє це для конкретної згадки у своїй доповіді "Нульові довідки: Помилка мільярду доларів":

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

Цілу годину варто спостерігати, але за її коментар погляд з 30 хвилин далі з конкретною стає критикою близько 39 хвилин.

Сподіваємось, це спричинить ваш апетит до всієї розмови, яка звертає увагу на те, як нам потрібні більш офіційні докази коректності мов і як мовні дизайнери повинні звинувачуватись у помилках у своїх мовах, а не у програміста. Це, мабуть, було всім сумнівним приводом для дизайнерів поганих мов перекласти провину на програмістів під виглядом "програмістської свободи".

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.