C++ Part I · Module 10 · Dynamic Memory and Object Lifetime
Double Delete and Use-After-Free
These two bugs are near-twins. Both involve an object whose lifetime has already ended: one reads or writes it anyway, the other tries to end it a second time. Both get zero help from the compiler. And the reason they need a chapter rather than a paragraph is not that they are destructive — it is that they are inconsistent about being destructive, and the inconsistency runs in opposite directions. Measured here on the same machine with the same compiler, the double delete killed the process at the instant it happened, with a signal and a non-zero exit code, which is the best possible outcome for a bug because it gets fixed that afternoon. The use-after-free did nothing of the kind. It read the freed object and produced a small, plausible, entirely wrong integer, then wrote to the freed object and reported success, then exited zero. No warning, no crash, no sanitizer in the default build, and a value that would pass most sanity checks a reviewer might add. That is the chapter's subject: not what these bugs are, which is simple, but why the quiet one is the expensive one, and what the freed memory actually contained — which turned out to be neither the old value nor garbage, for a reason worth understanding.
Advanced17 min readC++Part IFundamentalsMemory
Part I · Module 10 · Chapter 10.7 · Double Delete and Use-After-Free
1. Two Bugs, One Cause
Chapter 10.5 supplied the definitions:
Use-after-free — an access to an object whose lifetime has ended.
Double delete — destruction begun on an object whose lifetime has already ended.
Both follow from one fact, stated in Chapter 10.3 §4 and again in Chapter 10.5 §4:
delete pends the object's lifetime and does nothing at all top.
The pointer still holds the same address. Nothing marks it stale. The object is gone and every pointer that referred to it looks exactly as it did before.
2. Why They Are Hard to Avoid in Practice
The single-function versions in this chapter look too obvious to write. The real versions are not:
Two pointers to one object. One is deleted; the other is used. Neither line is wrong by itself.
An object in two containers. Both are cleared.
A deleted object whose address was stored elsewhere — a cache, an observer list, a scoreboard entry.
An exception during cleanup causing a release path to run twice.
And a copied handle. A struct holding a raw pointer, copied by value: two objects now hold the same address and both destructors will run. Chapter 13.4 — Shallow Copy, Deep Copy and the Double Free That Follows — is where this becomes its own subject, and it is the most common source of double deletes in real code.
In every case the delete and the use are in different places, often different files.
3. Use-After-Free, Measured
#include <cstdio>
int main()
{
int* p = new int(42);
std::printf("before delete: *p = %d\n", *p);
delete p;
std::printf("after delete: *p = %d <-- reading freed memory\n", *p);
*p = 99;
std::printf("after write : *p = %d <-- writing freed memory\n", *p);
return 0;
}Compiler warnings: 0. Measured output:
before delete: *p = 42
after delete: *p = 5 <-- reading freed memory
after write : *p = 99 <-- writing freed memory
exit=0Read the second line again.
4. It Printed 5
Not 42. Not a crash. Not an obviously absurd number like -273071872, which is what Chapter 10.2 §7's released stack frame produced. Five.
The explanation is the chapter's central mechanism. When delete released the block, the allocator did not erase it — Chapter 10.5 §6 established that storage outliving an object is normal. What it did was write its own bookkeeping into those bytes, because a free block is tracked by data stored inside the block itself. The first four bytes of a freed block now hold part of a free-list structure, and interpreted as an int that happened to be 5.
So the read did not fail and did not return the old value. It returned the allocator's internal state, reinterpreted as your type.
Three consequences, and they are why this chapter is rated Critical:
No crash, because the memory is still mapped. The process still owns that page; the block is merely available for reuse. Reading it is a perfectly ordinary memory access as far as the hardware is concerned.
A plausible value, which is worse than an absurd one. 5 passes a range check. 5 could be a valid count, index, or state. A reviewer adding assert(value >= 0) would be satisfied. Chapter 10.2 §7's garbage value at least announced itself.
And the write "succeeded". *p = 99 wrote 99 into the allocator's free-list bookkeeping and then read it back as 99, which is exactly what a working program looks like. The real damage — a corrupted free list — surfaces at some later, unrelated allocation, which is the kind of bug that gets investigated for days in the wrong file.
That last point is the general form: a use-after-free write does its damage somewhere else, later. The symptom has no connection to the cause.
5. Double Delete, Measured
std::setvbuf(stdout, nullptr, _IONBF, 0); // per Chapter 10.4 §5
int* p = new int(42);
delete p;
std::printf("first delete ok\n");
delete p;
std::printf("second delete ok\n");Compiler warnings: 0. Measured:
first delete ok
exit=133second delete ok never prints. Exit 133 is 128 + 5 — SIGTRAP. The allocator detected that the block being released was already free and aborted the process on the spot.
(The unbuffered-output call is there because of the mistake Chapter 10.4 §5 records: a killed process discards buffered output, and the program appears to have printed nothing.)
6. The Asymmetry
| Compiler | Plain run | Exit | ASan | |
|---|---|---|---|---|
| use-after-free | 0 warnings | printed 5, then 99 | 0 | heap-use-after-free uaf.cpp:7 |
| double delete | 0 warnings | first delete ok, then died | 133 | double-free dd.cpp:2 |
Same category of error, opposite observable behaviour — and the one that behaves itself is the dangerous one.
A bug that aborts the process is found immediately, by whoever ran it, on the day it was written. A bug that prints a plausible number and exits zero is found when a customer reports wrong results, or never. Chapter 10.4 §7 reached the identical conclusion comparing the two delete/delete[] mismatch directions, and Chapter 10.2 §7 reached it comparing optimisation levels. This is the third time in one module that the quiet failure is the expensive one, and it is the module's most transferable lesson.
Under AddressSanitizer both are caught, and the use-after-free report is worth reading in full because of what it gives you:
==73459==ERROR: AddressSanitizer: heap-use-after-free on address 0x6020000000d0
freed by thread T0 here:
previously allocated by thread T0 here:
SUMMARY: AddressSanitizer: heap-use-after-free uaf.cpp:7 in mainThree locations: where it was accessed, where it was freed, and where it was originally allocated. For the realistic version of this bug — where those three are in different files — that is the entire investigation done for you. ASan achieves it by not returning freed blocks to use immediately, holding them in quarantine so an access is detectable; that is also why it costs memory and speed and is a test-build tool rather than a production one.
7. Reducing the Damage
These are mitigations, not solutions. Chapter 10.9's answer is the solution.
Set the pointer to nullptr after deleting, when it stays in scope:
delete p;
p = nullptr;This is worth doing and it is worth knowing exactly why it works, which rests on two earlier measurements. Chapter 10.3 §8 verified that delete nullptr is a well-defined no-op — the instrumented operator delete was not called at all — so a second delete p after this becomes harmless rather than fatal. And Chapter 8.3 measured that dereferencing null is a reliable immediate crash (SIGSEGV), so a later use-after-free becomes §5's loud failure instead of §4's silent one.
So the habit converts both bugs into their survivable forms. What it cannot do is help with any other pointer to the same object, which is the realistic case in §2 — and that is precisely why it is a mitigation.
Build tests with AddressSanitizer. It catches both, with three locations.
Never store a raw pointer to something you do not own. Chapter 9.6 §5 measured the string_view version of exactly this.
Be suspicious of a copied handle. Two objects holding one address means two destructors; Chapter 13.4 is the subject.
And treat a plausible-but-wrong value as a lifetime suspect. 5 where 42 was expected is the signature.
8. What to Actually Do
Set a pointer to nullptr after delete when it remains in scope — it converts both bugs into loud ones.
Build and run tests under AddressSanitizer. It is the only thing here that caught both.
Treat a plausible-but-wrong value as a lifetime suspect. The measured signature is 42 becoming 5.
Never keep a raw pointer to an object you do not own.
Audit any struct holding a raw pointer for copying — two holders, two destructors (Chapter 13.4).
Never conclude correctness from exit 0. The use-after-free exited 0 having done two undefined things.
Expect a use-after-free write to surface elsewhere. It corrupts allocator state; the symptom is unrelated to the cause.
And prefer the structural fix. Chapter 10.9's question, then Module 14's types.
9. Mistakes Worth Making Once
Expecting a use-after-free to crash. Measured: exit 0.
Expecting freed memory to hold the old value. Measured: 42 became 5.
Expecting garbage. It was the allocator's own bookkeeping — plausible, small, wrong.
Believing a successful write means valid memory. *p = 99 read back 99 and corrupted a free list.
Guarding with if (p) after delete. p is unchanged and non-null.
Reading "no output" from the aborted double delete as "nothing ran". Chapter 10.4 §5's buffering lesson.
Thinking nulling the pointer fixes it. It fixes that pointer.
Copying a struct that holds a raw pointer. Two destructors, one object.
Trusting a default build. Both bugs needed ASan to be named at all.
10. Interview Relevance
"What is a use-after-free, and what happens?" The strong answer refuses the simple version: the memory is still mapped so there is usually no crash, the allocator has written free-list bookkeeping into the block, so the read returns a plausible wrong value — measured here as 42 becoming 5 — and the program exits zero. Candidates who say "it crashes" or "you get garbage" are wrong in both directions.
"Why is a double delete usually easier to deal with than a use-after-free?" Because it tends to abort immediately — measured, SIGTRAP, exit 133 — so it is found the day it is written, whereas the use-after-free ships. The asymmetry is the insight being probed.
"Does the compiler catch these?" No. Zero warnings for both.
"Why set a pointer to null after deleting it?" Because delete nullptr is a defined no-op, so a double delete becomes harmless, and dereferencing null crashes reliably, so a later use becomes loud. Knowing both halves is the complete answer — and so is noting it does nothing for other pointers to the same object.
"How does AddressSanitizer detect it?" By quarantining freed blocks instead of reusing them immediately, which is why it reports the access, the free and the original allocation, and why it is not a production tool.
11. What You Should Be Able to Say Now
- Both bugs follow from one fact:
delete pends the object and leavespuntouched. - Measured, use-after-free: 0 warnings, read gave 5 not 42, write "succeeded", exit 0.
- 5 is the allocator's free-list bookkeeping reinterpreted as an
int— not the old value, not garbage. - No crash because the memory is still mapped; the block is merely available.
- A plausible value is worse than an absurd one — it passes review and sanity checks.
- A use-after-free write corrupts allocator state and surfaces later, somewhere unrelated.
- Measured, double delete: 0 warnings,
first delete ok, then SIGTRAP, exit 133. - The asymmetry: the quiet bug is the expensive one — the module's third instance of that lesson.
- ASan catches both and gives three locations: access, free, original allocation.
- It does so by quarantining freed blocks, which is why it costs memory and is a test tool.
- Nulling after
deleteworks becausedelete nullptris a no-op (Chapter 10.3 §8) and null dereference crashes (Chapter 8.3) — but only for that pointer. - A copied struct holding a raw pointer gives two destructors for one object.
- The correctness of a
deletecannot be determined from the function it is in.
12. Exercises
Exercise 1 — predict, then explain. For int* p = new int(7); delete p; std::printf("%d", *p); say what is most likely printed and why it is neither 7 nor a crash.
Exercise 2 — the two outcomes. Explain why a double delete typically aborts while a use-after-free typically does not, in terms of what the allocator can detect.
Exercise 3 — why 5 is worse. Argue why a plausible wrong value is more dangerous than -273071872, referring to Chapter 10.2 §7.
Exercise 4 — the mitigation's limit. Show a two-pointer case where p = nullptr; after delete p; does not help, and say which chapter's fix does.
Exercise 5 — read the report. Given ASan's three locations, describe how you would use each to investigate a use-after-free spanning three files.
Exercise 6 — the copied handle. Write a four-line struct holding a raw pointer whose copy causes a double delete, and mark the two destructor calls.
Exercise 7 — the five facts. List the five things the callout says you must know at a delete p, and for each, say where you would look to verify it.
13. Summary
- Both bugs come from one fact:
delete pends the object and leavespunchanged. - Measured use-after-free: 0 warnings, 42 became 5, the write "succeeded", exit 0.
- 5 was the allocator's free-list bookkeeping read as an
int; no crash because the page is still mapped. - A plausible wrong value passes review; a use-after-free write corrupts allocator state and surfaces elsewhere.
- Measured double delete: 0 warnings, then SIGTRAP, exit 133 — loud, and therefore cheap.
- The quiet failure is the expensive one — the third time this module has measured that.
- ASan catches both with three locations, by quarantining freed blocks.
- Null the pointer after
delete— defined no-op plus reliable crash — but only for that pointer. - A
delete's correctness is not determinable from its own function, which is why the fix is structural.
Every bug in this module so far has been a line that was missing or a line that ran twice, and all of them are fixable by writing the right line in the right place. The next chapter removes that comfort. It takes a function in which the new and the delete are both present, correctly paired, and exactly where a reviewer would put them — and then does to it what time does to all code: adds an early return for a condition somebody discovered later, and lets a function it calls throw. The delete is still right there in the source, and it is no longer reached. Measured: three leaks, ninety-six bytes, and zero compiler warnings, in code that a careful author wrote and a careful reviewer would pass. That is the chapter that makes the rest of this curriculum's approach to resources necessary rather than merely recommended.
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.
