Improperly Controlled Sequential Memory Allocation

Description

Improperly Controlled Sequential Memory Allocation occurs when the product manages a group of objects or resources and performs a separate memory allocation for each object, but does not properly limit the total amount of memory that is consumed by all of the combined objects. While individual allocations might be bounded, repeated allocations across multiple operations can accumulate beyond developer expectations, creating denial-of-service vectors through memory exhaustion.

Risk

Improperly controlled sequential allocation has severe implications. Total memory exhaustion possible. Out-of-memory conditions triggered. Application crashes. System-wide resource starvation. Denial of service enabled. Stack exhaustion. NULL pointer dereferences from failed allocations. Service degradation. High likelihood when aggregate limits are not enforced.

Solution

Track cumulative allocations across sessions and requests during implementation phase. Establish aggregate limits with administrative configuration options. Monitor total memory consumption across all managed objects. Enforce system-level resource limits to contain impact during operational phase. Implement memory pools with fixed maximum sizes.

Common Consequences

ImpactDetails
AvailabilityScope: Availability

Denial of service through resource consumption (memory), out-of-memory conditions, or application crashes.

Example Code

Vulnerable Code

// Vulnerable: Sequential allocation without aggregate limit

#include <stdlib.h>
#include <string.h>

#define MAX_SINGLE_ALLOC 1024  // Individual limit

// VULNERABLE: Tracks individual but not total allocations
typedef struct {
    char** strings;
    size_t count;
    // Missing: total_bytes tracking
} StringCollection;

StringCollection* create_collection() {
    StringCollection* col = malloc(sizeof(StringCollection));
    col->strings = NULL;
    col->count = 0;
    return col;
}

// VULNERABLE: No aggregate limit checking
int vulnerable_add_string(StringCollection* col, const char* str) {
    size_t len = strlen(str);

    // Individual allocation is bounded
    if (len > MAX_SINGLE_ALLOC) {
        return -1;  // Single allocation limit
    }

    // VULNERABLE: No check on total accumulated memory
    col->strings = realloc(col->strings, (col->count + 1) * sizeof(char*));
    if (!col->strings) {
        return -1;
    }

    col->strings[col->count] = malloc(len + 1);
    if (!col->strings[col->count]) {
        return -1;
    }

    strcpy(col->strings[col->count], str);
    col->count++;

    return 0;

    // Attack: Call add_string millions of times
    // Each allocation is under 1024 bytes
    // But total memory consumed is unlimited
}

// VULNERABLE: Processing loop without memory limit
void vulnerable_process_packets(void* socket) {
    while (1) {
        char* buffer;
        size_t packet_size;

        // Read packet size (attacker controlled)
        recv(socket, &packet_size, sizeof(packet_size), 0);

        // Individual limit check
        if (packet_size > 65536) {
            continue;  // Skip oversized packets
        }

        // VULNERABLE: Accumulates indefinitely
        buffer = malloc(packet_size);
        if (buffer) {
            recv(socket, buffer, packet_size, 0);
            // Store buffer for later processing
            // Never freed until processing completes
        }
    }

    // Attack: Send many packets just under 65KB each
    // Memory accumulates without limit
}
// Vulnerable: Node.js string concatenation without limit

class VulnerableDecoder {
    constructor() {
        this.buffer = '';
        // VULNERABLE: No limit on total buffer size
    }

    // VULNERABLE: Unbounded string concatenation
    appendData(chunk) {
        // Individual chunk might be small
        // But accumulated buffer grows without limit
        this.buffer += chunk;  // O(n^2) memory pattern!
    }

    // Attack: Send many small chunks
    // Buffer grows without bound, causes OOM
}

// VULNERABLE: Per-request allocations without session limit
const sessions = new Map();

function vulnerableHandleRequest(sessionId, data) {
    if (!sessions.has(sessionId)) {
        sessions.set(sessionId, { items: [] });
    }

    const session = sessions.get(sessionId);

    // Individual item limit
    if (data.length > 10000) {
        throw new Error('Item too large');
    }

    // VULNERABLE: No limit on items per session
    // VULNERABLE: No limit on total sessions
    session.items.push(data);

    // Attack:
    // 1. Create thousands of sessions
    // 2. Add thousands of items per session
    // 3. Memory exhaustion
}
# Vulnerable: Sequential file buffer allocation

