Ray's Knowledge Base

A range-for over a member of a temporary reads freed memory

PitfallVerified 28 Sep 2026Holds anywhere
Pitfall. The symptom, what causes it, and the fix that was run and seen to work.

Symptom#

Clang warns, and the loop sees wrong or missing elements:

warning: object backing the pointer will be destroyed at the end of the full-expression [-Wdangling-gsl]

The code looked like this:

for (const auto& e : ix.manifest().at(0).extensions)
    if (e.name == EXT) return e;

In the observed case the loop did not find an element that the container did hold, and four doctest cases failed with REQUIRE( e ) is NOT correct!.

The same bug can give no warning at all. With simdjson, this loop compiled clean and passed every normal test, but AddressSanitizer stopped it:

for (auto el : v.element().get_array().value_unsafe()) { ... }
ERROR: AddressSanitizer: stack-use-after-scope ... in simdjson::internal::tape_ref::usable() const

Cause#

A range-for keeps alive only the object that the range expression returns directly. Here that is a reference into a temporary: the std::vector that manifest() returned, or the simdjson_result<dom::array> that get_array() returned (value_unsafe() on an rvalue result returns a reference into it). The temporary is destroyed at the end of the full expression, before the loop runs, so the loop reads freed memory. Lifetime extension does not reach through a member access, at() or value_unsafe().

Fix#

Store the temporary in a named local first, then loop over the member:

const auto manifest = ix.manifest();
for (const auto& e : manifest.at(0).extensions)
    if (e.name == EXT) return e;

const simdjson::dom::array arr = v.get_array().value_unsafe();
for (auto el : arr) { ... }

C++23 extends the lifetime of temporaries in the range initializer (P2718), but do not rely on it in C++20 code. Treat -Wdangling-gsl as an error to fix, not as noise, and search for the pattern (for (.*:.*value_unsafe())) because the compiler does not always warn.

Evidence#

  • Clang on macOS (Apple clang, C++20) printed the warning for the manifest loop in tests/utilities/plugins/test_plugin_index_extension.cpp (dftracer-utils). With the loop as written, 4 of 5 test cases failed at REQUIRE( e ); with the named local all 5 passed and the warning was gone.
  • The simdjson loop was in the duql evaluator's any() check and in append_canonical_json (src/dftracer/utils/json/canonical.cpp), in dftracer-utils. No compiler warning; the asan-ubsan build of duql/test_evaluator and duql/test_differential failed with stack-use-after-scope. After binding the array or object to a named local, the same tests passed under ASan and UBSan (2026-09-28).