Detecting Memory Corruption in C and C++
By Arthur Hicken
May 24, 2012
3 min read
A quick look at why C and C++ memory corruption is so difficult to find through code inspection – and how to use a memory error detection tool to help (and save you from those all-night debugging sessions).
Programmers continue to use C and C++ programming languages because of their power, performance, and efficiency. However, these languages are prone to having subtle memory problems like memory leaks, buffer overflow, numeric overflow, and more. It’s unfortunately all-too-common for such errors to remain hidden during normal testing. Software with subtle problems such as memory corruption may run flawlessly on one machine, but crash on another, or may run fine for a certain amount of time, only to crash unexpectedly when the system has been up for a long number of days.
These kinds of memory corruption, as well as other common errors like string manipulation problems, improper initialization, and pointer errors, lead to crashes in production. With the increase in embedded software today in airplanes, cars, medical devices, and the growing IoT market, the consequences of buggy software have become more than just unhappy customers but can be life-threatening.
Understanding Memory Corruption
Memory corruption errors like those are unpleasant, especially if well disguised. When they do manifest themselves, they can be deceptively difficult to reproduce and track down. As an example of what can happen, consider the program shown below.
This program concatenates the arguments given on the command line and prints the resulting string:
/* * File: hello.c */ #include <string.h> #include <stdio.h> int main(argc, argv) int argc; char *argv[]; { int i; char str[16]; str[0] = '\0'; for(i=0; i<argc; i++) { strcat(str, argv[i]); if(i < (argc-1)) strcat(str, " "); } printf("You entered: %s\n", str); return (0); }
If you compile and run this program with your normal compiler, you’ll probably see nothing interesting. For example:
c:\source> cc -o hello hello.c c:\source> hello You entered: hello c:\source>hello world You entered: hello world c:\source>hello cruel world You entered: hello cruel world
If this were the extent of your test procedures, you would probably conclude that this program works correctly, despite the fact that it has a very serious memory corruption bug, it just didn’t manifest itself by producing incorrect output. This is common with memory issues – they can often go undetected because they may not affect output directly and thus will not be detected by normal unit tests or functional tests.
This kind of error looks simple enough when it’s in a small example program where it will not be overlooked, but when buried inside complicated code with hundreds of thousands of lines and lots of dynamic allocation, it can easily avoid detection until after release.
Runtime and Memory Error Detection and Visualization With Parasoft Insure++
Detecting Memory Errors
The best way to approach finding complex memory defects is to use a memory error detection tool (or “runtime debugger”). It’s easy to use – you just replace your compiler name (cc) with “insure” – so i.e.
cc -o hello hello.c
becomes
insure -o hello hello.c
and then you just run the program. If you have a well-formatted makefile, you can use Parasoft Insure++ by setting your compiler command to insure:
make CC=insure hello
Once you have compiled with the runtime debugger, you can run the command:
hello cruel world
and it will generate the errors shown below, because the string that is being concatenated becomes longer than the 16 characters allocated in the declaration at line 11:
[hello.c:14] **WRITE_OVERFLOW** >> strcat(str, argv[i]); Writing overflows memory: <argument 1> bbbbbbbbbbbbbbbbbbbbbbbbbb | 16 | 2 | wwwwwwwwwwwwwwwwwwwwwwwwwwwwww Writing (w) : 0xbfffeed0 thru 0xbfffeee1 (18 bytes) To block (b) : 0xbfffeed0 thru 0xbfffeedf (16 bytes) str, declared at hello.c, 11 Stack trace where the error occurred: strcat() (interface) main() hello.c, 14 **Memory corrupted. Program may crash!!** [hello.c:17] **READ_OVERFLOW** >> printf("You entered: %s\n", str); String is not null terminated within range: str Reading : 0xbfffeed0 From block: 0xbfffeed0 thru 0xbfffeedf (16 bytes) str, declared at hello.c, 11 Stack trace where the error occurred: main() hello.c, 17 You entered: hello cruel world
You probably noticed something interesting in the output, namely that there are actually two errors stemming from this problem – one is the write overflow when you try to put too many bytes into the string buffer, and then a read overflow when you read from the string buffer. As you can see, the error can manifest itself in different ways in different places – so imagine what can happen inside a real program. It’s almost axiomatic that all working C and C++ programs have memory leaks and other memory errors in them.
If you want to find these errors without spending weeks chasing obscure problems, take a look at Parasoft Insure++. It can find all problems related to overwriting memory or reading past the legal bounds of an object, regardless of whether it is allocated statically (that is, a global variable), locally on the stack, dynamically (with malloc or new), or even as a shared memory block. It can even detect situations where a pointer crosses from one block of memory into another and starts to overwrite memory there, even if the memory blocks are adjacent. Runtime error detection with Insure++ will harden your application and keep you from those all-night debugging sessions.