class VulnerableFileProcessor:
    def __init__(self):
        self.buffers = []
        # VULNERABLE: No aggregate limit

    def load_file(self, filepath, max_file_size=1024*1024):
        """Load file with individual size limit but no aggregate limit."""
        import os

        size = os.path.getsize(filepath)

        # Individual file limit
        if size > max_file_size:
            raise ValueError("File too large")

        # VULNERABLE: Total memory not tracked
        with open(filepath, 'rb') as f:
            data = f.read()
            self.buffers.append(data)

        # Attack: Load 1000 files of 1MB each = 1GB memory
        # Each file passes individual limit check

    # VULNERABLE: Stack exhaustion via recursion
    def vulnerable_parse(self, data, depth=0):
        # No depth limit!
        if is_nested(data):
            for child in get_children(data):
                self.vulnerable_parse(child, depth + 1)  # Stack grows

        # Attack: Deeply nested data causes stack overflow

Fixed Code

// Fixed: Sequential allocation with aggregate limit

#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define MAX_SINGLE_ALLOC 1024
#define MAX_TOTAL_ALLOC (10 * 1024 * 1024)  // FIXED: 10MB aggregate limit
#define MAX_STRING_COUNT 10000               // FIXED: Count limit

typedef struct {
    char** strings;
    size_t count;
    size_t total_bytes;  // FIXED: Track total allocation
} StringCollection;

StringCollection* create_collection() {
    StringCollection* col = malloc(sizeof(StringCollection));
    if (!col) return NULL;

    col->strings = NULL;
    col->count = 0;
    col->total_bytes = 0;  // FIXED: Initialize
    return col;
}

// FIXED: Check both individual and aggregate limits
int secure_add_string(StringCollection* col, const char* str) {
    size_t len = strlen(str);

    // Individual allocation limit
    if (len > MAX_SINGLE_ALLOC) {
        return -1;
    }

    // FIXED: Count limit
    if (col->count >= MAX_STRING_COUNT) {
        return -2;  // Too many strings
    }

    // FIXED: Aggregate memory limit
    size_t new_total = col->total_bytes + len + 1 + sizeof(char*);
    if (new_total > MAX_TOTAL_ALLOC) {
        return -3;  // Aggregate limit exceeded
    }

    // Proceed with allocation
    char** new_strings = realloc(col->strings, (col->count + 1) * sizeof(char*));
    if (!new_strings) {
        return -4;
    }
    col->strings = new_strings;

    col->strings[col->count] = malloc(len + 1);
    if (!col->strings[col->count]) {
        return -4;
    }

    strcpy(col->strings[col->count], str);
    col->count++;
    col->total_bytes = new_total;  // FIXED: Update tracking

    return 0;
}

// FIXED: Packet processing with memory pool
typedef struct {
    char* pool;
    size_t pool_size;
    size_t used;
} MemoryPool;

MemoryPool* create_pool(size_t max_size) {
    MemoryPool* pool = malloc(sizeof(MemoryPool));
    if (!pool) return NULL;

    pool->pool = malloc(max_size);
    if (!pool->pool) {
        free(pool);
        return NULL;
    }

    pool->pool_size = max_size;
    pool->used = 0;
    return pool;
}

void* pool_alloc(MemoryPool* pool, size_t size) {
    // FIXED: Check against pool limit
    if (pool->used + size > pool->pool_size) {
        return NULL;  // Pool exhausted
    }

    void* ptr = pool->pool + pool->used;
    pool->used += size;
    return ptr;
}

void secure_process_packets(void* socket, size_t max_memory) {
    MemoryPool* pool = create_pool(max_memory);
    if (!pool) return;

    while (1) {
        size_t packet_size;
        recv(socket, &packet_size, sizeof(packet_size), 0);

        if (packet_size > 65536) {
            continue;
        }

        // FIXED: Allocate from bounded pool
        char* buffer = pool_alloc(pool, packet_size);
        if (!buffer) {
            // FIXED: Pool exhausted - process what we have
            process_accumulated_packets(pool);
            pool->used = 0;  // Reset pool

            buffer = pool_alloc(pool, packet_size);
            if (!buffer) continue;
        }

        recv(socket, buffer, packet_size, 0);
    }
}
// Fixed: Bounded buffer management

class SecureDecoder {
    constructor(maxBufferSize = 10 * 1024 * 1024) {
        this.buffer = '';
        this.maxSize = maxBufferSize;  // FIXED: Configurable limit
    }

