Use of Pointer Subtraction to Determine Size

Description

Use of Pointer Subtraction to Determine Size occurs when code calculates buffer sizes or offsets by subtracting pointers without ensuring they point to the same array or memory allocation. In C/C++, pointer subtraction is only defined when both pointers reference elements within the same array (or one past the end). Subtracting pointers from different allocations produces undefined behavior. Even within the same allocation, the result might be incorrectly used if the programmer misunderstands the return type (ptrdiff_t, which is signed and may overflow).

Risk

Incorrect pointer subtraction leads to undefined behavior, integer overflows, and security vulnerabilities. When pointers from different allocations are subtracted, the result is meaningless and can cause buffer overflows when used for memory operations. Signed overflow in ptrdiff_t can wrap to negative values, causing logic errors or passing huge sizes to memory functions. Attackers can manipulate memory layouts to exploit these calculations for buffer overflows and information disclosure.

Solution

Only subtract pointers that reference elements within the same array or allocated block. Validate that both pointers are within the expected range before subtraction. Use size_t for non-negative sizes, but handle the signed-to-unsigned conversion carefully. Prefer explicit size tracking over pointer arithmetic when possible. Use address sanitizers during testing to detect invalid pointer operations. Consider using std::distance() in C++ with proper iterator validation.

Common Consequences

ImpactDetails
IntegrityScope: Memory Corruption

Incorrect size calculations lead to buffer overflows.
AvailabilityScope: Crash/DoS

Undefined behavior from invalid subtraction causes crashes.
SecurityScope: Exploitation

Manipulated sizes enable buffer overflow attacks.

Example Code + Solution Code

Vulnerable Code

// VULNERABLE: Subtracting pointers from different allocations
size_t calculate_size_vulnerable(char* start, char* end) {
    // Undefined if start and end are from different allocations!
    return end - start;
}

void process_vulnerable(char* buf1, char* buf2) {
    // buf1 and buf2 might be different allocations!
    size_t size = calculate_size_vulnerable(buf1, buf2);
    memset(buf1, 0, size);  // Potentially huge or negative size!
}

// VULNERABLE: Signed overflow in pointer difference
void copy_data_vulnerable(int* dest, int* src, int* end) {
    // If end - src is huge, ptrdiff_t might overflow
    ptrdiff_t count = end - src;

    // If count overflowed to negative, this is wrong
    if (count > 0) {
        memcpy(dest, src, count * sizeof(int));
    }
}

// VULNERABLE: Pointer comparison without same-array check
int* find_in_range_vulnerable(int* haystack_start, int* haystack_end,
                               int* search_start, int* search_end) {
    // Assumes all pointers are comparable - not guaranteed!
    for (int* p = search_start; p < search_end; p++) {
        for (int* h = haystack_start; h < haystack_end; h++) {
            if (*p == *h) {
                // Calculating offset across different arrays!
                size_t offset = h - haystack_start;
                return haystack_start + offset;
            }
        }
    }
    return NULL;
}

// VULNERABLE: Using pointer difference as array index
void index_with_diff_vulnerable(int* base, int* current) {
    // What if current is before base? Negative index!
    int* arr = base;
    arr[current - base] = 42;  // Undefined if out of bounds!
}

// VULNERABLE: Relying on pointer order
size_t span_size_vulnerable(void* a, void* b) {
    char* start = (char*)a;
    char* end = (char*)b;

    // Assumes a < b, but what if b < a?
    return end - start;  // Could be negative (wrapped to huge)!
}

// VULNERABLE: Buffer remaining calculation
size_t remaining_vulnerable(char* current, char* buffer_start, size_t buffer_size) {
    char* buffer_end = buffer_start + buffer_size;

    // What if current is outside the buffer?
    return buffer_end - current;  // Could be negative/huge!
}

// VULNERABLE: Structure with separate allocations
struct Message {
    char* data;
    char* current_pos;
};

size_t bytes_read_vulnerable(struct Message* msg) {
    // Assumes data and current_pos are in same allocation
    // But what if they were set independently?
    return msg->current_pos - msg->data;
}

