Every object has a storage duration that determines its lifetime: static, thread, automatic, or dynamic.lifetime in which it can be used in a well-defined manner. The lifetime of an object begins when sufficient, properly-aligned storage has been obtained for it, and its initialization is complete. The lifetime of an object ends when a nontrivial destructor, if any, is called for the object, and the storage for the object has been reused or released. Use of an object, or a pointer to an object, outside of its lifetime frequently results in undefined behavior.
The C++ Standard, [basic.life], paragraph 5 [ISO/IEC 14882-2003] Section 3.8, "Object Lifetime" describes a number of situations in which trying to access an object outside of its lifetime leads to undefined behavior.
Attempting to access an object outside of its lifetime can result in an exploitable vulnerability.
Noncompliant Code Example (Differing Storage Durations)
2014], describes the lifetime rules for pointers:
Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see 12.7. Otherwise, such a pointer refers to allocated storage, and using the pointer as if the pointer were of type
void*
, is well-defined. Indirection through such a pointer is permitted but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:
— the object will be or was of a class type with a non-trivial destructor and the pointer is used as the operand of a delete-expression,
— the pointer is used to access a non-static data member or call a non-static member function of the object, or
— the pointer is implicitly converted to a pointer to a virtual base class, or
— the pointer is used as the operand of astatic_cast
, except when the conversion is to pointer to cvvoid
, or to pointer to cvvoid
and subsequently to pointer to either cvchar
or cvunsigned char
, or
— the pointer is used as the operand of adynamic_cast
.
Paragraph 6 similarly describes the lifetime rules for nonpointers:
Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. For an object under construction or destruction, see 12.7. Otherwise, such a glvalue refers to allocated storage, and using the properties of the glvalue that do not depend on its value is well-defined. The program has undefined behavior if:
— an lvalue-to-rvalue conversion is applied to such a glvalue,
— the glvalue is used to access a non-static data member or call a non-static member function of the object, or
— the glvalue is bound to a reference to a virtual base class, or
— the glvalue is used as the operand of adynamic_cast
or as the operand oftypeid
.
Do not use an object outside of its lifetime, except in the ways described above as being well-defined.
Noncompliant Code Example
In this noncompliant code example, a pointer to an object is used, prior to its lifetime starting, to call a non-static member function of the object, resulting in undefined behavior:In this noncompliant code example, the address of the variable c_str
with automatic storage duration is assigned to the variable p
, which has static storage duration. The assignment itself is valid, but it is invalid for c_str
to go out of scope while p
holds its address, as happens at the end of dont_do_this
()
.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <stdio.h>struct S { const char *p;void mem_fn(); }; void dont_do_thisf(void) { const char c_str[] = "This will change"S *s; p = c_str; /* Dangerous */ } void innocuous(void) { printf("%s\n", p); } int main(void) { dont_do_this(); innocuouss->mem_fn(); } |
Compliant Solution
In this compliant solution, storage is obtained for the pointer prior to calling S::mem_fn()
:
Code Block | ||||
---|---|---|---|---|
| ||||
struct S { void mem_fn(); }; void f() { S *s = new S; s->mem_fn(); returndelete 0s; } |
Compliant Solution (Same Storage Durations)
An improved compliant solution would not dynamically allocate memory directly, but would instead use an automatic local variable to obtain the storage and perform initialization. If a pointer was required, use of a smart pointer like std::unique_ptr
would be a marked improvement. However, these suggested compliant solutions would distract from the lifetime demonstration of this compliant solution, and are consequently not shown.
Noncompliant Code Example
In this noncompliant code example, a pointer to an object is implicitly converted to a virtual base class after the object's lifetime has ended, resulting in undefined behaviorIn this compliant solution, p
is declared with the same storage duration as c_str
, preventing p
from taking on an indeterminate value outside of this_is_OK()
:
Code Block | ||||
---|---|---|---|---|
| ||||
void this_is_OK(void)struct B {}; struct D1 : const char c_str[] = "Everything OK"; const char *p = c_str; /* ... */ } /* p is inaccessible outside the scope of string c_str */ |
Compliant Solution (Differing Storage Durations)
virtual B {};
struct D2 : virtual B {};
struct S : D1, D2 {};
void f(const B *b) {}
void g() {
S *s = new S;
// Use s
delete s;
f(s);
} |
Despite the fact that f()
never makes use of the object, the fact that it is passed as an argument to f()
is sufficient to trigger undefined behavior.
Compliant Solution
In this compliant solution, the lifetime of s
is extended to cover the call to f()
:If it is necessary for p
to be defined with static storage duration but c_str
with a more limited duration, then p
can be set to NULL
before c_str
is destroyed. This practice prevents p
from taking on an indeterminate value, although any references to p
must check for NULL
.
Code Block | ||||
---|---|---|---|---|
| ||||
const char *p; void is_this_OK(void) { const char c_str[] = "Everything OK?"; p = c_strstruct B {}; struct D1 : virtual B {}; struct D2 : virtual B {}; struct S : D1, D2 {}; void f(const B *b) {} void g() { S *s = new S; /*/ ... */ p = NULLUse s f(s); delete s; } |
Noncompliant Code Example
...
In this noncompliant code sampleexample, the function init_array
()
returns a pointer to a character array with automatic storage duration, which is accessible to the calleraddress of a local variable is returned from f()
. When the resulting pointer is passed to h()
, the lvalue-to-rvalue conversion applied to i
results in undefined behavior:
Code Block | ||||
---|---|---|---|---|
| ||||
charint *init_arrayg(void) { int i = 12; char array[10]return &i; } void h(int /* Initialize array */ return array; } *i); void f() { int *i = g(); h(i); } |
Some compilers generate a diagnostic message when a pointer to an object with automatic storage duration is returned from a function, as in this example. Programmers should compile code at high warning levels and resolve any diagnostic messages (see MSC00-CPP. Compile cleanly at high warning levels).
Compliant Solution
...
In this compliant solution, the local variable returned from g()
has static storage duration instead of automatic storage duration, extending its lifetime sufficiently for use within f
The solution, in this case, depends on the intent of the programmer. If the intent is to modify the value of array
and have that modification persist outside the scope of init_array()
, the desired behavior can be achieved by declaring array
elsewhere and passing it as an argument to init_array()
:
Code Block | ||||
---|---|---|---|---|
| ||||
#include <stddef.h> void init_array(char *array, size_t lenint *g() { static /*int Initializei array= */12; return &i; } void h(int main(void) { char array[10]; init_array(array, sizeof(array) / sizeof(array[0])); /* ... */ return 0*i); void f() { int *i = g(); h(i); } |
Noncompliant Code Example
...
In this noncompliant code example, the function squirrel_awayfunction g()
stores a pointer to local variable local
into a location pointed to by function parameter ptr_param
. Upon the return of squirrel_away()
, the pointer ptr_param
points to a variable that has an expired lifetime. returns a lambda which captures the automatic local variable i
by reference. When that lambda is returned from the call, the reference it captured will refer to a variable whose lifetime has ended. As a result, when the lambda is executed in f()
, the use of the dangling reference in the lambda results in undefined behavior. As a general rule of thumb, functions returning lambdas should not capture by reference.
Code Block | ||||
---|---|---|---|---|
| ||||
voidauto squirrel_away(char **ptr_paramg() { int i = 12; charreturn local[10&]; { /* Initializei array= */100; *ptr_param = local return i; }; } void rodentf(void) { char *ptr; squirrel_away(&ptr); /* ptr is live but invalid here */ int i = g()(); } |
Compliant
...
Solution
In this compliant solution, the variable local
has static storage duration; consequently, ptr
can be used to reference the local
array within the rodent()
function:lambda does not capture i
by reference, but instead captures it by copy. Consequently, the lambda contains an implicit data member named i
whose lifetime is that of the lambda.
Code Block | ||||
---|---|---|---|---|
| ||||
char local[10]; void squirrel_away(char **ptr_paramauto g() { /*int Initializei array= */12; *ptr_paramreturn = local; } void rodent(void) { char *ptr; squirrel_away(&ptr); /* ptr is valid in this scope */[=] () mutable { i = 100; return i; }; } void f() { int i = g()(); } |
Risk Assessment
Referencing an object outside of its lifetime can result in an attacker being able to run arbitrary code.
Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
---|---|---|---|---|---|
DCL30EXP34-CPP | High | Probable | High | P6 | L2 |
Automated Detection
Tool | Version | Checker | Description | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Compass/ROSE | Can detect violations of this rule. It automatically detects returning pointers to local variables. Detecting more general cases, such as examples where static pointers are set to local variables which then go out of scope, would be difficult | ||||||||||||
6.5 | RETURN_LOCAL | Finds many instances where a function will return a pointer to a local stack variable. Coverity Prevent cannot discover all violations of this rule, so further verification is necessary | |||||||||||
7.6.0 | Can detect violations when an array is declared in a function and then a pointer to that array is returned | ||||||||||||
9.1 | LOCRET.*|||||||||||||
LDRA tool suite | 8.5.4 | 42 D | Fully implemented | Splint | 3.1.1 | PRQA QA-C++ | Include Page | | PRQA QA-C++_V | 2515, 2516, 2527, 2528, 4028, 4624, 4629 |
Related Vulnerabilities
Search for vulnerabilities resulting from the violation of this rule on the CERT website.
Related Guidelines
CERT C Secure Coding Standard | |
CERT C++ Coding Standard | MSC00-CPP. Compile cleanly at high warning levels |
SO/IEC TR 24772:2013 | Dangling References to Stack Frames [DCM] |
ISO/IEC TS 17961 | Escaping of the address of an automatic object [addrescape]
Bibliography
[ | [ | Coverity 2007]ISO/IEC 14882- | 20032014] | Sections 3.7, "Storage duration";3.8, "Object Lifetime" |
[Coverity 2007] |
...