CertC++-EXP54

Do not access an object outside of its lifetime

Required inputs: IR, StaticSemanticAnalysis

Every object has a 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-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 a static_cast, except when the conversion is to pointer to cv void, or to pointer to cv void and subsequently to pointer to either cv char or cv unsigned char, or
    - the pointer is used as the operand of a dynamic_cast.

Paragraph 6 describes the lifetime rules for non-pointers:

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 a dynamic_cast or as the operand of typeid.

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 to call a non-static member function of the object prior to the beginning of the pointer's lifetime, resulting in undefined behavior.

struct S {
  void mem_fn();
};
 
void f() {
  S *s;
  s->mem_fn();
}
Compliant Solution

In this compliant solution, storage is obtained for the pointer prior to calling  S::mem_fn().

struct S {
  void mem_fn();
};

void f() {
  S *s = new S;
  s->mem_fn();
  delete s;
}

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 were required, use of a smart pointer, such as std::unique_ptr, would be a marked improvement. However, these suggested compliant solutions would distract from the lifetime demonstration of this compliant solution and consequently are 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 behavior.

struct B {};
 
struct D1 : 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, its being 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().

struct B {};

struct D1 : virtual B {};
struct D2 : virtual B {};

struct S : D1, D2 {};

void f(const B *b) {}

void g() {
  S *s = new S;
  // Use s
  f(s);
 
  delete s;
}
Noncompliant Code Example

In this noncompliant code example, the address 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.

int *g() {
  int i = 12;
  return &i;
}
 
