The vulnerability as expressed in ISO/IEC IS 24772-1:2024 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 [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{
operator=(nta const &) & = default; // lvalue-ref qualified
nta &
};
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};
// Any further access of bad_ref dangles
bad_ref = local;
} }
A class type with pointer-like members can lead to dangling when those members refer to constructor arguments.
struct X{
int const &rci;
int i):rci{i}{} // No lifetime extension of parameter object by binding reference to it
X( };
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
A C++-specific way of causing dangling_references to the stack is by means of the placement_new
construct (see clause 7.4).
To avoid the vulnerability or mitigate its ill effects, C++ software developers can:
Prefer value types, pass-by-value, and return-by-value over pointer-like types and passing or returning pointer-like objects.
Prohibit the following uses of a pointer-like value referring to a variable with automatic storage duration or referring to a temporary object:
Avoid capturing by reference in a lambda that will be used non-locally, that is
Avoid relying on lifetime extension of temporaries by binding them to named references; use (local) variables instead.
Show that the range-based for dangling vulnerability does not apply or take steps to avoid it, for example, use a variable representing the range and not an expression that yields a reference to a temporary.
Consider making member functions ref-qualified, that return pointer-like types to members or objects managed by the class.
If required, provide an rvalue-ref-qualified overload that either returns a copy by value, or is defined as =delete
to prevent calling it on a temporary.
Perform lifetime analysis when using a pointer-like object beyond the expression that created it and ensure it is not used in a dangling situation.
Employ static and dynamic analysis tools to detect dangling pointer-like objects.