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
| Impact | Details |
|---|---|
| Availability | Scope: 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.
Related CWEs
- 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
- MITRE Corporation. "CWE-1325: Improperly Controlled Sequential Memory Allocation." https://cwe.mitre.org/data/definitions/1325.html
- OWASP. "Denial of Service Cheat Sheet"