From 61b7ab8521c909e67757b88c16efd5d0ae60f2e1 Mon Sep 17 00:00:00 2001 From: xermicus Date: Wed, 8 Jul 2026 17:41:05 +0200 Subject: [PATCH] Fix/ny fn native fmp oog (#567) Bug found by @jubnzv: `--newyork`: a full-width value stored to the free-memory-pointer slot (e.g. a function parameter `>= 2^64`) was narrowed to 64 bits, inserting a checked truncation that consumed all gas. --------- Signed-off-by: xermicus Co-authored-by: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + .../integration/contracts/FnNativeFmpBug.yul | 31 +++++++++++++++++++ crates/integration/src/tests.rs | 28 +++++++++++++++++ crates/newyork/src/type_inference.rs | 15 +++------ 4 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 crates/integration/contracts/FnNativeFmpBug.yul diff --git a/CHANGELOG.md b/CHANGELOG.md index 67c3f54..b6149f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Supported `polkadot-sdk` rev: `2604.2.0` - `--newyork`: dead-store elimination could drop a store that a later overlapping unaligned `mload` reads, returning zeroed memory instead of the stored bytes. - `--newyork`: an unaligned full-word `mload`/`mstore` overlapping a word kept native (little-endian) by the heap optimization returned byte-swapped bytes. - `--newyork`: a `calldatacopy`/`codecopy`/`returndatacopy` to a dynamic destination that could overwrite the free-memory-pointer word truncated a subsequent `mload(0x40)` to `0`. [#564](https://github.com/paritytech/revive/pull/564) +- `--newyork`: a full-width value stored to the free-memory-pointer slot (e.g. a function parameter `>= 2^64`) was narrowed to 64 bits, inserting a checked truncation that consumed all gas. [#567](https://github.com/paritytech/revive/pull/567) ## v1.3.0 diff --git a/crates/integration/contracts/FnNativeFmpBug.yul b/crates/integration/contracts/FnNativeFmpBug.yul new file mode 100644 index 0000000..2751327 --- /dev/null +++ b/crates/integration/contracts/FnNativeFmpBug.yul @@ -0,0 +1,31 @@ +/// Soundness PoC (newyork type inference): a value stored to the free-memory-pointer +/// slot `0x40` must not be narrowed to a 64-bit pointer width when it is also +/// observable at full width elsewhere. +/// +/// `f0`'s parameter `p0` is stored to the FMP slot (`mstore(64, p0)`) — which +/// inference treated as a bounded pointer and narrowed to i64 — but `p0` is also +/// stored full-width at offset 8 and read back by `mload(24)`. The i64 narrowing +/// of the parameter inserts a call-boundary guard that traps (consumes all gas) +/// when a caller passes an argument `>= 2^64`. The first call passes `b` +/// (`calldataload(32)`, a full 256-bit word), so newyork ran out of gas where EVM +/// (and the stock pipeline) execute successfully. The fix records the FMP-stored +/// value as an ordinary full-width memory value. +object "FnNativeFmpBug" { + code { datacopy(0, dataoffset("FnNativeFmpBug_deployed"), datasize("FnNativeFmpBug_deployed")) return(0, datasize("FnNativeFmpBug_deployed")) } + object "FnNativeFmpBug_deployed" { + code { + let a := calldataload(0) let b := calldataload(32) let d := calldataload(96) let r := 0 + function f0(p0, p1) -> ret { + mstore(8, p0) + mstore(64, p0) + mstore8(8, p1) + ret := mload(24) + } + r := add(mul(r, 0x100000001b3), f0(b, a)) + r := add(mul(r, 0x100000001b3), f0(2719014241053524200, d)) + r := add(mul(r, 0x100000001b3), f0(391540675041506305, b)) + mstore(0, r) + return(0, 32) + } + } +} diff --git a/crates/integration/src/tests.rs b/crates/integration/src/tests.rs index ec668cb..d99c1ef 100644 --- a/crates/integration/src/tests.rs +++ b/crates/integration/src/tests.rs @@ -4769,6 +4769,34 @@ fn array_bounds_probe() { } } +/// Regression (newyork type inference): a value stored to the free-memory-pointer +/// slot `0x40` was narrowed to i64 (treated as a bounded pointer) even when the +/// same value is observable at full width elsewhere. `f0`'s parameter `p0` is +/// stored to the FMP slot *and* full-width at offset 8 (read by `mload(24)`); the +/// i64 narrowing of the parameter inserts a call-boundary guard that traps +/// (consumes all gas) for an argument `>= 2^64`. The first call passes `b` +/// (`calldataload(32)`, a full 256-bit word), so newyork ran out of gas where EVM +/// executes successfully. The fix records the FMP-stored value as an ordinary +/// full-width memory value. Compared newyork-PVM vs solc-EVM. +#[test] +fn fn_native_fmp_store_no_spurious_trap() { + let mut actions = instantiate_yul("contracts/FnNativeFmpBug.yul", "FnNativeFmpBug"); + // Four lane-distinct words; word1 (`b`) is >= 2^64 to exercise the guard. + let mut data = Vec::new(); + for byte in [0x11u8, 0x22, 0x33, 0x44] { + data.extend_from_slice(&[byte; 32]); + } + actions.push(Call { + origin: TestAddress::Alice, + dest: TestAddress::Instantiated(0), + value: 0, + gas_limit: Some(GAS_LIMIT), + storage_deposit_limit: None, + data, + }); + run_differential(actions); +} + /// Regression (newyork dead-store elimination vs `msize`): a store whose memory /// expansion an intervening `msize()` observes must not be eliminated as dead. /// `mstore(288, 1); let r := msize(); mstore(288, 2)` — the dead-store pass diff --git a/crates/newyork/src/type_inference.rs b/crates/newyork/src/type_inference.rs index 3647954..15294f8 100644 --- a/crates/newyork/src/type_inference.rs +++ b/crates/newyork/src/type_inference.rs @@ -1051,6 +1051,7 @@ impl TypeInference { } Statement::For { condition_statements, + condition, body, post, .. @@ -1058,6 +1059,7 @@ impl TypeInference { let saved = self.in_unconditional_context; self.in_unconditional_context = false; self.collect_uses_statements(condition_statements); + self.collect_uses_expression(condition); self.collect_uses_statements(&body.statements); self.collect_uses_statements(&post.statements); self.in_unconditional_context = saved; @@ -1108,19 +1110,10 @@ impl TypeInference { /// walks nested regions so the conditional context is tracked). fn collect_uses_statement(&mut self, statement: &Statement) { match statement { - Statement::MStore { - offset, - value, - region, - } => { + Statement::MStore { offset, value, .. } => { self.record_use(offset.id, UseContext::MemoryOffset); self.narrow_from_use(offset.id, BitWidth::I64); - if *region == MemoryRegion::FreePointerSlot { - self.record_use(value.id, UseContext::MemoryOffset); - self.narrow_from_use(value.id, BitWidth::I64); - } else { - self.record_use(value.id, UseContext::MemoryValue); - } + self.record_use(value.id, UseContext::MemoryValue); } Statement::MStore8 { offset, value, .. } => { self.record_use(offset.id, UseContext::MemoryOffset);