Каких функций из стандартной библиотеки следует (следует) избегать?

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

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

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

13 ответов

Решение

Устаревшие функции
небезопасный
Прекрасным примером такой функции является get (), потому что нет способа определить, насколько велик целевой буфер. Следовательно, любая программа, которая читает ввод с помощью 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.

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

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

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

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

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

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

Несколько ответов здесь предлагают использовать 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() (среди других функций).

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

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

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(), Изменяет строку вызывающей стороны и использует внутренние переменные состояния, что может сделать ее небезопасной в многопоточной среде.

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

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

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

Избежать

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

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

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

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

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

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

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

Это очень трудно использовать 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;
    }
}

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

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

  2. Windows: http://msdn.microsoft.com/en-us/library/2ts7cx93%28VS.71%29.aspx

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

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

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

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

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

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

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

Во всех сценариях копирования / перемещения строк - strcat (), strncat (), strcpy (), strncpy () и т. Д. - дела идут намного лучше (безопаснее), если применяется пара простых эвристик:

1. Всегда NUL-заполнять буфер (ы) перед добавлением данных.
2. Объявите символьные буферы как [SIZE+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 (), даже если мы инициализировали Buffer во время компиляции, потому что мы не знаем, какой мусор помещен в него другим кодом до вызова нашей функции. Функция strncpy () усекает скопированные данные до "1234567890" и не завершает их NUL. Однако, так как мы уже заполнили NUL весь буфер - sizeof(Buffer), а не BUFSIZE - гарантированно будет окончательный NUL "вне области", заканчивающий NUL в любом случае, если мы ограничиваем наши записи с использованием BUFSIZE константа, а не sizeof (буфер).

Буфер и 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 в конце буфера... мы предотвращаем переполнение буфера и неопределенные строковые условия при довольно небольшой стоимости памяти.

Мой вызов strcat () и strncat () более жесткий: не используйте их. Трудно безопасно использовать strcat (), а API для strncat () настолько нелогичен, что усилия, необходимые для его правильного использования, сводят на нет любую выгоду. Я предлагаю следующее раскрытие:

#define strncat(target,source,bufsize) snprintf(target,source,"%s%s",target,source)

Соблазнительно создать плагин strcat (), но не очень хорошая идея:

#define strcat(target,source) snprintf(target,sizeof(target),"%s%s",target,source)

потому что target может быть указателем (таким образом, sizeof() не возвращает нужную нам информацию). У меня нет хорошего "универсального" решения для экземпляров strcat () в вашем коде.

Проблема, с которой я часто сталкиваюсь у программистов, ориентированных на strFunc(), - это попытка защиты от переполнения буфера с помощью strlen (). Это нормально, если содержимое гарантированно завершено NUL. В противном случае сам strlen () может вызвать ошибку переполнения буфера (обычно приводящую к нарушению сегментации или другой ситуации с дампом ядра), прежде чем вы когда-либо достигнете "проблемного" кода, который вы пытаетесь защитить.

Атои не является потокобезопасным. Вместо этого я использую strtol, по рекомендации со страницы руководства.

Другие вопросы по тегам