Loading learning content...
In March 2014, security researchers disclosed CVE-2014-1776—a use-after-free vulnerability in Internet Explorer that was being actively exploited in the wild. Attackers could craft a web page that, when visited, would execute arbitrary code on the victim's computer with full system privileges. The vulnerability existed because IE freed a CMarkup object but continued using a pointer to it. When the object's memory was reallocated for attacker-controlled content, using the stale pointer gave attackers complete control.
This is a use-after-free (UAF) vulnerability—perhaps the most dangerous class of memory safety bugs in modern software. While buffer overflows have declined due to mitigations like stack canaries and ASLR, UAF bugs have risen to become the dominant exploitation vector in browser, kernel, and application attacks.
UAF vulnerabilities are particularly insidious because they involve a temporal rather than spatial memory error. The program doesn't write past buffer bounds; instead, it uses memory that was previously valid but is no longer. This temporal nature makes UAF bugs harder to detect, harder to mitigate, and often easier to exploit reliably than buffer overflows.
By the end of this page, you will understand: (1) The precise definition and taxonomy of use-after-free bugs, (2) How memory reuse creates exploitation opportunities, (3) Common patterns that lead to UAF vulnerabilities, (4) Exploitation techniques including heap feng shui, (5) Modern mitigations and their limitations, and (6) Coding practices and tools to prevent and detect UAF bugs.
A use-after-free vulnerability occurs when a program continues to use a pointer after the memory it points to has been freed. Let's establish a precise definition and understand why this is dangerous.
Formal Definition:
Use-After-Free (UAF): A memory safety vulnerability where a program dereferences a pointer to memory that has been deallocated (freed). The pointer, called a dangling pointer, refers to memory that may have been reallocated for a different purpose.
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Basic use-after-free example #include <stdlib.h>#include <string.h>#include <stdio.h> typedef struct { char name[32]; int id; void (*callback)(void); // Function pointer - attractive target!} User; void greet_user(void) { printf("Hello, user!\n");} void use_after_free_demo(void) { // Step 1: Allocate and initialize User* user = malloc(sizeof(User)); strcpy(user->name, "Alice"); user->id = 1234; user->callback = greet_user; // Step 2: Free the memory free(user); // 'user' is now a DANGLING POINTER // The memory it points to is no longer valid // But 'user' still contains the same address // Step 3: Use the freed memory (THE BUG) printf("User ID: %d\n", user->id); // UAF read - undefined behavior user->callback(); // UAF call - may execute anything! // What might happen: // - If memory not reused: may appear to work (dangerous - hides bug) // - If memory reused: reads/writes wrong data // - If function pointer overwritten: arbitrary code execution} // Why is this exploitable?// The allocator may give this memory to the next malloc().// Attacker controls what gets allocated.// Attacker's data overwrites the User struct.// user->callback now points to attacker-controlled address.// When called, attacker's code executes.The Three Stages of a UAF Bug:
| Stage | Action | State | Danger Level |
|---|---|---|---|
| Memory allocated, pointer stored | Pointer is valid, memory is owned | Safe |
| Memory freed, pointer NOT nullified | Pointer is dangling, memory is freed | Latent vulnerability |
| Memory given to another allocation | Pointer points to different data | Critical - exploitable |
| Dangling pointer is used | Program accesses wrong/attacker data | Exploitation in progress |
UAF vs. Other Memory Bugs:
| Vulnerability | Nature | Corruption Type | Detection Difficulty |
|---|---|---|---|
| Buffer Overflow | Spatial (write past bounds) | Adjacent memory | Moderate (bounds checking) |
| Use-After-Free | Temporal (use after lifetime) | Reallocated memory | Hard (track all pointers) |
| Double Free | Temporal (free same twice) | Allocator metadata | Moderate (track frees) |
| Memory Leak | Lifetime (never freed) | Resource exhaustion | Easy (track allocations) |
| Uninitialized Memory | Temporal (use before init) | Stale data | Moderate (track init) |
Buffer overflows require the attacker to write 'enough' data to reach critical structures—often detected by canaries. UAF attacks can work with arbitrarily small allocations: if the freed object was 40 bytes and contained a function pointer, a 40-byte attacker-controlled allocation is enough. No 'smashing' required.
The exploitability of UAF vulnerabilities stems from how memory allocators work. When you free memory, it doesn't disappear—it returns to a pool for reuse. This efficiency is also what makes UAF dangerous.
Allocator Reuse Behavior:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Demonstrating memory reuse by allocators #include <stdio.h>#include <stdlib.h>#include <string.h> int main() { // Allocate and free a 64-byte block char* first = malloc(64); printf("First allocation: %p\n", (void*)first); strcpy(first, "Original data"); free(first); // first is now a dangling pointer pointing to freed memory // Allocate another 64-byte block char* second = malloc(64); printf("Second allocation: %p\n", (void*)second); // High probability: second == first // Most allocators use LIFO (last-in-first-out) for free lists // The most recently freed block of the right size is reused first if (first == second) { printf("Memory was reused!\n"); // This demonstrates the UAF danger: strcpy(second, "Attacker controlled!"); // Now access through dangling pointer: printf("Via dangling pointer: %s\n", first); // Prints "Attacker controlled!" - not "Original data" } free(second); return 0;} /*Typical output:First allocation: 0x55a1b2c3d000Second allocation: 0x55a1b2c3d000Memory was reused!Via dangling pointer: Attacker controlled!*/The Exploitation Model:
Exploiting UAF typically follows this pattern:
Trigger the Free — Cause the vulnerable object to be freed while keeping a dangling pointer
Reclaim the Memory — Allocate new memory of the same size to reuse the freed block
Control the Content — Fill the new allocation with attacker-controlled data that mimics the original object's layout
Trigger the Use — Cause the dangling pointer to be dereferenced, using attacker data
Achieve Control — If a function pointer is used, gain code execution; if data is used, corrupt program state
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// Conceptual UAF exploitation #include <stdio.h>#include <stdlib.h>#include <string.h> // Vulnerable struct containing function pointertypedef struct { int type; char data[24]; void (*handler)(void*); // Offset 28 in struct} Object; void legitimate_handler(void* obj) { printf("Legitimate handler called\n");} void evil_function(void* obj) { printf("PWNED! Attacker code running!\n"); // In real exploit: spawn shell, install malware, etc.} // Global dangling pointer (simulating vulnerability)Object* global_ptr = NULL; void create_object() { global_ptr = malloc(sizeof(Object)); global_ptr->type = 1; strcpy(global_ptr->data, "legitimate"); global_ptr->handler = legitimate_handler;} void free_object() { free(global_ptr); // BUG: global_ptr not set to NULL - becomes dangling} void use_object() { if (global_ptr) { // Check passes - pointer is non-NULL global_ptr->handler(global_ptr); // Uses dangling pointer! }} // Attacker's reallocationvoid attacker_controlled_alloc() { // Allocate same size to reclaim the freed memory Object* fake = malloc(sizeof(Object)); // Craft fake object with malicious function pointer fake->type = 1; strcpy(fake->data, "attack payload"); fake->handler = evil_function; // Point to attacker's code // Now global_ptr points to our fake object // because allocator reused the same memory} int main() { create_object(); // global_ptr = valid Object // Legitimate use works fine use_object(); // Prints "Legitimate handler called" free_object(); // global_ptr becomes dangling attacker_controlled_alloc(); // Reclaim memory with fake object use_object(); // Prints "PWNED! Attacker code running!" return 0;}Real-world exploitation often requires 'heap feng shui' — careful manipulation of heap state through specific allocation/free patterns to ensure the vulnerable free'd memory is reallocated with attacker content. Browser exploits may perform hundreds of allocations to achieve the desired heap layout.
UAF bugs emerge from various programming patterns, often in well-intentioned code. Understanding these patterns helps in both prevention and code review.
Pattern 1: Event Handlers and Callbacks
Objects register callbacks that may be invoked after the object is freed:
1234567891011121314151617181920212223242526272829303132333435363738
// UAF in event handler pattern typedef struct Widget Widget; struct Widget { char* name; void (*on_click)(Widget* self); Widget* parent; // Complex object graph}; Widget* g_registered_widgets[100];int g_widget_count = 0; void register_widget(Widget* w) { g_registered_widgets[g_widget_count++] = w;} void destroy_widget(Widget* w) { free(w->name); free(w); // BUG: Widget still in g_registered_widgets array! // No removal from registration list} void dispatch_click_event(int x, int y) { for (int i = 0; i < g_widget_count; i++) { Widget* w = g_registered_widgets[i]; // May be freed! if (point_in_widget(w, x, y)) { w->on_click(w); // UAF if widget was destroyed } }} // Real-world example: Browser DOM// - JavaScript removes an element: element.remove()// - C++ DOM node is freed// - Event listener still references the node// - Event fires → UAFPattern 2: Reference Counting Bugs
Incorrect reference count management leads to premature frees:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// UAF from reference counting errors class RefCounted { int ref_count;public: RefCounted() : ref_count(1) {} void AddRef() { ref_count++; } void Release() { if (--ref_count == 0) { delete this; // Self-deletion } }}; class Element : public RefCounted {public: Element* parent; std::vector<Element*> children; void AddChild(Element* child) { children.push_back(child); child->AddRef(); child->parent = this; } void RemoveChild(Element* child) { children.erase(std::find(children.begin(), children.end(), child)); child->parent = nullptr; child->Release(); // May delete child }}; void vulnerable_operation(Element* parent) { Element* child = parent->children[0]; // Some operation that triggers removal: parent->RemoveChild(child); // child is now freed! // But we still have the pointer: child->DoSomething(); // UAF!} // The fix: Use smart pointers (shared_ptr, intrusive_ptr)// that prevent this entire class of bugsPattern 3: Iterator Invalidation
Iterating over a container while elements are removed:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// UAF from iterator invalidation #include <vector>#include <algorithm> struct Task { int id; bool completed; void (*callback)(Task*);}; std::vector<Task*> pending_tasks; void process_tasks() { // DANGEROUS: Modifying container while iterating for (Task* task : pending_tasks) { task->callback(task); // Callback might remove tasks! if (task->completed) { // Remove from vector while iterating pending_tasks.erase( std::find(pending_tasks.begin(), pending_tasks.end(), task) ); delete task; // task pointer in loop is now dangling } // Next iteration may use invalidated iterator or freed task }} // SAFE VERSION: Collect removals, process separatelyvoid process_tasks_safe() { std::vector<Task*> to_remove; for (Task* task : pending_tasks) { task->callback(task); if (task->completed) { to_remove.push_back(task); } } for (Task* task : to_remove) { pending_tasks.erase( std::find(pending_tasks.begin(), pending_tasks.end(), task) ); delete task; }}Pattern 4: Race Conditions in Multithreaded Code
One thread frees while another uses:
12345678910111213141516171819202122232425262728293031323334353637383940414243
// UAF from race condition #include <pthread.h>#include <stdlib.h> typedef struct { int data; pthread_mutex_t lock;} SharedResource; SharedResource* g_resource = NULL; void* reader_thread(void* arg) { while (1) { // CHECK: Is resource valid? if (g_resource != NULL) { // WINDOW: Between check and use, other thread may free! pthread_mutex_lock(&g_resource->lock); // UAF possible here! int data = g_resource->data; pthread_mutex_unlock(&g_resource->lock); use_data(data); } usleep(1000); }} void* manager_thread(void* arg) { while (1) { // Periodically replace resource SharedResource* old = g_resource; g_resource = create_new_resource(); // Free old resource while readers may be using it pthread_mutex_destroy(&old->lock); free(old); // RACE: readers may still be accessing old sleep(10); }} // Fix: Use RCU (Read-Copy-Update), hazard pointers, // or epoch-based reclamation for lock-free data structuresThese patterns appear constantly in real vulnerabilities. Chrome's security team reports that ~70% of high-severity security bugs are memory safety issues, with UAF being the single largest category. Browser engines are particularly vulnerable due to complex object graphs and JavaScript's ability to trigger arbitrary operations.
Exploiting UAF vulnerabilities requires precise control over heap state. Modern exploitation has evolved sophisticated techniques to achieve reliable exploitation despite mitigations.
Heap Feng Shui:
Heap feng shui is the art of manipulating heap layout to achieve predictable memory reuse:
1234567891011121314151617181920212223242526272829303132333435
// Heap Feng Shui concept for UAF exploitation // Goal: Ensure that when vulnerable object is freed,// our controlled allocation takes its place // Step 1: Fill heap with same-sized allocations// This depletes the free list and makes allocation behavior predictablevoid* fillers[1000];for (int i = 0; i < 1000; i++) { fillers[i] = malloc(TARGET_SIZE);} // Step 2: Create "holes" of the target size// Free every other one to create a predictable patternfor (int i = 0; i < 1000; i += 2) { free(fillers[i]); fillers[i] = NULL;} // Step 3: Trigger the vulnerability// The vulnerable object gets allocated in one of our holestrigger_vulnerable_alloc(); // Gets placed in predictable location // Step 4: Trigger the freetrigger_vulnerable_free(); // Freed object goes to free list head // Step 5: Immediately reallocate with controlled content// This should reclaim the exact memory of the vulnerable objectchar* evil = malloc(TARGET_SIZE);build_fake_object(evil); // Fill with attack payload // Step 6: Trigger use of dangling pointertrigger_vulnerable_use(); // Uses our fake object // Success: Attacker controls what the dangling pointer referencesType Confusion Through UAF:
UAF enables type confusion—making the program treat one type of object as another:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Type confusion via UAF // Original object with vtableclass Victim {public: virtual void DoSomething() { /* legitimate code */ } virtual void DoOther() { /* legitimate code */ }private: int value; // offset 8 (after vtable pointer) char data[24]; // offset 12};// Total size: 40 bytes (8 vtable + 4 int + 24 char + padding) // Attacker's fake object matching sizestruct FakeObject { void* fake_vtable; // offset 0: points to fake vtable char payload[32]; // offset 8: controlled data};// Total size: 40 bytes (same as Victim) void* fake_vtable[2] = { (void*)attacker_function_1, // Replaces DoSomething (void*)attacker_function_2 // Replaces DoOther}; void exploit() { Victim* victim = new Victim(); // 40 byte allocation delete victim; // Freed, but retains pointer // Reallocate with fake object FakeObject* fake = (FakeObject*)malloc(40); fake->fake_vtable = fake_vtable; // Point to our vtable strcpy(fake->payload, "attacker data"); // Victim pointer now points to our FakeObject // (because allocator reused the memory) // Virtual call through dangling pointer victim->DoSomething(); // Calls attacker_function_1! // Because: // 1. victim points to memory now containing FakeObject // 2. C++ looks up vtable at object start // 3. vtable is our fake_vtable // 4. Virtual call resolves to our function}Exploitation Targets in UAF:
| Target | Location in Object | What Attacker Achieves | Difficulty |
|---|---|---|---|
| C++ vtable pointer | Offset 0 (typically) | Control all virtual calls → Code execution | Medium |
| Function pointer | Anywhere in object | Single function call hijack → Code execution | Easy-Medium |
| Data pointer | Anywhere | Read/write arbitrary memory | Easy |
| Length field | Anywhere | Create buffer overflow in subsequent operations | Easy |
| Object reference | Anywhere | Chain to other vulnerabilities | Medium |
| Security token/flag | Anywhere | Privilege escalation, bypass checks | Easy |
Browser exploits commonly use JavaScript to: (1) Create objects that allocate memory of the target size, (2) Trigger the vulnerability to create dangling pointer, (3) Use ArrayBuffer or typed arrays to reclaim freed memory with controlled content, (4) Trigger use of dangling pointer for code execution. This entire process can happen in milliseconds upon visiting a malicious webpage.
Double free is closely related to UAF—both involve misuse of freed memory. A double free occurs when the same memory is freed twice, corrupting allocator metadata.
How Double Free Occurs:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
// Double free scenarios #include <stdlib.h> // Scenario 1: Simple mistakevoid double_free_simple() { char* buffer = malloc(100); // Use buffer... free(buffer); // Later, forgotten it was freed: free(buffer); // DOUBLE FREE!} // Scenario 2: Ownership confusionchar* global_buffer = NULL; void set_buffer(char* buf) { global_buffer = buf; // Shared pointer!} void cleanup_a() { free(global_buffer); // First free global_buffer = NULL;} void process_b() { char* local = malloc(100); set_buffer(local); // Later... free(local); // Second free of same memory! // Because local == global_buffer after set_buffer} // Scenario 3: Error handling pathsint complex_operation() { char* a = malloc(100); char* b = malloc(200); if (!a || !b) { free(a); free(b); return -1; } if (operation_fails()) { free(a); free(b); // If b allocation failed above, this double-frees return -1; } // More code... free(a); free(b); return 0;}Why Double Free Is Dangerous:
Double free corrupts the allocator's free list. On a second free:
1234567891011121314151617181920212223242526272829303132333435363738394041
// Double free exploitation concept #include <stdio.h>#include <stdlib.h> int main() { // Allocate a block char* a = malloc(32); printf("First alloc: %p\n", a); // Free it (legitimate) free(a); // Double free (bug!) free(a); // Now 'a' is in free list TWICE // Next allocation gets this block char* b = malloc(32); printf("Second alloc: %p\n", b); // Third allocation ALSO gets the same block! char* c = malloc(32); printf("Third alloc: %p\n", c); // b and c point to the same memory! if (b == c) { printf("Double allocation succeeded!\n"); // Now writes through b affect c and vice versa strcpy(b, "From B"); printf("Via c: %s\n", c); // Prints "From B" strcpy(c, "From C"); printf("Via b: %s\n", b); // Prints "From C" } return 0;} // Note: Modern allocators (glibc 2.27+) detect simple double frees// with tcache and abort. More complex patterns can still work.The simplest prevention: Set pointers to NULL after freeing. A second free(NULL) is safe (does nothing). Combine with ownership discipline—clearly document who is responsible for freeing each allocation, and only free from that owner.
UAF vulnerabilities are harder to mitigate than buffer overflows because they don't inherently involve writing past bounds. However, modern systems employ various techniques to reduce exploitability.
Allocator-Level Mitigations:
Chrome's PartitionAlloc:
Chrome's allocator demonstrates advanced UAF mitigation:
123456789101112131415161718192021222324
PartitionAlloc Mitigations: 1. SLOT SPAN RANDOMIZATION - Free slots are randomized within span - Attacker can't predict which slot is allocated 2. PARTITION SEGREGATION - Different object types use different partitions - UAF in one partition can only be exploited with same-type allocation - Limits attacker's ability to control reallocation content 3. BACKUP REF PTR (BackupRefPtr / MiraclePtr) - Reference counting at the raw pointer level - Detects use of stale pointers - Can crash safely instead of allowing exploitation 4. MEMORY TAGGING EXTENSION (MTE) SUPPORT - Hardware-enforced memory tags - Freed memory gets different tag - Access through stale pointer → hardware fault 5. LIGHTWEIGHT CHECKS - Fast inline checks for common UAF patterns - Crash on detection rather than exploitationLanguage and Compiler Mitigations:
| Approach | Implementation | Effectiveness | Overhead |
|---|---|---|---|
| Garbage Collection | Java, Go, Python, JavaScript | Eliminates UAF entirely | Depends on GC algorithm |
| Ownership/Borrowing | Rust's borrow checker | Prevents at compile time | Zero runtime overhead |
| Smart Pointers | C++ unique_ptr, shared_ptr | Prevents if used consistently | Small (refcount) |
| AddressSanitizer | Quarantines freed memory | Detects most UAF in testing | 2-3x slowdown |
| Memory Tagging (MTE) | ARM hardware feature | Probabilistic detection | ~3% slowdown |
| Scudo Hardened Allocator | Android, Fuchsia | Quarantine + checks | Minimal |
ARM Memory Tagging Extension (MTE) assigns 4-bit 'colors' to memory regions. Pointers carry matching color tags. On access, hardware checks if pointer tag matches memory tag. Freed memory gets a different color—UAF causes tag mismatch and immediate hardware trap. This provides probabilistic (1/16) UAF detection with minimal overhead.
The best defense against UAF is prevention through disciplined coding practices and appropriate tools.
Coding Practices:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
// UAF Prevention Patterns // PATTERN 1: Use smart pointers (C++)#include <memory> class Safe { std::unique_ptr<Resource> owned_; // Sole ownership std::shared_ptr<SharedRes> shared_; // Shared ownership std::weak_ptr<SharedRes> observer_; // Non-owning reference public: void UseOwned() { // owned_ automatically freed when Safe destroyed owned_->DoWork(); } void UseShared() { // shared_ freed when last shared_ptr destroyed shared_->DoWork(); } void UseObserver() { // weak_ptr doesn't prevent deletion // Must lock() to get shared_ptr, which may fail if (auto ptr = observer_.lock()) { ptr->DoWork(); // Safe - we hold shared_ptr } else { // Object was deleted, handle gracefully } }}; // PATTERN 2: Set pointers to NULL after free (C)void safe_free(void** ptr) { if (ptr && *ptr) { free(*ptr); *ptr = NULL; // Prevents use-after-free }} // Usage:char* buffer = malloc(100);// ...safe_free((void**)&buffer); // buffer is now NULL// Later access: if (buffer) { ... } // Safe - buffer is NULL // PATTERN 3: RAII for non-memory resourcesclass FileHandle { int fd_;public: FileHandle(const char* path) : fd_(open(path, O_RDONLY)) {} ~FileHandle() { if (fd_ >= 0) close(fd_); } // Prevent copying to avoid double-close FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete; // Allow moving FileHandle(FileHandle&& other) : fd_(other.fd_) { other.fd_ = -1; }}; // PATTERN 4: Clear references on destructionclass Observable { std::vector<Observer*> observers_;public: ~Observable() { for (auto obs : observers_) { obs->OnSubjectDestroyed(this); // Notify so they can clear refs } }};Runtime Detection with AddressSanitizer:
AddressSanitizer (ASan) is highly effective at detecting UAF:
123456789101112131415161718192021222324252627282930313233343536373839404142
// Detecting UAF with AddressSanitizer #include <stdlib.h> int main() { int* array = malloc(10 * sizeof(int)); free(array); return array[5]; // UAF!} /*Compile:$ clang -fsanitize=address -g uaf.c -o uaf Run:$ ./uaf ===================================================================12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x604000000024 at pc 0x... bp 0x... sp 0x...READ of size 4 at 0x604000000024 thread T0 #0 0x... in main uaf.c:7 ... 0x604000000024 is located 20 bytes inside of 40-byte region [0x604000000010,0x604000000038)freed by thread T0 here: #0 0x... in free #1 0x... in main uaf.c:6 ... previously allocated by thread T0 here: #0 0x... in malloc #1 0x... in main uaf.c:5 ... SUMMARY: AddressSanitizer: heap-use-after-free uaf.c:7 in main*/ // ASan works by:// 1. Quarantining freed memory (not immediately reused)// 2. Marking freed memory as "poisoned" in shadow memory// 3. Checking shadow memory on every access// 4. Reporting with full allocation/free stack tracesStatic Analysis:
Static analyzers can detect some UAF patterns at compile time:
123456789101112131415161718192021
# Static analysis for UAF detection # Clang Static Analyzerscan-build --use-analyzer=clang make # GCC -fanalyzer (GCC 10+)gcc -fanalyzer -Wall source.c # Infer (Facebook)infer run -- clang -c source.c # Cppcheckcppcheck --enable=all source.c # Coverity (commercial, very thorough)cov-build --dir cov-int makecov-analyze --dir cov-int # CodeQL (GitHub)codeql database create db --language=cpp --command="make"codeql database analyze db codeql/cpp-queries:cpp/use-after-freeCombine multiple approaches: (1) Use smart pointers and RAII in C++, (2) Nullify pointers after free in C, (3) Run with ASan during testing, (4) Use static analysis in CI/CD, (5) Enable allocator hardening in production, (6) Consider memory-safe languages for new code.
Use-after-free vulnerabilities represent one of the most dangerous and prevalent memory safety issues in modern software. Their temporal nature makes them particularly challenging to prevent and detect.
Key Takeaways:
What's Next:
We've now covered the three major memory error classes: leaks, overflows, and use-after-free. The next page introduces Valgrind—the comprehensive memory debugging tool that can detect all these issues and more. Valgrind's ability to instrument programs at runtime makes it invaluable for finding memory bugs that static analysis and code review miss.
You now understand use-after-free vulnerabilities at a deep level—from basic mechanics to advanced exploitation techniques and modern mitigations. This knowledge is essential for writing secure systems code, understanding security advisories, and performing vulnerability research. The patterns and prevention strategies apply across all software that manages memory manually.