Evaluating pointers into memory that have been deallocated by a memory management function, including dereferencing, acting a pointer—including dereferencing the pointer, using it as an operand of an arithmetic operation, type casting it, or and using the pointer it as the right-hand side of an assignment, assignment—into memory that has been deallocated by a memory management function is undefined behavior. Pointers to memory that have has been deallocated are referred to as called dangling pointers. Accessing a dangling pointer can result in exploitable vulnerabilities.
It is at the memory manager's discretion when to reallocate or recycle the freed memory. When memory is freed, all pointers into it become invalid, and its contents might either be returned to the operating system, making the freed space inaccessible, or remain intact and accessible. As a result, the data at the freed location can appear to be valid but change unexpectedly. Consequently, memory must not be written to or read from once it is freed.
Page properties | ||
---|---|---|
| ||
This rule could probably stand to cover memory which has yet to be allocated. For instance:
This isn't really covered by EXP53-CPP. Do not read uninitialized memory because it has nothing to do with reading an uninitialized value. |
Noncompliant Code Example (new
and delete
)
In this noncompliant code example, s
is dereferenced after it has been deallocated. If this access results in a write-after-free, the vulnerability can be exploited to run arbitrary code with the permissions of the vulnerable process and are seldom this obvious. Typically, dynamic memory allocations and deallocations are far removed, making it difficult to recognize and diagnose such problems.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <new> struct S { void f(); }; void fg() noexcept(false) { S *s = new S; // ... delete s; // ... s->f(); } |
The function fg()
is marked noexcept(false)
to comply with MEM32MEM52-CPP. Detect and handle memory allocation errors.
Compliant Solution (new
and delete
)
In this compliant solution, the dynamically allocated memory isn't is not deallocated until it is no longer required:.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <new> struct S { void f(); }; void fg() noexcept(false) { S *s = new S; // ... s->f(); delete s; } |
Compliant Solution (Automatic Storage Duration)
When possible, it is preferred to use automatic storage duration instead of dynamic storage duration. Since s
is not required to live beyond the scope of fg()
, this compliant solution uses automatic storage duration to limit the lifetime of s
to the scope of fg()
:.
Code Block | ||||
---|---|---|---|---|
| ||||
struct S { void f(); }; void fg() { S s; // ... s.f(); } |
Noncompliant Code Example (std::unique_ptr
)
In the following noncompliant code example, the dynamically allocated memory managed by the buff
object is accessed after it has been implicitly deallocated by the object's destructor:.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <iostream> #include <memory> #include <cstring> int main(int argc, const char *argv[]) { const char *s = ""; if (argc > 1) { enum { BUFFER_SIZEBufferSize = 32 }; try { std::unique_ptr<char[]> buff(new char[BUFFER_SIZE])BufferSize]); std::memset(buff.get(), 0, BufferSize); // ... s = std::strncpy(buff.get(), argv[1], BUFFER_SIZEBufferSize - 1); } catch (std::bad_alloc &) { // Handle error } } std::cout << s << std::endl; } |
This code always creates a null-terminated byte string, despite its use of strncpy()
, because it leaves the final char
in the buffer set to 0.
Compliant Solution (std::unique_ptr
)
In this compliant solution, the lifetime of the buff
object extends past the point at which the memory managed by the object is accessed:.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <iostream> #include <memory> #include <cstring> int main(int argc, const char *argv[]) { std::unique_ptr<char[]> buff; const char *s = ""; if (argc > 1) { enum { BUFFER_SIZEBufferSize = 32 }; try { buff.reset(new char[BUFFER_SIZEBufferSize]); std::memset(buff.get(), 0, BufferSize); // ... s = std::strncpy(buff.get(), argv[1], BUFFER_SIZEBufferSize - 1); } catch (std::bad_alloc &) { // Handle error } } std::cout << s << std::endl; } |
Compliant Solution
In this compliant solution, a variable with automatic storage duration of type std::string
is used in place of the std::unique_ptr<char[]>
, which reduces the complexity and increases the improves the security of the solution:.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <iostream> #include <string> int main(int argc, const char *argv[]) { std::string str; if (argc > 1) { str = argv[1]; } std::cout << str << std::endl; } |
Noncompliant Code Example (std::string::c_str()
)
In this noncompliant code example, std::string::c_str()
is being called on a temporary std::string
object. The resulting pointer will point to released memory once the std::string
object is destroyed at the end of the assignment expression, resulting in undefined behavior when accessing elements of that pointer.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <string> std::string someStringReturningFunctionstr_func(); void displayStringdisplay_string(const char *); void f() { const char *str = someStringReturningFunctionstr_func().c_str(); displayStringdisplay_string(str); /* Undefined behavior */ } |
Compliant solution (std::string::c_str()
)
In this compliant solution, a local copy of the string returned by someStringReturningFunctionstr_func()
is made to ensure that ensures the string str
will be valid when the call to displayString
display_string()
is made:.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <string> std::string someStringReturningFunctionstr_func(); void displayStringdisplay_string(const char *s); void f() { std::string str = someStringReturningFunctionstr_func(); const char *strcstr = str.c_str(); displayStringdisplay_string(strcstr); /* ok */ } |
Noncompliant Code Example
In this noncompliant code example, an attempt is made to allocate zero bytes of memory through a call to operator new()
. If this request succeeds, operator new()
is required to return a non-null pointer value. However, according to the C++ Standard, [basic.stc.dynamic.allocation], paragraph 2 [ISO/IEC 14882-2014], attempting to dereference memory through such a pointer results in undefined behavior.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <new>
void f() noexcept(false) {
unsigned char *ptr = static_cast<unsigned char *>(::operator new(0));
*ptr = 0;
// ...
::operator delete(ptr);
} |
Compliant Solution
The compliant solution depends on programmer intent. If the programmer intends to allocate a single unsigned char
object, the compliant solution is to use new
instead of a direct call to operator new()
, as this compliant solution demonstrates.
Code Block | ||||
---|---|---|---|---|
| ||||
void f() noexcept(false) {
unsigned char *ptr = new unsigned char;
*ptr = 0;
// ...
delete ptr;
} |
Compliant Solution
If the programmer intends to allocate zero bytes of memory (perhaps to obtain a unique pointer value that cannot be reused by any other pointer in the program until it is properly released), then instead of attempting to dereference the resulting pointer, the recommended solution is to declare ptr
as a void *
, which cannot be dereferenced by a conforming implementation.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <new> void f() noexcept(false) { void *ptr = ::operator new(0); // ... ::operator delete(ptr); } |
Risk Assessment
Reading previously dynamically allocated memory after it has been deallocated can lead to abnormal program termination and denial-of-service attacks. Writing memory that has been deallocated can lead to the execution of arbitrary code with the permissions of the vulnerable process.
Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
---|
MEM50-CPP | High | Likely | Medium | P18 | L1 |
Automated Detection
Tool | Version | Checker | Description | ||||||
---|---|---|---|---|---|---|---|---|---|
Astrée |
| dangling_pointer_use | |||||||
Axivion Bauhaus Suite |
| CertC++-MEM50 | |||||||
Clang |
| clang-analyzer-cplusplus.NewDelete | Checked by clang-tidy , but does not catch all violations of this rule. | ||||||
CodeSonar |
| ALLOC.UAF | Use after free | ||||||
Compass/ROSE |
| USE_AFTER_FREE | Can detect the specific instances where memory is deallocated more than once or read/written to the target of a freed pointer |
5.0
Double Free
Helix QAC |
| C++4303, C++4304 |
Klocwork |
| UFM.DEREF.MIGHT UFM.DEREF.MUST UFM. |
FFM.MIGHT UFM. |
FFM.MUST UFM.RETURN.MIGHT UFM.RETURN.MUST UFM.USE.MIGHT UFM.USE. |
MUST |
LDRA tool suite |
|
51 D
Fully implemented
483 S, 484 S | Partially implemented | ||||||||
Parasoft C/C++test |
| CERT_CPP-MEM50-a | Do not use resources that have been freed | ||||||
Parasoft Insure++ | Runtime detection | ||||||||
Polyspace Bug Finder |
| CERT C++: MEM50-CPP | Checks for:
Rule partially covered. | ||||||
PVS-Studio |
| V586, V774 | |||||||
Splint |
|
Related Vulnerabilities
VU#623332 describes VU#623332 describes a double-free vulnerability in the MIT Kerberos 5 function krb5_recvauth() [VU# 623332].
Search for other vulnerabilities resulting from the violation of this rule on the CERT website.
Related Guidelines
SEI CERT C++ Coding Standard |
SEI CERT C |
Coding Standard | MEM30-C. Do not access freed memory |
MITRE CWE |
Bibliography
[ISO/IEC 14882-2014] | Subclause 3.7.4.1, "Allocation Functions" Subclause 3.7.4.2, "Deallocation Functions" |
[Seacord 2013b] | Chapter 4, "Dynamic Memory Management" |
...