void h(int *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.

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().

int *g() {
  static int i = 12;
  return &i;
}

void h(int *i);

void f() {
  int *i = g();
  h(i);
}
Noncompliant Code Example

std::initializer_list<> object is constructed from an initializer list as though the implementation allocated a temporary array and passed it to the  std::initializer_list<> constructor. This temporary array has the same lifetime as other temporary objects except that initializing a  std::initializer_list<> object from the array extends the lifetime of the array exactly like binding a reference to a temporary [ ISO/IEC 14882-2014].

In this noncompliant code example, a member variable of type  std::initializer_list<int> is list-initialized within the constructor's ctor-initializer. Under these circumstances, the conceptual temporary array's lifetime ends once the constructor exits, so accessing any elements of the  std::initializer_list<int> member variable results in undefined behavior.

#include <initializer_list>
#include <iostream>

class C {
  std::initializer_list<int> l;

public:
  C() : l{1, 2, 3} {}

  int first() const { return *l.begin(); }
};

void f() {
  C c;
  std::cout << c.first();
}
Compliant Solution

In this compliant solution, the  std::initializer_list<int> member variable is replaced with a  std::vector<int>, which copies the elements of the initializer list to the container instead of relying on a dangling reference to the temporary array.

#include <iostream>
#include <vector>

class C {
  std::vector<int> l;

public:
  C() : l{1, 2, 3} {}

  int first() const { return *l.begin(); }
};

void f() {
  C c;
  std::cout << c.first();
}
Noncompliant Code Example

In this noncompliant code example, a lambda object is stored in a function object, which is later called (executing the lambda) to obtain a constant reference to a value. The lambda object returns an  int value, which is then stored in a temporary int object that becomes bound to the  const int & return type specified by the function object. However, the temporary object's lifetime is not extended past the return from the function object's invocation, which causes undefined behavior when the resulting value is accessed.

#include <functional>
 
void f() {
  auto l = [](const int &j) { return j; };
  std::function<const int&(const int &)> fn(l);
 
  int i = 42;
  int j = fn(i);
}
Compliant Solution

In this compliant solution, the  std::function object returns an  int instead of a  const int &, ensuring that the value is copied instead of bound to a temporary reference. An alternative solution would be to call the lambda directly instead of through the  std::function<> object.

#include <functional>

void f() {
  auto l = [](const int &j) { return j; };
  std::function<int(const int &)> fn(l);

  int i = 42;
  int j = fn(i);
}
Noncompliant Code Example

In this noncompliant code example, the constructor for the automatic variable  s is not called because execution does not flow through the declaration of the local variable due to the  goto statement. Because the constructor is not called, the lifetime for  s has not begun. Therefore, calling  S::f() uses the object outside of its lifetime and results in undefined behavior.

class S {
  int v;
 
public:
  S() : v(12) {} // Non-trivial constructor
 
  void f();
};  
 
void f() {
 
  // ...  
 
  goto bad_idea;  
 
  // ...
 
  S s; // Control passes over the declaration, so initialization does not take place.  
 
  bad_idea:
    s.f();
}
Compliant Solution

This compliant solution ensures that  s is properly initialized prior to performing the local jump.

class S {
  int v;
 
public:
  S() : v(12) {} // Non-trivial constructor

  void f();
};  
 
void f() {
  S s;
 
  // ...
 
  goto bad_idea;
 
  // ...
 
  bad_idea:
    s.f();
}
Noncompliant Code Example

In this noncompliant code example,  f() is called with an iterable range of objects of type  S. These objects are copied into a temporary buffer using  std::copy(), and when processing of those objects is complete, the temporary buffer is deallocated. However, the buffer returned by  std::get_temporary_buffer() does not contain initialized objects of type  S, so when  std::copy() dereferences the destination iterator, it results in undefined behavior because the object referenced by the destination iterator has yet to start its lifetime. This is because while space for the object has been allocated, no constructors or initializers have been invoked.

#include <algorithm>
#include <cstddef>
#include <memory>
#include <type_traits>
 
class S {
  int i;

public:
  S() : i(0) {}
  S(int i) : i(i) {}
  S(const S&) = default;
  S& operator=(const S&) = default;
};

template <typename Iter>
void f(Iter i, Iter e) {
  static_assert(std::is_same<typename std::iterator_traits<Iter>::value_type, S>::value,
                "Expecting iterators over type S");
  ptrdiff_t count = std::distance(i, e);
  if (!count) {
    return;
  }

  // Get some temporary memory.
  auto p = std::get_temporary_buffer<S>(count);
  if (p.second < count) {
    // Handle error; memory wasn't allocated, or insufficient memory was allocated.
    return;
  }
  S *vals = p.first;

  // Copy the values into the memory.
  std::copy(i, e, vals);

  // ...

  // Return the temporary memory.
  std::return_temporary_buffer(vals);
}

Implementation Details

A reasonable implementation of  std::get_temporary_buffer() and  std::copy() can result in code that behaves like the following example (with error-checking elided).

unsigned char *buffer = new (std::nothrow) unsigned char[sizeof(S) * object_count];
S *result = reinterpret_cast<S *>(buffer);
while (i != e) {
  *result = *i; // Undefined behavior
  ++result;
  ++i;
}

The act of dereferencing  result is undefined behavior because the memory pointed to is not an object of type  S within its lifetime.

Compliant Solution ( std::uninitialized_copy())

In this compliant solution,  std::uninitialized_copy() is used to perform the copy, instead of  std::copy(), ensuring that the objects are initialized using placement new instead of dereferencing uninitialized memory. Identical code from the noncompliant code example has been elided for brevity.

//...
  // Copy the values into the memory.
  std::uninitialized_copy(i, e, vals);
// ...
Compliant Solution ( std::raw_storage_iterator)

This compliant solution uses std::copy() with a  std::raw_storage_iterator as the destination iterator with the same well-defined results as using  std::uninitialized_copy(). As with the previous compliant solution, identical code from the noncompliant code example has been elided for brevity.

//...
  // Copy the values into the memory.
  std::copy(i, e, std::raw_storage_iterator<S*, S>(vals));
// ...
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
EXP54-CPP High Probable High P6 L2
Related Guidelines
SEI CERT C Coding Standard DCL30-C. Declare objects with appropriate storage durations
Bibliography
[ Coverity 2007]
[ ISO/IEC 14882-2014] Subclause 3.8, "Object Lifetime"
Subclause 8.5.4, "List-Initialization" 
Excerpt from SEI CERT C++ Coding Standard [https://cmu-sei.github.io/secure-coding-standards/sei-cert-cpp-coding-standard/rules/expressions-exp/exp54-cpp], Copyright (C) 1995-2026 Carnegie Mellon University. See section 9.4. "3rd-Party Licenses" in the documentation for full details.

Possible Messages

Key

Text

Severity

Disabled

assigned_to_pointer_to_const

Assigning the address of a partially initialized variable to some pointer-to-const

None

False

backing_array_lifetime

std::initializer_list has longer lifetime than it’s temporary backing array.

None

False

pass_as_pointer_to_const_param

Passing uninitialized variable by pointer as function parameter with pointer-to-const type

None

False

possible_return_value_uninit

Function return value is potentially not initialized

None

False

possible_uninit

Use of possibly uninitialized variable

None

False

possible_use_after_free

Dynamic memory possibly used after it was previously released

None

False

possibly_initialized

Use of possibly uninitialized variable (previous call {node0} might have initialized the variable)

None

False

possibly_leaking_reference_to_local_variable

Potentially leaking reference/pointer to local variable.

None

False

possibly_leaking_reference_to_temporary_variable

Reference/pointer to temporary object is assigned to longer-lived variable.

None

False

return_value_uninit

Function return value is not initialized

None

False

uninit

Use of uninitialized variable

None

False

use_after_free

Dynamic memory used after it was previously released

None

False

Options

additional_leaking_functions

additional_leaking_functions : set[str] = set()

Set of qualified names of functions. Each of these functions will be assumed to return an address that is owned by its first (including this pointer) parameter and must therefore not be dereferenced after the owner is destroyed.

Alternatively, the functions' definitions in source code can be annotated as returning a pointer/reference to one of its arguments using the attribute [[axivion::return_based_on(parameter_name)]], e.g.:

int* f(int &i1, int &i2) [[axivion::return_based_on(i2)]] { return &i2; }
or struct MyStruct { int m_i; int* f() [[axivion::return_based_on(this)]] { return &m_i; } };
 

additional_local_array_check

additional_local_array_check : bool = True

Invoke an additional analysis that tries to remove false positives involving accesses to local array variables and in particular their initialization. The analysis attempts to report only the first use of an uninitialized value. Consider e.g. the following example:
    int example()
    {
        int a[10];
        int b[20];
        int uninit_var;
        for (int i = 0; i < 10; ++i)
        {
L1:         a[i] = uninit_var; // use of uninit_var reported
            b[i] = i;
        }
        int result = a[3]; // not reported, since already reported at L1
        result += b[15]; // reported; c[] is not (completely) initialized
        return result;
    }
    
 

additional_pointer_returns

additional_pointer_returns : set[str] = set()

Set of qualified names of member functions that are considered to return a reference or pointer to this or a subobject thereof. For this option to have effect, consider_pointer_returns has to be enabled, too.
 

allow_longer_living_local

allow_longer_living_local : bool = False

Whether assignment to a longer-living local variable should be accepted.
 

assume_globals_are_initialized

assume_globals_are_initialized : bool = True

Whether global and local static variables should be treated as initialized (as specified by the language).
 

check_array_access_with_unknown_index

check_array_access_with_unknown_index : bool = False

Whether array accesses like a[i] with non-literal index i should be checked as well.
 

consider_constructors_as_capturing

consider_constructors_as_capturing : bool = False

Whether passing a reference or pointer to a local variable into a constructor should be considered as capturing. If the constructed object is assigned to some nonlocal object, a message is issued. If set to False, passing references or pointers into a constructor call has no effect on the analysis.
 

consider_pointer_returns

consider_pointer_returns : bool = False

Whether the return value of a function that returns a reference or pointer to its argument or to an object owned by its argument should be considered, when called on a local variable. E.g., std::string::data
 

consider_std_addressof

consider_std_addressof : bool = True

Consider a call to std::addressof as an address-taking operation.
 

exclude_from_pointer_to_const_param_check

exclude_from_pointer_to_const_param_check : set[bauhaus.analysis.config.QualifiedName] = {'__builtin_object_size'}

Names of routines whose parameters should be excluded from the check for passing uninitialized variables by pointer as parameter with pointer-to-const type.
 

functions_with_ignored_deallocators

functions_with_ignored_deallocators : set[str] = set()

Set of functions (given by their qualified name) where all deallocators are ignored. For these functions, the check will never report a use-after-free. It will also assume that these functions never create freed pointers, neither by return value, out param, nor by modifying global state.
 

report_freed_this_at_call

report_freed_this_at_call : bool = False

This option controls findings when a freed pointer is used in C++ to call a non-static member function. When set to true, the use at the call is directly reported. When false, the analysis waits for an actual dereference (of the this-pointer then) inside the callee, and only reports those.
 

report_read_pointer_args_in_calls_to_undefined

report_read_pointer_args_in_calls_to_undefined : bool = True

Report when freed pointers are passed to undefined (external) functions.
 

resources

resources

Type: set[str]

Default: {'C++ArrayHeapMemory', 'C++HeapMemory', 'CudaAsyncMemory', 'CudaDeviceMemory', 'CudaDriverAsyncMemory', 'CudaHostMemory', 'CudaManagedMemory', 'FileHandle', 'HeapMemory', 'UniquePtrHeapMemory'}

Set of resources to be checked (selection of rules in the Resources group).
 

restrict_to_functions

restrict_to_functions : set[str] = set()

If not empty, check only for leaks that initially occur through the functions given by qualified name here.
 

track_conditional_initialization

track_conditional_initialization : bool = True

Whether higher precision should be used to eliminate cases where the initialization and the access are controlled by conditions in a way that the variable access is only executed when the initialization was executed. Requires more memory and runtime but can eliminate some false positives.
 

use_semantic_analysis

use_semantic_analysis : bool = True

When enabled, use semantic analysis. Otherwise filter uninitialized variable messages from the compiler.
 

witness_paths

witness_paths : bool = True

Whether witness paths should be determined and included in the issue.
 

writing_into_pointer_to_const

writing_into_pointer_to_const

Type: dict[bauhaus.analysis.config.QualifiedName, int]

Default:

{
   'cudaMemcpyToSymbol': 0
}
Names of routines (mapping to parameter index, starting at 0) having a parameter declared as pointer-to-const yet they are still writing into the pointee.