argotorg / argotorg/solidity

Legacy new memory arrays can OOG instead of panicking on overlarge allocation

Open
#16,837 3 comments 0 reactions 0 assignees View on GitHub
low effort low impact nice to have
Dominant language
C++
Stars
25.7k
Forks
6.2k
Avg merge
2d 19h
Merged PRs (30d)
29

Description

## Environment

- Compiler version: `0.8.36-develop.2026.6.25+commit.6deed02c` (argotorg/solidity `develop`, commit `6deed02c`, 2026-06-25)
- Built from source; Linux
- **Verified to reproduce on this commit.**

## Summary

The legacy code generator for `new T[](length)` checks only that `length <=
2**64 - 1`. It then multiplies the length by the memory element stride, adds the
array header, and stores the resulting free-memory pointer without the
allocation finalization guard used by the IR/Yul backend.

For value arrays, `length == type(uint64).max` passes the legacy check, but the
requested allocation size is larger than the compiler's `0xffffffffffffffff`
memory-resource limit. The generated code proceeds into a huge zero-initializing
memory operation and can run out of gas instead of reverting with
`Panic(0x41)`.

This affects the legacy, non-`--via-ir` backend.

## Evidence

`ExpressionCompiler::visit(FunctionCall)` handles
`FunctionType::Kind::ObjectCreation` directly:

```cpp
m_context << u256(0xffffffffffffffff);
m_context << Instruction::DUP2;
m_context << Instruction::GT;
m_context.appendConditionalPanic(PanicCode::ResourceError);
...
m_context << arrayType.baseType()->memoryHeadSize() << Instruction::MUL;
m_context << u256(32) << Instruction::ADD;
m_context << Instruction::DUP3 << Instruction::ADD;
utils().storeFreeMemoryPointer();
```

`CompilerUtils::storeFreeMemoryPointer()` is a raw `mstore(0x40, value)`, so it
does not perform the `newFreePtr <= 0xffffffffffffffff` and overflow checks that
`YulUtilFunctions::finalizeAllocationFunction()` performs in the via-IR path.

Reproducer:

```solidity
pragma solidity ^0.8.30;

contract C {
function f() external pure returns (uint256[] memory) {
return new uint256[](type(uint64).max);
}
}
```

Legacy assembly for the allocation contains the length-only guard and then the
unchecked allocation-size arithmetic:

```text
0xffffffffffffffff
dup2
gt
iszero
jumpi(tag_9)
...
mload(0x40)
swap1
dup1
dup3
mstore
dup1
0x20
mul
0x20
add
dup3
add
0x40
mstore
...
calldatacopy
```

The same source with `--via-ir --ir-optimized` uses the guarded allocation
finalizer:

```yul
function finalize_allocation(memPtr, size)
{
let newFreePtr := add(memPtr, round_up_to_mul_of(size))
if or(gt(newFreePtr, 0xffffffffffffffff), lt(newFreePtr, memPtr)) { panic_error_0x41() }
mstore(64, newFreePtr)
}
```

## Runtime reproduction (Foundry)

Compiling this source with the from-source compiler
(`0.8.36-develop.2026.6.25+commit.d8fbf367`, built from source) into both legacy
and via-IR bytecode, deploying each raw, and calling `f()` under a fixed
2,000,000-gas stipend in the EVM:

```text
[PASS] test_via_ir_reverts_with_panic_0x41_cheaply()
via-IR f() reverted?: yes
via-IR returndata: 0x4e487b71…0041 (Panic(0x41))
via-IR gas used (of 2,000,000): 1360
[PASS] test_legacy_runs_out_of_gas_instead_of_panicking()
legacy f() reverted?: yes
legacy returndata length: 0
legacy gas used (of 2,000,000): 2000619
```

The two backends produce two different failure modes for the same source: via-IR
reverts cheaply (~1,360 gas) with `Panic(0x41)`, while legacy consumes the entire
forwarded stipend and returns empty data — i.e. out-of-gas, not a catchable
panic. Full repro and `repro.sh` in `repros/014/`.

## Impact

High-level Solidity can request this allocation without invalid storage or
inline assembly. The legacy backend then performs an impossible memory write and
is expected to exhaust gas rather than producing the specified memory resource
panic. This is a backend semantic mismatch and an observability/catchability
issue for callers that distinguish `Panic(0x41)` from an out-of-gas failure.

The required allocation is deliberately enormous, so this is not a practical
memory corruption primitive.

## Recommendation

Route legacy object creation through the same allocation finalization check used
by generated Yul, or explicitly check the computed allocation size and final
free-memory pointer before storing `0x40`. Add a legacy codegen semantic test for
`new uint256[](type(uint64).max)` that expects `Panic(0x41)` instead of
out-of-gas behavior.

## Steps to reproduce

Save the source(s) below, then run:

```bash
solc --asm repro_legacy_new_array_memory_allocation_overflow.sol
```

`repro_legacy_new_array_memory_allocation_overflow.sol`:

```solidity
pragma solidity ^0.8.30;

contract C {
function f() external pure returns (uint256[] memory) {
return new uint256[](type(uint64).max);
}
}
```

Contributor guide

Open the contributing guide

Research direction

Start at ExpressionCompiler::visit(FunctionCall) for legacy ObjectCreation and compare its allocation sequence with YulUtilFunctions::finalizeAllocationFunction(). Inspect CompilerUtils::storeFreeMemoryPointer() and the repros/014 reproduction first. Done means the legacy new uint256[](type(uint64).max) path produces catchable Panic(0x41), with a semantic test covering the behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, solidity
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
56/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.