Legacy new memory arrays can OOG instead of panicking on overlarge allocation
- 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
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