Skip to main content
Sigvex

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.

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.

References