Основная задача статических анализаторов — поиск ошибок, пропущенных разработчиками. Недавно команда 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 может принимать намного больше символов, чем может хранить. Это приводит к переполнению буфера и неопределенному поведению. Итак, мы получили неожиданное предупреждение, потому что здесь нет ложного срабатывания!

Вот так мы нашли новые поводы вспомнить о ключевом преимуществе статического анализа — инструмент намного внимательнее человека. Таким образом, вдумчивый просмотр предупреждений анализатора экономит время и силы разработчиков при отладке. Это также защищает от ошибок и поспешных суждений.