Evaluating a pointer including dereferencing the pointer, using it as an operand of an arithmetic operation, type casting it, and using it as the right-hand side of an assignmentinto memory that has been deallocated by a memory management function is undefined behavior. Pointers to memory that has been deallocated are 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.
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. Typically, dynamic memory allocations and deallocations
are far removed, making it difficult to recognize and diagnose such
problems.
#include <new>
struct
S {
void
f();
};
void
g() noexcept(
false
) {
S *s =
new
S;
// ...
delete
s;
// ...
s->f();
}
|
The function
g()
is marked
noexcept(false)
to comply with MEM52-CPP.
Detect and handle memory allocation errors.
Compliant Solution (
new
and
delete
)
In this compliant solution, the dynamically allocated memory is not deallocated until it is no longer required.
#include <new>
struct
S {
void
f();
};
void
g() noexcept(
false
) {
S *s =
new
S;
// ...
s->f();
delete
s;
}
|
Compliant Solution (Automatic Storage Duration)
When possible, use automatic storage duration instead of dynamic
storage duration. Since
s
is not required to live beyond the scope of
g()
, this compliant solution uses automatic storage duration to limit the
lifetime of
s
to the scope of
g().
struct
S {
void
f();
};
void
g() {
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.
#include
<iostream>
#include <memory>
#include
<cstring>
int
main(
int
argc,
const
char
*argv[]) {
const
char
*s =
""
;
if
(argc > 1) {
enum
{ BufferSize = 32 };
try
{
std::unique_ptr<
char
[]> buff(
new
char
[BufferSize]);
std::
memset
(buff.get(), 0, BufferSize);
// ...
s = std::
strncpy
(buff.get(), argv[1],
BufferSize - 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.
#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
{ BufferSize = 32 };
try
{
buff.reset(
new
char
[BufferSize]);
std::
memset
(buff.get(), 0, BufferSize);
// ...
s = std::
strncpy
(buff.get(), argv[1],
BufferSize - 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 improves the security of the
solution.
#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.
#include <string>
std::string str_func();
void
display_string(
const
char
*);
void
f() {
const
char
*str = str_func().c_str();
display_string(str);
/* Undefined behavior */
}
|
Compliant solution (
std::string::c_str()
)
In this compliant solution, a local copy of the string returned
by
str_func()
is made to ensure that string
str
will be valid when the call to
display_string()
is made.
#include <string>
std::string str_func();
void
display_string(
const
char
*s);
void
f() {
std::string str = str_func();
const
char
*cstr = str.c_str();
display_string(cstr);
/* 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.
#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.
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.
#include <new>
void
f() noexcept(
false
) {
void
*ptr = ::operator
new
(0);
// ...
::operator
delete
(ptr);
}
|