Мы в PVS-Studio часто проверяем открытые проекты и пишем статьи об этом. Иногда при написании статьи случаются интересные ситуации или попадаются особенно эпичные ошибки, тогда хочется написать про это отдельную небольшую заметку. Сейчас совпали оба случая.
Вступление
В данный момент я пишу статью про проверку проекта 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 <typename T>
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 <typename T>
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<char*>(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<wchar_t*>
(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. С меня пиво.
Заключение
Любой статический анализатор, к сожалению, выдаёт ложно-положительные срабатывания. Это приводит к тому, что иногда программисты слишком спешат посчитать показавшееся им неубедительное сообщение ложным. Хочется предостеречь от спешки и постараться подходить к изучению сообщений с вниманием.
Кстати, у нас уже были забавные статьи на подобную тему, например:
Как PVS-Studio оказался внимательнее, чем три с половиной программиста.
Ложные срабатывания в PVS-Studio: как глубока кроличья нора.
Приятного прочтения. И приглашаем попробовать PVS-Studio в деле.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Vladislav Stolyarov. How a PVS-Studio developer defended a bug in a checked project.
Комментарии (17)
svistkovr
01.11.2021 18:25+6в куске кода как минимум 2 потенциальные утечки
на 512 и более байт:
//... message_buf = std::malloc message_len = FormatLogMessageForDisplay //... if (message_len <= 0) return;
и 2048 и более байт:
//... wmessage_buf = std::malloc wmessage_buflen = MultiByteToWideChar //... if (wmessage_buflen <= 0) return;
Судя по всему это какой-то отладочный логгер и для больших проектов это может быть проблемой.
unsignedchar
02.11.2021 10:22+4Ручное управление памятью + несколько return. (Жаль что goto отменили). Что может пойти не так?
pakager
02.11.2021 13:24В первом случае утечки не будет: если
if ((message_len = FormatLogMessageForDisplay( ...
вернет значение <= 0, тоmalloc
не отработает и освобождать ничего не нужно.Во втором случае — да, действительно может быть утечка, если
wmessage_buflen = MultiByteToWideChar(
вернет 0. Кстати,MultiByteToWideChar(...)
по документации не может вернуть отрицательное число.svistkovr
03.11.2021 05:20message_len = FormatLogMessageForDisplay
вызывается дважды в коде до malloc и после.wmessage_buflen
там 3 присваивания :
1)int wmessage_buflen = countof(wbuf) - 1; // wmessage_buflen==511
2)wmessage_buflen = message_len; //если message_len<=0 то сюда точно не дойдёт
3)wmessage_buflen = MultiByteToWideCharтак зачем нужна проверка после
MultiByteToWideChar
, если по документации не может вернуть отрицательное число или 0?pakager
03.11.2021 13:58Я не знаю семантики функции
FormatLogMessageForDisplay
, но похоже, что это "условно чистая" функция, которая вернет одинаковый результат при одинаковых аргументах, т.е. если она без ошибок отработала в первый раз, она и во второй раз отработает без ошибок.MultiByteToWideChar
может вернуть 0, я говорил только, что не может вернуть отрицательное число.Хотя согласен — это опасный стиль программирования, и потенциально приводит к ошибкам.
ncr
02.11.2021 19:05+6Там в середине под условием есть динамическое выделение памяти под сообщение функцией malloc.
Вам, наверное, интересно, что это за malloc и откуда он взялся.
Не интересно, совсем. Если в коде естьwchar_t wbuf[512]; ... std::free(wbuf);
то это баг, независимо от того, что там за malloc и что там за условия.
technic93
Судя по
std::
это С++ (а на Си), в таких случаях нужно писать абстракцию для буффера, который может быть аллоцирован на стеке или куче. Тогда не надо думать об освобождении памяти при написании логики, потомучто об этом позаботиться RAII. Уверен в бусте уже есть что-то готовое.Vindicar
Интересно, чем стандартный vector не подошёл.
technic93
он будет инициализировать всё нулями
ncpuma
Нет, не будет. Есть конструктор без инициализации. Ну или можно конструктор по умолчанию использовать.
technic93
Нету конструктора без инициализации, есть конструктор с инициализацией-по-умолчанию n элементов, что для char инициализирует нулём. Но можно сделать свою обёртку для char (или опять же взять из буста).
На самом деле я немного смешал разные проблемы, как избежать забиванием нулей это один вопрос. Брать std::vector и std::array вместо си ужасов это другой вопрос, и третий вопрос это взять готовую абстракцию для помеси array и vector.
technic93
апд. как раз вектор делает value инициализацию, а не default - запутался в этой идиотской терминологии
ncpuma
Да, в vector инициализация происходит в аллокаторе, но что мешает поставить свой аллокатор без инициализации?
http://stackoverflow.com/questions/15097783/value-initialized-objects-in-c11-and-stdvector-constructor/15119665#15119665
technic93
это хорошее решение
tangro
Даже самый банальный std::string имеет такую оптимизацию: небольшие строки размещаются на стеке, а большие - в куче. Можно было бы это использовать ну или скопипастить код и поправить под свои задачи.
technic93
В std:string на порядок меньше (чем 512 в примере) елементов на стеке. Зависит от реализации, но std::string использует управляющий блок (ptr + size + capacity) чтобы хранить в нём котроткие строки. Это даёт грубо говоря максимум 3 * 8 байт, но посколько что-то всё таки надо хранить для управления, то в gcc выходит 15 байт.
Поэтому копировать оттуда реализацию нету никакого смысла.
Revertis
Вспомнилась статья о том, как отличить сишника от плюсовика :)