Double Free
Description
Double Free occurs when a program calls free() on the same memory address twice. The second free() corrupts memory management data structures, which can lead to program crashes, heap corruption, and potentially arbitrary code execution. When memory is freed, the allocator adds it to a free list. Freeing it again corrupts this list, and subsequent allocations may return the same memory to different parts of the program (use-after-free) or allow attackers to manipulate heap metadata for exploitation.
Risk
Double free vulnerabilities are highly exploitable and have been used in numerous high-profile attacks. They can lead to arbitrary code execution by corrupting heap metadata or function pointers. Modern heap allocators include some protections, but sophisticated exploits can bypass them. Double frees often occur in complex codepaths involving error handling, exception handling, or resource cleanup. They can be difficult to detect during code review and may only manifest under specific conditions.
Solution
Set pointers to NULL after freeing them (though this doesn't prevent all cases). Use smart pointers in C++ (unique_ptr, shared_ptr) that handle memory automatically. Implement clear ownership semantics—one component should own and free each resource. Use AddressSanitizer during testing to detect double frees. Conduct thorough code review of error handling paths. Use static analysis tools. Consider using garbage-collected languages for security-critical code.
Common Consequences
| Impact | Details |
|---|---|
| Availability | Scope: Denial of Service Double frees typically crash the program due to heap corruption. |
| Integrity | Scope: Code Execution Corrupted heap metadata can be exploited for arbitrary code execution. |
| Confidentiality | Scope: Information Disclosure Heap corruption may expose sensitive data from other allocations. |
Example Code + Solution Code
Vulnerable Code
// VULNERABLE: Simple double free
void process_data(char *data) {
char *buffer = malloc(100);
strcpy(buffer, data);
if (strlen(buffer) > 50) {
free(buffer);
return; // Early return
}
// ... process buffer ...
free(buffer); // Double free if early return was taken!
}
// VULNERABLE: Double free in error handling
int read_file(const char *filename) {
FILE *fp = NULL;
char *buffer = NULL;
fp = fopen(filename, "r");
if (!fp) goto error;
buffer = malloc(1024);
if (!buffer) goto error;
if (fread(buffer, 1, 1024, fp) == 0) {
free(buffer); // Free here
goto error; // And again in error handler!
}
// ... process ...
free(buffer);
fclose(fp);
return 0;
error:
free(buffer); // Double free!
if (fp) fclose(fp);
return -1;
}
// VULNERABLE: Double free through aliases
void process_item(Item *item) {
Item *temp = item; // Alias to same memory
// ... use temp ...
free(temp); // Free through alias
// Later...
free(item); // Double free - same memory!
}
// VULNERABLE: Double free in exception-like pattern
struct Resource {
char *data;
int size;
};
void cleanup_resource(struct Resource *r) {
free(r->data); // May have already been freed
free(r);
}
int create_resource(struct Resource **out) {
struct Resource *r = malloc(sizeof(struct Resource));
if (!r) return -1;
r->data = malloc(100);
if (!r->data) {
free(r);
return -1;
}
if (init_data(r->data) != 0) {
free(r->data);
free(r); // This path is OK
return -1;
}
*out = r;
return 0;
}
void use_resource(void) {
struct Resource *r;
if (create_resource(&r) != 0) {
// Error already cleaned up
return;
}
if (process(r) != 0) {
cleanup_resource(r); // First free
}
cleanup_resource(r); // Double free if process() failed!
}
// VULNERABLE: C++ with raw pointers
class DataProcessor {
int* buffer;
public:
DataProcessor() : buffer(new int[100]) {}
~DataProcessor() {
delete[] buffer; // First delete
}
void reset() {
delete[] buffer; // Another delete
buffer = new int[100];
}
void process() {
// If exception thrown after reset(), destructor
// will double-delete
reset();
throw std::runtime_error("Error");
}
};
Fixed Code
// SAFE: Set pointer to NULL after free
void process_data_safe(char *data) {
char *buffer = malloc(100);
if (!buffer) return;
strcpy(buffer, data);
if (strlen(buffer) > 50) {
free(buffer);
buffer = NULL; // Prevent double free
return;
}
// ... process buffer ...
free(buffer);
buffer = NULL;
}
// SAFE: Single cleanup path
int read_file_safe(const char *filename) {
FILE *fp = NULL;
char *buffer = NULL;
int result = -1;
fp = fopen(filename, "r");
if (!fp) goto cleanup;
buffer = malloc(1024);
if (!buffer) goto cleanup;
if (fread(buffer, 1, 1024, fp) == 0) {
goto cleanup; // Go to single cleanup point
}
// ... process ...
result = 0;
cleanup:
// Single cleanup point - each resource freed once
free(buffer); // free(NULL) is safe
if (fp) fclose(fp);
return result;
}
// SAFE: Clear ownership
void process_item_safe(Item *item) {
// Clear: this function takes ownership and frees
// ... use item ...
free(item);
// Don't pass item anywhere else after freeing
}
// SAFE: Proper resource management with explicit ownership
struct Resource {
char *data;
int size;
bool owns_data; // Ownership flag
};
void cleanup_resource_safe(struct Resource *r) {
if (r) {
if (r->owns_data) {
free(r->data);
r->data = NULL;
r->owns_data = false;
}
free(r);
}
}
int create_resource_safe(struct Resource **out) {
struct Resource *r = calloc(1, sizeof(struct Resource));
if (!r) return -1;
r->data = malloc(100);
if (!r->data) {
free(r);
return -1;
}
r->owns_data = true;
if (init_data(r->data) != 0) {
cleanup_resource_safe(r);
return -1;
}
*out = r;
return 0;
}
void use_resource_safe(void) {
struct Resource *r = NULL;
if (create_resource_safe(&r) != 0) {
return; // Already cleaned up
}
if (process(r) != 0) {
// Don't cleanup here - do it once at the end
}
cleanup_resource_safe(r); // Single cleanup
r = NULL;
}
// SAFE: Use smart pointers in C++
#include <memory>
#include <vector>
class DataProcessorSafe {
std::unique_ptr<int[]> buffer;
public:
DataProcessorSafe() : buffer(std::make_unique<int[]>(100)) {}
// Destructor automatically handles cleanup
// No double delete possible
void reset() {
buffer = std::make_unique<int[]>(100);
// Old buffer automatically deleted
}
void process() {
reset();
// If exception thrown, unique_ptr handles cleanup
throw std::runtime_error("Error");
// No double delete!
}
};
// SAFE: RAII pattern for custom resources
class FileHandle {
FILE* fp;
public:
explicit FileHandle(const char* filename)
: fp(fopen(filename, "r")) {
if (!fp) throw std::runtime_error("Cannot open file");
}
~FileHandle() {
if (fp) fclose(fp);
}
// Prevent copying
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Allow moving
FileHandle(FileHandle&& other) noexcept : fp(other.fp) {
other.fp = nullptr;
}
FILE* get() { return fp; }
};
// SAFE: Using shared_ptr for shared ownership
std::shared_ptr<Data> createSharedData() {
return std::make_shared<Data>();
}
void useSharedData() {
auto data = createSharedData();
// Multiple owners OK - freed when last reference dies
auto data2 = data; // Shared ownership
// No double free - reference counted
}
Exploited in the Wild
WhatsApp Remote Code Execution (2019)
CVE-2019-3568 was a double free vulnerability in WhatsApp's VOIP stack that allowed remote code execution via specially crafted SRTCP packets, exploited in targeted attacks.
PHP Double Free (2016)
CVE-2016-5771 was a double free in PHP's garbage collection that could be triggered by specially crafted serialized data, leading to remote code execution.
Sudo Heap-Based Buffer Overflow/Double Free (2021)
CVE-2021-3156 (Baron Samedit) involved heap memory corruption in sudo that could be exploited for privilege escalation, demonstrating the severe impact of memory management bugs.
Tools to test/exploit
-
AddressSanitizer (ASan) — detects double frees at runtime.
-
Valgrind — memory error detector including double frees.
-
Electric Fence — memory debugger.
-
Heap exploitation research — educational heap exploitation techniques.
CVE Examples
-
CVE-2019-3568 — WhatsApp VOIP double free RCE.
-
CVE-2016-5771 — PHP garbage collection double free.
-
CVE-2017-9800 — Apache Subversion double free.
References
-
MITRE. "CWE-415: Double Free." https://cwe.mitre.org/data/definitions/415.html
-
CERT. "MEM30-C: Do not access freed memory." https://wiki.sei.cmu.edu/confluence/display/c/MEM30-C.+Do+not+access+freed+memory