Каких функций из стандартной библиотеки следует (следует) избегать? - PullRequest
87 голосов
/ 02 апреля 2010

Я прочитал о переполнении стека, что некоторые функции C «устарели» или «их следует избегать». Можете ли вы дать мне несколько примеров такого рода функций и причины?

Какие альтернативы этим функциям существуют?

Можем ли мы использовать их безопасно - какие-либо хорошие практики?

Ответы [ 13 ]

56 голосов
/ 02 апреля 2010

Устаревшие функции
Небезопасно
Прекрасным примером такой функции является gets () , потому что невозможно определить, насколько велик целевой буфер. Следовательно, любая программа, которая читает ввод с помощью gets (), имеет уязвимость переполнения буфера . По тем же причинам следует использовать strncpy () вместо strcpy () и strncat () вместо strcat () .

Еще несколько примеров включают функции tmpfile () и mktemp () из-за потенциальных проблем безопасности с перезаписью временных файлов , которые заменяются более безопасная функция mkstemp () .

не реентерабельный
Другие примеры включают gethostbyaddr () и gethostbyname () , которые не являются реентерабельными (и, следовательно, не гарантированно безопасными для потоков) и были заменены реентерабельным getaddrinfo ( ) и freeaddrinfo () .

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

Устаревший, непереносимый
Некоторые другие функции просто устаревают, потому что они дублируют функциональность и не так переносимы, как другие варианты. Например, bzero () устарела в пользу memset () .

Безопасность и возврат резьбы
Вы спрашивали в своем посте о безопасности и повторяемости потоков. Есть небольшая разница. Функция является реентерабельной, если она не использует общее, изменяемое состояние. Так, например, если вся необходимая информация передается в функцию, а все необходимые буферы также передаются в функцию (а не разделяются всеми вызовами функции), то она реентерабельна. Это означает, что различные потоки, используя независимые параметры, не рискуют случайно разделить состояние. Повторный вход является более надежной гарантией, чем безопасность потока. Функция является поточно-ориентированной, если она может использоваться несколькими потоками одновременно. Функция является поточно-ориентированной, если:

  • Это реентерабельный (т.е. он не разделяет состояние между вызовами), или:
  • Это не реентерабельный, но он использует синхронизацию / блокировку, необходимые для общего состояния.

Как правило, в спецификации Single UNIX и IEEE 1003.1 (т.е. "POSIX") любая функция, которая не гарантируется для повторного входа, не гарантируется как поточно-ориентированная. Таким образом, другими словами, только функции, которые гарантированно являются реентерабельными, могут использоваться в многопоточных приложениях (без внешней блокировки). Это не означает, однако, что реализации этих стандартов не могут сделать нереентеративную функцию безопасной для потоков. Например, в Linux часто добавляется синхронизация к не входящим функциям, чтобы добавить гарантию (помимо той, что указана в Единой спецификации UNIX) безопасности потоков.

Строки (и буферы памяти, в общем)
Вы также спросили, есть ли какой-то фундаментальный недостаток в строках / массивах. Некоторые могут утверждать, что это так, но я бы сказал, что нет, в языке нет фундаментального недостатка. C и C ++ требуют, чтобы вы передавали длину / емкость массива отдельно (это не свойство ".length", как в некоторых других языках). Это не недостаток, как таковой. Любой разработчик на C и C ++ может написать правильный код, просто передавая длину в качестве параметра, где это необходимо. Проблема заключается в том, что нескольким API, которым требовалась эта информация, не удалось указать ее в качестве параметра. Или предполагается, что будет использована некоторая константа MAX_BUFFER_SIZE. Такие API теперь устарели и заменены альтернативными API, которые позволяют указывать размеры массива / буфера / строки.

