The collector frees live objects
Garbage collection bugs are use-after-free in the runtime. How tricolour invariants, write barriers, and moving collectors break, and why EDR misses it.
The Garbage Collection Handbook, second edition, shipped in 2023. Richard Jones, Antony Hosking, Eliot Moss. It is an academic text on automatic memory management. It documents every major collector design running in production today. Read it from an offensive seat and it reads as a map of where memory safety breaks.
Automatic memory management removes one bug class. The manual free(). It introduces others. A collector decides when an object dies. If that decision is wrong by one reference, a live object is reclaimed or a dead slot is reused. The result is a use-after-free that no application code can be blamed for. The bug is in the runtime, not the program.
The core invariant is tricolour marking. Objects are white, grey, or black. White is unproven, unmarked, a candidate for reclamation. Grey is reached but not yet scanned. Black is reached and fully scanned. The collector’s contract is one line: at the end of marking, no black object points to a white object. Violate it and a reachable object is treated as garbage.
In a stop-the-world collector the mutator is frozen during the cycle, so the invariant holds by construction. Concurrent and incremental collectors run the mutator alongside the collector to cut pause times. Now the mutator can write a pointer to a white object into a black object after that black object was already scanned. The collector never revisits black. The white object is freed. The pointer inside the black object dangles. This is the strong tricolour invariant, and the write barrier exists to preserve it.
The write barrier is the enforcement mechanism. Dijkstra’s barrier greys the target on every pointer store. Yuasa’s snapshot-at-the-beginning barrier greys the overwritten value before the store lands. Both close the window. The handbook spends chapters on barrier correctness because the barrier is the single point where mutator and collector race. A barrier missing on one store path, elided by the JIT as an optimisation, or skipped under a fast-path allocation is a latent UAF. The collector is correct in the general case and wrong on one edge. That edge is the bug.
Moving collectors add a second failure mode. Copying and compacting designs relocate live objects and rewrite every pointer to them. A raw pointer held across a collection point, a derived or interior pointer the collector does not track, or a pinning request that gets dropped leaves a pointer aimed at the object’s old address. The memory there is now free or reallocated. JavaScript engines hit this repeatedly. V8’s Scavenge moves young-generation objects. JIT-compiled code that caches a raw object pointer across an allocation, where that allocation can trigger a scavenge, reads the stale slot on return. V8 researchers know the class as a missing GC root or stale handle bug.
Generational collection narrows the scan and widens the failure. A minor collection traces only the young generation. Old-to-young pointers must be recorded by the write barrier, usually into a card table or remembered set. If the barrier misses one old-to-young store, the minor collector never sees that reference. The young object it points to is unreached, unmarked, freed. The old object is left holding a dangling pointer into a reused nursery slot. Same use-after-free, different barrier.
Concurrent compaction pushes correctness into a read barrier. ZGC, Shenandoah, and Azul’s C4 relocate objects while the mutator runs and use load barriers plus forwarding pointers to redirect every access to the moved copy. A read barrier that fails to follow the forwarding pointer reads the old, relocated, soon-to-be-reclaimed copy. The mutator and collector now disagree on where the object lives. The handbook treats forwarding correctness as a first-class concern because a single unbarriered load reintroduces the stale-pointer condition the whole design was built to remove.
The JIT contributes its own root bug. Every safepoint must report its live object pointers in a stack map so the collector can find and update them. If a deoptimisation, an on-stack replacement, or a register-allocation edge produces a stack map that omits a live object, the collector misses a root. It frees an object the running code still holds. The fault is in compiler metadata, not in the collector, and it surfaces as the same dangling pointer.
Reference counting fails differently. The count is the object’s life. Increment and decrement must balance exactly across every alias, every exception unwind, every concurrent thread. An imbalance frees the object early or leaks it. A narrow count that overflows wraps toward zero and drops a live object. Microsoft’s scripting engines carried this class for years. CVE-2018-8174 was a use-after-free in the VBScript engine’s reference handling, exploited in the wild as Double Kill and later folded into the Rig and Fallout exploit kits. CVE-2014-1776 was the same class in Internet Explorer, used in Operation Clandestine Fox. Reference counting is the oldest automatic scheme in the book and the most exploited in the field.
Finalizers and weak references widen the surface again. A finalizer can store a dying object back into a reachable location, object resurrection, after the collector has already begun tearing down its state. Code then touches a half-dead object. Weak reference clearing races against the marking phase. Conservative collectors, Boehm-Demers-Weiser style, treat any stack or register word that looks like a pointer as a pointer. A hidden alias, masked, xored, or stored only as an offset, is invisible to the scan. The collector frees an object still referenced through that hidden pointer. The handbook names each of these conditions. Each is a documented path to a dangling pointer.
The exploit path from any of these has one shape. The attacker drives the runtime into the bad state. A crafted object graph, a specific allocation pattern, a barrier-eliding code sequence, a concurrent store timed against the marking phase. The collector frees an object the attacker still references, or the attacker forces the freed slot to be reclaimed by an object whose contents the attacker controls. The stale pointer now reads or writes a structure with an attacker-shaped layout. From a confused read or write inside the managed heap, the standard primitives follow. Length-field corruption on a typed array. Backing-store overwrite. Construction of an arbitrary read and write confined to the runtime’s address space. The handbook does not write the exploit. It describes the exact runtime behaviour an exploit depends on.
Real-world use sits in the browser and script-engine layer. Drive-by delivery lands the trigger, MITRE T1189. The malicious page or document runs script that walks the runtime into the collector bug, T1203, exploitation for client execution. Managed-heap UAF in V8, JavaScriptCore, and Blink’s Oilpan collector accounts for a large share of Chrome and Safari renderer CVEs. Many ship with an in-the-wild flag and a CVSS v3 base around 8.8 for renderer RCE before any sandbox escape. The pattern repeats every release cycle. A collector edge case, a stale pointer, a reused slot, a renderer owned.
In telemetry the GC bug is close to invisible at the instant it fires. The corruption happens inside the runtime’s own heap. No syscall. No new process. No network. EDR instruments the native allocator, the syscall boundary, and process lifecycle. It does not instrument the engine’s internal object graph. A successful managed-heap UAF produces nothing observable until the chain reaches a real syscall. A failed attempt produces a crash. Windows Error Reporting logs an access violation, 0xc0000005, in the content or renderer process. Sysmon records the process exit. The faulting address points into the heap, not into named code, so the crash carries no symbol that names the collector. Defenders see a renderer that died, not a collector that freed a live object. With page heap enabled through GFlags, or an AddressSanitizer build, the same fault resolves to a precise use-after-free report with allocation and free stacks. That instrumentation runs in research, not on endpoints. The production detection gap is the managed heap itself.
What does fire is the stage after. Renderer RCE is not the objective. The sandbox still holds. The escape and the post-exploitation steps cross the syscall boundary, T1055-class injection, child process creation, credential access, Sysmon Event ID 10 on an LSASS read. SIEM correlation catches the chain there, downstream of the memory corruption, because the corruption left no record of its own. The signal lives entirely in the activity that the freed pointer eventually enables, never in the free.
There is no patch boundary here. The handbook is not a CVE. It is the reference that explains why a whole class of CVEs exists and keeps recurring. Every concurrent collector ships a write barrier. Every moving collector ships a pointer-rewrite pass. Every reference-counted runtime ships increment and decrement on every alias. Each is correct in the common case and one missed edge from a use-after-free. The second edition documents those mechanisms in full, including the concurrent and real-time designs the first edition only sketched. For a defender that is the value. The bug class will not be patched out of existence, because automatic memory management is the feature, not the defect. Knowing where the collector’s contract breaks is the only durable position. The collector is trusted to decide when memory dies. The handbook shows every place that trust sits one reference from wrong.
Keep Reading
heap exploitationSandi Metz's 2016 essay is a bug-class taxonomy
The 2016 'wrong abstraction' rule read as a memory-corruption bug class: use-after-free, type confusion, double-free, from DirtyCOW to Dirty Pipe and DirtyCred.
memory corruptionThe 64KB segment where every overflow rewrote a free list pointer
Win16's Local Heap overflow defines the metadata corruption class that still drives modern browser and kernel exploitation in 2026.
memory-safetyCVE-2009-1897 is back, now under every @bitCast
How Zig's @bitCast lowering and LLVM's optimizer can synthesize exploitable use-after-free bugs that no source review or EDR will ever see.
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.