﻿# Баги на диком западе: топ\-10 ошибок в C и C\+\+ проектах за 2025 год

Весь год мы скакали по бескрайним полям открытого кода, расследуя преступления, отстреливая уязвимости и собирая трофеи\. Но сегодня мы решили заглянуть в самый пыльный бар, где у стойки стоит опытный шериф и вспоминает десять самых лихих и опасных багов на диком западе\.

![1330_top_cpp_2025_ru/image1.png](https://import.viva64.com/docx/blog/1330_top_cpp_2025_ru/image1.png)

## Хочешь услышать интересную историю?

В течение целого года мы сражались с разными ошибками из Open Source проектов на языках C и C\+\+\. Каждого мы ловили, допрашивали и записывали его проделки в наше досье\. А теперь пришло время вспомнить самые громкие дела\.

Сегодня я расскажу вам историю про 10 самых интересных ошибок, с которыми мы встретились в разных местах дикого запада\. По каждому из этих мест у нас заведено отдельное досье — целая статья\. А для наиболее увлечённых мы выбрали 5 самых популярных по C и C\+\+ за этот год:

* [О том, как легкомысленное использование полиморфных аллокаторов может испортить вам жизнь](https://pvs-studio.ru/ru/blog/posts/cpp/1212/)
* [Путеводитель C\+\+ программиста по неопределённому поведению](https://pvs-studio.ru/ru/blog/posts/cpp/1215/)
* [История C и C\+\+\. Часть вторая: стандартизация C и C\+\+, Qt, Clang, Unreal Engine](https://pvs-studio.ru/ru/blog/posts/cpp/1224/)
* [std::array в С\+\+ быстрее массива в С\. Иногда](https://pvs-studio.ru/ru/blog/posts/cpp/1231/)
* [Безопасная работа с массивами? Нет, не слышали](https://pvs-studio.ru/ru/blog/posts/cpp/1241/)

Полный список статей нашего блога можно на сайте по [ссылке](https://pvs-studio.ru/ru/blog/posts/)\.

## Присядь, путник, история будет долгой

**N10**

Было однажды у нас дело о пропавшей лошади\. Кобыла всегда была в стойле, но, когда мы хотели поехать в город, она как будто растворялась\.

Предупреждение PVS\-Studio: [V674](https://pvs-studio.ru/ru/docs/warnings/v674/) The literal '0\.5f' of 'float' type is being implicitly cast to 'unsigned char' type while calling the 'SetRenderColor' function\. Inspect the second argument\. [grenade\_bugbait\.cpp](https://github.com/ValveSoftware/source-sdk-2013/blob/68c8b82fdcb41b8ad5abde9fe1f0654254217b8e/src/game/server/hl2/grenade_bugbait.cpp#L168) 168

```cpp
typedef unsigned char byte;

inline void CBaseEntity::SetRenderColor( byte r, byte g, byte b, byte a )
{
  m_clrRender.Init( r, g, b, a );
}

void CGrenadeBugBait::BugBaitTouch( CBaseEntity *pOther )
{
  ....
  if ( pSporeExplosion )
  {
    ....
    pSporeExplosion->SetRenderColor( 0.0f, 0.5f, 0.25f, 0.15f ); // <=
    ....
  }
  ....
}
```

Функция `SetRenderColor` устанавливает значение цвета по **RGBA**, где каждый параметр имеет тип `unsigned char` с возможным диапазоном значений `[0 .. 255]`\. При попытке передать аргументы с типом `float` дробная часть будет отброшена\. Это приведёт к тому, что параметры функции `r`, `g`, `b`, `a` будут иметь значения, равные `0`\.

К сожалению, в репозитории отсутствует информация по blame, поэтому у меня есть два предположения, как ошибка появилась в кодовой базе:

1. Когда\-то функция обрабатывала цвета в вещественном представлении\. Затем обработку сменили на целые числа, но забыли поправить все точки вызова\.
1. Разработчик ошибочно посчитал, что функция `SetRenderColor` обрабатывает вещественные числа, и задал их таким способом\.

Дополнительно несколько аналогичных предупреждений:

* V674 The literal '0\.5f' of 'float' type is being implicitly cast to 'unsigned char' type while calling the 'SetRenderColor' function\. Inspect the second argument\. weapon\_bugbait\.cpp 171
* V674 The literal '25\.6' of 'double' type is being implicitly cast to 'int' type while calling the 'SetScrollRate' function\. Inspect the first argument\. grenade\_tripmine\.cpp 179


> Этот баг был обнаружен в проекте Source SDK, а полную версию статьи можно найти по \[ссылке\]\(https://pvs\-studio\.ru/ru/blog/posts/cpp/1281/\)\\\.



**N9**

Когда\-то пришлось скакать в прерии по сотне одинаковых троп, и случилось, что свернул на ту, по которой уже проезжал\. Вот только вела она уже в другое место\.

Предупреждение PVS\-Studio: [V517](https://pvs-studio.ru/ru/docs/warnings/v517/) The use of 'if \(A\) \{\.\.\.\} else if \(A\) \{\.\.\.\}' pattern was detected\. There is a probability of logical error presence\. Check lines: 2903, 3053\. [erl\_bif\_info\.c 2903](https://github.com/erlang/otp/blob/maint-28/erts/emulator/beam/erl_bif_info.c#L2903)

```cpp
BIF_RETTYPE system_info_1(BIF_ALIST_1)
{
  ....
  if (is_tuple(BIF_ARG_1)) {                                    // L2778
    ....
  } else if (BIF_ARG_1 == am_scheduler_id) {                    // L2782
    ....
  }
  ....
  else if (BIF_ARG_1 == am_garbage_collection) {                // L2903
    ....
  } else if (BIF_ARG_1 == am_fullsweep_after) {                 // L2921
    ....
  }
  else if (BIF_ARG_1 == am_garbage_collection) {                // L3053
    ....
  } else if (BIF_ARG_1 == am_instruction_counts) {              // L3056
    ....
  }
  ....
  else if (ERTS_IS_ATOM_STR("halt_flush_timeout", BIF_ARG_1)) { // L3552
    ....
  }
}
```

Анализатор обнаружил в функции, содержащей огромное количество `if-else if` \([\~800 строк кода](https://github.com/erlang/otp/blob/maint-28/erts/emulator/beam/erl_bif_info.c#L2771-L3564)\), несколько веток с одинаковыми проверками\. При этом логика в них отличается: [первая проверка](https://github.com/erlang/otp/blob/maint-28/erts/emulator/beam/erl_bif_info.c#L2903-L2920), [вторая проверка](https://github.com/erlang/otp/blob/maint-28/erts/emulator/beam/erl_bif_info.c#L3053-L3054)\. С учётом числа веток в этой функции и дистанции между одинаковыми проверками \(150 строк\) неудивительно, что такое могло произойти\. Предотвратить такие случаи как раз и помогает статический анализ\.


> Этот баг был обнаружен в проекте Erlang, а полную версию статьи можно найти по \[ссылке\]\(https://pvs\-studio\.ru/ru/blog/posts/cpp/1305/\)\\\.



**N8**

Знал я одного шерифа, у которого был единственный вердикт на все преступления — свободен\. Странный он был\.

Предупреждение PVS\-Studio: [V523](https://pvs-studio.ru/ru/docs/warnings/v523/) The 'then' statement is equivalent to the subsequent code fragment\. [cmComputeLinkInformation\.cxx 1748](https://gitlab.kitware.com/cmake/cmake/-/blob/v4.1.0/Source/cmComputeLinkInformation.cxx#L1748)

```cpp
bool cmComputeLinkInformation::CheckImplicitDirItem(LinkEntry const& entry)
{
  BT<std::string> const& item = entry.Item;

  // We only switch to a pathless item if the link type may be
  // enforced.  Fortunately only platforms that support link types
  // seem to have magic per-architecture implicit link directories.
  if (!this->LinkTypeEnabled) {
    return false;
  }

  // Check if this item is in an implicit link directory.
  std::string dir = cmSystemTools::GetFilenamePath(item.Value);
  if (!cm::contains(this->ImplicitLinkDirs, dir)) {
    // Only libraries in implicit link directories are converted to
    // pathless items.
    return false;
  }

  // Only apply the policy below if the library file is one that can
  // be found by the linker.
  std::string file = cmSystemTools::GetFilenameName(item.Value);
  if (!this->ExtractAnyLibraryName.find(file)) {
    return false;
  }

  return false;
}
```

Анализатор намекает, что с функцией `CheckImplicitDirItem` что\-то определённо не так:

* `then`\-ветка последней конструкции `if` совпадает с нижележащим кодом \(`return false;`\);
* все ветви выполнения функции заканчиваются возвратом значения `false`;
* её вызов в функции [`AddFullItem`](https://gitlab.kitware.com/cmake/cmake/-/blob/v4.1.0/Source/cmComputeLinkInformation.cxx#L1685) никогда не приведёт к раннему возврату;
* тело функции можно целиком заменить на `return false;`, т\.к\. это не изменит наблюдаемого поведения программы\.

Отметим, что функция написана с помощью паттерна "[ранний возврат](https://szymonkrajewski.pl/why-should-you-return-early/)"\. Если кто не знает, этот способ помогает сократить уровень вложенности кода\. При его применении в конце функции располагают наиболее "положительный" результат\. Остальной код в случае расхождения с целью функции должен завершить её как можно раньше\.

В нашем примере можно предположить, что наиболее "положительный" результат функции — это прохождение объектом типа `LinkEntry` всех необходимых проверок с возвратом значения `true`\.

Вариант исправленного кода:

```cpp
....
std::string file = cmSystemTools::GetFilenameName(item.Value);
if (!this->ExtractAnyLibraryName.find(file)) {
  return false;
}
return true;
```


> Этот баг был обнаружен в проекте CMake, а полную версию статьи можно найти по \[ссылке\]\(https://pvs\-studio\.ru/ru/blog/posts/cpp/1277/\)\\\.



**N7**

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

Предупреждение PVS\-Studio:

[V783](https://pvs-studio.ru/ru/docs/warnings/v783/) Dereferencing of the invalid iterator 'shades\.end\(\)' might take place\. [ColorHelper\.cpp 194](https://github.com/microsoft/terminal/blob/fc0a06c3b6cae2ede1326eef202a800244365ebd/src/cascadia/TerminalApp/ColorHelper.cpp#L194)

```cpp
winrt::Windows::UI::Color ColorHelper::GetAccentColor(
    const winrt::Windows::UI::Color& color
)
{
    ....
    auto shades = std::map<float, HSL>();
    ....

    // 3f is quite nice if the whole non-client area is painted
    constexpr auto readability = 1.75f;
    for (auto shade : shades)
    {
        if (shade.first >= readability)
        {
            return HslToRgb(shade.second);
        }
    }
    return HslToRgb(shades.end()->second);    // <=
}
```

Возможно ли, что ни один оттенок не будет соответствовать критерию читабельности? Точно сказать нельзя, но такая ситуация вполне вероятна\. Здесь отлично _читается_ [неопределённое поведение](https://pvs-studio.ru/ru/blog/terms/0066/), и психобумагу предъявлять не надо для убедительности, потому что разыменование `std::map::end()` приводит именно к нему, так как этот итератор указывает на элемент следующий за последним в `std::map`\.


> Этот баг был обнаружен в проекте Windows Terminal, а полную версию статьи можно найти по \[ссылке\]\(https://pvs\-studio\.ru/ru/blog/posts/cpp/1267/\)\\\.



**N6**

Заключил как\-то раз договор с попутчиком разделить найденный клад\. Тот кивнул, развернулся и растворился, как мираж\. С тех пор его больше никто не видел\.

Предупреждение PVS\-Studio: [V758](https://pvs-studio.ru/ru/docs/warnings/v758/) The 'graph' reference becomes invalid when smart pointer returned by a function is destroyed\. [utils\.cpp 391](https://github.com/opencv/opencv/blob/12b64b0e2abd91f235cf3cc49df0f0f985aa0fee/modules/calib3d/src/usac/utils.cpp#L391)

```cpp
template<typename T>
struct Ptr : public std::shared_ptr<T>;
// ....
Ptr<FlannNeighborhoodGraph> FlannNeighborhoodGraph::create(
  const Mat &points, int points_size,
  int k_nearest_neighbors_, bool get_distances,
  int flann_search_params_, int num_kd_trees) 
{           
    return makePtr<FlannNeighborhoodGraphImpl>(points, points_size,
                               k_nearest_neighbors_, get_distances,
                               flann_search_params_, num_kd_trees);
}

void Utils::densitySort (const Mat &points, int knn, 
                         Mat &sorted_points, std::vector<int> &sorted_mask) 
{
  // ....
  // get neighbors
  FlannNeighborhoodGraph &graph =                                  // <=
    *FlannNeighborhoodGraph::create(points, points_size, knn,
                                    true /*get distances */, 6, 1);

  std::vector<double> sum_knn_distances (points_size, 0);
  for (int p = 0; p < points_size; p++) {
    const std::vector<double> &dists = graph.getNeighborsDistances(p);
    for (int k = 0; k < knn; k++)
      sum_knn_distances[p] += dists[k];
  }
  // ....
}
```

<details>
   <summary>Ещё немного контекста по Ptr</summary>

```cpp
template<typename T>
struct Ptr : public std::shared_ptr<T>
{
  inline Ptr(const std::shared_ptr<T>& o) 
    CV_NOEXCEPT : std::shared_ptr<T>(o) {}
  inline Ptr(std::shared_ptr<T>&& o) 
    CV_NOEXCEPT : std::shared_ptr<T>(std::move(o)) {}
  typename std::add_lvalue_reference<T>::type operator*() const 
    CV_NOEXCEPT { return *std::shared_ptr<T>::get(); }
  // ....
}

template<typename _Tp, typename ... A1> static inline
Ptr<_Tp> makePtr(const A1&... a1)
{
  static_assert( !has_custom_delete<_Tp>::value,
                 "Can't use this makePtr with custom DefaultDeleter");
  return (Ptr<_Tp>)std::make_shared<_Tp>(a1...);
}
```


</details>
Если вы думаете, что использование умных указателей раз и навсегда решает проблему "висячих" ссылок и доступов к памяти, то здесь всё пошло не так\. Давайте разбираться\. Сейчас код работает следующим образом:

1. Функция `create` создаёт и возвращает умный указатель на тип `FlannNeighborhoodGraphImpl`, и его счётчик ссылок на объект равен единице;
1. Создаётся ссылка `graph` на значение этого умного указателя, при этом счётчик ссылок на объект не изменяется;
1. Указатель является временным объектом, и поэтому после завершения инициализации счётчик ссылок уменьшится до нуля, что приведёт к освобождению управляемого объекта\. Теперь ссылка указывает на разрушенный объект;
1. В цикле `for` происходит обращение к невалидной ссылке\.

В итоге код, который казался правильным, приводит к неопределённому поведению\. Кроме того, эту проблему находит не только PVS\-Studio, но и санитайзер\. [Пруф](https://godbolt.org/z/rxf3xEc37)\.

Чтобы исправить проблему, необходимо сохранить умный указатель, тогда объект типа `FlannNeighborhoodGraph` будет жить до конца блока\. Например, можно сделать [так](https://godbolt.org/z/rxf3xEc37):

```cpp
std::vector<double> sum_knn_distances (points_size, 0);

{
  // get neighbors
  auto graph = FlannNeighborhoodGraph::create(points, points_size, knn,
                                              true /*get distances */, 6, 1);

  for (int p = 0; p < points_size; p++) {
    const std::vector<double> &dists = graph->getNeighborsDistances(p);
    for (int k = 0; k < knn; k++) 
      sum_knn_distances[p] += dists[k];
  }
}
```

Мы дополнительно ограничили область видимости `graph`, чтобы ресурс освободился после выполнения циклов\.


> Этот баг был обнаружен в проекте OpenCV, а полную версию статьи можно найти по \[ссылке\]\(https://pvs\-studio\.ru/ru/blog/posts/cpp/1321/\)\\\.



**N5**

Однажды местный знаток начертил карту переправы, но у него кончился уголь\. Поэтому самый опасный участок, начерченный остатками угля, смыло первым же дождём\. В том месте все и пропадали\.

Предупреждения PVS\-Studio:

[V629](https://pvs-studio.ru/ru/docs/warnings/v629/) Consider inspecting the '1 << \(brake\-\>type \+ 1\)' expression\. Bit shifting of the 32\-bit value with a subsequent expansion to the 64\-bit type\. [phpdbg\_bp\.c 1209](https://github.com/php/php-src/blob/c998c36b97d13cf936a9f5c4180d1104a6db6b80/sapi/phpdbg/phpdbg_bp.c#L1209)

[V784](https://pvs-studio.ru/ru/docs/warnings/v784/) The size of the bit mask is less than the size of the first operand\. This will cause the loss of higher bits\. [phpdbg\_bp\.c 1209](https://github.com/php/php-src/blob/c998c36b97d13cf936a9f5c4180d1104a6db6b80/sapi/phpdbg/phpdbg_bp.c#L1209) 

```cpp
uint64_t flags
....
PHPDBG_API void phpdbg_delete_breakpoint(zend_ulong num)
{
  ....     
  if ((brake = phpdbg_find_breakbase_ex(num, &table, &numkey, &strkey))) {
      int type = brake->type;
      char *name = NULL;
      size_t name_len = 0L;

      switch (type) {
        ....
        default: {
          if (zend_hash_num_elements(table) == 1) {
            PHPDBG_G(flags) &= ~(1<<(brake->type+1));              // <=
          }
        }
      }
    ....
  }
}
```

Математики, не расслабляемся\. Переменная `flags` имеет тип `unsigned long int`, в то время как `brake->type` имеет тип `int`\. Код призван снять определённый бит из `flags`\. А теперь следим за руками:

1. Константу `1` типа `int` сдвигают влево на какое\-то число бит\. Тип `int` чаще всего 32\-битный\. Очень надеемся, что сдвиг не производится на 32 и более битов, иначе поведение [не определено](https://en.cppreference.com/w/c/language/operator_arithmetic.html#Shift_operators)\.
1. Результат сдвига побитово инвертируется\. Результат инверсии по\-прежнему имеет тип `int`\.
1. Результат инверсии расширяется до 64\-битного беззнакового типа из\-за левого операнда\. Поскольку исходный тип был знаковым, то произойдёт преобразование с расширением знака\. Это значит, что для положительных чисел в 32 старших бита попадут нулевые биты, а для отрицательных — единичные\.
1. Результат преобразования накладывается побитовым "И" на `flags`\. Потеря старших значимых битов во `flags` будет происходить, когда правый операнд положительный\. А им он будет только когда происходит сдвиг на 31 бит влево, т\.е\. когда нужно сбросить 31\-й бит во `flags`\. [Пруф](https://godbolt.org/z/ejnze5xMh)\.

Заметили, сколько всего нужно держать в голове для такого безобидного выражения? А проблема кроется в разных размерах операндов и различной знаковости некоторых подвыражений\. А надо всего лишь поменять у константы `1` тип с `int` до `unsigned long long`, и всё будет работать, как и задумывалось:

```cpp
PHPDBG_G(flags) &= ~( 1uLL <<(brake->type+1));
```


> Этот баг был обнаружен в проекте PHP, а полную версию статьи можно найти по \[ссылке\]\(https://pvs\-studio\.ru/ru/blog/posts/cpp/1280/\)\\\.



**N4**

Как\-то раз я увидел, как молодой ковбой гнался за бандитом\. Он загнал его в тупик в одном из местных баров, но выстрелил не в преступника, а в своё отражение в пыльном зеркале, разбив его вдребезги\.

Предупреждение PVS\-Studio: [V794](https://pvs-studio.ru/ru/docs/warnings/v794/) The assignment operator should be protected from the case of 'this \=\= &other'\. [fs\_path\.cpp 36](https://github.com/NauEngine/NauEnginePublic/blob/020cbfec7bd4cff7ff4303e0f31d75eebed584be/engine/core/kernel/src/io/fs_path.cpp#L36)

```cpp
FsPath& FsPath::operator=(FsPath&& other)
{
    m_path = std::move(other.m_path);
    other.m_path.clear();
    return *this;
}
```

В представленном фрагменте реализован оператор перемещения присваивания класса `FsPath`, в котором происходит перенос данных из объекта `other` в текущий экземпляр\. Следует отметить, что отсутствует проверка на самоприсваивание \(проверка вида `(this == &other)`\), что может привести к нежелательным последствиям\.

Если объект пытаются присвоить самому себе, то операция `m_path = std::move(other.m_path);` перемещает содержимое `other.m_path` в `m_path`, а последующий вызов `other.m_path.clear();` очищает данные\. В результате `m_path` оказывается в неожиданном состоянии, и остаётся лишь пожелать удачной отладки :\)

Чтобы устранить этот риск, рекомендую добавить в начало оператора следующую проверку:

```cpp
if (this == std::addressof(other))
{
    return *this;
}
```

Использование `std::addressof` вместо оператора `&` гарантирует корректное сравнение адресов даже при перегрузке оператора `&` в классе\.


> Этот баг был обнаружен в проекте Nau Engine, а полную версию статьи можно найти по \[ссылке\]\(https://pvs\-studio\.ru/ru/blog/posts/cpp/1244/\)\\\.



**N3**

Однажды видал я, как шаман пытался вызвать духа, не дойдя до священного места\. Дух пришёл, но совсем другой — койот из ближайшего оврага\.

Предупреждение PVS\-Studio: [V1099](https://pvs-studio.ru/ru/docs/warnings/v1099/) sing the 'window\_id' function of uninitialized derived class while initializing the 'modal\_dialog' base class will lead to undefined behavior\. [install\_dependencies\.hpp](https://github.com/wesnoth/wesnoth/blob/master/src/gui/dialogs/addon/install_dependencies.hpp#L29) 29

```cpp
class install_dependencies : public modal_dialog
{
public:
  explicit install_dependencies(const addons_list& addons)
    : modal_dialog(window_id()), addons_(addons)   // <=
  {}
....
private:
  virtual const std::string& window_id() const override;
....
}
```

Благодаря этому фрагменту кода я расскажу вам чуточку больше о неопределённом поведении\.

Как мы видим выше, класс `install_dependencies` является производным от класса `modal_dialog`\. В конструкторе `install_dependencies` происходит инициализация родительского класса значением, возвращаемым \(внимание\!\) нестатической функцией `window_id`\. То есть произойдёт следующее:

1. Исполнение списка инициализации:
    * вызов метода `install_dependencies::window_id`;
    * вызов конструктора класса `modal_dialog`;
    * инициализация поля `addons_`;
1. Исполнение тела конструктора класса `install_dependencies`\.

А это значит, что произойдёт вызов функции у объекта класса, который ещё не был инициализирован\! А это, в свою очередь, нарушает правило стандарта, [согласно которому](https://timsong-cpp.github.io/cppwp/n4861/class.base.init#16):

> Member functions \(including virtual member functions, \[class\.virtual\]\) can be called for an object under construction
>
> Similarly, an object under construction can be the operand of the typeid operator \(\[expr\.typeid\]\) or of a dynamic\_cast \(\[expr\.dynamic\.cast\]\)\.
>
> However, if these operations are performed in a ctor\-initializer \(or in a function called directly or indirectly from a ctor\-initializer\) before all the mem\-initializers for base classes have completed, the program has undefined behavior\.

И это ещё не всё\. Как вы могли заметить, функция\-член `window_id` является виртуальной и переопределена в классе `install_dependencies`\. Проблемы могут появиться в дальнейшем, когда программист напишет производный от него класс, в котором будет переопределён `window_id`\.

В момент создания объекта этого производного класса, когда будет отрабатывать конструктор `installed_dependencies`, информации о существовании нового переопределения ещё нет\. Поэтому в списке инициализации всегда будет вызываться именно функция `installed_dependencies::window_id`\. Это может отличаться от того, что задумывал изначально программист\. Подробнее об этом можно почитать [здесь](https://pvs-studio.ru/ru/blog/posts/cpp/0891/)\.


> Этот баг был обнаружен в проекте Wesnoth, а полную версию статьи можно найти по \[ссылке\]\(https://pvs\-studio\.ru/ru/blog/posts/cpp/1261/\)\\\.



**N2**

Знал я одного ковбоя, который выстрелил в тень на стене, приняв её за затаившегося врага\. Выстрел грохнул, штукатурка осыпалась, осталась только дыра в стене\. Цели и не существовало, а восстанавливать пришлось много\.

Предупреждение PVS\-Studio: [V575](https://pvs-studio.ru/ru/docs/warnings/v575/) The null pointer is passed into 'fseek' function\. Inspect the first argument\. [vid\_ati\_eeprom\.c 61](https://github.com/86Box/86Box/blob/v5.0/src/video/vid_ati_eeprom.c#L61)

```cpp
void
ati_eeprom_load_mach8(ati_eeprom_t *eeprom, char *fn, int mca)
{
    FILE *fp;
    ....
    fp   = nvr_fopen(eeprom->fn, "rb");
    size = 128;
    if (!fp) {
        if (mca) {
            (void) fseek(fp, 2L, SEEK_SET);             // <=
            memset(eeprom->data + 2, 0xff, size - 2);
            fp = nvr_fopen(eeprom->fn, "wb");
            fwrite(eeprom->data, 1, size, fp);
    ....
}
```

Нам надо загрузить данные, сохранённые в NVRAM видеоадаптера, и храним мы их в файле в двоичном виде\. Если файла нет, то надо создать в нём "начальные" данные\. Вот как раз сценарий, когда файла нет\. Мы смещаем указатель на файл, а он нулевой, и получаем разыменование нулевого указателя `fp` как из палаты мер и весов\.

Взглянем подробнее на `fseek`\. [Стандарт C11](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf#page=354) не определяет требования к первому параметру функции и, соответственно, не гарантирует проверку на `NULL`\. Это значит, что его обработка остаётся на совести разработчиков стандартной библиотеки\. В студию приглашаются:

* GNU glibc;
* BSD libc из FreeBSD 14\.3;
* Microsoft Universal CRT из Windows SDK 10\.0\.26100;
* musl v1\.2\.5\.

Последние две реализации стандартной библиотеки языка C здесь в качестве гостей: 86Box не рассчитан на использование с ними или их совместимость не проверялась\. В [инструкции по сборке](https://86box.readthedocs.io/en/v5.0/dev/buildguide.html) альтернативные реализации тоже не упомянуты\. Поэтому начнём с ожидаемых к использованию стандартных библиотек и попросим их повторить те же самые действия над нулевым указателем на файл\.

**Подаём напряжение**

Достаём с воображаемого стеллажа IBM PS/2 model 55SX и "вставляем" в него 2D\-ускоритель IBM 8514/A в исполнении ATI\.

![1330_top_cpp_2025_ru/image2.png](https://import.viva64.com/docx/blog/1330_top_cpp_2025_ru/image2.png)

Первым испытуемым станет собранный с помощью MinGW экземпляр для Windows\. Убеждаемся в отсутствии файла NVRAM перед стартом: для этого нужно проверить папку `%userprofile%\86Box VMs\<имя виртуальной машины>\nvr` на наличие файла `ati8514_mca.nvr`\. Если есть, то удаляем\.

![1330_top_cpp_2025_ru/image3.png](https://import.viva64.com/docx/blog/1330_top_cpp_2025_ru/image3.png)

Включаем наш агрегат, и\.\.\.

![1330_top_cpp_2025_ru/image4.png](https://import.viva64.com/docx/blog/1330_top_cpp_2025_ru/image4.png)

Ничего не взорвалось\! Всё хорошо, файл NVRAM записан, компьютер работает и smoke\-тест на glibc окончен\. Дефект не обнаружен\.

![1330_top_cpp_2025_ru/image5.png](https://import.viva64.com/docx/blog/1330_top_cpp_2025_ru/image5.png)

Переходим к FreeBSD\. Стандартную библиотеку языка C в этой операционной системе реализует libc\. В принципе, это справедливо для всех операционных систем семейства BSD\.

Конфигурацию используем ту же самую\. Отсутствие файла NVRAM `ati8514_mca.nvr` проверяем по пути `~/.local/share/86Box/Virtual Machines/<имя виртуальной машины>/nvr`\. Три, два, один, включаем\.\.\.

![1330_top_cpp_2025_ru/image6.png](https://import.viva64.com/docx/blog/1330_top_cpp_2025_ru/image6.png)

Ну, лучше эту ситуацию опишет только произошедшее давным\-давно у [Макса Крюкова](https://www.youtube.com/watch?v=YTQ2ZYPunLg&t=20288s) :\)

![1330_top_cpp_2025_ru/image7.png](https://import.viva64.com/docx/blog/1330_top_cpp_2025_ru/image7.png)

Открываем зажмуренные после взрыва глаза и смотрим в консоль: у нас подтверждён ненормальный выход\!

```cpp
void VMManagerSystem::launchMainProcess() Full Command:
"/root/86Box/build_freebsd/src/86Box"
("--vmpath", "/root/.local/share/86Box/Virtual Machines/somevm",
 "--vmname",
 "somevm")
Connection received on 86Box.socket.5876c5
Connection disconnected
Abnormal program termination while launching main process:
exit code 11, exit status QProcess::CrashExit
```

Рядом с исполняемым файлом эмулятора появился дамп ядра\. Приглашаем в студию LLDB:

```cpp
root@freebsd:~/86Box/build_freebsd/src # lldb 86Box -c 86Box.core
(lldb) target create "86Box" --core "86Box.core"
Core file '/root/86Box/build_freebsd/src/86Box.core' (x86_64) was loaded.
(lldb) bt
* thread #1, name = '86Box', stop reason = signal SIGSEGV
  * frame #0: 0x0000000832f880bf
              libc.so.7`_flockfile(fp=0x0000000000000000)
              at _flock_stub.c:65:20
    frame #1: 0x0000000832f8b675
              libc.so.7`fseek(fp=0x0000000000000000, offset=2, whence=0)
              at fseek.c:62:2
    frame #2: 0x00000000018cd964
              86Box`ati_eeprom_load_mach8(eeprom=...., fn=<unavailable>, mca=1)
              at vid_ati_eeprom.c:61:20
```

Нулевой указатель `fp` устраивает светошумовое представление: не получается заблокировать файл, потому что нет действительного файлового дескриптора\. К сожалению, LLDB очень не хотел работать в реальном времени и падал то с тихим `lost connection`, то с грохотом и спецэффектами\. Поэтому хождения по коду, как в Windows, я показать не смогу\.


> Этот баг был обнаружен в проекте 86Box, а полную версию статьи можно найти по \[ссылке\]\(https://pvs\-studio\.ru/ru/blog/posts/cpp/1304/\)\\\.



**N1**

Знакомый шериф из соседней деревни однажды в протокол допроса записал, что свидетель подтвердил показания самого себя\. Суд так и не понял, было ли это ошибкой или хитрым приёмом защиты\.

Предупреждение PVS\-Studio: [V501](https://pvs-studio.ru/ru/docs/warnings/v501/) There are identical sub\-expressions to the left and to the right of the '\=\=' operator: PeekArg\.getValNo\(\) \=\= PeekArg\.getValNo\(\) [PPCISelLowering\.cpp 7865](https://github.com/llvm/llvm-project/blob/253d18d032bb17ced8a26d72623b6ade5933f6f3/llvm/lib/Target/PowerPC/PPCISelLowering.cpp#L7865)

```cpp
SDValue PPCTargetLowering::LowerCall_AIX(....) const {
  ....
  for (unsigned I = 0, E = ArgLocs.size(); I != E;) {
    ....
    CCValAssign &GPR1 = VA;
    ....
    if (I != E) {
      // If only 1 GPR was available, there will only be one custom GPR and
      // the argument will also pass in memory.
      CCValAssign &PeekArg = ArgLocs[I];
      if (PeekArg.isRegLoc() && PeekArg.getValNo() == PeekArg.getValNo()) // <=
      {
        assert(PeekArg.needsCustom() && "A second custom GPR is expected.");
        CCValAssign &GPR2 = ArgLocs[I++]; 
        RegsToPass.push_back(std::make_pair(GPR2.getLocReg(),
          DAG.getZExtOrTrunc(ArgAsInt, dl, MVT::i32)));
      }
    }
  ....
}
```

Предварительный диагноз — ещё одна жертва копипасты\. Сразу же проверяем `getValNo` на наличие побочных эффектов:

```cpp
class CCValAssign{
  ....
  unsigned ValNo;
  unsigned getValNo() const { return ValNo; } 
}
```

Что ж, ничего необычного\. Посмотрим код до последнего затронувшего его [коммита](https://github.com/llvm/llvm-project/commit/b373ec8ce76a00cd079f70d108b13129c3a4a7b9#diff-909a72141a3ecd6bfde54a6344f80d2a194c3eb79622c5845f6c7c119145af6f):

```cpp
CCValAssign &GPR1 = VA;
....
assert(I != E && "A second custom GPR is expected!");
CCValAssign &GPR2 = ArgLocs[I++];
assert(GPR2.isRegLoc() && GPR2.getValNo() == GPR1.getValNo() &&
       GPR2.needsCustom() && "A second custom GPR is expected!");
RegsToPass.push_back(
  std::make_pair(GPR2.getLocReg(),
                 DAG.getZExtOrTrunc(ArgAsInt, dl, MVT::i32)));
```

Суть понятна: исключительный случай, ранее прикрывавшийся `assert`, редизайнился в рядовое ветвление\. Об этом говорит и текст коммита:

> This patch implements the caller side of placing function call arguments
>
> in stack memory\. This removes the current limitation where LLVM on AIX
>
> will report fatal error when arguments can't be contained in registers\.

Можно заметить, что кроме найденной ошибки есть ещё странное присваивание:

```cpp
CCValAssign &PeekArg = ArgLocs[I];
....
CCValAssign &GPR2 = ArgLocs[I++]; // здесь PeekArg == GPR2
```

Скорее всего, автор хотел написать вот так:

```cpp
if (I != E) {
  CCValAssign &GPR2 = ArgLocs[I];
  if (GPR2.isRegLoc() && PeekArg.getValNo() == GPR1.getValNo()) 
  {
    assert(PeekArg.needsCustom() && "A second custom GPR is expected.");
    I++;
    RegsToPass.push_back(std::make_pair(
             GPR2.getLocReg(), DAG.getZExtOrTrunc(ArgAsInt, dl, MVT::i32)));
  }
}
```

Но для большей выразительности разделил `GPR2` на `PeekArg`, дабы показать, что в отличие от безусловного прежнего кода нужно сперва "подсмотреть" аргумент\. А в процессе копипасты потерял `GPR1` в условии\.

Думаю, корректная версия `if` должна выглядеть так:

```cpp
if (PeekArg.isRegLoc() && PeekArg.getValNo() == GPR1.getValNo())
```

Что интересно, до недавнего полного переезда на GitHub у LLVM был отдельный сайт для код\-ревью, и в коммите есть [ссылка](https://reviews.llvm.org/D73209?id=240887#inline-672042) на него\. Там можно заметить, что ручное ревью не всегда спасает:

![1330_top_cpp_2025_ru/image8.png](https://import.viva64.com/docx/blog/1330_top_cpp_2025_ru/image8.png)


> Этот баг был обнаружен в проекте LLVM, а полную версию статьи можно найти по \[ссылке\]\(https://pvs\-studio\.ru/ru/blog/posts/cpp/1318/\)\\\.

## Заключение

В баре воцарилась тишина, нарушаемая лишь скрипом входной двери и потрескиванием поленьев в камине\. Десять самых лихих багов 2025 года — теперь лишь истории, которые передают из уст в уста\.

На этом диком западе без надёжного напарника делать нечего, поэтому моим верным помощником в каждом деле был анализатор [PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/)\. Он готов помочь и вам найти опасные участки кода:

* для [проектов с открытым исходным кодом](https://pvs-studio.ru/ru/order/open-source-license/);
* для [образовательных целей](https://pvs-studio.ru/ru/order/for-students/);
* [30\-дневная пробная версия](https://pvs-studio.ru/ru/pvs-studio/try-free/)\.

А если ваш путь проходит и по другим территориям, то предлагаю послушать истории про баги из проектов, написанных на Java и C\#: 

* [10 самых интересных ошибок в Java проектах за 2025 год](https://pvs-studio.ru/ru/blog/posts/java/1326/);
* [Топ\-10 ошибок, найденных в C\# проектах за 2025 год](https://pvs-studio.ru/ru/blog/posts/csharp/1328/)\.