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

ImpactDetails
AvailabilityScope: Denial of Service

Double frees typically crash the program due to heap corruption.
IntegrityScope: Code Execution

Corrupted heap metadata can be exploited for arbitrary code execution.
ConfidentialityScope: 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


CVE Examples


References

  1. MITRE. "CWE-415: Double Free." https://cwe.mitre.org/data/definitions/415.html

  2. CERT. "MEM30-C: Do not access freed memory." https://wiki.sei.cmu.edu/confluence/display/c/MEM30-C.+Do+not+access+freed+memory