I recently came across a discussion on LinkedIn where someone had run into memory related undefined behaviour, which prompted me to write this post as it’s a common, subtle and often not very well understood bug that’s easily introduced into C++ code.
Before we start, please keep in mind that what we’re talking about here is undefined behaviour in any standardized version of C++. You’re not guaranteed to see this behaviour in your particular environment and pretty much anything might happen, including your cat hitting the power button on your computer.
The behaviour I am describing in this blog post is fairly common though – especially on Windows – and it’s one of the harder problems to debug, especially if you haven’t encountered it before or it’s been so long you can’t remember it (lucky you).
Let’s assume you have the following code:
notMuch(char const* str)
strncpy(m_str, str, 315);
notMuch* nm = new notMuch("Oops");
Obviously this is a contrived example and decidedly bad code on multiple levels, but I constructed it like this to make a point.
Now assume that you are debugging the above code because you are trying to track down some odd memory behaviour. For some reason, everything is working fine when you run the code under the debugger, even though there is clearly something wrong in this code and you’d expect it to fail in a pretty spectacular fashion. Only that it doesn’t. You run the code and everything seems to be working, even though the code is clearly accessing an invalid object.
In order to understand what is happening here, we need to delve a bit deeper into C++ memory handling.
In desktop and server OSs, the call to
operator new will be handled by your runtime library’s memory manager. The memory manager will attempt to locate a suitably sized chunk of memory and return it to
operator new, which then makes use of this memory. In case of a low memory condition, the runtime’s memory manager will request additional memory from the operating system, but as OS calls tend to be a fairly expensive operations compared to checking an internal list, the runtime tries to minimize these calls and keep the requested memory around even if it’s not in use. When the object’s destructor is called, it takes care of its business – not much in this case as the object only contains an array of PODs, so the destructor is essentially a no-op – and then uses operator delete to “free” the memory.
Notice the quotation marks around “free”? That’s because in most cases, freeing the memory results in an entry in a list or bit field being updated. In the interest of performance, the memory isn’t being touched otherwise and now patiently waits to be reused, but the data in the chunk of memory remains unaltered until the memory chunk is being reused. The pointer to the object
nm is still pointing to the (now freed) memory location. It’s invalid, but neither the runtime nor the user can determine that by simply looking at the pointer itself.
Unfortunately this means you now have an invalid pointer that’s pointing to an unusable object somewhere in the weeds that you shouldn’t be able to dereference. The trouble is that in a lot of implementations, you are still able to dereference the pointer and make use of the underlying object as if it hadn’t been deleted. In the example above, this works the majority of the time. If we don’t assume a multithreaded environment, there is no way the memory containing the invalid object will be touched, so it’s safe to access and the user ends up seeing the correct data.
So it’s bad, but not really, right?
No so fast.
Let’s now assume that before our call to
getStr(), we insert another function call that triggers a memory allocation. Depending on the size of the requested chunk of memory and the current availability of other chunks of memory, the memory manager will occasionally reuse the memory that was previously occupied by
nm. Not all the time, but occasionally. And as already mentioned, “nm” is indistinguishable from a valid pointer. All of a sudden, the attempt to create the string “hmmm” results in the string containing garbage or the program crashing, but the behaviour cannot be reproduced consistently and the crashes seem to depend on the phase of the moon.
The big question is, what can we do about this problem?
First, using RAII is a much better idea than raw pointer slinging. The above example contains a good reason why – if nm had been handled in an RAII fashion by using a std::shared_ptr or simply allocating the object on the stack the string hmmm would be constructed when nm was still in scope and a valid object.
Second, a lot of developers advocate nullptr’ing nm after the delete, at least in debug mode. That’s a valid approach and it will allow you to see the problem in the debugger, but it relies on the weakest link – the one located between the chair and the keyboard – to apply this pattern consistently. In any reasonably sized code base one can pretty much expect that someone will forget to apply the pattern in a critical piece of code, with predictable late night debugging sessions being the result.
Third, the use of a good memory debugger like Valgrind or Rational Purify will catch this issue and a whole raft of other, similar issues like double deletes, buffer overruns, memory leaks etc. If you’re writing production C++ code and you care about the quality of your work, you need to one of these tools.