Scanf (в ответ на ваш последний вопрос)
Лично я использую библиотеку C ++ iostreams (std :: cin, std :: cout, операторы << и >>, std :: getline, std :: istringstream, std :: ostringstream и т. Д.), Поэтому я не делаю как правило, имеют дело с этим. Если бы я был вынужден использовать чистый C, я бы лично использовал fgetc () или getchar () в сочетании с strtol () , strtoul () и т. д. и разбираю вещи вручную, так как я не большой поклонник varargs или форматных строк. Тем не менее, насколько мне известно, нет проблем с [f] scanf () , [f] printf () и т. Д., Пока вы создаете формат сами строки, вы никогда не передаете строки произвольного формата и не разрешаете использовать пользовательский ввод в качестве строк формата, и вы используете макросы форматирования, определенные в , где это уместно. (Обратите внимание, snprintf () следует использовать вместо sprintf () , но это связано с невозможностью указать размер буфера назначения, а не с использованием строк формата ). Я также должен отметить, что в C ++ boost :: format обеспечивает printf-подобное форматирование без varargs.

22 голосов
/ 02 апреля 2010

Еще раз люди повторяют, подобно мантре, смехотворное утверждение, что "n" версия функций str является безопасными версиями.

Если бы это было то, для чего они были предназначены, они всегда заканчивали бы строки нулем.

"n" версии функций были написаны для использования с полями фиксированной длины (такими как записи каталога в ранних файловых системах), где терминатор nul требуется только в том случае, если строка не заполняет поле. Это также причина того, что функции имеют странные побочные эффекты, которые бессмысленно неэффективны, если их просто использовать в качестве замены - возьмем, например, функцию strncpy ():

Если массив, на который указывает s2, является строка, которая короче n байтов, нулевые байты добавляются к копии в массив, на который указывает s1, до n всего записано байтов.

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

Если вы хотите «предположительно» безопасные версии, тогда получите - или напишите свои собственные - процедуры strl (strlcpy, strlcat и т. Д.), Которые всегда обнуляют строки и не имеют побочных эффектов. Обратите внимание, что они не совсем безопасны, поскольку могут молча обрезать строку - это редко лучший способ действий в любой реальной программе. Есть случаи, когда это нормально, но есть также много обстоятельств, когда это может привести к катастрофическим результатам (например, распечатка медицинских рецептов).

19 голосов
/ 02 апреля 2010

Несколько ответов здесь предлагают использовать strncat() более strcat(); Я бы предложил, что strncat()strncpy()) также следует избегать. Есть проблемы, которые затрудняют правильное использование и приводят к ошибкам:

  • параметр длины до strncat() относится (но не совсем точно - см. 3-й пункт) к максимальному количеству символов, которое можно скопировать в место назначения, а не к размеру буфера назначения. Это делает strncat() более сложным в использовании, чем должно быть, особенно если несколько пунктов будут соединены с пунктом назначения.
  • может быть трудно определить, был ли результат усечен (что может или не может быть важным)
  • Легко иметь ошибку «один за другим». Как отмечено в стандарте C99, «таким образом, максимальное количество символов, которые могут оказаться в массиве, на который указывает s1, равно strlen(s1)+n+1» для вызова, который выглядит как strncat( s1, s2, n)

strncpy() также имеет проблему, которая может привести к ошибкам, которые вы пытаетесь использовать его интуитивно понятным способом - это не гарантирует, что назначение окончено нулем. Чтобы убедиться, что вам нужно убедиться, что вы специально обрабатываете этот угловой случай, опуская '\0' в последнем месте буфера самостоятельно (по крайней мере, в определенных ситуациях).

Я бы предложил использовать что-то вроде OpenBSD strlcat() и strlcpy() (хотя я знаю, что некоторым людям не нравятся эти функции; я считаю, что их гораздо проще безопасно использовать, чем strncat() / strncpy()).

Вот немного из того, что Тодд Миллер и Тео де Раадт сказали о проблемах с strncat() и strncpy():

