Як запобігти scanf, що спричиняє переповнення буфера в C?


83

Я використовую цей код:

Що було б найкращим способом запобігти можливому переповненню буфера, щоб йому можна було передавати рядки випадкової довжини?

Я знаю, що можу обмежити вхідний рядок, викликаючи наприклад:

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

Відповіді:


66

У своїй книзі "Практика програмування" (яку варто прочитати) Керніган та Пайк обговорюють цю проблему і вирішують її, використовуючи snprintf()для створення рядка з правильним розміром буфера для передачі до scanf()сімейства функцій. В ефекті:

Зауважте, це все ще обмежує вхідні дані розміром, що надається як "буфер". Якщо вам потрібно більше місця, то вам доведеться розподілити пам’ять або скористатися нестандартною функцією бібліотеки, яка виконує розподіл пам’яті за вас.


Зверніть увагу , що версія POSIX 2008 (2013) з scanf()сімейства функцій підтримує модифікатор формату m(характер присвоєння розподілу) для строкових входів ( %s, %c, %[). Замість того, щоб приймати char *аргумент, він бере char **аргумент і виділяє необхідний простір для значення, яке читає:

Якщо sscanf()функція не може задовольнити всі специфікації перетворення, то вся пам'ять, яку вона виділила для %msподібних перетворень, звільняється до повернення функції.


@Sam: Так, повинно бути buflen-1- Дякую. Тоді вам доведеться турбуватися про неповноцінне заповнення (обгортання до досить великої кількості), отже, і ifтест. Я б дуже спокусився замінити це на assert()або, або підкріпити його на assert()перед тим, ifщо спрацює під час розробки, якщо хтось досить необережний, щоб передати 0 як розмір. Я не ретельно переглядав документацію щодо того, що %0sозначає sscanf()- тест може бути кращим if (buflen < 2).
Джонатан Леффлер,

Отже, snprintfзаписує деякі дані в буфер рядків і sscanfчитає з цього створеного рядка. Де саме це замінює scanfтим, що читається зі stdin?
krb686,

Також дуже заплутано те, що ви використовуєте слово "format" для вашого рядка результатів і, таким чином, snprintfпередаєте "format" як перший аргумент, проте це не є фактичним параметром формату.
krb686,

@ krb686: Цей код написаний таким чином, що дані, що підлягають скануванню, знаходяться в параметрі dataі, отже, sscanf()є відповідним. Якщо ви хочете читати зі стандартного вводу, замість цього опустіть dataпараметр і зателефонуйте scanf(). Що стосується вибору імені formatдля змінної, яка стає рядком формату у виклику sscanf(), ви маєте право перейменувати її, якщо хочете, але її назва не є неточною. Я не впевнений, яка альтернатива має сенс; б in_formatзробити його більш ясним? Я не планую змінювати це в цьому коді; Ви можете використовувати цю ідею у своєму коді.
Джонатан Леффлер

1
@mabraham: Це все ще вірно під macOS Sierra 10.12.5 (до 06.06.2017) - scanf()на macOS не задокументовано як допоміжний %ms, хоча корисний.
Джонатан Леффлер,

31

Якщо ви використовуєте gcc, ви можете використовувати aспецифікатор розширення GNU, щоб scanf () виділив пам'ять для зберігання вводу:

Змінити: Як зазначив Джонатан, вам слід проконсультуватися зі scanfсторінками, оскільки специфікатор може бути іншим ( %m), і вам може знадобитися включити певні дефініції під час компіляції.


8
Це більше проблема використання glibc (бібліотеки GNU C), ніж використання компілятора GNU C.
Джонатан Леффлер,

3
І зауважте, що стандарт POSIX 2008 передбачає mмодифікатор для виконання тієї ж роботи. Див scanf(). Вам потрібно буде перевірити, чи підтримують використовувані вами системи цей модифікатор.
Джонатан Леффлер

4
GNU (як знайдено в Ubuntu 13.10, у всякому разі) підтримує %ms. Позначення %aє синонімом %f(на виході він запитує шістнадцяткові дані з плаваючою комою). Сторінка GNU для scanf()каже: _ Вона недоступна, якщо програма компілюється з gcc -std=c99gcc -D_ISOC99_SOURCE (якщо _GNU_SOURCEце також не вказано), і в цьому випадку aінтерпретується як специфікатор для чисел з плаваючою комою (див. Вище) ._
Джонатан Леффлер

8

Велику частину часу поєднання fgetsі sscanfробить свою роботу. Інша справа - написати власний синтаксичний аналізатор, якщо вхід добре відформатований. Також зверніть увагу, що ваш другий приклад потребує трохи модифікацій, щоб використовувати його безпечно:

Вищезазначене відкидає вхідний потік до, але не включаючи символ newline ( \n). Вам потрібно буде додати a, getchar()щоб споживати це. Також перевірте, чи досягли ви кінця потоку:

і все про це.


2
Не могли б ви поставити feofкод у більший контекст? Я запитую, оскільки ця функція часто використовується неправильно.
Роланд Ілліг,

1
arrayмає бутиchar array[LENGTH+1];
jxh

4

Безпосереднє використання scanf(3)та його варіанти створює ряд проблем. Як правило, користувачі та випадки неінтерактивного використання визначаються з точки зору рядків введення. Рідко можна спостерігати випадок, коли, якщо не знайдено достатньо об’єктів, проблему вирішить більше рядків, але це режим за замовчуванням для scanf. (Якщо користувач не знав ввести номер у першому рядку, другий та третій рядки, мабуть, не допоможуть.)

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


1

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

Але це багато роботи, тому більшість програмістів на С просто відсікають вхід на довільній довжині. Припускаю, ви це вже знаєте, але використання fgets () не дозволить вам приймати довільну кількість тексту - вам все одно потрібно буде встановити обмеження.


То хто-небудь знає, як це зробити за допомогою scanf?
goe

3
Використання fgets в циклі може дозволити вам приймати довільний обсяг тексту - просто зберігайте realloc()свій буфер.
bdonlan

1

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

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


sizeof (char)є за визначенням 1. Тут вам це не потрібно.
RastaJedi

Зазвичай є гарною практикою тримати розподіл / звільнення покажчиків на одному рівні, тобто ваша функція не повинна виділяти пам’ять самостійно, оскільки абонент повинен її звільнити. Більшість стандартних функцій бібліотеки / posix дотримуються цього принципу, повертаючи статичний рядок (начебто strerror(3)) або очікуючи попередньо виділеного рядка, переданого (на зразок ( strerror_r(3)- або scanf(3)) ...
Майкл Беер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.