Skip to content
VLSI Mentor

C++ Part I · Module 25 · Debugging, Undefined Behavior, and Performance

Lifetime Failures: Dangling, Use-After-Free and Invalidated Iterators

A dangling pointer, a use after free and an invalidated iterator are usually taught as three bugs. They are one bug with three locations: an object's lifetime ended and something still refers to it. Recognising that is practical rather than tidy, because the reasoning that finds all three is the same reasoning — who owns this object, when does that owner's lifetime end, and does anything outlive it — which is the question Modules 10 and 12 were about. The chapter measures four instances of the single bug and then measures which tools report each, and the result is the most useful thing in it: the compiler's warnings and the address sanitiser are almost exactly complementary, each catching the cases the other is silent about, and one of the four is reported by no run-time configuration available on this machine. That pattern is why the answer to this bug class is a build configuration with both, plus the ownership discipline that makes the dangling reference impossible to write.

Advanced18 min readC++Part IFundamentalsDebugging

Part I · Module 25 · Chapter 25.4 · Lifetime Failures: Dangling, Use-After-Free and Invalidated Iterators

1. One Bug, Three Locations

An object's lifetime begins when its initialisation completes and ends when its destructor starts — or, for an object with dynamic storage, when the storage is deallocated. Accessing an object outside its lifetime is undefined behaviour, and that single rule covers all of this chapter:

the name it is givenwhere the object livedwhat ended its lifetime
dangling pointer / referencethe stackthe enclosing scope exited
use after freethe heapdelete or free
invalidated iteratora container's bufferthe container reallocated or erased
dangling string_view / c_stra temporary or a localthe owner was destroyed

Nothing about the mechanism differs. The pointer or iterator still holds an address; the storage at that address is no longer the object it referred to; reading it reads whatever is there now.

What differs between them is who was responsible for the lifetime, and that is what makes them feel like separate bugs. In the stack case it is the scope. In the heap case it is whoever called delete. In the container case it is a method you called on the container, which is the one people find surprising — push_back ends the lifetime of every element at its old address, and the standard says so.

Chapter 12's ownership question is therefore the diagnostic question for all four: who owns this, and does anything outlive the owner?

2. The Four Instances

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 1. a dangling pointer to an automatic object
const int* danglingLocal() { int local = 0x1234; return &local; }

// 2. use after free
int useAfterFree() { int* p = new int(7); delete p; return *p; }

// 3. an invalidated iterator
int invalidatedIterator()
{
    std::vector<int> v{1,2,3};
    auto it = v.begin();
    v.push_back(4);                  // may reallocate -> it dangles
    return *it;
}

// 4. a dangling reference into a destroyed string
const char* danglingTemp() { std::string s = "SLVERR"; return s.c_str(); }

Run plainly at -O1, all four produce output and the process exits 0. The values they print are garbage and are not reproducible between runs, so there is no figure to quote — which is itself the first thing to notice about this bug class. Chapter 25.2 §3's uninitialised values were stable; these are not, because what occupies freed heap or a dead frame depends on everything the program has done since.

3. Which Tools Report Which

The measurement that organises the chapter. Each case isolated into its own program, so no abort masks a later one:

-Wall -Wextraplain runASan (default)ASan + detect_stack_use_after_return=1UBSan
1. dangling localCATCHsilent, exit 0missCATCH — stack-use-after-return, exit 134miss
2. use after free—silent, exit 0CATCH — heap-use-after-free, exit 134CATCHmiss
3. invalidated iterator—silent, exit 0CATCH — heap-use-after-free, exit 134CATCHmiss
4. dangling c_strCATCHsilent, exit 0missmissmiss

The compile-time half is exactly two warnings, and both are the same diagnostic:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
three.cpp:10:13: warning: address of stack memory associated with local variable 'local' returned [-Wreturn-stack-address]
three.cpp:34:12: warning: address of stack memory associated with local variable 's' returned [-Wreturn-stack-address]

-Wall -Wextra caught the two stack cases and said nothing about the two heap cases. ASan, with its default options, caught the two heap cases and nothing about the two stack cases. The two detectors partition the four bugs between them with no overlap.