При использовании strncpy() и strncat() в качестве безопасных версий strcpy() и strcat() возникает несколько проблем. Обе функции имеют дело с NUL-завершением и параметром length различными и неинтуитивными способами, которые смущают даже опытных программистов. Они также не обеспечивают простой способ обнаружения, когда происходит усечение. ... Из всех этих проблем наиболее важна путаница, вызванная параметрами длины, и связанная с этим проблема NUL-завершения. Когда мы проверили дерево исходных текстов OpenBSD на наличие потенциальных брешей в безопасности, мы обнаружили неоправданное злоупотребление strncpy() и strncat(). Хотя не все из них привели к появлению дыр в безопасности, они ясно дали понять, что правила использования strncpy() и strncat() в безопасных строковых операциях часто неправильно понимаются.

Аудит безопасности OpenBSD обнаружил, что ошибки с этими функциями были "безудержными". В отличие от gets(), эти функции могут использоваться безопасно, но на практике возникает много проблем, потому что интерфейс запутанный, не интуитивно понятный и трудный в использовании. Я знаю, что Microsoft также провела анализ (хотя я не знаю, какую часть своих данных они могли опубликовать), и в результате запретили (или, по крайней мере, очень сильно обескуражили - «запрет» может быть не абсолютным) использование strncat() и strncpy() (среди других функций).

Некоторые ссылки с дополнительной информацией:

7 голосов
/ 04 октября 2017

Стандартные библиотечные функции, которые никогда не должны использоваться:

setjmp.h

  • setjmp(). Вместе с longjmp() эти функции широко признаны невероятно опасными для использования: они приводят к программированию спагетти, имеют множество форм неопределенного поведения, они могут вызывать непреднамеренные побочные эффекты в программной среде, такие как воздействие на значения, хранящиеся в стек. Справочные материалы: MISRA-C: правило 2012 года 21.4, CERT C MSC22-C .
  • longjmp(). См setjmp().

stdio.h

  • gets(). Функция была удалена из языка Си (согласно C11), так как она была небезопасна согласно проекту. Эта функция уже помечена как устаревшая в C99. Используйте fgets() вместо этого. Ссылки: ISO 9899: 2011 K.3.5.4.1, также см. Примечание 404).

stdlib.h

  • atoi() семейство функций. Они не обрабатывают ошибок, но вызывают неопределенное поведение при возникновении ошибок. Полностью лишние функции, которые можно заменить семейством функций strtol(). Ссылки: MISRA-C: правило 2012 года 21.7.

string.h

  • strncat(). Имеет неудобный интерфейс, которым часто злоупотребляют. Это в основном лишняя функция. Также см. Примечания для strncpy().
  • strncpy(). Цель этой функции никогда не заключалась в том, чтобы быть более безопасной версией strcpy(). Его единственной целью было всегда обрабатывать древний формат строки в системах Unix, и то, что он был включен в стандартную библиотеку, является известной ошибкой. Эта функция опасна, потому что она может оставить строку без нулевого завершения, и, как известно, программисты часто используют ее неправильно. Список литературы: Почему strlcpy и strlcat считаются небезопасными? .

Стандартные библиотечные функции, которые следует использовать с осторожностью:

assert.h

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

signal.h

  • signal(). Ссылки: MISRA-C: правило 2012 года 21.5, CERT C SIG32-C .

stdarg.h

  • va_arg() семейство функций. Наличие функций переменной длины в программе на Си почти всегда свидетельствует о плохом дизайне программы. Следует избегать, если у вас нет особых требований.

stdio.h
Как правило, вся эта библиотека не рекомендуется для производственного кода , так как она сопровождается многочисленными случаями плохо определенного поведения и плохой безопасности типов.

  • fflush(). Идеально подходит для потокового вывода. Вызывает неопределенное поведение, если используется для входных потоков.
  • gets_s(). Безопасная версия gets() включена в интерфейс проверки границ C11. Вместо этого предпочтительно использовать fgets() в соответствии со стандартной рекомендацией C. Справочные материалы: ISO 9899: 2011 K.3.5.4.1.
  • printf() семейство функций. Ресурсные функции с большим количеством неопределенного поведения и плохой безопасностью типов. sprintf() также имеет уязвимости. Эти функции следует избегать в производственном коде. Справочные материалы: MISRA-C: правило 2012 года 21.6.
  • scanf() семейство функций. Смотрите замечания по поводу printf(). Кроме того, - scanf() уязвим для переполнения буфера, если не используется правильно. fgets() предпочтительно использовать, когда это возможно. Ссылки: CERT C INT05-C , MISRA-C: правило 2012 года 21.6.
  • tmpfile() семейство функций. Поставляется с различными проблемами уязвимости. Список литературы: CERT C FIO21-C .

