Memory Expansion DoS
Detects unbounded memory allocation patterns that can cause out-of-gas denial of service attacks.
Memory Expansion DoS
Overview
The memory expansion DoS detector identifies functions where memory allocation size is derived from untrusted input without bounds checking. In the EVM, memory costs scale quadratically after 724 bytes: the gas cost formula is memory_cost = memory_size_word^2 / 512 + 3 * memory_size_word. An attacker who controls the size parameter of CALLDATACOPY, RETURNDATACOPY, or MSTORE in a loop can force a transaction to consume all available gas.
This vulnerability class causes an estimated $20M+ in annual losses from out-of-gas DoS attacks, particularly in batch processing and data copying functions.
Why This Is an Issue
Unlike storage operations with fixed gas costs, memory expansion gas grows quadratically. A function that copies user-supplied calldata to memory using calldatacopy(dest, offset, msg.data.length) without bounding msg.data.length allows an attacker to send a transaction with a very large calldata payload. The gas consumed by the memory expansion alone can exceed the block gas limit, making the function permanently uncallable for that input.
When this pattern appears inside a loop, each iteration expands memory further, compounding the gas cost.
How to Resolve
// Before: Vulnerable -- unbounded calldata copy
function processData(bytes calldata data) external {
bytes memory copy;
assembly {
let size := calldatasize()
copy := mload(0x40)
calldatacopy(copy, 0, size) // Unbounded memory allocation
}
}
// After: Fixed -- bounded copy size
uint256 constant MAX_DATA_SIZE = 10000;
function processData(bytes calldata data) external {
require(data.length <= MAX_DATA_SIZE, "Data too large");
bytes memory copy = data; // Solidity handles bounded copy
}
Examples
Vulnerable
function batchStore(uint256[] calldata values) external {
uint256[] memory temp = new uint256[](values.length); // Unbounded
for (uint256 i = 0; i < values.length; i++) {
temp[i] = values[i] * 2;
}
}
Fixed
uint256 constant MAX_BATCH = 100;
function batchStore(uint256[] calldata values) external {
require(values.length <= MAX_BATCH, "Batch too large");
uint256[] memory temp = new uint256[](values.length);
for (uint256 i = 0; i < values.length; i++) {
temp[i] = values[i] * 2;
}
}
Sample Sigvex Output
[HIGH] memory-expansion-dos
Unbounded memory allocation inside a loop in batchStore
Location: batchStore @ block 1, instruction 3
Confidence: 0.75
Function batchStore contains Memory store (MSTORE) inside a loop
that uses untrusted input to control memory allocation size.
Detection Methodology
- Untrusted source tracking: Identifies variables originating from calldata (
CALLDATALOAD,CALLDATASIZE), return data (RETURNDATASIZE), or storage (SLOAD). - Taint propagation: Tracks how untrusted values flow through arithmetic and unary operations.
- Memory operation matching: Flags
MSTORE,CALLDATACOPY,CODECOPY, andRETURNDATACOPYinstructions where the size or offset operand is tainted. - Loop detection: Back-edge analysis identifies memory operations inside loops, which receive higher severity.
- Context modifiers: Confidence is reduced for access-controlled or pausable contracts, standard ERC-20 functions, and admin-only operations.
Limitations
False positives: Functions that validate the size parameter through a require statement before the assembly block may still be flagged if the bounds check is in a different basic block than the memory operation. False negatives: Memory expansion via intermediate helper functions that perform the actual copy are not traced cross-function.
Related Detectors
- DoS — detects general denial of service patterns
- Loop Gas Exhaustion — detects unbounded loops
- Controlled Array Length — detects unbounded array iteration