Каких функций из стандартной библиотеки следует избегать?
Я читал о переполнении стека, что некоторые функции C "устарели"или" следует избегать". Не могли бы вы дать мне несколько примеров такого рода функции и почему?
какие существуют альтернативы этим функциям?
можем ли мы использовать их безопасно - любые хорошие практики?
13 ответов:
Устаревшие Функции
неуверенность
Прекрасным примером такой функции является получает(), потому что нет никакого способа сказать ему, насколько велик буфер назначения. Следовательно, любая программа, которая считывает входные данные с помощью gets () имеет уязвимость переполнения буфера. По аналогичным причинам следует использовать strncpy () на месте strcpy () и strncat() на месте strcat ().еще несколько примеров включают tmpfile() и mktemp().
Нереентерабельные
Другие примеры включают gethostbyaddr () и gethostbyname () которые не являются реентерабельными (и, следовательно, не гарантируется, чтобы быть ориентирован на многопотоковое исполнение) и были заменены реентерабельной getaddrinfo() и freeaddrinfo().вы можете заметить здесь закономерность... либо отсутствие безопасности (возможно, из-за того, что в подпись не было включено достаточно информации, чтобы обеспечить ее безопасную реализацию), либо отсутствие повторного доступа являются распространенными источниками устаревания.
Устаревший, Не Портативный
Некоторые другие функции просто становятся устарели, потому что они дублируют функциональность и не так переносимы, как другие варианты. Например, bzero() является устаревшим в пользу memset ().потокобезопасность и однако
Вы спросили, в вашем посте, о потокобезопасности и возобновления деятельности. Есть небольшая разница. Функция является реентерабельной, если она не использует общее изменяемое состояние. Так, например, если вся необходимая информация передается в функцию, а все необходимые буферы также передаются в функцию (а не общие для всех вызовы функции), то он является реентерабельным. Это означает, что различные потоки, используя независимые параметры, не рискуют случайно разделить состояние. Повторное подключение является более сильной гарантией, чем потокобезопасность. Функция является потокобезопасной, если она может использоваться несколькими потоками одновременно. Функция является потокобезопасной, если:
- он реентерабелен (т. е. он не разделяет состояние между вызовами), или:
- он не является реентерабельным, но он использует синхронизацию/блокировку по мере необходимости для общего состояния.
В общем, в Single Unix Specification и IEEE 1003.1 (т. е. "POSIX"), любая функция, которая не гарантируется реентерабельность, не гарантируется потокобезопасность. Таким образом, другими словами, только функции, которые гарантированно являются реентерабельными, могут быть переносимо использованы в многопоточных приложениях (без внешней блокировки). Это делает однако не значит, что внедрение этих стандартов не может выбрать, чтобы сделать нереентерабельных функций с поддержкой нитей. Например, Linux часто добавляет синхронизацию к нереентерабельным функциям, чтобы добавить гарантию (помимо одной спецификации UNIX) threadsafety.
строки (и буферы памяти, В общем)
Вы также спросили, есть ли какой-то фундаментальный недостаток со строками/массивами. Некоторые могут возразить, что это так, но я бы поспорил что нет, то нет и фундаментального изъяна в языке. 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.
еще раз люди повторяют, как мантру, нелепое утверждение, что "n" версия функций str являются безопасными версиями.
Если бы это было то, для чего они предназначались, то они всегда заканчивали бы строки null.
"n" версии функций были написаны для использования с полями фиксированной длины (например, записи каталогов в ранних файловых системах), где Терминатор nul требуется только в том случае, если строка не заполняет поле. Это тоже причина почему функции имеют странные побочные эффекты, которые бессмысленно неэффективны, если просто используются в качестве замены-возьмите strncpy () например:
Если массив, на который указывает s2, является a строку короче, чем n байт, пустые байты добавляются к копии в массив, на который указывает s1, пока n байты во всех записываются.
поскольку буферы, выделенные для обработки имен файлов, обычно составляют 4 КБ, это может привести к значительному ухудшению производительности.
Если вы хотите "предположительно" безопасные версии, то получите-или напишите свои собственные-процедуры strl (strlcpy, strlcat и т. д.), которые всегда обнуляют строки и не имеют побочных эффектов. Пожалуйста, обратите внимание, что они не очень безопасны, поскольку они могут молча усечь строку-это редко лучший курс действий в любой реальной программе. Есть случаи, когда это нормально, но есть также много обстоятельств, когда это может привести к катастрофическим результатам (например, печать медицинский рецепт.)
несколько ответов здесь предлагают использовать
strncat()
overstrcat()
; Я бы предположил, чтоstrncat()
(иstrncpy()
) также следует избегать. Он имеет проблемы, которые затрудняют правильное использование и приводят к ошибкам:
- параметр длины в
strncat()
Это связано с (но не совсем точно - см. 3-й пункт) максимальное количество символов, которые могут быть скопированы в месте назначения, а не размер буфера назначения. Это делаетstrncat()
труднее использовать, чем это должно быть, особенно если несколько элементов объединяются в пункт назначения.- может быть трудно определить, был ли результат усечен (что может быть или не быть важным)
- легко иметь ошибку off-by-one. Как отмечает стандарт C99, " таким образом, максимальное количество символов, которые могут оказаться в массиве, на который указывает
s1
иstrlen(s1)+n+1
" для вызова, который выглядит какstrncat( s1, s2, n)
strncpy()
также есть проблема, которая может привести в багах вы пытаетесь использовать его интуитивно понятным способом-это не гарантирует, что место назначения завершается null. Чтобы убедиться, что вы должны убедиться, что вы специально обрабатывать этот угловой случай, отбросив''
в последнем месте буфера самостоятельно (по крайней мере, в определенных ситуациях).я бы предложил использовать что-то вроде OpenBSD
strlcat()
иstrlcpy()
(хотя я знаю, что некоторые люди не любят эти функции; я считаю, что они гораздо проще в использовании безопасно, чемstrncat()
/strncpy()
).вот немного из того, что Тодд Миллер и Тео де Раадт должны были сказать о проблемах с
strncat()
иstrncpy()
:есть несколько проблем, с которыми сталкиваются, когда
strncpy()
иstrncat()
используются в качестве безопасных версийstrcpy()
иstrcat()
. Обе функции имеют дело с нулевым завершением и параметром длины различными и неинтуитивными способами, которые путают даже опытных программистов. Они также не обеспечивают простой способ обнаружить, когда усечение происходит. ... Из всех этих проблем наиболее важными являются путаница, вызванная параметрами длины и связанной с этим проблемой NUL-termination. Когда мы проверили исходное дерево OpenBSD на наличие потенциальных дыр в безопасности, мы обнаружили безудержное злоупотреблениеstrncpy()
иstrncat()
. Хотя не все из них привели к бреши безопасности, они дали понять, что правила использованияstrncpy()
иstrncat()
в безопасной строке операции широко неправильно понимаются.аудит безопасности OpenBSD оказалось, что баги с этими функциями были "безудержными". В отличие от
gets()
эти функции можете безопасно использовать, но на практике есть много проблем, потому что интерфейс запутанный, неинтуитивный и трудно использовать правильно. Я знаю, что Microsoft также провела анализ (хотя я не знаю, сколько их данных они могли опубликовать), и в результате запретили (или, по крайней мере, очень сильно не поощряли - "запрет" может быть не абсолютным) использованиеstrncat()
иstrncpy()
(среди прочих функций).некоторые ссылки с более
некоторые люди утверждают, что
strcpy
иstrcat
следует избегать, в пользуstrncpy
иstrncat
. Это несколько субъективно, на мой взгляд.их определенно следует избегать при работе с пользовательским вводом-без сомнения, здесь.
в коде "далеко" от пользователя, когда вы просто знаю буферы достаточно долго,
strcpy
иstrcat
может быть немного более эффективным, поскольку вычисленияn
перейти к своим двоюродным братьям может быть излишний.
избежать
strtok
для многопоточных программ, поскольку он не является потокобезопасным.gets
как это может вызвать переполнение буфера
стандартные библиотечные функции, которые должны никогда используется:
setjmp.h
setjmp()
. Вместе сlongjmp()
, эти функции широко признаны невероятно опасными для использования: они приводят к спагетти-программированию, они приходят с многочисленными формами неопределенного поведения, они могут вызывать непреднамеренные побочные эффекты в программной среде, такие как влияние на значения, хранящиеся в стеке. Ссылки на литературу: Мисра-с: 2012 правило 21.4,CERT C MSC22-C.longjmp()
. Смотритеsetjmp()
.stdio.h
gets()
. Функция была удалена из языка C (согласно C11), так как это было небезопасно в соответствии с дизайном. Функция уже была помечена как устаревшая в C99. Используйтеfgets()
вместо. Ссылки: ISO 9899: 2011 K. 3.5.4.1, Также см. Примечание 404).stdlib.h
atoi()
семейство функций. Они не имеют обработки ошибок, но вызывают неопределенное поведение при возникновении ошибок. Совершенно лишние функции, которые можно заменить наstrtol()
семейство функций. Список литературы: Мисра-с: 2012 правило 21.7.строку.h
strncat()
. Имеет неудобный интерфейс, который часто используется не по назначению. Это в основном лишняя функция. Также см. Примечания дляstrncpy()
.strncpy()
. Цель этой функции никогда не была более безопасной версиейstrcpy()
. Его единственной целью всегда была обработка древнего строкового формата в системах Unix, и то, что он был включен в стандартную библиотеку, является известной ошибкой. Эта функция опасна, потому что она может оставить строку без нулевого завершения, и программисты, как известно, часто используют ее неправильно. Ссылки: почему рассматриваются strlcpy и strlcat небезопасно?.
стандартные библиотечные функции, которые следует использовать с осторожностью:
утверждать.h
assert()
. Поставляется с накладными расходами и обычно не должен использоваться в производственном коде. Лучше использовать обработчик ошибок для конкретного приложения, который отображает ошибки, но не обязательно закрывает всю программу.сигнал.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 К. 3.5.4.1.printf()
семейство функций. Ресурсные тяжелые функции, которые приходят с большим количеством неопределенного поведения и плохой безопасности типа.sprintf()
тоже есть уязвимые места. Эти функции следует избегать в производственном коде. Список литературы: Мисра-с: 2012 правило 21.6.scanf()
семейство функций. См. примечания оprintf()
. Кроме того, -scanf()
уязвим для переполнения буфера, если не используется правильно.fgets()
предпочтительно использовать, когда это возможно. Ссылки на литературу: CERT C INT05-C, MISRA-C:2012 правило 21.6.tmpfile()
семейство функций. Поставляется с различными проблемами уязвимости. Ссылки: СЕРТИФИКАТ C ФИО21-С.stdlib.h
malloc()
семейство функций. Отлично подходит для использования в размещенных системах, хотя будьте в курсе известных проблем в C90 и, следовательно,не бросайте результат. Элементmalloc()
семья функции не должны использоваться в автономных приложениях. Список литературы: Мисра-с: 2012 правило 21.3.также обратите внимание, что
realloc()
опасно, если вы перезаписываете старый указатель с результатомrealloc()
. В случае сбоя функции, вы создаете утечку.system()
. Поставляется с большим количеством накладных расходов и, хотя портативный, часто лучше использовать системные функции API вместо этого. Поставляется с различным плохо определенным поведением. Ссылки: сертификат C ENV33-C.строку.h
strcat()
. См. примечания дляstrcpy()
.strcpy()
. Отлично подходит для использования, если размер копируемых данных неизвестен или больше, чем буфер назначения. Если проверка размера входящих данных не выполняется,возможно переполнение буфера. И это не винаstrcpy()
сам, но вызывающего приложения-тоstrcpy()
небезопасно в основном миф, созданный Microsoft.strtok()
. Изменяет строку вызывающего объекта и использует внутренние переменные состояния, что может сделать его небезопасным в многопоточной среде.
наверное стоит добавить, что
strncpy()
не является универсальной заменой дляstrcpy()
что это имя может предложить. Он предназначен для полей фиксированной длины, которые не нуждаются в Nul-Терминаторе (он изначально был разработан для использования с записями каталога UNIX, но может быть полезен для таких вещей, как поля ключа шифрования).это легко, однако, использовать
strncat()
как замена дляstrcpy()
:if (dest_size > 0) { dest[0] = ''; strncat(dest, source, dest_size - 1); }
(The
также проверьте список Microsoft Запрещенные API. Это API (включая многие уже перечисленные здесь), которые запрещены в коде Microsoft, поскольку они часто используются неправильно и приводят к проблемам безопасности.
вы можете не согласиться со всеми из них, но все они заслуживают рассмотрения. Они добавляют API в список, когда его неправильное использование привело к ряду ошибок безопасности.
практически любая функция, которая имеет дело с нулевыми завершенными строками, потенциально небезопасна. Если вы получаете данные из внешнего мира и манипулируете ими с помощью функций str* (), то вы настраиваете себя на катастрофу
Не забывайте о sprintf-это причина многих проблем. Это верно, потому что альтернатива snprintf иногда имеет разные реализации, которые могут сделать ваш код не переносимым.
в случае 1 (linux) возвращаемое значение-это сумма данные, необходимые для хранения всего буфера (если он меньше размера данного буфера, то вывод был усечен)
в случае 2 (windows) возвращаемое значение является отрицательным числом в случае усечения вывода.
Как правило, вы должны избегать функций, которые не являются:
безопасное переполнение буфера (многие функции уже упоминаются здесь)
потокобезопасный / не реентерабельный (strtok для пример)
в руководстве каждой функции вы должны искать ключевые слова, такие как: safe, sync, async, thread, buffer, bugs
это очень трудно использовать
scanf
безопасно. Хорошее использованиеscanf
можно избежать переполнения буфера, но вы все еще уязвимы для неопределенного поведения при чтении чисел, которые не вписываются в запрошенный тип. В большинстве случаев,fgets
с последующим самостоятельным разбором (с помощьюsscanf
,strchr
и т. д.) это лучший вариант.но я бы не сказал "не
scanf
все время".scanf
имеет свое применение. В качестве примера предположим, что вы хотите прочитать пользовательский ввод вchar
массив, который составляет 10 байт длинный. Вы хотите, чтобы удалить пустую строку, если таковые имеются. Если пользователь вводит более 9 символов перед новой строкой, вы хотите сохранить первые 9 символов в буфере и отбросить все до следующей новой строки. Вы можете сделать:char buf[10]; scanf("%9[^\n]%*[^\n]", buf)); getchar();
как только вы привыкнете к этой идиоме, она короче и в некотором смысле чище, чем:
char buf[10]; if (fgets(buf, sizeof buf, stdin) != NULL) { char *nl; if ((nl = strrchr(buf, '\n')) == NULL) { int c; while ((c = getchar()) != EOF && c != '\n') { ; } } else { *nl = 0; } }
во всех сценариях копирования/перемещения строк-strcat(), strncat(), strcpy(), strncpy () и т. д. - дела идут гораздо лучше (безопасное) если применяются несколько простых эвристик:
1. Всегда NUL-заполняйте буфер(ы) перед добавлением данных.
2. Объявите символьные буферы как [размер+1], с макро-константой.
Например, дано:#define BUFSIZE 10 char Buffer[BUFSIZE+1] = { 0x00 }; /* The compiler NUL-fills the rest */
мы можем использовать следующий код:
memset(Buffer,0x00,sizeof(Buffer)); strncpy(Buffer,BUFSIZE,"12345678901234567890");
относительно безопасно. Функции memset() должен появляются перед strncpy (), хотя мы инициализировали буфер во время компиляции, потому что мы не знаем, какой мусор другой код помещен в него до вызова нашей функции. Strncpy () будет усекать скопированные данные до "1234567890", и будет не NUL-прекратить его. Однако, поскольку мы уже заполнили весь буфер-sizeof (Buffer), а не BUFSIZE-в любом случае гарантируется окончательное "вне области действия", завершающее NUL, пока мы ограничиваем наши записи с помощью константа BUFSIZE, а не sizeof(буфер).
Buffer и BUFSIZE также отлично работают для snprintf ():
memset(Buffer,0x00,sizeof(Buffer)); if(snprintf(Buffer,BUFIZE,"Data: %s","Too much data") > BUFSIZE) { /* Do some error-handling */ } /* If using MFC, you need if(... < 0), instead */
хотя snprintf () специально пишет только символы BUFIZE-1, а затем NUL, это работает безопасно. Таким образом, мы "тратим" лишний байт NUL в конце буфера...мы предотвращаем как переполнение буфера, так и unterminated строковые условия, для довольно небольшой стоимости памяти.
мой вызов на strcat () и strncat() является более жестким: не используйте их. Трудно безопасно использовать strcat (), а API для strncat () настолько противоречит интуиции, что усилия, необходимые для его правильного использования, сводят на нет любую выгоду. Я предлагаю следующее падение-в:
#define strncat(target,source,bufsize) snprintf(target,source,"%s%s",target,source)
заманчиво создать strcat () drop-in, но не очень хорошая идея:
#define strcat(target,source) snprintf(target,sizeof(target),"%s%s",target,source)
потому что target может быть указателем (таким образом, sizeof () не возвращает необходимую нам информацию). У меня нет хорошего " универсала" решение для экземпляров strcat () в вашем коде.
проблема, с которой я часто сталкиваюсь от программистов "strFunc()-aware",-это попытка защитить от переполнения буфера с помощью strlen(). Это нормально, если содержимое гарантированно будет аннулировано. В противном случае сам strlen() может вызвать ошибку переполнения буфера (обычно приводящую к нарушению сегментации или другой ситуации с дампом ядра), прежде чем вы когда-либо достигнете "проблемного" кода, который вы пытаетесь защитить.