>
>
>
Один день из жизни разработчика PVS-Stu…

Владислав Столяров
Статей: 20

Один день из жизни разработчика PVS-Studio, или отладка диагностики, которая оказалась внимательнее трёх программистов

Главное предназначение статических анализаторов – найти те ошибки, которые остались незамеченными разработчиком. И недавно команда PVS-Studio снова столкнулась с интересным примером мощи этой методики.

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

Недавно мы доработали ядро анализатора, и коллега, который просматривал новые срабатывания, обнаружил среди них ложное. Он выписал срабатывание для тимлида, тот бегло просмотрел код и создал задачу. Задачу взял я. Таким нехитрым образом и образовалась цепочка из трёх программистов.

Предупреждение анализатора: V645 The 'strncat' function call could lead to the 'а.consoleText' buffer overflow. The bounds should not contain the size of the buffer, but a number of characters it can hold.

Фрагмент кода:

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? Давайте немного углубимся во внутренние механизмы анализатора и попробуем разобраться.

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

В данном случае значение операнда strlen(a.consoleText) неизвестно нам на этапе компиляции. Давайте посмотрим диапазон.

Спустя несколько минут отладки мы получаем от data-flow аж 2 диапазона:

[0, 507] U [0xFFFFFFFFFFFFFFFC, 0xFFFFFFFFFFFFFFFF]

На первый взгляд, кажется, что второй диапазон лишний. Однако это не так. Просто мы забыли о том, что результатом выражения может стать отрицательное число. Например, такое может случиться при strlen(a.consoleText) = 508. В таком случае произойдет переполнение беззнакового числа, и результатом выражения будет максимальное значение результирующего типа, в данном случае — size_t.

Получается, что анализатор прав! В данном выражении к полю consoleText может добавиться гораздо большее количество символов, чем его размер, что приведёт к переполнению буфера и, как следствие, неопределённому поведению. Причина неожиданного предупреждения – то, что ложного срабатывания тут нет!

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