Remediating Memory Expansion DoS
How to prevent out-of-gas denial of service by bounding memory allocation size in functions that process untrusted input.
Remediating Memory Expansion DoS
Overview
Related Detector: Memory Expansion DoS
Memory expansion DoS occurs when a function allocates memory based on untrusted input without bounds checking. EVM memory costs grow quadratically, so an attacker who controls the allocation size can force out-of-gas reverts. The fix is to validate the size parameter against a maximum constant before any memory operation.
Recommended Fix
Bound the Copy/Allocation Size
// BEFORE: Unbounded calldata copy
function processPayload(bytes calldata payload) external {
bytes memory data = payload; // Copies payload.length bytes to memory
_process(data);
}
// AFTER: Bounded copy size
uint256 constant MAX_PAYLOAD = 10_000; // 10 KB
function processPayload(bytes calldata payload) external {
require(payload.length <= MAX_PAYLOAD, "Payload too large");
bytes memory data = payload;
_process(data);
}
Alternative Mitigations
Process Data In-Place Using Calldata
Avoid copying to memory entirely by working directly with calldata slices:
function processItems(uint256[] calldata items) external {
require(items.length <= MAX_BATCH, "Too many items");
for (uint256 i = 0; i < items.length; i++) {
// Read directly from calldata -- no memory allocation
_processItem(items[i]);
}
}
Pagination for Large Datasets
Split large operations into bounded batches:
uint256 constant PAGE_SIZE = 50;
function processBatch(uint256 offset) external {
uint256 end = offset + PAGE_SIZE;
if (end > totalItems) end = totalItems;
for (uint256 i = offset; i < end; i++) {
_processItem(i);
}
}
Assembly-Level Bounds Checking
When using inline assembly for data copies, validate the size before the operation:
uint256 constant MAX_COPY = 10_000;
function copyData(uint256 offset, uint256 size) external {
require(size <= MAX_COPY, "Copy size exceeds limit");
assembly {
let dest := mload(0x40)
calldatacopy(dest, offset, size)
mstore(0x40, add(dest, size))
}
}
Common Mistakes
Mistake: Checking Length After Allocation
function processData(bytes calldata data) external {
bytes memory copy = data; // Memory already allocated here
require(copy.length <= MAX_SIZE, "Too large"); // Too late
}
The require must precede any memory allocation. Solidity allocates memory when assigning calldata to a memory variable.
Mistake: Bounding Array Length but Not Element Size
function processBatch(bytes[] calldata items) external {
require(items.length <= 100, "Too many");
for (uint256 i = 0; i < items.length; i++) {
bytes memory item = items[i]; // Each item can be arbitrarily large
_process(item);
}
}
Bound both the array length and the individual element sizes.