Как один разработчик PVS-Studio защищал баг в проверяемом проекте

Мы в PVS-Studio часто проверяем открытые проекты и пишем статьи об этом. Иногда при написании статьи случаются интересные ситуации или попадаются особенно эпичные ошибки, тогда хочется написать про это отдельную небольшую заметку. Сейчас совпали оба случая.

image-loader.svg

Вступление

В данный момент я пишу статью про проверку проекта DuckStation. Это эмулятор консоли Sony PlayStation. Проект довольно интересный и активно развивающийся. Я нашёл там несколько занимательных багов, одним из которых хочу поделиться прямо сейчас. Он продемонстрирует:

  • как всё же невнимателен может быть человек, даже если является опытным специалистом.

  • как статический анализ подстраховывает человека от его рассеянности и невнимательности.

Показательный пример ошибки

Предупреждение анализатора PVS-Studio: V726 An attempt to free memory containing the 'wbuf' array by using the 'free' function. This is incorrect as 'wbuf' was created on stack. log.cpp 216

template 
static ALWAYS_INLINE void FormatLogMessageAndPrintW(....)
{
  ....
  wchar_t wbuf[512];
  wchar_t* wmessage_buf = wbuf;
  ....
  if (wmessage_buf != wbuf)
  {
    std::free(wbuf);        // <=
  }
  if (message_buf != buf)
  {
    std::free(message_buf);
  }
  ....
}

В изначальной версии пишущейся статьи, я описал этот баг так:

Здесь анализатор обнаружил ошибочный код, в котором осуществляется попытка удаления массива, выделенного на стеке через функцию std: free. Так как память не была выделена на куче, не стоит также и вызывать каких-либо специальных функций для её очистки — она будет произведена автоматически при уничтожении объекта.

Казалось бы, отличная ошибка для описания в статье, статический буфер и динамическое освобождение памяти, что же могло пойти не так? Сейчас расскажу.

У нас в компании после написания статьи программистом её вычитывает опытный коллега и даёт рекомендации по её доработке. Этот случай не исключение, и вот какой комментарий дал ревьювер в процессе вычитки статьи на моё описание ошибки:

Здесь нет ошибки. Это ложное срабатывание, анализатор не осилил. Там в середине под условием есть динамическое выделение памяти под сообщение функцией malloc. Проверка if (wmessage_buf!= wbuf) служит для того, чтобы определить, нужно вызывать std: free или нет.

Вам, наверное, интересно, что это за malloc и откуда он взялся. Что же, это мой недочёт и самое время его исправить. Давайте я покажу полный код функции, фрагмент из которой я уже приводил выше при описании ошибки и который как раз изучил человек, который проверял статью:

template 
static ALWAYS_INLINE void FormatLogMessageAndPrintW(
                                             const char* channelName, 
                                             const char* functionName, 
                                             LOGLEVEL level,
                                             const char* message, 
                                             bool timestamp, 
                                             bool ansi_color_code,
                                             bool newline, 
                                             const T& callback)
{
  char buf[512];
  char* message_buf = buf;
  int message_len;
  if ((message_len = FormatLogMessageForDisplay(message_buf,
                       sizeof(buf), channelName, functionName,
                       level, 
                       message, timestamp,
                       ansi_color_code, newline)) > (sizeof(buf) - 1))
  {
    message_buf = static_cast(std::malloc(message_len + 1));
    message_len = FormatLogMessageForDisplay(message_buf, 
                 message_len + 1, channelName, functionName, 
                 level, message, timestamp, ansi_color_code, newline);
  }
  if (message_len <= 0)
    return;

  // Convert to UTF-16 first so unicode characters display correctly.
  // NT is going to do it anyway...
  wchar_t wbuf[512];
  wchar_t* wmessage_buf = wbuf;
  int wmessage_buflen = countof(wbuf) - 1;
  if (message_len >= countof(wbuf))
  {
    wmessage_buflen = message_len;
    wmessage_buf = static_cast
               (std::malloc((wmessage_buflen + 1) * sizeof(wchar_t)));
  }

  wmessage_buflen = MultiByteToWideChar(CP_UTF8, 0, message_buf,
                    message_len, wmessage_buf, wmessage_buflen);

  if (wmessage_buflen <= 0)
    return;

  wmessage_buf[wmessage_buflen] = '\0';
  callback(wmessage_buf, wmessage_buflen);

  if (wmessage_buf != wbuf)
  {
    std::free(wbuf);                        // <=
  }

  if (message_buf != buf)
  {
    std::free(message_buf);
  }
}

Действительно, если длина сообщения больше или равна countof (wbuf), под него создастся новый буфер на куче.

Может показаться, что пример всё больше и больше начинает походить на ложное срабатывание. Однако, я посмотрел на код из функции несколько минут и написал такой ответ, со ссылками на репозиторий:

Категорически не cогласен, давай смотреть на код: буфер на стеке, динамическая аллокация нового буфера на куче, освобождение не того буфера. Если в локальный буфер на стеке не помещается строчка, то мы кладём её в динамически выделенный буфер по указателю wmessage_buf. И, как видно по коду, ниже есть 2 ветки с освобождением памяти, если динамическая аллокация произошла (это делают проверками вроде wmessage_buf!= wbuf). Только в первой ветке освобождается не та память, на что мы и ругаемся. А уже во второй освобождается правильный буфер, и на этот код мы как раз не ругаемся.

Действительно, ошибка всё же есть, и программисту, написавшему этот код, следовало очистить буфер wmessage_buf, по аналогии с тем, как он сделал это ниже.

Ответ коллеги был краток:

Согласен. Поспешил.

P.S. С меня пиво.

Заключение

Любой статический анализатор, к сожалению, выдаёт ложно-положительные срабатывания. Это приводит к тому, что иногда программисты слишком спешат посчитать показавшееся им неубедительное сообщение ложным. Хочется предостеречь от спешки и постараться подходить к изучению сообщений с вниманием.

Кстати, у нас уже были забавные статьи на подобную тему, например:

  1. Как PVS-Studio оказался внимательнее, чем три с половиной программиста.

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

  3. Ложные срабатывания в PVS-Studio: как глубока кроличья нора.

Приятного прочтения. И приглашаем попробовать PVS-Studio в деле.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Vladislav Stolyarov. How a PVS-Studio developer defended a bug in a checked project.

© Habrahabr.ru