// VULNERABLE: Stack vs heap confusion
void mixed_memory_vulnerable() {
    char stack_buffer[100];
    char* heap_buffer = malloc(100);

    // Completely undefined behavior!
    ptrdiff_t diff = heap_buffer - stack_buffer;
    printf("Difference: %td\n", diff);

    free(heap_buffer);
}
// VULNERABLE: C++ iterator subtraction
class VulnerableContainer {
    std::vector<int> data1;
    std::vector<int> data2;

public:
    size_t distanceBetween_vulnerable(int* p1, int* p2) {
        // Could be from different vectors!
        return p2 - p1;  // Undefined behavior!
    }

    void process_vulnerable() {
        int* it1 = data1.data();
        int* it2 = data2.data();

        // Subtracting iterators from different containers!
        size_t diff = it2 - it1;  // Undefined!
    }
};

// VULNERABLE: Template with unsafe subtraction
template<typename T>
size_t unsafe_distance(T* first, T* last) {
    // No validation that pointers are related
    return last - first;
}

// VULNERABLE: Custom allocator confusion
class VulnerableAllocator {
    char* pool1;
    char* pool2;

public:
    size_t offset_from_pool(char* ptr) {
        // Which pool is ptr from?
        // Subtracting from wrong pool is undefined!
        return ptr - pool1;  // Might be from pool2!
    }
};

Fixed Code

// SAFE: Track size explicitly, avoid pointer subtraction
struct SafeBuffer {
    char* data;
    size_t size;
    size_t used;
};

size_t remaining_safe(struct SafeBuffer* buf) {
    // No pointer subtraction needed
    return buf->size - buf->used;
}

// SAFE: Validate pointer range before subtraction
size_t calculate_size_safe(char* buffer, size_t buffer_size,
                           char* start, char* end) {
    char* buffer_end = buffer + buffer_size;

    // Validate both pointers are within buffer
    if (start < buffer || start > buffer_end) {
        return 0;  // Invalid start pointer
    }
    if (end < buffer || end > buffer_end) {
        return 0;  // Invalid end pointer
    }
    if (end < start) {
        return 0;  // Invalid range
    }

    return (size_t)(end - start);  // Now safe
}

// SAFE: Explicit bounds checking
void copy_data_safe(int* dest, size_t dest_size,
                    int* src, size_t src_size) {
    // Use explicit sizes instead of pointer subtraction
    size_t count = (src_size < dest_size) ? src_size : dest_size;
    memcpy(dest, src, count * sizeof(int));
}

// SAFE: Assert same allocation (debug builds)
#include <assert.h>

size_t array_span_safe(int* arr, size_t arr_size, int* p1, int* p2) {
    int* arr_end = arr + arr_size;

    // Validate both pointers are in the same array
    assert(p1 >= arr && p1 <= arr_end);
    assert(p2 >= arr && p2 <= arr_end);

    // Ensure correct order
    if (p2 < p1) {
        int* temp = p1;
        p1 = p2;
        p2 = temp;
    }

    return (size_t)(p2 - p1);
}

// SAFE: Using offset instead of pointer subtraction
struct SafeMessage {
    char* data;
    size_t data_size;
    size_t current_offset;  // Offset, not pointer!
};

size_t bytes_read_safe(struct SafeMessage* msg) {
    return msg->current_offset;  // No subtraction needed!
}

char* current_position_safe(struct SafeMessage* msg) {
    return msg->data + msg->current_offset;
}

void advance_safe(struct SafeMessage* msg, size_t bytes) {
    if (msg->current_offset + bytes <= msg->data_size) {
        msg->current_offset += bytes;
    }
}

// SAFE: Bounded buffer with explicit tracking
struct BoundedBuffer {
    char* base;
    size_t capacity;
    size_t position;
};

int write_to_buffer_safe(struct BoundedBuffer* buf, const char* data, size_t len) {
    // Check space available
    size_t remaining = buf->capacity - buf->position;

    if (len > remaining) {
        return -1;  // Not enough space
    }

    memcpy(buf->base + buf->position, data, len);
    buf->position += len;

    return 0;  // Success
}

// SAFE: Pointer range with validation function
typedef struct {
    void* start;
    void* end;
    size_t element_size;
} ValidatedRange;

int validate_range(ValidatedRange* range, void* base, size_t count, size_t elem_size) {
    char* base_end = (char*)base + count * elem_size;

    if (range->start < base || range->start > base_end) return 0;
    if (range->end < base || range->end > base_end) return 0;
    if (range->end < range->start) return 0;

    return 1;  // Valid
}

