Основная задача статических анализаторов — поиск ошибок, пропущенных разработчиками. Недавно команда PVS-Studio снова нашла интересный пример, доказывающий силу статического анализа.
Вы должны быть очень внимательны при работе с инструментами статического анализа. Часто код, вызвавший срабатывание анализатора, кажется правильным. Таким образом, у вас возникает соблазн пометить предупреждение как ложное срабатывание. На днях мы попали в такую ловушку. Вот как это оказалось.
Недавно мы доработали ядро анализатора. При просмотре новых предупреждений мой коллега обнаружил среди них ложное. Он отметил предупреждение показать тимлиду, который просмотрел код и создал задачу. Я принял задание. Вот что объединило трех программистов.
Предупреждение анализатора: V645 Вызов функции strncat мог привести к переполнению буфера a.consoleText. Границы должны содержать не размер буфера, а количество символов, которое он может содержать.
Фрагмент кода:
struct A { char consoleText[512]; };
void foo(A a) { char inputBuffer[1024]; .... strncat(a.consoleText, inputBuffer, sizeof(a.consoleText) – strlen(a.consoleText) - 5); .... }
Прежде чем мы рассмотрим пример, давайте вспомним, что делает функция strncat:
char *strncat(
char *strDest,
const char *strSource,
size_t count
);
где:
- «destination» — указатель на строку, к которой нужно добавить;
- ‘source’ — указатель на строку, из которой нужно скопировать;
- count — максимальное количество символов для копирования.
На первый взгляд код кажется отличным. Код вычисляет количество свободного места в буфере. И кажется, что у нас есть 4 лишних байта… Мы подумали, что код написан правильно, поэтому отметили его как пример ложного предупреждения.
Давайте посмотрим, так ли это на самом деле. В выражении:
sizeof(a.consoleText) – strlen(a.consoleText) – 5
максимальное значение может быть достигнуто при минимальном значении второго операнда:
strlen(a.consoleText) = 0
Тогда результат 507, и переполнения не происходит. Почему PVS-Studio выдает предупреждение? Давайте углубимся во внутреннюю механику анализатора и попробуем разобраться.
Статические анализаторы используют анализ потока данных для вычисления таких выражений. В большинстве случаев, если выражение состоит из констант времени компиляции, поток данных возвращает точное значение выражения. Во всех остальных случаях, как и в случае с предупреждением, поток данных возвращает только диапазон возможных значений выражения.
В этом случае значение операнда strlen(a.consoleText) неизвестно во время компиляции. Посмотрим на ассортимент.
После нескольких минут отладки получаем целых 2 диапазона:
[0, 507] U [0xFFFFFFFFFFFFFFFC, 0xFFFFFFFFFFFFFFFF]
Второй диапазон кажется избыточным. Однако это не так. Мы забыли, что выражение может получить отрицательное число. Например, такое может произойти, если strlen(a.consoleText) = 508. В этом случае происходит переполнение беззнакового целого числа. Результатом выражения является максимальное значение результирующего типа — size_t.
Получается, что анализатор прав! В этом выражении поле consoleText может принимать намного больше символов, чем может хранить. Это приводит к переполнению буфера и неопределенному поведению. Итак, мы получили неожиданное предупреждение, потому что здесь нет ложного срабатывания!
Вот так мы нашли новые поводы вспомнить о ключевом преимуществе статического анализа — инструмент намного внимательнее человека. Таким образом, вдумчивый просмотр предупреждений анализатора экономит время и силы разработчиков при отладке. Это также защищает от ошибок и поспешных суждений.