Free of Memory not on the Heap

Description

Free of Memory not on the Heap occurs when code attempts to free memory that was not dynamically allocated via malloc, calloc, realloc, or equivalent heap allocation functions. This includes attempting to free stack-allocated variables, global variables, string literals, or portions of allocated blocks (interior pointers). The memory management system cannot handle such requests, resulting in undefined behavior, heap corruption, or crashes.

Risk

Freeing non-heap memory corrupts the heap management structures, leading to unpredictable behavior. Immediate crashes are common, but the corruption may go undetected until subsequent heap operations fail. Attackers can potentially exploit heap corruption for arbitrary code execution through techniques like heap feng shui. The bug may appear to work in some environments but fail catastrophically in others, making it dangerous in production.

Solution

Only free memory that was obtained from malloc/calloc/realloc (or new in C++). Track allocation sources—consider wrapper functions that record whether memory was heap-allocated. Don't free stack addresses, global variables, or string literals. Never free interior pointers (pointers to middle of allocated blocks). Use static analysis tools that track allocation/deallocation pairs. In C++, prefer smart pointers that manage their own memory.

Common Consequences

ImpactDetails
AvailabilityScope: Crash

Heap corruption from invalid free causes immediate or delayed crashes.
IntegrityScope: Memory Corruption

Heap metadata corruption affects subsequent allocations.
SecurityScope: Code Execution

Heap corruption can be exploited for arbitrary code execution.

Example Code + Solution Code

Vulnerable Code

// VULNERABLE: Free stack-allocated memory
void free_stack_vulnerable() {
    int local_array[100];
    process(local_array);
    free(local_array);  // BUG! Stack memory!
}

// VULNERABLE: Free global variable
int global_data[50];

void free_global_vulnerable() {
    process(global_data);
    free(global_data);  // BUG! Global/static memory!
}

// VULNERABLE: Free string literal
void free_literal_vulnerable() {
    char* str = "Hello, World!";
    process(str);
    free(str);  // BUG! String literal in read-only memory!
}

// VULNERABLE: Free interior pointer
void free_interior_vulnerable() {
    char* buffer = malloc(100);
    char* middle = buffer + 50;  // Points to middle of block
    // ... use middle ...
    free(middle);  // BUG! Not the start of allocation!
}

// VULNERABLE: Conditional allocation, unconditional free
void conditional_alloc_vulnerable(int use_heap) {
    char* data;

    if (use_heap) {
        data = malloc(256);
    } else {
        char local[256];
        data = local;
    }

    process(data);
    free(data);  // BUG if use_heap was false!
}

// VULNERABLE: Return stack address, caller frees
char* get_data_vulnerable() {
    char buffer[100];
    strcpy(buffer, "data");
    return buffer;  // Returns stack address!
}

void caller_vulnerable() {
    char* data = get_data_vulnerable();  // Stack address!
    // ... use data ...
    free(data);  // Double trouble: dangling AND free of non-heap!
}

// VULNERABLE: Free struct containing stack pointer
struct Container {
    char* data;
    size_t size;
};

void process_container_vulnerable(struct Container* c) {
    char local_buffer[64];
    c->data = local_buffer;  // Stack address!
    c->size = sizeof(local_buffer);
    // ... container is used ...
}

void cleanup_container_vulnerable(struct Container* c) {
    free(c->data);  // BUG! May be stack memory!
    free(c);
}

// VULNERABLE: Array address treated as heap
void array_address_vulnerable(int arr[], int size) {
    if (size > 100) {
        free(arr);  // BUG! arr might be stack array from caller!
    }
}
// VULNERABLE: C++ with non-heap delete
void delete_stack_vulnerable() {
    int local = 42;
    int* ptr = &local;
    delete ptr;  // BUG! Stack variable!
}

// VULNERABLE: Delete string literal
void delete_literal_vulnerable() {
    const char* str = "constant";
    delete[] str;  // BUG! Literal in read-only section!
}

// VULNERABLE: Mixed allocation sources
class VulnerableManager {
    int* data;
    bool ownData;

public:
    void setData(int* d, bool heap_allocated) {
        data = d;
        ownData = heap_allocated;  // Track ownership
    }

    ~VulnerableManager() {
        delete[] data;  // BUG! Ignores ownData flag!
    }
};

void use_vulnerable() {
    int stack_array[10];
    VulnerableManager mgr;
    mgr.setData(stack_array, false);
    // Destructor will delete stack memory!
}

// VULNERABLE: Placement new then regular delete
void placement_delete_vulnerable() {
    char buffer[sizeof(MyClass)];
    MyClass* obj = new (buffer) MyClass();  // Placement new

    delete obj;  // BUG! buffer is on stack!
}

// VULNERABLE: Vector data pointer freed
void vector_data_vulnerable() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    int* data = vec.data();  // Pointer to internal storage

    // Later, mistakenly freed
    free(data);  // BUG! Vector manages its own memory!
}

Fixed Code

// SAFE: Only free heap-allocated memory
void free_heap_safe() {
    int* heap_array = malloc(100 * sizeof(int));
    if (heap_array == NULL) return;

    process(heap_array);
    free(heap_array);  // Correct: heap memory
}

// SAFE: Don't free globals or stack
int global_data[50];

void use_global_safe() {
    process(global_data);
    // No free - it's global/static
}

void use_local_safe() {
    int local_array[100];
    process(local_array);
    // No free - it's stack
}