Three details inside that table matter.

detect_stack_use_after_return is off by default, and turning it on is what makes ASan catch case 1:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: AddressSanitizer: stack-use-after-return

It is off because it is expensive — it has to keep frames alive after they return — which is a defensible default and a bad one for a debug build you are already running sanitised.

Case 4 is caught by nothing at run time. Not by ASan with either setting, not by UBSan. Its only detector on this machine is the compile-time warning, which is why the warning's presence in your build is not optional.

UBSan caught none of the four. It is not a memory tool; Chapter 25.3 §4's callout explains what each sanitiser's source of truth is, and lifetime is not among UBSan's.

4. Reading the Report You Get

Case 3 is worth a section on its own, because of what ASan calls it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: AddressSanitizer: heap-use-after-free

The word "iterator" does not appear. ASan reports the mechanism — a read of freed heap memory — because that is what it observed. The freed memory is the vector's old buffer, which push_back released after copying the elements to a larger one.

So the report is accurate and the name is one level below the bug. A developer who reads heap-use-after-free and goes looking for a delete will not find one, because there is no delete in the source. The free was inside vector::push_back.

That generalises: a sanitiser names what it saw, not what you did wrong, and the gap between the two is where the diagnosis happens. The full ASan report helps here — it includes the stack of the free as well as the stack of the access, and the free's stack will show push_back, which is the sentence that identifies the bug. Chapter 25.7 §4 is the technique for reading a trace whose frames are all library code.

5. The Invalidation Rules You Have to Know

Case 3 cannot be found by a tool in general — Chapter 20.3 §5 measured an erase-in-a-loop that was silent under every tool, because the stale iterator stayed inside a live allocation below size(). So the rules themselves are load-bearing knowledge:

operationwhat it invalidates
vector::push_back, insert, resize, reserveeverything, if it reallocates
vector::eraseiterators at and after the erased position
vector::cleareverything
deque::push_back / push_frontall iterators; references stay valid
list / forward_list insertnothing
list::eraseonly the erased element's iterator
map / set insertnothing
map / set eraseonly the erased element's
unordered_map insertall iterators on rehash; references stay valid
string append / operator+=everything, if it reallocates

Two things in that table are worth isolating. The node-based containers — list, map, set — invalidate almost nothing, which is a real reason to prefer them when you must hold references across mutation, and Chapter 19.4 measured what they cost to traverse. And deque and unordered_map draw a distinction between iterators and references that the others do not.

The practical rule that follows is simpler than the table: do not hold an iterator across an operation that modifies the container. Re-acquire it, or use an index, or use the erase-remove idiom, or collect what you want to change and apply it afterwards.

6. Localising the Cause, Not the Symptom

The frozen purpose asks for this specifically. When a lifetime bug surfaces, the symptom is a wrong value or a crash at the use site, and the cause is at the site where the lifetime ended — which may be in another function, another file, or inside a library.

Four moves, in the order that pays off:

Read the whole sanitiser report, not the first line. ASan prints the access stack, then the allocation stack, then the deallocation stack. The deallocation stack is the cause; the access stack is the symptom. Most of the diagnosis is in the third block, which people skip.

Ask Chapter 12's ownership question at the site that frees. Who owns this object, what is that owner's lifetime, and is there anything whose lifetime exceeds it that holds a reference? If the owner is a scope, the answer is in the braces; if it is a unique_ptr, in its member or variable; if it is a container, in the invalidation table.

Check the return types on the path. A function returning const char*, string_view, span, a reference, or an iterator is a function returning a non-owning view, and each one is a candidate for case 4. A grep for those return types in the call chain is often faster than a debugger.

Use a watchpoint when the object is overwritten rather than freed. Chapter 25.5 §5 — the watchpoint reports the instruction that wrote, which tells you which other lifetime now occupies that storage.

7. Removing the Class

Return by value, or return an owner. The first fix for case 1 and case 4 is to stop returning a view of something that is about to die:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
std::string respName();              // owns its result
const char* respName();              // only safe if it points to a string literal
std::string_view respName();         // safe ONLY if the referent outlives the caller