    appendData(chunk) {
        // FIXED: Check before appending
        if (this.buffer.length + chunk.length > this.maxSize) {
            throw new Error('Buffer limit exceeded');
        }

        // FIXED: Use array join for efficiency
        this.chunks = this.chunks || [];
        this.chunks.push(chunk);
        this.totalSize = (this.totalSize || 0) + chunk.length;

        if (this.totalSize > this.maxSize) {
            throw new Error('Buffer limit exceeded');
        }
    }

    getBuffer() {
        return this.chunks.join('');  // More efficient than +=
    }
}

// FIXED: Session management with limits
const MAX_SESSIONS = 10000;
const MAX_ITEMS_PER_SESSION = 1000;
const MAX_TOTAL_MEMORY = 100 * 1024 * 1024;  // 100MB

class SecureSessionManager {
    constructor() {
        this.sessions = new Map();
        this.totalMemory = 0;
    }

    handleRequest(sessionId, data) {
        // FIXED: Session count limit
        if (!this.sessions.has(sessionId) && this.sessions.size >= MAX_SESSIONS) {
            throw new Error('Max sessions exceeded');
        }

        // FIXED: Individual item limit
        if (data.length > 10000) {
            throw new Error('Item too large');
        }

        // FIXED: Total memory limit
        if (this.totalMemory + data.length > MAX_TOTAL_MEMORY) {
            throw new Error('Total memory limit exceeded');
        }

        if (!this.sessions.has(sessionId)) {
            this.sessions.set(sessionId, { items: [], bytes: 0 });
        }

        const session = this.sessions.get(sessionId);

        // FIXED: Items per session limit
        if (session.items.length >= MAX_ITEMS_PER_SESSION) {
            throw new Error('Max items per session exceeded');
        }

        session.items.push(data);
        session.bytes += data.length;
        this.totalMemory += data.length;
    }

    removeSession(sessionId) {
        const session = this.sessions.get(sessionId);
        if (session) {
            this.totalMemory -= session.bytes;
            this.sessions.delete(sessionId);
        }
    }
}
# Fixed: Bounded file processing

class SecureFileProcessor:
    MAX_TOTAL_MEMORY = 100 * 1024 * 1024  # 100MB
    MAX_FILES = 1000
    MAX_RECURSION_DEPTH = 100

    def __init__(self, max_memory=None):
        self.buffers = []
        self.total_bytes = 0
        self.max_memory = max_memory or self.MAX_TOTAL_MEMORY

    def load_file(self, filepath, max_file_size=1024*1024):
        """Load file with individual and aggregate limits."""
        import os

        # FIXED: File count limit
        if len(self.buffers) >= self.MAX_FILES:
            raise MemoryError("Max file count exceeded")

        size = os.path.getsize(filepath)

        # Individual file limit
        if size > max_file_size:
            raise ValueError("File too large")

        # FIXED: Aggregate memory limit
        if self.total_bytes + size > self.max_memory:
            raise MemoryError("Total memory limit exceeded")

        with open(filepath, 'rb') as f:
            data = f.read()
            self.buffers.append(data)
            self.total_bytes += len(data)  # FIXED: Track total

    # FIXED: Recursion with depth limit
    def secure_parse(self, data, depth=0):
        # FIXED: Depth limit prevents stack exhaustion
        if depth > self.MAX_RECURSION_DEPTH:
            raise RecursionError("Max depth exceeded")

        if is_nested(data):
            for child in get_children(data):
                self.secure_parse(child, depth + 1)

    def clear(self):
        """Release all buffers."""
        self.buffers.clear()
        self.total_bytes = 0

CVE Examples

  • CVE-2020-36049: JavaScript packet decoder string concatenation causing out-of-memory.
  • CVE-2019-20176: Stack exhaustion via per-file buffer allocation without aggregate limit.
  • CVE-2013-1591: Integer overflow triggering infinite loop with unlimited sequential buffer allocation.

  • CWE-770: Allocation of Resources Without Limits or Throttling (parent)
  • CWE-789: Memory Allocation with Excessive Size Value (peer)
  • CWE-476: NULL Pointer Dereference (can follow)

References

  1. MITRE Corporation. "CWE-1325: Improperly Controlled Sequential Memory Allocation." https://cwe.mitre.org/data/definitions/1325.html
  2. OWASP. "Denial of Service Cheat Sheet"