// SAFE: Handle string literals properly
void use_string_safe() {
    // String literals don't need freeing
    const char* literal = "Hello, World!";
    process(literal);

    // For dynamic strings, allocate and free
    char* dynamic = malloc(100);
    if (dynamic) {
        strcpy(dynamic, "Dynamic string");
        process(dynamic);
        free(dynamic);
    }
}

// SAFE: Keep track of original allocation pointer
void interior_pointer_safe() {
    char* buffer = malloc(100);
    if (buffer == NULL) return;

    char* middle = buffer + 50;  // Interior pointer for use
    process(middle);

    free(buffer);  // Free the ORIGINAL allocation pointer
}

// SAFE: Track allocation source
struct SafeData {
    char* data;
    size_t size;
    int heap_allocated;  // Flag to track source
};

void init_safe_data(struct SafeData* sd, int use_heap) {
    if (use_heap) {
        sd->data = malloc(256);
        sd->heap_allocated = 1;
    } else {
        // Caller must ensure this outlives SafeData usage
        sd->data = NULL;  // Or point to static buffer
        sd->heap_allocated = 0;
    }
}

void cleanup_safe_data(struct SafeData* sd) {
    if (sd->heap_allocated && sd->data != NULL) {
        free(sd->data);
    }
    sd->data = NULL;
}

// SAFE: Return heap-allocated data
char* get_data_safe() {
    char* buffer = malloc(100);
    if (buffer) {
        strcpy(buffer, "data");
    }
    return buffer;  // Caller must free
}

void caller_safe() {
    char* data = get_data_safe();
    if (data) {
        process(data);
        free(data);  // Correct: heap memory
    }
}

// SAFE: Container with proper ownership tracking
struct SafeContainer {
    char* data;
    size_t size;
    int owns_data;
};

struct SafeContainer* create_container_heap(size_t size) {
    struct SafeContainer* c = malloc(sizeof(struct SafeContainer));
    if (c == NULL) return NULL;

    c->data = malloc(size);
    if (c->data == NULL) {
        free(c);
        return NULL;
    }

    c->size = size;
    c->owns_data = 1;
    return c;
}

struct SafeContainer create_container_view(char* external_data, size_t size) {
    return (struct SafeContainer){
        .data = external_data,
        .size = size,
        .owns_data = 0  // Don't own, don't free
    };
}

void destroy_container(struct SafeContainer* c) {
    if (c->owns_data && c->data) {
        free(c->data);
    }
    c->data = NULL;
}
// SAFE: Use smart pointers
void smart_pointer_safe() {
    auto ptr = std::make_unique<int>(42);
    // Automatically freed when ptr goes out of scope
}

// SAFE: Don't delete non-heap memory
void no_delete_stack() {
    int local = 42;
    int* ptr = &local;
    // Just use ptr, don't delete!
    process(*ptr);
}

// SAFE: Proper ownership tracking
class SafeManager {
    std::unique_ptr<int[]> ownedData;
    int* viewData = nullptr;
    bool hasOwnership = false;

public:
    void setOwnedData(std::unique_ptr<int[]> d) {
        ownedData = std::move(d);
        hasOwnership = true;
    }

    void setViewData(int* d) {
        viewData = d;
        hasOwnership = false;  // Don't own, don't delete
    }

    ~SafeManager() {
        // unique_ptr handles owned data
        // viewData is not deleted
    }
};

// SAFE: Proper placement new/delete
void placement_new_safe() {
    alignas(MyClass) char buffer[sizeof(MyClass)];
    MyClass* obj = new (buffer) MyClass();  // Placement new

    obj->~MyClass();  // Explicit destructor call, not delete!
    // buffer goes out of scope naturally
}

// SAFE: Don't free vector internals
void vector_safe() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    // Use data() for access, but vector manages memory
    const int* data = vec.data();
    for (size_t i = 0; i < vec.size(); i++) {
        process(data[i]);
    }
    // Vector destructor handles cleanup
}

// SAFE: Span for non-owning views
#include <span>

class SafeProcessor {
    std::span<int> view;  // Non-owning view

public:
    void setView(std::span<int> v) {
        view = v;
    }

    void process() {
        for (int val : view) {
            doSomething(val);
        }
    }

    // No destructor needed - span doesn't own memory
};

// SAFE: Clear ownership semantics with observer_ptr (Guideline Support Library)
void clear_ownership_example() {
    std::unique_ptr<Resource> owned = std::make_unique<Resource>();

    // Non-owning pointer for passing around
    Resource* observer = owned.get();
    use(observer);  // Don't delete observer!

    // owned handles deletion
}

Exploited in the Wild

Heap Corruption Exploits

Invalid free operations have corrupted heap metadata, enabling arbitrary write primitives in exploits.

Use-After-Free Chains

Free of non-heap memory has triggered heap corruption that was later exploited through subsequent allocations.

Denial of Service

Applications have crashed due to freeing non-heap memory, enabling denial-of-service attacks.


Tools to test/exploit


CVE Examples

  • Multiple CVEs involving heap corruption from invalid free.

  • Browser vulnerabilities from freeing non-heap memory.

  • Library crashes from improper memory management.


References

  1. MITRE. "CWE-590: Free of Memory not on the Heap." https://cwe.mitre.org/data/definitions/590.html

  2. CERT C. "MEM34-C: Only free memory allocated dynamically." https://wiki.sei.cmu.edu/confluence/display/c/