Мы провели эксперимент по проверке библиотеки glibc с помощью PVS-Studio. Цель эксперимента посмотреть, насколько успешно анализатор может проверять Linux-проекты. Пока плохо может. Возникает огромное количество ложных срабатываний из-за использования нестандартных расширений. Однако, всё равно удалось найти кое что интересное.
glibc
glibc — GNU C Library (GNU библиотека). Glibc является библиотекой Си, которая обеспечивает системные вызовы и основные функции, такие как open, malloc, printf и т.д. Библиотека C используется для всех динамически скомпонованных программ. Она написана Free Software Foundation для GNU операционных систем. glibc выпущена под лицензией GNU LGPL.
Взято из Wikipedia: glibc.
Не так давно в интернете появились новости, что вышла новая версия библиотеки glibc. Это подвигло нас на проверку этой библиотеки с помощью нашего анализатора PVS-Studio. А именно, была проверена версия glibc-2-19-90. К сожалению, на пару недель я отвлекся, поэтому нашёл время написать статью только сейчас. Я был занят большим сравнением нескольких статических анализаторов. Это очень важная для нас задача так как не перестают спрашивать, чем мы лучше Cppcheck и статического анализатора в Visual Studio 2013. Поэтому glibc пришлось немного подождать.
Найти что-то ужасное мы не рассчитывали и действительно не нашли. Библиотека glibc очень качественная и проверяемая многими анализаторами. Как минимум, это:
- Coverity;
- Clang;
- Cppcheck.
В чем сложность анализа
Незнакомым с внутренней кухней инструментов статического анализа, они кажутся очень простыми утилитами. Это не так. Это очень сложные программы.
С толку могут сбивать такие инструменты, как RATS. Если кто-то смотрел код RATS, то видел, что это просто поиск определённых имён функций в файлах. Этот инструмент тоже называется статическим анализатором кода. Однако, он очень далёк от того, чем занимаются настоящие анализаторы кода. Статический анализ, это вовсе не поиск с помощью регулярных выражений [1].
Неоднократно мы повторяли, что Linux-версия, это вовсе не тоже самое, что перекомпилированный исполняемый модуль [2]. Между исполняемым модулем и программным продуктом лежит пропасть. Одно из препятствий, поддержка специфичных расширений и тому подобное.
Что это такое, стороннему человеку совершенно непонятно. Вот от видит, в программе вызов функции strcmp():
cmpres = strcmp (newp->from_string, root->from_string);
И он даже не подозревает, в какой ужас может раскрываться эта строка после препроцессирования и какие нестандартные расширения будут использованы. Конкретно в нашем случае, строка превращается в это:
cmpres = __extension__ ({ size_t __s1_len, __s2_len;
(__builtin_constant_p (newp->from_string) &&
__builtin_constant_p (root->from_string) &&
(__s1_len = strlen (newp->from_string),
__s2_len = strlen (root->from_string),
(!((size_t)(const void *)((newp->from_string) + 1) -
(size_t)(const void *)(newp->from_string) == 1) ||
__s1_len >= 4) &&
(!((size_t)(const void *)((root->from_string) + 1) -
(size_t)(const void *)(root->from_string) == 1) ||
__s2_len >= 4)) ?
__builtin_strcmp (newp->from_string, root->from_string) :
(__builtin_constant_p (newp->from_string) &&
((size_t)(const void *)((newp->from_string) + 1) -
(size_t)(const void *)(newp->from_string) == 1) &&
(__s1_len = strlen (newp->from_string), __s1_len < 4) ?
(__builtin_constant_p (root->from_string) &&
((size_t)(const void *)((root->from_string) + 1) -
(size_t)(const void *)(root->from_string) == 1) ?
__builtin_strcmp (newp->from_string, root->from_string) :
(__extension__ ({ const unsigned char *__s2 =
(const unsigned char *) (const char *) (root->from_string);
int __result = (((const unsigned char *) (const char *)
(newp->from_string))[0] - __s2[0]);
if (__s1_len > 0 && __result == 0) {
__result = (((const unsigned char *) (const char *)
(newp->from_string))[1] - __s2[1]);
if (__s1_len > 1 && __result == 0) { __result =
(((const unsigned char *) (const char *)
(newp->from_string))[2] - __s2[2]);
if (__s1_len > 2 && __result == 0)
__result = (((const unsigned char *)
(const char *) (newp->from_string))[3] -
__s2[3]); } } __result; }))) :
(__builtin_constant_p (root->from_string) &&
((size_t)(const void *)((root->from_string) + 1) -
(size_t)(const void *)(root->from_string) == 1) &&
(__s2_len = strlen (root->from_string), __s2_len < 4) ?
(__builtin_constant_p (newp->from_string) &&
((size_t)(const void *)((newp->from_string) + 1) -
(size_t)(const void *)(newp->from_string) == 1) ?
__builtin_strcmp (newp->from_string, root->from_string) :
(- (__extension__ ({ const unsigned char *__s2 =
(const unsigned char *) (const char *) (newp->from_string);
int __result = (((const unsigned char *) (const char *)
(root->from_string))[0] - __s2[0]);
if (__s2_len > 0 && __result == 0) { __result =
(((const unsigned char *) (const char *)
(root->from_string))[1] - __s2[1]);
if (__s2_len > 1 && __result == 0)
{ __result = (((const unsigned char *)
(const char *) (root->from_string))[2] -
__s2[2]); if (__s2_len > 2 && __result == 0)
__result = (((const unsigned char *) (const char *)
(root->from_string))[3] - __s2[3]); } } __result; })))) :
__builtin_strcmp (newp->from_string, root->from_string))));
});
Анализатор не готов к такому повороту событий и временами выдаёт на подобных конструкциях бессмысленные ложные сообщения.
Поясню про ложные сообщения на более простом примере. Пусть у нас есть строка кода:
assert(MAP_FAILED == (void *) -1);
Макрос assert() раскрывается в следующий код:
((((void *) -1) == (void *) -1) ? (void) (0) :
__assert_fail ("((void *) -1) == (void *) -1",
"loadmsgcat.c", 840, __PRETTY_FUNCTION__));
Анализатор PVS-Studio выдаёт ложное предупреждение касательно сравнения (((void *) -1) == (void *) -1):
V501 There are identical sub-expressions to the left and to the right of the '==' operator: ((void *) — 1) == (void *) — 1 loadmsgcat.c 840
Мы не удивлены этому. Всё это мы уже проходили, ведя разработку для программ, собираемых с помощью Visual C++. Там тоже много всякого интересного и необычного. Нужно проделать большую работу, чтобы научить анализатор понимать, что есть что. Нужно научить его понять, что он имеет дело с макросом «assert» который безобиден и просто проверяет что макрос MAP_FAILED равен "(void *) -1". Всё это уже сделано для Visual C++. А для Linux нет.
В умении корректно работать с такими конструкциями и состоит огромная часть работы по поддержке других компиляторов. Внешне эта работа не видна. Но она требует изучения особенностей компилятора и стандартных библиотек. Эти особенности надо изучить, поддержать и протестировать.
Надеюсь я приоткрыл маленькую щелочку, чтобы вы могли заглянуть в ад. В дальнейшем я планирую написать ряд статей, которые покажут сложность разработки инструментов статического анализа. Думаю, будет интересно.
Найденные подозрительные участки кода
Хотя проект glibc проверяется многими инструментами, кое что интересного всё-таки удалось найти. Давайте посмотрим на эти участки кода.
Странное выражение
char *DCIGETTEXT (....)
{
....
/* Make CATEGORYVALUE point to the next element of the list. */
while (categoryvalue[0] != '\0' && categoryvalue[0] == ':')
++categoryvalue;
....
}
V590 Consider inspecting this expression. The expression is excessive or contains a misprint. dcigettext.c 582
Условие можно упросить до:
while (categoryvalue[0] == ':')
Возможно, это не ошибка и первая часть условия (categoryvalue[0] != '\0') просто лишняя. Однако, вдруг должно быть написано так:
while (categoryvalue[0] != '\0' && categoryvalue[0] != ':')
Разыменование указателя до проверки
Не обязательно, это место опасно. Возможно, указатель никогда не может быть равен нулю. Но тем не менее:
static enum clnt_stat
clntraw_call (h, proc, xargs, argsp, xresults, resultsp, timeout)
CLIENT *h;
u_long proc;
xdrproc_t xargs;
caddr_t argsp;
xdrproc_t xresults;
caddr_t resultsp;
struct timeval timeout;
{
struct clntraw_private_s *clp = clntraw_private;
XDR *xdrs = &clp->xdr_stream;
....
if (clp == NULL)
return RPC_FAILED;
....
}
V595 The 'clp' pointer was utilized before it was verified against nullptr. Check lines: 145, 150. clnt_raw.c 145
Рядом в этом файле можно увидеть аналогичный дефект: V595 The 'clp' pointer was utilized before it was verified against nullptr. Check lines: 232, 235. clnt_raw.c 232
Другой пример опасного кода:
int
__nss_getent_r (....)
{
....
if (res && __res_maybe_init (&_res, 0) == -1)
{
*h_errnop = NETDB_INTERNAL;
*result = NULL;
return errno;
}
....
if (status == NSS_STATUS_TRYAGAIN
&& (h_errnop == NULL || *h_errnop == NETDB_INTERNAL)
&& errno == ERANGE)
}
V595 The 'h_errnop' pointer was utilized before it was verified against nullptr. Check lines: 146, 172. getnssent_r.c 146
Если выполнится условие if (res && __res_maybe_init (&_res, 0) == -1), то функция возвращает информацию об ошибке. При этом она разыменовывает указатели 'h_errnop' и 'result'. Однако, эти указатели вполне могут быть равны NULL. Этот вывод можно делать, исследуя код ниже.
Опасная оптимизация (уязвимость)
char *
__sha256_crypt_r (key, salt, buffer, buflen)
const char *key;
const char *salt;
char *buffer;
int buflen;
{
....
unsigned char temp_result[32]
....
memset (temp_result, '\0', sizeof (temp_result));
....
.... // temp_result далее не используется
}
V597 The compiler could delete the 'memset' function call, which is used to flush 'temp_result' buffer. The RtlSecureZeroMemory() function should be used to erase the private data. sha256-crypt.c 385
Компилятор вправе удалить вызов функции memset() при компиляции Release версии. Точнее он не только в праве, но и обязан это сделать с целью оптимизации. Буфер 'temp_result' после вызова функции memset() нигде не используется, следовательно и сам вызов функции тоже лишний.
Мы имеем дело с уязвимостью, так как не будут очищены приватные данные. Следует заменить функцию memset() на более подходящую. Анализатор предлагает RtlSecureZeroMemory(), которой конечно нет в Linux. Но есть аналоги.
Аналогичная ситуация: V597 The compiler could delete the 'memset' function call, which is used to flush 'temp_result' buffer. The RtlSecureZeroMemory() function should be used to erase the private data. sha512-crypt.c 396
Undefined behavior
Казалось бы, библиотека glibc должна быть написана максимально переносимо. Однако мы встречаем в ней немало конструкций сдвига, которые нельзя назвать безопасными с точки зрения переносимости.
Вот что нам говорит стандарт языка Си о сдвигах:
The integer promotions are performed on each of the operands. The type of the result is that of the promoted left operand. If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behavior is undefined.
The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is E1 * 2 pow E2, reduced modulo one more than the maximum value representable in the result type. If E1 has a signed type and nonnegative value, and E1 * 2 pow E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.
5 The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1 / 2 pow E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined.
Из стандарта следует, что неправильно сдвигать отрицательные числа. Однако, это очень распространённая операция в библиотеке glibc.
Пример сдвига влево:
static void init_cacheinfo (void)
{
....
count_mask = ~(-1 << (count_mask + 1));
....
}
V610 Undefined behavior. Check the shift operator '<<. The left operand '-1' is negative. cacheinfo.c 645
Пример сдвига вправо:
utf8_encode (char *buf, int val)
{
....
*buf = (unsigned char) (~0xff >> step);
....
}
Выражение "~0xff" имеет тип 'int' и равно -256.
Вот список всех мест, где можно наблюдать некорректные сдвиги:
- strxfrm_l.c 68
- clock_nanosleep.c 38
- ifaddrs.c 786
- xdr_intXX_t.c 35
- xdr_intXX_t.c 41
- private.h 327
- private.h 331
- zic.c 696
- zdump.c 212
- zdump.c 216
- timer_create.c 47
- timer_create.c 49
- loop.c 331
- loop.c 437
- mktime.c 207
- mktime.c 208
- mktime.c 211
- mktime.c 212
- mktime.c 230
- mktime.c 298
- mktime.c 298
- ld-collate.c 298
Использование неинициализированной переменной
static int send_vc(....)
{
....
int truncating, connreset, resplen, n;
....
#ifdef _STRING_ARCH_unaligned
*anssizp2 = orig_anssizp - resplen;
*ansp2 = *ansp + resplen;
#else
....
}
V614 Uninitialized variable 'resplen' used. res_send.c 790
Некорректное форматирование строк
В некоторых местах для распечатки знаковых переменных используется '%u'. Ещё в некоторых местах для распечатки беззнаковых переменных используется '%d'. Это конечно мелочи, но их тоже стоит упомянуть.
Пример:
typedef unsigned int __uid_t;
typedef __uid_t uid_t;
int
user2netname (...., const uid_t uid, ....)
{
....
sprintf (netname, "%s.%d@%s", OPSYS, uid, dfltdom);
....
}
V576 Incorrect format. Consider checking the fourth actual argument of the 'sprintf' function. The SIGNED integer type argument is expected. netname.c 51
Другие соответствующие сообщения:
- Consider checking the second actual argument of the 'printf' function. The SIGNED integer type argument is expected. locarchive.c 1741
- Consider checking the fourth actual argument of the 'printf' function. The SIGNED integer type argument is expected. locarchive.c 1741
- Consider checking the fifth actual argument of the 'fprintf' function. The SIGNED integer type argument is expected. res_debug.c 236
- Consider checking the third actual argument of the 'sprintf' function. The UNSIGNED integer type argument is expected. inet_net_ntop.c 134
- Consider checking the fourth actual argument of the 'sprintf' function. The SIGNED integer type argument is expected. ns_print.c 500
- Consider checking the fifth actual argument of the 'sprintf' function. The SIGNED integer type argument is expected. ns_print.c 500
- Consider checking the third actual argument of the 'sprintf' function. The SIGNED integer type argument is expected. ns_print.c 572
- Consider checking the fourth actual argument of the 'sprintf' function. The SIGNED integer type argument is expected. ns_print.c 572
- Consider checking the fifth actual argument of the 'sprintf' function. The SIGNED integer type argument is expected. ns_print.c 572
- Consider checking the third actual argument of the 'sprintf' function. The UNSIGNED integer type argument is expected. ns_print.c 628
- Consider checking the fourth actual argument of the 'sprintf' function. The UNSIGNED integer type argument is expected. ns_print.c 628
- Consider checking the fifth actual argument of the 'sprintf' function. The UNSIGNED integer type argument is expected. ns_print.c 628
- Consider checking the third actual argument of the 'sprintf' function. The SIGNED integer type argument is expected. ns_print.c 645
- Consider checking the third actual argument of the 'sprintf' function. The UNSIGNED integer type argument is expected. ns_print.c 685
- Consider checking the second actual argument of the 'printf' function. The SIGNED integer type argument is expected. nis_print.c 209
- Consider checking the second actual argument of the 'printf' function. The SIGNED integer type argument is expected. sprof.c 480
Заключение
Выбран не удачный проект для начала проверки кода из мира Linux. Он слишком качественный. :) Сложно написать интересную статью про ошибки. Но не беда. нас ждёт немало других известных и интересных проектов в Linux, которые мы попроверяем для демонстрации возможностей анализатора PVS-Studio.
Дополнительные ссылки
- Андрей Карпов. Статический анализ и регулярные выражения.
- Дмитрий Ткаченко. Беседа с Андреем Карповым, техническим директором проектов PVS-Studio и CppCat.
Ответы на вопросы
Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio и CppCat, версия 2014.