A string_view or span return is correct when the referent is a literal or something the caller already owns, and is a bug when it views a local. That distinction is not in the type, which is why Chapter 21.7's strong types and a comment naming the owner are both worth having.

Give every heap object an owner. std::unique_ptr makes case 2 almost unwriteable: there is no delete to get wrong, and the double-free and use-after-free shapes both require you to have extracted a raw pointer deliberately. Chapter 14.2 is the mechanism and Chapter 22.4 §2 measured what it prevents.

Never store a raw pointer or reference you did not establish an outlives relationship for. A member holding Device* is a claim that the Device outlives this object, and the class should say so — in a comment at minimum, and by holding a shared_ptr if the claim cannot be guaranteed.

Do not hold an iterator across a mutation. §5's rule. Where you must, use an index into a vector (stable across reallocation, though not across erase of an earlier element) or a node-based container.

Prefer the erase-remove idiom and std::erase_if over an index-and-erase loop, because they cannot be written with a stale iterator:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
std::erase_if(v, [](const Txn& t) { return t.resp == Resp::Okay; });   // C++20

Build the debug configuration with both detectors. -Wall -Wextra at compile time and -fsanitize=address with detect_stack_use_after_return=1 at run time, which per §3 is what covers three of the four.

8. The Table

the questionthe answermeasured
are these three bugsone bug — use outside an object's lifetime§1
what ends a container element's lifetimean operation you called on the container§5
do the plain runs report anythingno — all four silent, exit 0§2
are the garbage values reproducibleno, unlike Chapter 25.2's§2
what does -Wall -Wextra catchthe two stack cases, via -Wreturn-stack-address§3
what does ASan catch by defaultthe two heap cases, as heap-use-after-free§3
what catches the dangling localASan with detect_stack_use_after_return=1§3
what catches the dangling c_stronly the compile warning§3
does UBSan catch anynone of the four§3
what does ASan call an invalidated iteratorheap-use-after-free — the mechanism§4
where is the cause in an ASan reportthe deallocation stack, the third block§6
why is coverage partialstorage class, and even string length§6 callout

9. What to Actually Do

  • Ask Chapter 12's ownership question: who owns this, and does anything outlive the owner? That one question finds all four shapes.
  • Keep -Wall -Wextra on, because for two of the four it is the only detector — and for case 4 the only one at all.
  • Run ASan with detect_stack_use_after_return=1 in the debug configuration; the default omits a whole storage class.
  • Read the deallocation stack in an ASan report, not just the first line. The cause is in the third block.
  • Return by value or return an owner. A const char*, string_view, span, reference or iterator return is a non-owning view and needs a stated outlives relationship.
  • Use unique_ptr for every heap object, which makes case 2 nearly unwriteable.
  • Never hold an iterator across a container mutation; re-acquire, use an index, or use a node-based container.
  • Prefer std::erase_if and erase-remove to index-and-erase loops.
  • Treat heap-use-after-free with no delete in your code as an invalidated iterator until proven otherwise.
  • Document every raw pointer member with the lifetime claim it is making.

10. Mistakes Worth Making Once

Searching for a delete after an ASan heap-use-after-free. The free was inside push_back.

Reading only the first line of an ASan report. The deallocation stack is the cause.

Assuming ASan covers lifetime bugs. It missed both stack cases with default options.

Assuming the compiler would have warned. It said nothing about either heap case.

Returning s.c_str() from a function. Caught at compile time here, and by nothing at run time — and whether ASan could see it depends on the string's length.

Holding v.begin() across a push_back. The standard says it is invalid whether or not it reallocates this time.

Testing with short strings and shipping with long ones. Chapter 25.9 §3 measures the 22-character threshold that changes the storage class.

A raw pointer member with no stated lifetime claim. Nothing checks it and the claim is invisible.

11. Interview Relevance

The frozen curriculum marks this chapter Critical. The best opening is to decline the premise of the question: a dangling pointer, a use after free and an invalidated iterator are one bug — an object used outside its lifetime — differing only in who was responsible for ending it, which is the scope, the delete, or a method you called on the container.

