Our website uses cookies to enhance your browsing experience.
Accept
to the top
>
>
>
Bugs in the Wild West: Top 10 C and...

Bugs in the Wild West: Top 10 C and C++ errors of 2025

Dec 30 2025

All year long, we've been riding across the vast plains of open-source code, investigating crimes, taking out vulnerabilities, and collecting trophies. Today, we decided to step into the dustiest saloon: an experienced sheriff leans against the bar and reminisces about ten most daring and dangerous bugs in the Wild West.

Want an interesting story?

For the entire year, we've been battling various bugs from C and C++ open-source projects. We caught each one, interrogated it, and recorded its misdeeds in the file. Now, it's time to recall the most notorious cases.

Today, I'll tell you a story about the 10 most interesting bugs we encountered in different corners of the Wild West. For each of these fragments, we have a separate file—a full article. And for the most dedicated, we've selected 5 most popular articles on C and C++ projects posted this year:

You can find the complete list of articles from our blog on our website via the link.

Sit down, traveler, the story will be a long one

N10

Once, we had a case about a missing horse. The mare was always in the stable, but whenever we wanted to ride to town, it seemed to vanish into thin air.

The PVS-Studio warning: 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 168

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 ); // <=
    ....
  }
  ....
}

The SetRenderColor function sets the RGBA color values, where each parameter is of the unsigned char type with a possible value range of [0 .. 255]. When attempting to pass arguments of the float type, the fractional part will be truncated. Therefore, r, g, b, a function parameters will have values equal to 0.

Unfortunately, the repository lacks blame information, so I have two scenarios on how this error appeared in the code.

  • The function once processed colors in a floating-point representation. The processing was later changed to integers, but not all call sites were updated.
  • The developer mistakenly believed that SetRenderColor handled floating-point numbers and set them accordingly.

Here are similar 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. 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

We detected this bug in the Source SDK project. You can find the full article at the link.

N9

We once had to ride a hundred same trails across the prairie. It so happened that we turned onto one we had already ridden. Only this time, it led somewhere completely different.

The PVS-Studio warning: 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

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
    ....
  }
}

The analyzer has detected several branches with identical checks in a function containing a huge number of if-else if statements—roughly 800 lines. Each of them, however, has different logic: the first check and second check. Given the number of branches and the 150-line gap between the duplicates, it's hardly surprising that this could happen. Static analysis helps prevent such cases.

We detected this bug in the Erlang project; you can find the full article at the link.

N8

I knew a sheriff who had only one verdict for all crimes: "Not guilty." He was a strange one.

The PVS-Studio warning: V523 The 'then' statement is equivalent to the subsequent code fragment. cmComputeLinkInformation.cxx 1748

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;
}

The analyzer hints that there is definitely something wrong with the CheckImplicitDirItem function:

  • the then branch of the last if statement duplicates the code below (return false;);
  • every branch of the function execution ends with false return;
  • when called from AddFullItem, it'll never trigger an early return;
  • the full function body can be replaced with return false;, as this won't change the program behavior.

Note that the function is written using the "early return" pattern that helps reduce code nesting: the most positive result is placed at the end of the function, and the rest of the code—in case of divergence from the function purpose—should terminate the function as early as possible.

In our example, we can assume that the most "positive" result of the function is that an object of the LinkEntry type has passed all the necessary checks with a true return value.

Here's an option how to fix the code:

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

We detected this bug in the CMake project; you can find the full article at the link.

N7

Once in a village, I saw a desperate player who bet all his gold on the card that wasn't even in the deck. The judge just threw up his hands, and the gold remained lying on the table.

The PVS-Studio warning:

V783 Dereferencing of the invalid iterator 'shades.end()' might take place. ColorHelper.cpp 194

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);    // <=
}

Is it possible that none of the shades will meet the readability criteria? We can't say for sure, but it's quite likely. This is a textbook case of undefined behavior—no need to flash any psychic paper to prove it, since dereferencing std::map::end() causes exactly that, as this iterator points just past the last element in a std::map.

We detected this bug in the Windows Terminal project; you can find the full article at the link.

N6

I once made a deal with a fellow traveler to split a treasure we found. He nodded, turned around, and vanished like a mirage. No one has seen him since.

The PVS-Studio warning: V758 The 'graph' reference becomes invalid when smart pointer returned by a function is destroyed. utils.cpp 391

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];
  }
  // ....
}
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...);
}

