Free of Pointer not at Start of Buffer

Description

Free of Pointer not at Start of Buffer is a memory management vulnerability that occurs when software calls free() on a pointer to heap-allocated memory, but the pointer does not reference the start of the originally allocated buffer. This typically happens after pointer arithmetic moves the pointer to an interior location or past the end of the buffer. The heap memory allocator expects to receive the exact pointer that was returned by malloc(), calloc(), or realloc(). Passing a different address causes heap corruption, potentially leading to crashes, memory corruption, or exploitable conditions.

Risk

Freeing a pointer not at the buffer start corrupts the heap's internal data structures. This can cause immediate crashes, but more dangerously, it can lead to exploitable conditions. Attackers may be able to manipulate heap metadata to achieve arbitrary write capabilities, potentially leading to code execution. The vulnerability is particularly dangerous because it may not crash immediately—the heap corruption might only manifest later, making debugging difficult and creating time-of-check to time-of-use windows. In some cases, attackers can carefully craft conditions to exploit corrupted heap metadata for privilege escalation.

Solution

Always preserve the original pointer returned by allocation functions. Use a separate variable or index for traversing the buffer while keeping the original pointer for deallocation. In C++, use smart pointers (unique_ptr, shared_ptr) that automatically manage memory. When using functions like strtok() that modify pointers, remember that the returned tokens point into the original buffer—don't free them individually. Consider using container classes or memory-safe languages where possible. Use runtime tools like AddressSanitizer or Valgrind during development to detect these issues.

Common Consequences

ImpactDetails
IntegrityScope: Integrity

Modify Memory - Freeing wrong address corrupts heap metadata, potentially allowing memory modification.
AvailabilityScope: Availability

DoS: Crash, Exit, or Restart - Heap corruption typically causes crashes.
ConfidentialityScope: Confidentiality, Integrity, Availability

Execute Unauthorized Code or Commands - In some cases, heap corruption can be exploited for code execution.

Example Code

Vulnerable Code

// Vulnerable: Freeing moved pointer after string search
#include <stdlib.h>
#include <string.h>

char searchChar(char c) {
    char* str = malloc(20 * sizeof(char));
    if (str == NULL) return -1;

    strcpy(str, "Search Me!");

    // Walk through string with pointer arithmetic
    while (*str != '\0') {
        if (*str == c) {
            // Vulnerable: str has been incremented!
            free(str);  // Freeing interior pointer
            return 1;   // Found
        }
        str = str + 1;  // Pointer moves away from original allocation
    }

    // Vulnerable: str now points past original allocation
    free(str);  // Freeing wrong address
    return 0;   // Not found
}

// Vulnerable: Freeing strtok result
void vulnerable_tokenize(char* input_copy) {
    char* input = malloc(strlen(input_copy) + 1);
    strcpy(input, input_copy);

    char* tok = strtok(input, " \t");

    while (tok != NULL) {
        if (is_malformed(tok)) {
            // Vulnerable: tok points INSIDE input buffer
            free(tok);  // Heap corruption!
        }
        tok = strtok(NULL, " \t");
    }

    // Original 'input' pointer is lost if we tried to free tokens
}

// Vulnerable: Arithmetic before free
void vulnerable_arithmetic() {
    int* array = malloc(10 * sizeof(int));

    // Fill array
    for (int i = 0; i < 10; i++) {
        array[i] = i;
    }

    // Process with pointer arithmetic
    int* ptr = array;
    ptr += 5;  // Now points to middle of array

    // ... later, programmer forgets ptr was moved
    free(ptr);  // Vulnerable: Freeing middle of buffer
}
// Vulnerable: Incrementing pointer in loop then freeing
char* vulnerable_skip_whitespace(char* original) {
    char* str = malloc(100);
    strcpy(str, original);

    // Skip leading whitespace
    while (*str == ' ' || *str == '\t') {
        str++;  // Pointer moves forward
    }

    // ... use str ...
    process(str);

    // Vulnerable: str no longer points to allocation start
    free(str);  // Heap corruption

    return NULL;
}

// Vulnerable: Function returns interior pointer
char* vulnerable_find_substring(char* haystack, char* needle) {
    char* buffer = malloc(strlen(haystack) + 1);
    strcpy(buffer, haystack);

    char* found = strstr(buffer, needle);

    // Vulnerable: Returning pointer into buffer
    // Caller might try to free this pointer
    return found;  // Points inside buffer, not at start
}

void use_vulnerable() {
    char* result = vulnerable_find_substring("Hello World", "World");
    if (result) {
        printf("Found: %s\n", result);
        free(result);  // CRASH: Freeing interior pointer
    }
}

Fixed Code

// Fixed: Use index instead of pointer arithmetic
#include <stdlib.h>
#include <string.h>