stdlib.h

  • malloc() семейство функций. Идеально подходит для использования в размещенных системах, хотя следует помнить об известных проблемах в C90, и поэтому не приводит к результату . Семейство функций malloc() никогда не должно использоваться в автономных приложениях. Ссылки: MISRA-C: правило 2012 года 21.3.

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

  • system(). Поставляется с большим количеством накладных расходов и, хотя и переносимым, часто лучше вместо этого использовать специфичные для системы функции API. Поставляется с различным плохо определенным поведением. Список литературы: CERT C ENV33-C .

string.h

  • strcat(). См. Примечания для strcpy().
  • strcpy(). Идеально подходит для использования, если только размер копируемых данных неизвестен или не превышает целевой буфер. Если проверка размера входящих данных не выполняется, возможны переполнения буфера. Это не вина самого strcpy(), а вызывающего приложения - то, что strcpy() небезопасно, в основном миф, созданный Microsoft .
  • strtok(). Изменяет строку вызывающей стороны и использует внутренние переменные состояния, что может сделать ее небезопасной в многопоточной среде.
7 голосов
/ 02 апреля 2010

Некоторые люди утверждают, что следует избегать strcpy и strcat в пользу strncpy и strncat. Это несколько субъективно, на мой взгляд.

Их определенно следует избегать при работе с пользовательским вводом - без сомнения здесь.

В коде "далеко" от пользователя, когда вы просто знаете буферы достаточно длинные, strcpy и strcat могут быть немного более эффективными, потому что вычисление n для передачи их кузены могут быть лишними.

6 голосов
/ 02 апреля 2010

Избегать

  • strtok для многопоточных программ, поскольку они не поточнобезопасны.
  • gets, так как это может вызвать переполнение буфера
5 голосов
/ 03 апреля 2010

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

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

5 голосов
/ 02 апреля 2010

Вероятно, стоит добавить еще раз, что strncpy() не является заменой общего назначения для strcpy(), что может предложить его название. Он предназначен для полей фиксированной длины, которые не нуждаются в nul-terminator (изначально он был разработан для использования с записями каталога UNIX, но может быть полезен для таких вещей, как поля ключей шифрования).

Однако легко использовать strncat() вместо strcpy():

if (dest_size > 0)
{
    dest[0] = '\0';
    strncat(dest, source, dest_size - 1);
}

(тест if, очевидно, можно отбросить в общем случае, когда вы знаете, что dest_size определенно не равен нулю).

2 голосов
/ 02 апреля 2010

Не забывайте про sprintf - это причина многих проблем. Это правда, потому что альтернатива snprintf иногда имеет разные реализации, которые могут сделать ваш код непереносимым.

  1. linux: http://linux.die.net/man/3/snprintf

  2. окна: http://msdn.microsoft.com/en-us/library/2ts7cx93%28VS.71%29.aspx

В случае 1 (linux) возвращаемое значение - это объем данных, необходимый для хранения всего буфера (если он меньше размера данного буфера, то вывод был усечен)

В случае 2 (окна) возвращаемое значение является отрицательным числом, если вывод усекается.

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

  1. Безопасное переполнение буфера (здесь уже упоминалось множество функций)

  2. потокобезопасен / не реентерируем (например, strtok)

В руководстве по каждой функции вы должны искать такие ключевые слова, как: safe, sync, async, thread, buffer, bugs

2 голосов
/ 02 апреля 2010

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

...