Accessing a memory block that was already freed is undefined behavior. This rule flags access via a pointer or a reference to released heap
memory.
Why is this an issue?
A program may allocate an additional memory block using the malloc
function. When no longer needed, such memory blocks are released
using the free
function. After it is released, reading or writing to a heap-allocated memory block leads to undefined behavior.
char *cp = (char*)malloc(sizeof(char)*10); // memory is allocated
// all bytes in cp can be used here
free(cp); // memory is released
cp[9] = 0; // Noncompliant: memory is used after it was released
In addition to the malloc
and free
pair, in C++ a heap memory may be acquired by use of the operator new
,
and later released using the operator delete
.
int *intArray = new int[20]; // memory is allocated
// elements of intArray can be written or read here
delete[] intArray; // memory is released
intArray[3] = 10; // Noncompliant: memory is used after it was released
Releasing a memory block by invoking free
or operator delete
informs the memory management system that the program no
longer uses the given block. Depending on the state and load of the program, such block can be then:
- reused, i.e., the allocation function returns the same pointer,
- released to the operating system, making it inaccessible to the program.
What is the potential impact?
Accessing released memory causes undefined behavior. This means the compiler is not bound by the language standard anymore, and your program has no
meaning assigned to it.
Practically this has a wide range of effects:
- The program may crash due to the memory no longer being accessible, or due to unexpected value being read or written via the pointer.
- Reading from the released memory may produce a garbage value.
- When the memory was already reused to store sensitive data, such as passwords, it may lead to a vulnerability that uses this defect to extract
information from an instance of the program.
- Writing to released memory may change the value of the unrelated object in a remote part of the code if the memory was reused by it. As
different objects may reuse same the block of memory between runs, this leads to unintuitive and hard diagnose bugs.
How to fix it
In most situations, the use of an uninitialized object is a strong indication of a defect in the code, and fixing it requires a review of the
object allocation and deallocation strategies. Generally, the fix requires adjusting the code, so either:
- Moving accesses to the memory before the deallocation
- Moving the deallocation so it happens after all the uses
If possible, it is desired to remove manual memory allocation, and replace it with stack-allocated objects, or in the case of C++, stack objects
that manage memory (using RAII idiom).
Code examples
Noncompliant code example
int *intArray = (int*)malloc(sizeof(int)*10);
// ...
free(intArray);
intArray[9] = 0; // Noncompliant
Compliant solution
Release the memory after all of its uses.
int *intArray = (int*)malloc(sizeof(int)*10);
// ...
intArray[9] = 0; // Compliant
free(intArray);
Alternatively, allocate the array on the stack, if the size of the array is known at compile-time:
int intArray[10];
// ...
intArray[9] = 0; // Compliant
In C++, use std::vector
with an arbitrary number of elements:
std::vector<int> intArray;
intArray.resize(10);
// ...
intArray[9] = 0; // Compliant
Going the extra mile
In C++, manually allocating and deallocating memory is considered a code smell.
It is recommended to follow the RAII idiom and create a class that manages the memory by allocating it when the object is constructed and
freeing it when it is destroyed. Furthermore, copy and move operations on such objects are designed such that this object can be passed by value
between functions (either as an argument or by being returned) in place of raw pointers.
Depending on the type, passing an RAII object operations may either:
- Allocate a new block of memory and copy the elements (
std::vector
).
- Transfer ownership of the memory to constructed object (
std::unique_ptr
).
- Use shared ownership and free memory when the last object is destroyed (
std::shared_ptr
).
This guarantees that accessing a memory managed by such an object is not released as long as such an object is not modified or destroyed (some
RAII types provide a stronger guarantee).
std::vector<int> intArray(10); // manages an array of 10 integers, on the heap
std::unique_ptr<Class> objPtr = std::make_unique<Class>(); // manages an object on the heap
intArray[5]; // OK
objPtr->foo(); // OK
However, any raw pointers or references to memory held by RAII object may still lead to a use after free:
int* p1 = &intArray[0]; // becomes dangling when intArray is destroyed
int* p2 = intArray.data(); // same as above
Class* p3 = objPtr.get(); // becomes dangling, when objPtr releases the pointer
Resources
Documentation
Standards
Related rules
- S5025 recommends avoiding manual memory management