Memory Safety & Fuzzing

Heap Use-After-Free: Detection, Exploitation, and Prevention

Published ·11 min read·Vorthix Research Team

A heap use-after-free (CWE-416) is a memory-safety bug where a program keeps and uses a pointer to heap-allocated memory after that memory has been freed. Because the allocator is free to hand the same address to a new, unrelated allocation, the dangling pointer ends up reading or writing whatever now occupies that address — turning a lifetime bug into a data-corruption or code-execution primitive depending on what gets reallocated there and how it's used.

How Use-After-Free Bugs Occur

The bug requires three ingredients in sequence: an allocation, a free, and a subsequent access through a pointer that still holds the freed address. In small, linear code this is easy to avoid. In real codebases the three steps are rarely adjacent — the free happens in one function, the dangling access happens in a callback triggered indirectly through several layers of abstraction, and the connection between them is only visible by tracing the object's full lifetime across the call graph. Callback-heavy APIs, reference-counted objects with a bug in the count, and manually managed caches are the three most common settings for real-world UAF bugs.

How AddressSanitizer Catches Use-After-Free

AddressSanitizer instruments every load and store to check a shadow-memory byte that tracks whether the corresponding real memory is valid, unaddressable, or freed. When memory is freed, ASan does not return it to the allocator right away — it poisons the shadow bytes and holds the block in a quarantine queue so the address is unlikely to be immediately reused. Any subsequent access checks the shadow byte first; a freed byte causes an immediate, deterministic abort with a report that includes the faulting access, the stack trace of the original allocation, and the stack trace of the free — the exact evidence needed to fix the bug without a debugging session.

Detecting Use-After-Free Before It Ships

The most reliable detection strategy combines three techniques. Coverage-guided fuzzing drives the program through as many code paths as possible under sanitizer instrumentation, catching UAF bugs that require a specific, non-obvious sequence of operations to trigger. Manual object-lifetime auditing — tracing every pointer to a heap object from allocation to every possible free site — catches bugs in code paths a fuzzer's input grammar doesn't reach. And incomplete fix detection catches the specific, recurring pattern where a previous UAF fix added a guard around one access path and missed a sibling path that reaches the same freed object.

Case Study: A Re-Entrancy Use-After-Free in libexpat

CVE-2026-56412 is a heap use-after-free reached through exactly this last pattern. A prior fix added a re-entrancy guard to prevent five parser functions — including XML_ParserFree— from being called while a callback was executing, closing off a use-after-free where a handler frees the parser it's still executing inside. The guard checked a handler-depth counter that was correctly incremented and decremented around one call site, doContent(), and never touched at a second call site, doCdataSection(). A character-data handler invoked from inside a CDATA section could still call XML_ParserFree() on the parser it was running inside, and the guard — reading a counter that had never left zero — did nothing to stop it. Full sanitizer output and the 21-line reproducer are in the CVE-2026-56412 writeup.

Preventing Use-After-Free Bugs at the Design Level

Beyond detection, the most effective prevention strategies remove the possibility structurally: smart pointers and RAII in C++ tie an object's lifetime to a scope so it cannot outlive its last reference; reference counting, correctly implemented, defers the free until the last user is done; and memory-safe languages with ownership models — Rust chief among them — enforce the invariant at compile time in safe code. None of these eliminate the need for detection: C and C++ dominate the infrastructure layer of the software supply chain, and even memory-safe languages depend on FFI boundaries into C libraries where the same class of bug can still occur.

Frequently Asked Questions

What is a heap use-after-free vulnerability?

A heap use-after-free (CWE-416) occurs when a program continues to use a pointer to heap memory after that memory has been freed. The freed block can be reallocated for a different, attacker-influenced object, so the original pointer ends up reading or writing data that belongs to something else entirely — a primitive attackers routinely escalate into arbitrary code execution.

How does AddressSanitizer detect use-after-free?

AddressSanitizer poisons freed memory in shadow memory rather than returning it to the allocator immediately, and quarantines the block for a period after free. Any subsequent read or write to that address checks the shadow byte, sees it marked as freed, and the program aborts immediately with a report showing the access location, the allocation site, and the free site.

Why are use-after-free bugs so dangerous?

A dangling pointer reads or writes memory that has already been reused. If an attacker can control what gets reallocated into that freed slot, they control the data the vulnerable program reads back through the dangling pointer — turning a memory-safety bug into a data-corruption or control-flow-hijacking primitive, which is why UAF bugs are disproportionately represented among high-severity, actively exploited CVEs.

Can use-after-free bugs happen in Rust or memory-safe languages?

Rust’s ownership model prevents use-after-free in safe code at compile time. It can still occur inside unsafe blocks, in FFI boundaries calling into C/C++ libraries, or in logic bugs around manually managed lifetimes with raw pointers — which is why binding libraries and FFI layers remain a meaningful attack surface even in memory-safe host languages.

What is the difference between use-after-free and double-free?

Use-after-free is reading or writing freed memory through a dangling pointer. Double-free is calling the deallocator a second time on memory that was already freed, which can corrupt the allocator’s internal metadata. They frequently occur together — a double-free is often the second half of a use-after-free bug where the dangling pointer is freed again.

Real-World Case Studies

Related Reading