argotorg / argotorg/solidity

Yul optimizer pass UnusedStoreEliminator (S) + optimizeStackAllocation=false produces bytecode that hits INVALID opcode at runtime

Open
#16,642 2 comments 0 reactions 1 assignee Claimed by @cameel View on GitHub
Dominant language
C++
Stars
25.7k
Forks
6.2k
Avg merge
1d 11h
Merged PRs (30d)
21

Description

## Summary

Running the Yul optimizer with the single pass `S` (`UnusedStoreEliminator`) and
`optimizeStackAllocation = false` produces bytecode that, when deployed and
called with empty calldata, hits the `INVALID` opcode (`0xfe`) at PC 95. The
unoptimized version, the SSACFG version, and the legacy version with
`optimizeStackAllocation = true` all return `SUCCESS` on the same input. So the
combination `{ steps = "S", optimizeStackAllocation = false }` is what triggers it.

Found by `yul_proto_ossfuzz_evmone_single_pass_S` in
[argotorg/solidity-fuzzing](https://github.com/argotorg/solidity-fuzzing).

## Versions / commits

- `solidity-fuzzing`: `f81c9ae67eba82f7a1fc47fcc4a8d4973abe5081`
- `solidity` submodule: `b83005c900d356e82f4d2da52be1601e1d8b3539`

## Reproducer (`bad.yul`)

```yul
{
pop(memoryguard(0x10000))
sstore(mload(mod(calldataload(0), 65504)), 1)
{
sstore(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
function foo_m_0(x_3, x_4, x_5, x_6) -> x_7, x_8, x_9
{
function foo_s_11(x_12, x_13) -> x_14
{
function foo_m_16() -> x_17, x_18, x_19, x_20
{
}
let x_22, x_23, x_24, x_25 := foo_m_16()
mstore(256, x_22)
mstore(0, x_23)
mstore(64, x_24)
mstore(128, x_25)
}
let x_35 := foo_s_11(mload(mod(0x10000000000000000000000, 65504)),calldataload(32))
sstore(96, x_35)
{
}
}
let x_44, x_45, x_46 := foo_m_0(calldataload(288),sload(32),calldataload(96),sload(160))
sstore(224, x_44)
sstore(288, x_45)
sstore(32, x_46)
sstore(0x1000000000000000000000000000000000000000000000, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
function foo_m_1() -> x_63, x_64, x_65, x_66
{
}
let x_68, x_69, x_70, x_71 := foo_m_1()
mstore(128, x_68)
mstore(192, x_69)
mstore(256, x_70)
mstore(0, x_71)
}
}
```

## Failing optimizer settings

- `yulOptimiserSteps = "S"` (UnusedStoreEliminator)
- `yulOptimiserCleanupSteps = ""` *or* `"fDnTOcmuO"` (default) — both reproduce
- `optimizeStackAllocation = false`
- EVM version: `osaka` (latest)

## Reproduce with `yul_debug_runner` (recommended path)

```bash
# from the solidity-fuzzing repo, with both build trees built (build/ and build_ossfuzz/):
./build/tools/runners/yul_debug_runner --optimizer-sequence S bad.yul
```

Diagnostic excerpt:

```
--- Comparing optimized_legacy_no_stack_alloc vs optimized_legacy ---
Status: DIFFER (INVALID_INSTRUCTION vs SUCCESS)
[optimized_legacy_no_stack_alloc] failure at PC=95 (0x5f) opcode=0xfe (INVALID) gas=303673 stack_height=0 status=INVALID_INSTRUCTION
bytecode window [79...111] / 128 bytes:
20 60 60 5f 39 5f 51 90 5f 52 60 01 60 b4 1b 55 fe 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
^^
```

The 4 configs the runner exercises (note which ones differ):

| Config | `runYulOptimiser` | steps | optimizeStackAllocation | viaSSACFG | Status |
| --------------------------------- | ----------------- | ----- | ----------------------- | --------- | ------ |
| `unoptimized` | false | — | false | false | SUCCESS |
| `optimized_legacy` | true | `S` | **true** | false | SUCCESS |
| `optimized_ssacfg` | true | `S` | true | true | SUCCESS |
| `optimized_legacy_no_stack_alloc` | true | `S` | **false** | false | **INVALID_INSTRUCTION** |

## Reproduce with the fuzzer binary (ground truth)

```bash
# Dump the Yul source the fuzzer used:
PROTO_FUZZER_DUMP_PATH=bad.yul \
./build_ossfuzz/tools/ossfuzz/yul_proto_ossfuzz_evmone_single_pass_S \
crash-86860c0cccd70c15d27d6136bc733216e42a652e

# Replay (will abort with InternalCompilerError wrapping the differential):
./build_ossfuzz/tools/ossfuzz/yul_proto_ossfuzz_evmone_single_pass_S \
crash-86860c0cccd70c15d27d6136bc733216e42a652e
```

The fuzzer uses `yulOptimiserSteps = "S"`, cleanup = `""`,
`optimizeStackAllocation = false`. Run A (steps = `""`) returns `EVMC_SUCCESS`;
Run B (steps = `"S"`) returns `EVMC_INVALID_INSTRUCTION` (4).

## Crash artifact

`crash-86860c0cccd70c15d27d6136bc733216e42a652e` is the libFuzzer crash input
that produced `bad.yul` above. Available on request — the protobuf is binary
and tied to the fuzzer's grammar revision, so the `bad.yul` source above is
the durable reproducer.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.