Featured Webinar: MISRA C++ 2023: Everything You Need to Know | Watch Now

Detecting Memory Corruption in C and C++

Headshot of Ricardo Camacho, Director of Safety & Security Compliance
November 22, 2023
7 min read

Check out this brief explanation of why memory corruption in C and C++ is so difficult to detect by code analysis and instructions for using a memory fault detection tool that will save you from long hours of debugging sessions.

Programmers continue to use C and C++ programming languages because they can easily interact with memory, work closely with the hardware, and offer the power, performance, and efficiency needed in embedded development. 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 it 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 unhappy customers. They can be life-threatening.

What Is Memory Corruption?

Memory corruption errors are unpleasant, especially if well disguised. When they do manifest, 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.

Types of Memory Corruption in C and C++

Memory corruption is a critical issue in programming, particularly in C and C++, as these languages provide direct access to memory management, increasing the potential for errors. As we pointed out above, memory corruption occurs when a program accesses or modifies memory in an unintended way, leading to data corruption, crashes, or security vulnerabilities.

Understanding the various types of memory corruption is crucial for detecting memory problems and also paves ways for mitigating them. The following sections provide insights into four common types of memory corruption.

1. Buffer Overflows

Buffer overflows occur when a program attempts to write more data to a buffer than it was designed to hold. This can result in data being overwritten in adjacent memory locations, potentially corrupting other data or causing the program to crash. Buffer overflows are a common source of security vulnerabilities, as they can be exploited to execute arbitrary code.
There are many ways in which Buffer overflows can occur. For example, if a program copies a string to a buffer without checking the length of the string, it may overwrite memory beyond the end of the buffer. Additionally, if a program uses an array index that is out of bounds, it may access memory that it does not own and corrupt the data.

To prevent buffer overflows, programmers should always check the size of the destination buffer before copying data to it. They should also use safe programming practices, such as using the strcpy_s and strncpy_s functions in C or the std::string class in C++, which provides bounds checking. Coding standards like MISRA C/C++ identify the use of insecure system functions and provide alternatives to remediate these identified vulnerabilities.

2. Use-After-Free

Use-after-free errors arise when a program attempts to access or modify memory that has already been freed. This can happen if a pointer to the freed memory is not properly invalidated, or if the pointer is passed to another part of the program that does not know that the memory is no longer valid. This type of memory corruption error can result in unpredictable behavior, as the freed memory may be reallocated for a different purpose. Common causes include failing to set pointers to NULL after freeing associated memory and incorrectly using a pointer after it has been passed to a function that frees the associated memory.

Use-after-free errors can be difficult to detect and debug, as the program may appear to work correctly until the freed memory is reused. This can lead to unpredictable behavior, such as crashes or data corruption.

Developers can mitigate this type of memory corruption by freeing up memory when it is no longer needed. They should also use proper pointer management techniques, such as setting pointers to NULL after freeing the memory they point to.

3. Double-Free

Double-free errors, also known as double deletion errors, occur when a program attempts to free the same block of memory twice. This can happen if the program manages multiple pointers to the same memory block and frees it multiple times, or if it passes the same pointer to the free function multiple times.

Double-free errors are serious memory corruption issues that can lead to unpredictable program behavior, crashes, and security vulnerabilities if left unresolved. To prevent double-free errors, programmers should maintain proper records of allocated memory blocks and ensure they are deallocated only once. Before freeing a memory block, they should validate the pointer to check if it has already been freed, preventing attempts to free the same memory twice.

4. Memory Leaks

Memory leaks happen when a program allocates memory but fails to deallocate or free it when it is no longer needed. This type of memory corruption can lead to a gradual depletion of available memory, causing performance issues. Forgetting to free dynamically allocated memory and losing all references to allocated memory without freeing it are common causes of memory leaks.

Like the other types of memory corruption issues, they can also be difficult to detect and debug, as the program may appear to work correctly for some time before running out of memory.
To prevent memory leaks, programmers should always free memory explicitly when it is no longer needed. They should also use an automated memory debugger for C and C++, like Parasoft Insure ++.

Common Causes and Consequences of Corruption in Memory C++

Memory corruption in C++ can result from various causes, ranging from programming errors to unsafe practices, and its consequences can negatively impact program behavior, security, and data integrity. Consider allocating memory at the start of the application and managing that block of memory with overloaded new and free functions to control and remediate memory corruption issues

The following sections provide an overview of some common causes and consequences of memory corruption in C++.

1. Undefined Behavior

Undefined behavior is one of the notable causes of memory corruption in C and C++. It occurs when the program executes code that does not conform to the language specifications. In the context of memory, accessing uninitialized memory, reading/writing beyond array bounds, and dereferencing null or dangling pointers can all lead to undefined behavior. The consequences of undefined behavior are unpredictable, making it a critical concern for developers.

2. Security Vulnerabilities

Memory corruption poses a severe threat to the security of C and C++ programs. Exploiting memory vulnerabilities is a common technique for attackers to compromise systems. Buffer overflows, use-after-free, and other memory-related issues can be exploited to execute arbitrary code, inject malicious payloads, or manipulate program behavior. Understanding and mitigating these vulnerabilities is essential for developing secure software.

3. Program Crashes and Instability

Memory corruption often manifests in program crashes and instability. When corrupted memory is accessed or manipulated, it can lead to unexpected behavior, causing the program to crash. Identifying the root cause of such crashes can be challenging, requiring thorough debugging and analysis. Proper memory management practices and tools can help prevent these issues and enhance program stability.

4. Data Corruption

Memory corruption can result in the corruption of critical data structures and variables within a program. This can lead to incorrect computations, data loss, or unintended behavior. For example, buffer overflows may overwrite important control structures, leading to data corruption. Preventing data corruption involves careful memory management, proper bounds checking, and adherence to secure coding practices.

How to Detect 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. Just replace your compiler name (cc) with “insure” like below.

cc -o hello hello.c

becomes

insure -o hello hello.c

Next, 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

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 two errors stemming from this problem:

  1. The write overflow when you try to put too many bytes into the string buffer.
  2. 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 evident 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’s 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.

Check out the ultimate memory debugger for C and C++.

“MISRA”, “MISRA C” and the triangle logo are registered trademarks of The MISRA Consortium Limited. ©The MISRA Consortium Limited, 2021. All rights reserved.