parts/6.33.DanglingReferencesToStackFrames-DCM.md

6.33 Dangling References to Stack Frames [DCM]

6.33.1 Applicability to language

The vulnerability as expressed in ISO/IEC TR 24772-1:2019 and ISO/IEC TR 24772-3:2020 C exists in C++ by indirect access to variables with automatic storage duration or to temporary objects.

The lifetime model of C++ makes it undefined behaviour (see subclause [EWF]) to access an object outside of its lifetime. This results in undefined behavior, when an object access is attempted after its destruction. C++ provides a rich set of pointer-like types whose values may refer to temporaries or variables with automatic storage duration and can dangle (see Subclause [XYK]).

A C++ class type with a pointer-like member will behave as a pointer-like type, unless the class itself manages the lifetime of the object referred to by its member.

In general, any caller storing the pointer-like object returned from a function call risks dangling; such situations require thorough lifetime analysis to ensure that access via the pointer-like object doesn’t dangle.

The efficiency of return-by-value, copy-elision, and move-semantics as specified by C++ reduces the incentive to return a pointer-like type from a function or bind a temporary to a local reference.

The lifetime of a temporary object usually ends at the end of a full expression where it was created. Dangling can occur, when an expression including the creation of a temporary object results in a pointer-like value referring to the temporary object. For example, std::max returns the const-reference given as parameter, which might be bound to a temporary argument:

int g(int i){
    int const &m = std::max(i,20);
    return m; // access dangling reference to temporary if i < 20
}

In some situations binding a reference to a temporary will extend the lifetime of the temporary.

This lifetime extension is not transitive across function calls, therefore, changes in the code, such as replacing a data member access with an accessor member function, can silently lead to dangling in such lifetime-extension situations.

struct A{
    int a;
    int const &getA(){return a;}
};
void h(){
    int && ra = A{42}.a; // lifetime extended
    int const & cra = A{42}.getA(); // dangling
}

The range-based for statement contains a subtle situation with lifetime extension.
A temporary in the range expression will have its lifetime extended, unless it is accessed indirectly. As a mitigation C++ permits the creation of a variable for such situations that has the scope of the range-for loop, as shown in the following example:

extern std::vector<std::string> make(); // creates a vector

for(char c : make().front()) { // attempt to iterate over first string in vector
   // vector and thus contained string is already destroyed before C++23
}

for(auto range = make().front(); char c : range){ // mitigation, create a variable for the range to be iterated over
  // string to be iterated over remaings valid throughout
}

This issue is no longer present from C++23 onwards, as temporaries within the for-range-initializer are lifetime extended until the end of the statement.

Returning a pointer-like object from a function is problematic, if the return value refers to a temporary or an object with automatic storage duration, either directly or indirectly. The following example show different situations with this problem:

int *bad_pointer() {
  int a = 0;
  return &a;      // Returning the address of a local variable "a".
 }

int& bad_reference(int b) {
  return b;      // Returning a reference to a local (parameter) variable "b" .
 }

std::array<int,3>::iterator bad_iterator() {
  std::array<int,3> c = { 1, 2, 3 };
  return c.begin();
  // Returning an iterator that refers the first element of the local array "c".
}

auto bad_lambda() {
    int d = 0;
    return [&] { return d = 1; };
    // Returning a lambda that captures local variable "d" by reference
    // and thus indirectly returns a reference to the local variable
}
decltype(auto) bad_assign(){ // deduces: std::string &
    return std::string{} = "hello\n"s;
    // Returns reference to temporary object returned from copy-assignment operator
}

void erroneous_use() {
  std::cout << *bad_pointer();
  std::cout << bad_reference(42);
  std::cout << *bad_iterator();
  std::cout << bad_lambda()();
  std::cout << bad_assign();
 }

In the examples above, the function bad_assign returns a std::string & that was itself returned from the copy-assignement operator of std::string. Such an assignement operator (including the compiler-provied ones) can be called with a temporary as its left-hand operand, because it is an unqualified member function (for historical reasons).

Dangling may occur by calling a member function on a temporary that returns a pointer-like object referring to *this, a sub-object of *this, or an object managed by *this. This can be prevented by - For a non-const member function: adding an lvalue ref-qualification (&), - For a const member function: adding an lvalue ref-qualification (const &) and declaring an rvalue ref-qualified overload (&&) either defined as =delete or declared to return by value.

In the following example, class nta declares its copy assignment with lvalue ref-qualification to avoid the situation created in the example function bad_assign:

struct ta{}; // default allows assignment to temporary
struct nta{
nta & operator=(nta const &) & = default; // lvalue-ref qualified
};
ta & check_ta(){
    return ta{} = ta{}; // returns dangling reference to temporary
}
nta & check_nta(){
    return nta{} = nta{}; // won't compile
};

Referring to a variable with automatic storage duration from a pointer-like variable with static or thead-local storage duration usually means dangling, when the indirect access happens.

int const init{42};
std::reference_wrapper<int const> bad_ref = init; // static storage duration
void bad_global_assign(){
    if (bad_ref == 42){ // undefined behavior on 2nd call
       int local{44};
       bad_ref = local; // Any further access of bad_ref dangles
    }
}

A class type with pointer-like members can lead to dangling when those members refer to constructor arguments.

struct X{
int const &rci;
X(int i):rci{i}{} // No lifetime extension of parameter object by binding reference to it
};

Similarly, in the following example the vulnerability exists in the conversion operator string_view() of std::string, that returns a pointer-like type from a member function callable on a temporary object.

std::string_view bad_var("a string"s); // dangling view on temporary string object

6.33.2 Avoidance mechanisms for language users

To avoid the vulnerability or mitigate its ill effects, C++ software developers can: