Skip to main content
Sigvex

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

  1. Untrusted source tracking: Identifies variables originating from calldata (CALLDATALOAD, CALLDATASIZE), return data (RETURNDATASIZE), or storage (SLOAD).
  2. Taint propagation: Tracks how untrusted values flow through arithmetic and unary operations.
  3. Memory operation matching: Flags MSTORE, CALLDATACOPY, CODECOPY, and RETURNDATACOPY instructions where the size or offset operand is tainted.
  4. Loop detection: Back-edge analysis identifies memory operations inside loops, which receive higher severity.
  5. 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.

References