size_t range_size_bytes(ValidatedRange* range) {
    return (char*)range->end - (char*)range->start;
}
// SAFE: C++ with proper container usage
class SafeContainer {
    std::vector<int> data;

public:
    // Use iterators from same container
    size_t distanceSafe(std::vector<int>::iterator first,
                        std::vector<int>::iterator last) {
        // std::distance validates iterators from same container (debug mode)
        return std::distance(first, last);
    }

    // Better: use indices
    int& at(size_t index) {
        return data.at(index);  // Bounds-checked
    }

    size_t size() const {
        return data.size();  // No pointer math
    }
};

// SAFE: Span with built-in bounds
#include <span>

class SafeSpan {
public:
    size_t process(std::span<int> data) {
        // Span knows its own size
        return data.size();  // No pointer subtraction
    }

    void iterate(std::span<int> data) {
        for (size_t i = 0; i < data.size(); i++) {
            data[i] = static_cast<int>(i);
        }
    }
};

// SAFE: Custom range class with validation
template<typename T>
class ValidRange {
    T* base_;
    size_t size_;
    size_t start_offset_;
    size_t end_offset_;

public:
    ValidRange(T* base, size_t size, size_t start = 0, size_t end = 0)
        : base_(base), size_(size), start_offset_(start),
          end_offset_(end == 0 ? size : end) {
        if (start_offset_ > size_ || end_offset_ > size_) {
            throw std::out_of_range("Invalid range offsets");
        }
        if (end_offset_ < start_offset_) {
            throw std::invalid_argument("End before start");
        }
    }

    size_t length() const {
        return end_offset_ - start_offset_;  // Safe: same allocation, validated
    }

    T* begin() { return base_ + start_offset_; }
    T* end() { return base_ + end_offset_; }
};

// SAFE: Allocator with tracked regions
class SafeAllocator {
    std::vector<std::pair<char*, size_t>> allocations_;

public:
    char* allocate(size_t size) {
        char* ptr = new char[size];
        allocations_.emplace_back(ptr, size);
        return ptr;
    }

    // Validate pointer belongs to an allocation
    std::optional<size_t> offset_in_allocation(char* ptr) {
        for (const auto& [base, size] : allocations_) {
            if (ptr >= base && ptr < base + size) {
                return ptr - base;  // Safe: validated same allocation
            }
        }
        return std::nullopt;  // Not from our allocations
    }

    bool same_allocation(char* p1, char* p2) {
        for (const auto& [base, size] : allocations_) {
            bool p1_in = (p1 >= base && p1 <= base + size);
            bool p2_in = (p2 >= base && p2 <= base + size);
            if (p1_in && p2_in) return true;
            if (p1_in || p2_in) return false;  // One in, one not
        }
        return false;
    }

    ptrdiff_t safe_subtract(char* p1, char* p2) {
        if (!same_allocation(p1, p2)) {
            throw std::invalid_argument("Pointers from different allocations");
        }
        return p1 - p2;
    }
};

// SAFE: Use std::addressof and explicit size tracking
template<typename Container>
class SafeIterator {
    Container* container_;
    size_t index_;

public:
    SafeIterator(Container& c, size_t i = 0)
        : container_(&c), index_(i) {}

    size_t operator-(const SafeIterator& other) const {
        if (container_ != other.container_) {
            throw std::invalid_argument("Iterators from different containers");
        }
        return index_ - other.index_;
    }
};

Exploited in the Wild

Heap Spray Attacks

Attackers have manipulated heap layouts to make pointer subtraction return attacker-controlled values, enabling heap overflow exploits.

Integer Overflow Exploits

Large pointer differences causing ptrdiff_t overflow have been exploited to bypass size checks and overflow buffers.

Memory Disclosure

Invalid pointer subtraction has been used to calculate offsets that leak memory contents from adjacent allocations.


Tools to test/exploit


CVE Examples

  • CVE-2016-9066 — Firefox integer overflow in pointer calculation.

  • CVE-2018-16395 — Ruby pointer subtraction vulnerability.

  • Multiple CVEs involving integer overflow in size calculations from pointer differences.


References

  1. MITRE. "CWE-469: Use of Pointer Subtraction to Determine Size." https://cwe.mitre.org/data/definitions/469.html

  2. CERT C. "ARR36-C: Do not subtract or compare two pointers that do not refer to the same array." https://wiki.sei.cmu.edu/confluence/display/c/