Using smart pointers doesn't resolve the issues of dangling references and memory access here. Let's dig into this. This is how the code works.

  • The create function creates and returns a smart pointer to the FlannNeighborhoodGraphImpl type, and its object reference count is one.
  • The graph reference is created for the value of this smart pointer while the object reference count remains unchanged.
  • Since the pointer is a temporary object, the reference counter will reach null after initialization is done, releasing the managed object. Now, the reference points to a destroyed object.
  • The for loop references an invalid reference.

As a result, the code that seemed correct leads to undefined behavior. Moreover, PVS-Studio isn't the only tool that detects this issue; the sanitizer does this as well. Here's the proof.

To fix this, we need to save the smart pointer so that the FlannNeighborhoodGraph object remains until the end of the block. For example, we can do like this:

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];
  }
}

We also limited the graph scope to free the resource after the loop execution.

We detected this bug in the OpenCV project; you can find the full article at the link.

N5

Once, a local expert drew a map of a river crossing, but he ran out of charcoal. So, the most dangerous section, drawn with the last bits of charcoal, washed away with the first rain. That's where everyone kept disappearing.

The PVS-Studio 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

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

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));              // <=
          }
        }
      }
    ....
  }
}

Math guys, no time to relax. The flags variable is of the unsigned long int type, while brake->type is of the int type. The code is designed to remove a specific bit from flags. Now, let's take a closer look at what's really going on:

  • The 1 constant of the int type is shifted left by a certain number of bits. Most often, the int type is of 32 bits. We hope that the shift isn't by 32 or more bits, otherwise we get undefined behavior.
  • The result of the shift is bitwise inverted. The result of the inversion still has the int type.
  • The result of the inversion is expanded to a 64-bit unsigned type due to the left operand. Since the original type is signed, sign extension will occur. This means that for positive numbers, the 32 most significant bits will contain zero bits, and for negative numbers, they'll contain ones.
  • The bitwise "AND" applies the conversion result to flags. The loss of significant bits in flags will occur when the right operand is positive. It'll only be so when there is a 31-bit shift to the left—when the 31st bit in flags has to be cleared. Catch a proof.

Notice how much we need to keep in mind for such a harmless expression? The problem lies in the different operand sizes and signs of some subexpressions. To fix it, developers just need to change the type of the 1 constant from int to unsigned long long, and the code will execute as intended:

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

We discovered this bug in the PHP project; you can find the full article via the link.

N4

I once saw a young cowboy chasing an outlaw. He cornered him in a dead end in one of the local bars, but he fired not at the criminal, but at his own reflection in a dusty mirror, shattering it to pieces.

The PVS-Studio warning: V794 The assignment operator should be protected from the case of 'this == &other'. fs_path.cpp 36

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

In this snippet, we have the move assignment operator for the FsPath class, which transfers data from the other object to the current instance. However, there's no check for self-assignment (this == &other), which could lead to unintended consequences.

If an attempt is made to assign the object to itself, the m_path = std::move(other.m_path); operation moves the contents of other.m_path into m_path, and the subsequent call to other.m_path.clear(); clears the data. As a result, m_path ends up in an unexpected state, and one can only wish developers happy debugging :)

To eliminate the risk, we recommend adding the following check at the beginning of the operator:

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

Using std::addressof instead of the & operator ensures correct address comparison even when the operator & is overloaded in the class.

We detected this bug in the Nau Engine project; you can find the full article at the link.

N3

I once saw a shaman trying to summon a spirit without reaching the sacred grounds. A spirit came, but it was a completely different one—a coyote from the nearest ravine.

The PVS-Studio warning: 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 29

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;
....
}

Thanks to this code snippet, I can tell you more about undefined behavior.

As we can see above, the install_dependencies class is derived from the modal_dialog class. In the install_dependencies constructor, the base class is initialized with the value returned by (wait for it...) the non-static window_id function. So, this will happen:

  • Execution of the initialization list:
    • a call to the install_dependencies::window_id method;
    • a constructor call to the modal_dialog class;
    • an initialization of the addons_ data member;
  • Execution of the constructor body of the install_dependencies class.

This results in a function call of a class object which hasn't been initialized yet! This violates the following rule of the standard:

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.

But wait, there's more! As you may have noticed, the window_id member function is virtual and overridden in the install_dependencies class. Issues may arise later when a programmer writes a derived class where window_id is overridden.

When an object of this derived class is created and the installed_dependencies constructor is executed, there's no information about the existence of the new override yet. So, the installed_dependencies::window_id function will always be called in the initialization list. This may differ from the developers' original intention. You can read more about it here.

We detected this bug in the Wesnoth project; you can find the full article at the link.

N2