char searchChar_fixed(char c) {
    char* str = malloc(20 * sizeof(char));
    if (str == NULL) return -1;

    strcpy(str, "Search Me!");

    // Fixed: Use index, preserve original pointer
    int i = 0;
    while (i < strlen(str)) {
        if (str[i] == c) {
            free(str);  // Fixed: Original pointer unchanged
            return 1;
        }
        i = i + 1;  // Index increments, not pointer
    }

    free(str);  // Fixed: Still the original pointer
    return 0;
}

// Fixed: Copy tokens instead of freeing them
void fixed_tokenize(char* input_copy) {
    char* input = malloc(strlen(input_copy) + 1);
    if (input == NULL) return;

    strcpy(input, input_copy);

    char* tok = strtok(input, " \t");

    while (tok != NULL) {
        if (!is_malformed(tok)) {
            // Fixed: Copy token to new allocation
            char* command = malloc((strlen(tok) + 1) * sizeof(char));
            if (command != NULL) {
                strcpy(command, tok);
                add_to_command_queue(command);  // Takes ownership
            }
        }
        tok = strtok(NULL, " \t");
    }

    // Fixed: Free original buffer once, at the end
    free(input);
}

// Fixed: Preserve original pointer
void fixed_arithmetic() {
    int* array = malloc(10 * sizeof(int));
    if (array == NULL) return;

    // Fill array
    for (int i = 0; i < 10; i++) {
        array[i] = i;
    }

    // Fixed: Use separate pointer for traversal
    int* ptr = array + 5;  // Temporary pointer for access

    // ... use ptr for reading ...
    process(ptr);

    // Fixed: Free original pointer
    free(array);  // Correct: freeing original allocation
}
// Fixed: Keep original pointer for cleanup
char* fixed_skip_whitespace(char* original, char** out_start) {
    char* str = malloc(100);
    if (str == NULL) return NULL;

    strcpy(str, original);

    // Fixed: Use separate pointer for traversal
    char* current = str;

    // Skip leading whitespace
    while (*current == ' ' || *current == '\t') {
        current++;
    }

    // Return both pointers
    *out_start = current;  // Where content starts
    return str;            // Original allocation (for freeing)
}

void use_fixed() {
    char* content_start;
    char* allocation = fixed_skip_whitespace("  Hello  ", &content_start);

    if (allocation) {
        process(content_start);  // Use content
        free(allocation);        // Free original pointer
    }
}

// Fixed: Return offset or copy instead of interior pointer
typedef struct {
    char* buffer;
    size_t offset;
} SubstringResult;

SubstringResult fixed_find_substring(char* haystack, char* needle) {
    SubstringResult result = {NULL, 0};

    char* buffer = malloc(strlen(haystack) + 1);
    if (buffer == NULL) return result;

    strcpy(buffer, haystack);
    char* found = strstr(buffer, needle);

    result.buffer = buffer;  // Original allocation

    if (found) {
        result.offset = found - buffer;  // Return offset, not pointer
    } else {
        result.offset = (size_t)-1;  // Not found indicator
    }

    return result;
}

void use_fixed_substring() {
    SubstringResult result = fixed_find_substring("Hello World", "World");

    if (result.buffer && result.offset != (size_t)-1) {
        printf("Found at offset %zu: %s\n",
               result.offset, result.buffer + result.offset);
    }

    free(result.buffer);  // Fixed: Free original allocation
}
// Fixed: C++ using smart pointers
#include <memory>
#include <string>
#include <vector>

class SecureStringProcessor {
public:
    bool searchChar(char c) {
        // Fixed: unique_ptr manages memory automatically
        auto str = std::make_unique<char[]>(20);
        strcpy(str.get(), "Search Me!");

        // Use the string safely
        for (int i = 0; str[i] != '\0'; i++) {
            if (str[i] == c) {
                return true;
            }
        }
        return false;
        // Memory automatically freed when str goes out of scope
    }

    // Even better: use std::string
    bool searchCharString(char c) {
        std::string str = "Search Me!";

        for (char ch : str) {
            if (ch == c) {
                return true;
            }
        }
        return false;
        // No manual memory management needed
    }
};

CVE Examples

  • CVE-2019-11930: Function internally called calloc and returned a pointer at an index inside the allocated buffer, leading to invalid memory deallocation.
  • CVE-2010-2547: Double-free vulnerability caused by freeing an interior pointer.

References

  1. MITRE Corporation. "CWE-761: Free of Pointer not at Start of Buffer." https://cwe.mitre.org/data/definitions/761.html
  2. CERT C Coding Standard. "MEM34-C. Only free memory allocated dynamically."
  3. CERT C Coding Standard. "MEM31-C. Free dynamically allocated memory when no longer needed."