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

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

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

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

Ответ 1

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

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

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

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

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

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

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

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

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

Scanf (в ответ на ваш последний вопрос)

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

Ответ 2

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

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

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

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

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

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

Ответ 3

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

  • параметр длины до strncat() связан с (но не совсем точно - см. третью точку) максимальное количество символов, которые можно скопировать в место назначения, а не размер целевого буфера. Это делает 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-терминацией и параметром длины разными и неинтуитивными способами, которые путают даже опытных программистов. Они также не обеспечивают простой способ обнаружения, когда происходит усечение.... Из всех этих проблем наиболее важны путаница, вызванная параметрами длины и связанной с этим проблемой NUL-терминации. Когда мы проверяли исходное дерево OpenBSD для потенциальных дыр в безопасности, мы обнаружили безудержное злоупотребление strncpy() и strncat(). Хотя не все из них приводили к уязвимым местам безопасности, они ясно дали понять, что правила использования strncpy() и strncat() в безопасных операциях с строкой широко интерпретируются неправильно.

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

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

Ответ 4

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

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

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

Ответ 5

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

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(). Идеально подходит для использования, если только размер копируемых данных неизвестен или не превышает целевой буфер. Если проверка размера входящих данных не выполняется, возможны переполнения буфера. Это не вина самого TG434, но вызывающего приложения - то, что strcpy() небезопасен, в основном - миф, созданный Microsoft.
  • strtok(). Изменяет строку вызывающей стороны и использует внутренние переменные состояния, что может сделать ее небезопасной в многопоточной среде.

Ответ 6

Избегайте

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

Ответ 7

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

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

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

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

Ответ 8

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

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

Ответ 9

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

Ответ 10

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

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

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

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

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

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

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

Ответ 11

Очень безопасно использовать scanf. Хорошее использование scanf может предотвратить переполнение буфера, но вы по-прежнему уязвимы для поведения undefined при чтении чисел, которые не соответствуют запрошенному типу. В большинстве случаев fgets, за которым следует самоанализ (используя sscanf, strchr и т.д.), Является лучшим вариантом.

Но я бы не сказал "избегать scanf все время". scanf имеет свои применения. В качестве примера предположим, что вы хотите прочитать пользовательский ввод в массиве char длиной 10 байтов. Вы хотите удалить конечную новую строку, если таковая имеется. Если пользователь вводит более девяти символов перед новой строкой, вы хотите сохранить первые 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;
    }
}

Ответ 12

Следует избегать всех функций, имеющих n-версию. Вместо этого используйте strncpy, а не strcpy.

Ответ 13

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

Мой вызов 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() -aware", - это попытка защиты от переполнения буфера с помощью strlen(). Это нормально, если содержимое гарантированно завершено NUL. В противном случае сам strlen() может вызвать ошибку переполнения буфера (обычно приводящую к нарушению сегментации или другой ситуации с дампом ядра), прежде чем вы когда-либо достигнете "проблемного" кода, который вы пытаетесь защитить.

Ответ 14

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