I knew a cowboy who shot at a shadow on the wall, mistaking it for a lurking enemy. The shot boomed, plaster crumbled, leaving only a hole in the wall. The target never existed, and the repairs took a lot of work.

The PVS-Studio warning: V575 The null pointer is passed into 'fseek' function. Inspect the first argument. vid_ati_eeprom.c 61

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);
    ....
}

We need to load data stored in the video adapter's NVRAM, which we keep in a binary file. If the file doesn't exist, we need to create it with "default" data. Let's look at the case where the file is missing. We shift the file pointer, but it's null. As a result, we get an fp null pointer dereference.

Let's take a closer look at fseek. The C11 standard doesn't define requirements for the function first parameter and doesn't guarantee a check for NULL. This means it's up to the standard library developers to handle it properly. Now let's welcome:

  • GNU glibc;
  • BSD libc from FreeBSD 14.3;
  • Microsoft Universal CRT from Windows SDK 10.0.26100;
  • musl v1.2.5.

The last two C standard library implementations are here as guests: 86Box is not designed to work with them, or their compatibility hasn't been checked. The build instructions don't mention alternative implementations either. So, let's start with the expected standard libraries and ask them to repeat the same actions with a null file pointer.

Flicking the power switch

We take an IBM PS/2 model 55SX from the imaginary shelf and "plug in" the IBM 8514/A 2D accelerator made by ATI.

The first test subject is a Windows instance built using MinGW. We ensure the NVRAM file is absent before starting—we check the %userprofile%\86Box VMs\<virtual machine name>\nvr directory for the ati8514_mca.nvr. file. If it is there, we delete it.

Turning on the power supply, and...

Nothing exploded! Everything is fine: the NVRAM file is written, the computer is running, and the smoke test on glibc is complete. No defect detected.

Moving on to FreeBSD. The libc library implements the standard C library in this OS. This is generally true for all BSD-family operating systems.

We use the same configuration. We check for the absence of the ati8514_mca.nvr NVRAM file at the ~/.local/share/86Box/Virtual Machines/<virtual machine name>/nvr path. Three, two, one, power is on...

Well, only an event from Ben Grubbs' recent past could better describe this situation :)

After opening our eyes, squeezed shut after the explosion, we look at the console: we have confirmed an abnormal exit!

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

A core dump file appeared next to the emulator executable. Let's welcome LLDB:

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

The fp null pointer makes a spectacular fire show—there was no way to lock the file as its descriptor is invalid. Unfortunately, LLDB didn't really want to work in real time, crashing either with a quiet lost connection or with a loud boom and special effects. Therefore, I can't show you how the code is executed like in Windows.

We detected this bug in the 86Box project; you can find the full article at the link.

N1

A sheriff I know from a neighboring town once wrote in an interrogation transcript that a witness had confirmed his own testimony. The court never figured out if it was a mistake or a clever defense tactic.

The PVS-Studio warning: V501 There are identical sub-expressions to the left and to the right of the '==' operator: PeekArg.getValNo() == PeekArg.getValNo() PPCISelLowering.cpp 7865

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)));
      }
    }
  ....
}

We're assuming for now that it's yet another victim of copy paste. Let's check whether getValNo have any side effects:

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

Nothing strange here, though. Look at the last commit:

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)));

The idea is clear: an exceptional case that was previously guarded by an assert was redesigned into a regular branch. The commit text also points to that.

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.

Note that, in addition to the error found, there's another strange assignment:

CCValAssign &PeekArg = ArgLocs[I];
....
CCValAssign &GPR2 = ArgLocs[I++]; // here PeekArg == GPR2

The developer may intented to write something like this:

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)));
  }
}

But for clarity, developers broke out PeekArg from GPR2 to show that, unlike the previous unconditional code, the argument now needs to be "peeked" first. And during the copy-paste process, GPR1 was accidentally dropped from the condition.

The corrected if likely should be:

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

Interestingly, before migrating to GitHub, LLVM had a code review platform, and the commit included a link to this website. There, we can see that manual review can't always save the situation:

We found this bug in the LLVM project, the full article is available at the link.

Conclusion

Silence fell over the saloon, broken only by the creak of the entrance door and the crackling of logs in the fireplace. Ten most daring bugs of 2025 are now just stories passed from generation to generation.

In this Wild West, you can't get far without a reliable partner, so my trusted helper in every case has been PVS-Studio analyzer. It's ready to help you find dangerous code snippets:

And if your path leads you to other territories, I suggest listening to stories about bugs from projects written in Java and C#.

Posts: articles

Poll:

Subscribe
and get the e-book
for free!

book terrible tips


Comments (0)

Next comments next comments
close comment form