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.