The measurement to quote is the partition: -Wall -Wextra caught exactly the two stack cases via -Wreturn-stack-address, ASan with default options caught exactly the two heap cases as heap-use-after-free, UBSan caught none, and detect_stack_use_after_return=1 is what adds the dangling local. If you can say why — the compiler sees locally decidable escapes, ASan sees allocator-mediated lifetimes — you have explained the partition rather than memorised it.

The detail that lands hardest is case 4: returning s.c_str() is caught by nothing at run time, and whether ASan could even see it depends on whether the string exceeds the 22-character small-string buffer — so the detectability of one source line depends on a data value.

12. What You Should Be Able to Say Now

  • That the three names describe one bug, and that the difference between them is who ended the lifetime.
  • That a container operation you called is what ends an element's lifetime, and the invalidation rules for the common containers.
  • That all four instances ran silently with exit 0, and that their garbage values are not reproducible — unlike Chapter 25.2's.
  • The partition: -Wall -Wextra → the two stack cases; ASan default → the two heap cases; UBSan → none.
  • That detect_stack_use_after_return=1 is off by default and is what catches the dangling local.
  • That the dangling c_str has no run-time detector here, and that whether ASan could see it depends on the string's length crossing the 22-character inline buffer.
  • Why the partition exists: the compiler sees escapes decidable within one function; ASan sees lifetimes the allocator mediates.
  • That ASan reports an invalidated iterator as heap-use-after-free — the mechanism, not the bug — and that the deallocation stack is where the cause is.
  • That the remedy is ownership discipline: return values or owners, unique_ptr, no iterators across mutations, std::erase_if.

13. Exercises

Exercise 1 — the partition. Isolate each of the four cases into its own program and reproduce the §3 table, checking exit status as well as output.

Exercise 2 — the missing option. Run case 1 under ASan with and without detect_stack_use_after_return=1. Then measure what the option costs on a larger program.

Exercise 3 — the length threshold. Make case 4's string 30 characters instead of 6 and re-run under ASan. Explain the change using the small-string buffer.

Exercise 4 — the real report. Run case 3 under ASan and read the entire report. Find push_back in the deallocation stack and explain why there is no delete in your source.

Exercise 5 — invalidation. For each row of §5's table, write a program that holds an iterator across the operation and see which configurations report it. Chapter 20.3 §5's erase-in-a-loop is the one to end on.

Exercise 6 — remove the class. Rewrite all four functions so the bug cannot be expressed: return by value, use unique_ptr, use an index, and use std::erase_if.

14. Summary

  • A dangling pointer, a use after free and an invalidated iterator are one bug — an object accessed outside its lifetime — distinguished only by who ended the lifetime: a scope, a delete, or a method you called on a container.
  • All four measured instances ran silently and exited 0, printing garbage that was not reproducible between runs.
  • The detectors partition the class with no overlap. -Wall -Wextra caught exactly the two stack cases, both as -Wreturn-stack-address; ASan with default options caught exactly the two heap cases, both as heap-use-after-free with exit 134; UBSan caught none.
  • ASAN_OPTIONS=detect_stack_use_after_return=1 adds the dangling local as stack-use-after-return. It is off by default, so a plain sanitised build omits a storage class.
  • The dangling c_str is caught by no run-time configuration here — only by the compile-time warning — and whether ASan could see it depends on whether the string exceeds libc++'s 22-character inline buffer. One source line, detectability decided by a data value.
  • The partition is structural: the compiler sees escapes that are decidable inside one function; ASan sees lifetimes the allocator mediates. Neither can cover the other's half.
  • ASan names the mechanism, not the bug — an invalidated iterator is reported as heap-use-after-free, and the free was inside push_back. The deallocation stack in the report is the cause; the first line is only the symptom.
  • So the answer is ownership discipline, not detection: return values or owners, unique_ptr for every heap object, a stated outlives relationship for every raw pointer member, no iterator held across a mutation, and std::erase_if instead of index-and-erase.

Chapter 25.5 turns from the bugs to the method, and replaces re-reading the source with a hypothesis and a debugger.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.