CWE-416: Use After Free
Overview
Use-after-free occurs when memory is accessed after being freed, reading/writing freed memory that may be reallocated to different objects, enabling arbitrary code execution, information disclosure, or crashes. Exploited widely in browsers, kernels, and native applications.
Risk
Critical: Use-after-free enables arbitrary code execution (controlling freed object's vtable), information disclosure (reading freed memory), type confusion attacks, heap spray exploits, and is one of the most common memory corruption vulnerabilities exploited in the wild.
Remediation Steps
Core principle: Never use memory after it is freed; invalidate references and enforce lifetimes.
Locate the Use-After-Free Vulnerability
When reviewing security scan results:
- Examine data_paths: Identify where memory is accessed after being freed
- Trace pointer lifecycle: Find all uses of the pointer after free() or delete
- Check for aliases: Look for other pointers pointing to the same freed memory
- Review callbacks: Ensure callbacks don't access freed context data
- Analyze async operations: Check for operations using freed memory after completion
Use Smart Pointers for Automatic Lifetime Management (Primary Defense - C++)
// std::unique_ptr (single ownership)
auto ptr = std::make_unique<Object>();
ptr->method(); // Safe
ptr.reset(); // Freed and set to nullptr
// ptr is now nullptr, dereferencing will crash immediately
// std::shared_ptr (shared ownership)
auto ptr = std::make_shared<Object>();
auto alias = ptr; // Both point to same object
ptr.reset(); // Object still valid (alias holds reference)
alias.reset(); // Now freed (last reference gone)
Why this works: Smart pointers tie memory lifetime to object lifetime. unique_ptr provides single ownership with move semantics, making dangling pointers impossible. shared_ptr uses reference counting to ensure memory remains valid as long as any pointer references it, preventing use-after-free.
Set Pointers to NULL After Free (C)
For C code without smart pointers:
free(ptr);
ptr = NULL;
// Later access fails safely
if (ptr != NULL) {
ptr->field; // Won't execute
} else {
// Handle null case
}
Implementation details:
- Immediately set pointers to NULL after free() to create fail-fast behavior
- Always check for NULL before dereferencing
- Update all aliases to NULL when freeing shared memory
- Use static analysis tools to enforce NULL checks
Use RAII Patterns (Resource Acquisition Is Initialization)
class Resource {
public:
Resource() : data(new char[100]) {}
~Resource() {
delete[] data;
data = nullptr; // Prevent use-after-free in destructor
}
// Prevent copying to avoid shallow copies
Resource(const Resource&) = delete;
Resource& operator=(const Resource&) = delete;
private:
char* data;
};
// Automatic cleanup - memory freed when object goes out of scope
{
Resource r;
// Use r
} // Destructor frees data, no use-after-free possible
RAII benefits:
- Ties resource lifetime to object lifetime
- Automatic cleanup prevents forgetting to free
- Exception-safe resource management
- Makes ownership and lifetime explicit
Minimize Raw Pointer Lifetime and Scope
Design strategies:
- Use references instead of pointers when object lifetime is guaranteed
- Limit pointer scope to smallest necessary block
- Use containers (std::vector, std::string) that manage memory automatically
- Avoid storing pointers to local/stack objects
- Prefer value semantics over pointer semantics
- Make ownership transfer explicit with std::move
Example:
void processData(const std::string& data) { // Reference, not pointer
std::vector<int> values; // Automatic memory management
// No raw pointers needed
}
Test for Use-After-Free with Memory Sanitizers
Testing strategies:
- Use AddressSanitizer: compile with
-fsanitize=address - Run Valgrind:
valgrind --tool=memcheck --track-origins=yes - Enable MemorySanitizer for uninitialized memory detection
- Test with heap randomization to expose use-after-free bugs
- Use fuzzing to trigger edge cases
Verification steps:
# Compile with sanitizer
g++ -fsanitize=address -g -O1 program.cpp
# Run tests
./a.out
# AddressSanitizer will report use-after-free immediately
Additional validation:
- Test all code paths that free memory
- Verify callback and async operation handling
- Check edge cases: exceptions, early returns
Common Vulnerable Patterns
Classic Use-After-Free
// VULNERABLE - Classic use-after-free
char *buf = malloc(100);
free(buf);
strcpy(buf, "data"); // USE AFTER FREE!
Dangling Pointer
// VULNERABLE - Dangling pointer after free
struct Object *obj = malloc(sizeof(*obj));
struct Object *alias = obj;
free(obj);
alias->field = 5; // USE AFTER FREE!
Callback with Freed Data
// VULNERABLE - Callback with freed data
void callback(void *ctx) {
MyData *data = (MyData*)ctx;
data->value = 42; // ctx might be freed
}
Secure Patterns
Smart Pointers
auto obj = std::make_unique<Object>();
obj->method();
obj.reset(); // Explicitly freed
// obj is nullptr, safe
Why this works: std::unique_ptr enforces single ownership through move semantics - when ownership transfers via std::move(), the source pointer becomes null, making it impossible to use the old pointer and preventing use-after-free. reset() atomically frees the memory and sets the internal pointer to nullptr, so any subsequent dereference attempt will crash immediately with a null pointer exception rather than silently corrupting memory or executing attacker-controlled code. Automatic memory management eliminates manual delete calls where developers might accidentally use the pointer after freeing - the smart pointer's destructor handles cleanup when it goes out of scope, mechanically preventing the use-after-free condition through C++'s scoping rules.
RAII Pattern
class FileHandle {
FILE* file;
public:
FileHandle(const char* path) : file(fopen(path, "r")) {}
~FileHandle() { if (file) fclose(file); }
FILE* get() { return file; }
};
Why this works: RAII (Resource Acquisition Is Initialization) ties resource lifetime to object scope - when the FileHandle object goes out of scope (function return, exception, or block end), the destructor automatically runs and closes the file handle, ensuring resources cannot be used after being freed. Automatic cleanup in the destructor prevents developers from forgetting to close resources and eliminates timing bugs where code paths might free resources at different points. Exception safety is guaranteed - even if an exception occurs, C++ guarantees destructors run during stack unwinding, ensuring the file is closed and preventing use-after-close bugs. The pattern makes ownership explicit - whoever holds the FileHandle object owns the resource, and when that object dies, the resource is freed, making it architecturally impossible to use freed resources.