The bug itself is small: a pointer is used after the memory it references has been returned to the allocator. The danger is what the allocator does next. Modern heap allocators reuse freed blocks aggressively, often handing the same address back to the very next allocation of the matching size class.
An attacker who controls allocations — for example, by submitting strings to a web service or triggering features in a browser — can deliberately race in and take ownership of the freed block. They fill it with attacker-chosen bytes. When the original code dereferences its stale pointer, it reads those bytes as if they were still its own object.
In C++ and other object-oriented languages, the most common exploitation path is a virtual function call. Objects with virtual methods store a hidden pointer to a vtable — a table of function pointers — at the start of the object. Overwrite the vtable pointer, point it at attacker-controlled memory, and the next virtual call jumps wherever the attacker wants. Combined with techniques like ROP (Return-Oriented Programming), this gives full code execution.
The defensive answer is layered: smart pointers (unique_ptr, shared_ptr) that tie lifetime to scope; memory-safe languages like Rust whose borrow checker rejects use-after-free at compile time; and runtime defenses like delayed free, heap isolation, and type-after-type allocators that refuse to hand a freed block to a different type. None of these are perfect — but each one closes off attack surface that decades of CVEs have shown attackers will absolutely exploit.