From 4bbae6603b79a848a48cdd28338af2a1aa138649 Mon Sep 17 00:00:00 2001 From: xermicus Date: Tue, 22 Sep 2026 13:21:22 +0200 Subject: [PATCH 1/3] NY: flag mcopy and call return ranges that clobber the free pointer `taint_copy_destination` was the only writer of `fmp_could_be_unbounded` for copy shaped writes, and neither the `MCopy` nor the `ExternalCall` arm of the heap analysis reached it. An `mcopy` destination or a call return range covering `[0x40, 0x60)` therefore tainted the word, which only disables the native mode read, while the `FMP < heap_size` range proof stayed active and truncated the clobbered `mload(0x40)`. The coverage check now lives in `flag_write_covering_fmp`, which the copy opcodes, `mcopy` and the external call return range all consult. A write of statically zero length never covers the slot, so solc's `call(.., pos, 0)` followed by `returndatacopy` keeps the optimization. Regression fixture `CopyFmpBug.yul` covers an `mcopy` onto a calldata supplied destination and a `staticcall` returning into `0x40`. Fixes paritytech/bugbounty_reports#216 Co-Authored-By: Claude Fable 5.1 --- crates/integration/contracts/CopyFmpBug.yul | 30 ++++ crates/integration/src/tests.rs | 27 ++++ crates/newyork/src/heap_opt.rs | 148 ++++++++++++++++---- 3 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 crates/integration/contracts/CopyFmpBug.yul diff --git a/crates/integration/contracts/CopyFmpBug.yul b/crates/integration/contracts/CopyFmpBug.yul new file mode 100644 index 00000000..0a7b8608 --- /dev/null +++ b/crates/integration/contracts/CopyFmpBug.yul @@ -0,0 +1,30 @@ +/// Regression (newyork FMP range proof): an `mcopy` destination or an external call's +/// return range covering the free-memory-pointer word `[0x40, 0x60)` replaces the +/// pointer with arbitrary bytes, but neither statement flagged the pointer as possibly +/// unbounded. The `FMP < heap_size` range proof then truncated the clobbered +/// `mload(0x40)` to the heap size width instead of returning the copied word. +/// +/// Case 1 copies the calldata word at 32 onto a calldata-supplied destination (`0x40` +/// in the test). Case 2 calls the contract itself, which answers with `not(0)`, and +/// returns that word into `0x40`. Both cases then read the pointer back. +object "CopyFmpBug" { + code { datacopy(0, dataoffset("CopyFmpBug_deployed"), datasize("CopyFmpBug_deployed")) return(0, datasize("CopyFmpBug_deployed")) } + object "CopyFmpBug_deployed" { + code { + switch calldataload(0) + case 1 { + mstore(0x80, calldataload(32)) + mcopy(and(calldataload(64), 0xff), 0x80, 0x20) + } + case 2 { + if iszero(staticcall(gas(), address(), 0, 0, 0x40, 0x20)) { revert(0, 0) } + } + default { + mstore(0, not(0)) + return(0, 32) + } + mstore(0, mload(0x40)) + return(0, 32) + } + } +} diff --git a/crates/integration/src/tests.rs b/crates/integration/src/tests.rs index 2aaff9a9..9de7ddeb 100644 --- a/crates/integration/src/tests.rs +++ b/crates/integration/src/tests.rs @@ -4984,6 +4984,33 @@ fn calldatacopy_dynamic_dest_fmp_corruption() { run_differential(actions); } +/// Regression (newyork FMP range proof): an `mcopy` destination or an external +/// call's return range covering the free-memory-pointer word `[0x40, 0x60)` +/// clobbers the pointer, but neither flagged it as possibly unbounded, so the +/// surviving `FMP < heap_size` range proof truncated the clobbered `mload(0x40)`. +/// Case 1 copies a calldata word onto `0x40` with `mcopy`; case 2 returns the +/// contract's own `not(0)` answer into `0x40`. Compared newyork-PVM vs solc-EVM. +#[test] +fn copy_onto_fmp_word_disables_range_proof() { + let copied_word: U256 = (U256::from(1u64) << 200) | U256::from(0xabcdefu64); + let mut mcopy_case = U256::from(1).to_be_bytes::<32>().to_vec(); + mcopy_case.extend_from_slice(&copied_word.to_be_bytes::<32>()); + mcopy_case.extend_from_slice(&U256::from(0x40).to_be_bytes::<32>()); + let staticcall_case = U256::from(2).to_be_bytes::<32>().to_vec(); + for data in [mcopy_case, staticcall_case] { + let mut actions = instantiate_yul("contracts/CopyFmpBug.yul", "CopyFmpBug"); + 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): a store read back by an /// intervening unaligned *overlapping* load must not be eliminated as dead. /// `mem_opt` marked a pending store read only on an exact-offset load, so diff --git a/crates/newyork/src/heap_opt.rs b/crates/newyork/src/heap_opt.rs index 01219842..7f5cbb05 100644 --- a/crates/newyork/src/heap_opt.rs +++ b/crates/newyork/src/heap_opt.rs @@ -363,6 +363,7 @@ impl HeapAnalysis { let destination_start = self.extract_static_offset(destination); let source_start = self.extract_static_offset(source); let len = self.extract_static_offset(length); + self.flag_write_covering_fmp(destination, length); self.taint_range(destination_start, len); self.taint_range(source_start, len); } @@ -376,6 +377,7 @@ impl HeapAnalysis { } => { self.mark_escaping_range(args_offset, args_length); self.note_fmp_coverage(args_offset, args_length); + self.flag_write_covering_fmp(ret_offset, ret_length); self.mark_escaping_and_tainted_range(ret_offset, ret_length); self.note_fmp_coverage(ret_offset, ret_length); } @@ -589,50 +591,60 @@ impl HeapAnalysis { } } - /// Taints the destination of a copy opcode (`calldatacopy`, `codecopy`, - /// `returndatacopy`, …), which writes big-endian bytes that a later native - /// (little-endian) `mload` must not byte-reverse. + /// Flags the free-memory-pointer slot as possibly unbounded when a raw byte write of + /// `length` bytes to `destination` (a copy opcode's destination or an external call's + /// return range) can clobber it. /// - /// When the length is statically known, every word the copy covers is tainted - /// — not just the start word — so a multi-word copy can't leave a later word a - /// native candidate. When the length is dynamic we taint only the start word - /// and deliberately do NOT set `has_dynamic_accesses`: doing so would disable - /// native mode for the entire contract (e.g. every ABI-decode `calldatacopy`), - /// a large code-size regression for no soundness gain over the existing - /// dynamic-offset guards. - /// Records the memory tainted by a copy (`calldatacopy`/`codecopy`/`mcopy`/…) with - /// destination `destination` and length `length`, and flags the free-memory-pointer slot as - /// possibly unbounded when the copy can clobber it. - /// - /// A copy that can overwrite the FMP slot `[0x40, 0x60)` replaces the free-memory + /// A write that can overwrite the FMP slot `[0x40, 0x60)` replaces the free-memory /// pointer with arbitrary, possibly out-of-range bytes. Downstream codegen that assumes /// `FMP < heap_size` (the narrow `mload(0x40)` read and its range proof) would then - /// mis-read the corrupted value, so such a copy sets `fmp_could_be_unbounded` — exactly + /// mis-read the corrupted value, so such a write sets `fmp_could_be_unbounded` — exactly /// as an untrusted `mstore(0x40, ...)` does. Tainting word 0x40 alone only disables the /// *native-mode* FMP read; the FMP *range proof* in `to_llvm` is gated on /// `fmp_could_be_unbounded`, so that flag must be set too or the proof silently /// truncates the clobbered value. /// - /// `covers_fmp` is kept deliberately narrow to avoid a code-size regression: only a - /// static destination+length that provably overlap the slot, or a static destination - /// *inside* the FMP word with a dynamic length (whose first byte(s) land in the slot), - /// flag unboundedness. A fully-dynamic destination, or a static destination *outside* - /// the word (proxy `calldatacopy(0, 0, size)`, OZ's FMP-relative ABI-decode copies to - /// `mload(0x40) >= 0x80`), is left to `has_dynamic_accesses` / the native-mode guards. - fn taint_copy_destination(&mut self, destination: &Value, length: &Value) { + /// The check is kept deliberately narrow to avoid a code-size regression: a write of + /// statically zero length never covers the slot (solc passes a zero return length to + /// `call` when it fetches the return data with `returndatacopy`), and only a static + /// destination+length that provably overlap the slot, a static destination *inside* the + /// FMP word with a dynamic length (whose first byte(s) land in the slot), or a dynamic + /// destination that is not provably free-pointer-relative flag unboundedness. A static + /// destination *outside* the word (proxy `calldatacopy(0, 0, size)`, OZ's FMP-relative + /// ABI-decode copies to `mload(0x40) >= 0x80`) is left to `has_dynamic_accesses` / the + /// native-mode guards. + fn flag_write_covering_fmp(&mut self, destination: &Value, length: &Value) { let destination_start = self.extract_static_offset(destination); let len = self.extract_static_offset(length); let covers_fmp = match (destination_start, len) { - (Some(address), Some(size)) => { - size > 0 && address < 0x60 && address.saturating_add(size) > 0x40 - } + (_, Some(0)) => false, + (Some(address), Some(size)) => address < 0x60 && address.saturating_add(size) > 0x40, (Some(address), None) => (0x40..0x60).contains(&address), (None, _) => !self.is_free_pointer_relative(destination.id.0), }; if covers_fmp { self.fmp_could_be_unbounded = true; } + } + + /// Taints the destination of a copy opcode (`calldatacopy`, `codecopy`, + /// `returndatacopy`, …), which writes big-endian bytes that a later native + /// (little-endian) `mload` must not byte-reverse, and flags the free-memory-pointer + /// slot as possibly unbounded when the copy can clobber it. + /// + /// When the length is statically known, every word the copy covers is tainted + /// — not just the start word — so a multi-word copy can't leave a later word a + /// native candidate. When the length is dynamic we taint only the start word + /// and deliberately do NOT set `has_dynamic_accesses`: doing so would disable + /// native mode for the entire contract (e.g. every ABI-decode `calldatacopy`), + /// a large code-size regression for no soundness gain over the existing + /// dynamic-offset guards. + fn taint_copy_destination(&mut self, destination: &Value, length: &Value) { + let destination_start = self.extract_static_offset(destination); + let len = self.extract_static_offset(length); + + self.flag_write_covering_fmp(destination, length); match (destination_start, len) { (Some(address), Some(size)) => { @@ -2004,6 +2016,90 @@ mod tests { ); } + /// Builds an `mcopy(destination, 0x80, length)` with the operands bound to value IDs 10 + /// (`destination`), 11 (`0x80`) and 12 (`length`); `setup` binds the destination. + fn object_with_mcopy(setup: Vec, length: u64) -> Object { + use crate::ir::ValueId; + let mut statements = setup; + statements.push(literal(11, 0x80)); + statements.push(literal(12, length)); + statements.push(Statement::MCopy { + destination: Value::int(ValueId(10)), + source: Value::int(ValueId(11)), + length: Value::int(ValueId(12)), + }); + object_with_code(statements, vec![]) + } + + /// Builds a `staticcall` returning `return_length` bytes into `return_offset`, with the + /// operands bound to value IDs 10 (`return_offset`), 11 (`return_length`) and 12 (zero); + /// `setup` binds the return offset. + fn object_with_external_call(setup: Vec, return_length: u64) -> Object { + use crate::ir::{CallKind, ValueId}; + let mut statements = setup; + statements.push(literal(11, return_length)); + statements.push(literal(12, 0)); + statements.push(Statement::ExternalCall { + kind: CallKind::StaticCall, + gas: Value::int(ValueId(12)), + address: Value::int(ValueId(12)), + value: None, + args_offset: Value::int(ValueId(12)), + args_length: Value::int(ValueId(12)), + ret_offset: Value::int(ValueId(10)), + ret_length: Value::int(ValueId(11)), + result: ValueId(13), + }); + object_with_code(statements, vec![]) + } + + /// An `mcopy` onto the FMP word replaces the pointer with arbitrary bytes, so it must + /// disable the range proof like every other copy does. + #[test] + fn mcopy_onto_fmp_word_flags_unbounded() { + let results = object_with_mcopy(vec![literal(10, 0x40)], 0x20).analyze_heap(); + assert!(results.fmp_could_be_unbounded()); + } + + /// An `mcopy` to a destination that is not provably free-pointer-relative can land on + /// the FMP word. + #[test] + fn mcopy_dynamic_destination_flags_unbounded() { + use crate::ir::ValueId; + let setup = vec![Statement::Let { + bindings: vec![ValueId(10)], + value: Expression::CallDataLoad { + offset: Value::int(ValueId(0)), + }, + }]; + let results = object_with_mcopy(setup, 0x20).analyze_heap(); + assert!(results.fmp_could_be_unbounded()); + } + + /// An external call whose return range covers the FMP word writes the callee's return + /// data over the pointer. + #[test] + fn external_call_return_onto_fmp_word_flags_unbounded() { + let results = object_with_external_call(vec![literal(10, 0x40)], 0x20).analyze_heap(); + assert!(results.fmp_could_be_unbounded()); + } + + /// solc fetches return data with `returndatacopy` and passes a zero return length to the + /// call itself; a zero-length return range writes nothing and must not disable the FMP + /// optimization even when its offset is dynamic. + #[test] + fn external_call_zero_length_return_stays_bounded() { + use crate::ir::ValueId; + let setup = vec![Statement::Let { + bindings: vec![ValueId(10)], + value: Expression::CallDataLoad { + offset: Value::int(ValueId(0)), + }, + }]; + let results = object_with_external_call(setup, 0).analyze_heap(); + assert!(!results.fmp_could_be_unbounded()); + } + #[test] fn test_offset_info_from_literal() { let analysis = HeapAnalysis::new(); From 69e2f41377c049f8be30b0524c0ebb33409c8d6d Mon Sep 17 00:00:00 2001 From: xermicus Date: Tue, 22 Sep 2026 13:38:47 +0200 Subject: [PATCH 2/3] Add the changelog entry Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5241f9..a8d94ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Supported `polkadot-sdk` rev: `2604.2.0` ### Fixed - `--newyork`: A bug in dead call value analysis. [#589](https://github.com/paritytech/revive/pull/589) +- `--newyork`: an `mcopy` destination or an external call return range covering the free memory pointer word did not disable the `FMP < heap_size` range proof, which truncated the clobbered `mload(0x40)`. ## v1.4.0 From 589429b5f27b7007c770a02301a157f6c75db08f Mon Sep 17 00:00:00 2001 From: xermicus Date: Tue, 22 Sep 2026 15:21:53 +0200 Subject: [PATCH 3/3] Add the reporter's reproducers and trim the comments Co-Authored-By: Claude Fable 5.1 --- crates/integration/contracts/CopyFmpBug.yul | 10 +---- .../contracts/FmpMcopyStraddle.sol | 15 +++++++ .../contracts/FmpStaticcallReturn.sol | 16 +++++++ crates/integration/src/tests.rs | 30 ++++++++++--- crates/newyork/src/heap_opt.rs | 42 ++----------------- 5 files changed, 60 insertions(+), 53 deletions(-) create mode 100644 crates/integration/contracts/FmpMcopyStraddle.sol create mode 100644 crates/integration/contracts/FmpStaticcallReturn.sol diff --git a/crates/integration/contracts/CopyFmpBug.yul b/crates/integration/contracts/CopyFmpBug.yul index 0a7b8608..cc1fbb32 100644 --- a/crates/integration/contracts/CopyFmpBug.yul +++ b/crates/integration/contracts/CopyFmpBug.yul @@ -1,12 +1,4 @@ -/// Regression (newyork FMP range proof): an `mcopy` destination or an external call's -/// return range covering the free-memory-pointer word `[0x40, 0x60)` replaces the -/// pointer with arbitrary bytes, but neither statement flagged the pointer as possibly -/// unbounded. The `FMP < heap_size` range proof then truncated the clobbered -/// `mload(0x40)` to the heap size width instead of returning the copied word. -/// -/// Case 1 copies the calldata word at 32 onto a calldata-supplied destination (`0x40` -/// in the test). Case 2 calls the contract itself, which answers with `not(0)`, and -/// returns that word into `0x40`. Both cases then read the pointer back. +/// An `mcopy` onto a calldata supplied destination and a `staticcall` returning into `0x40`. object "CopyFmpBug" { code { datacopy(0, dataoffset("CopyFmpBug_deployed"), datasize("CopyFmpBug_deployed")) return(0, datasize("CopyFmpBug_deployed")) } object "CopyFmpBug_deployed" { diff --git a/crates/integration/contracts/FmpMcopyStraddle.sol b/crates/integration/contracts/FmpMcopyStraddle.sol new file mode 100644 index 00000000..f71e60e2 --- /dev/null +++ b/crates/integration/contracts/FmpMcopyStraddle.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8; + +/// Reproducer from paritytech/bugbounty_reports#216. +contract FmpMcopyStraddle { + function probe() external view returns (uint256 r) { + assembly { + mstore(0x80, calldataload(0)) + mcopy(0x38, 0x80, 42) + r := mload(0x40) + mstore(0x40, 0x80) + } + } +} diff --git a/crates/integration/contracts/FmpStaticcallReturn.sol b/crates/integration/contracts/FmpStaticcallReturn.sol new file mode 100644 index 00000000..d8c626d8 --- /dev/null +++ b/crates/integration/contracts/FmpStaticcallReturn.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8; + +/// Reproducer from paritytech/bugbounty_reports#216. +contract FmpStaticcallReturn { + function probe() external view returns (uint256 r) { + assembly { + mstore(0x80, 0x100000000000000000000000000000000000000000000000007) + pop(staticcall(gas(), 4, 0x80, 0x20, 0x40, 0x20)) + mstore(add(0x2000, calldatasize()), 0) + r := mload(0x40) + mstore(0x40, 0x80) + } + } +} diff --git a/crates/integration/src/tests.rs b/crates/integration/src/tests.rs index 9de7ddeb..e16793b7 100644 --- a/crates/integration/src/tests.rs +++ b/crates/integration/src/tests.rs @@ -4984,12 +4984,7 @@ fn calldatacopy_dynamic_dest_fmp_corruption() { run_differential(actions); } -/// Regression (newyork FMP range proof): an `mcopy` destination or an external -/// call's return range covering the free-memory-pointer word `[0x40, 0x60)` -/// clobbers the pointer, but neither flagged it as possibly unbounded, so the -/// surviving `FMP < heap_size` range proof truncated the clobbered `mload(0x40)`. -/// Case 1 copies a calldata word onto `0x40` with `mcopy`; case 2 returns the -/// contract's own `not(0)` answer into `0x40`. Compared newyork-PVM vs solc-EVM. +/// An `mcopy` or a call return range over the free memory pointer word must disable the range proof. #[test] fn copy_onto_fmp_word_disables_range_proof() { let copied_word: U256 = (U256::from(1u64) << 200) | U256::from(0xabcdefu64); @@ -5011,6 +5006,29 @@ fn copy_onto_fmp_word_disables_range_proof() { } } +/// Reproducers from paritytech/bugbounty_reports#216. +#[test] +fn fmp_staticcall_return_and_mcopy_straddle() { + let selector = keccak256(b"probe()")[..4].to_vec(); + let mut mcopy_calldata = selector.clone(); + mcopy_calldata.extend_from_slice(&[0xff; 32]); + for (contract, data) in [ + ("FmpStaticcallReturn", selector), + ("FmpMcopyStraddle", mcopy_calldata), + ] { + let mut actions = instantiate(&format!("contracts/{contract}.sol"), contract); + 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): a store read back by an /// intervening unaligned *overlapping* load must not be eliminated as dead. /// `mem_opt` marked a pending store read only on an exact-offset load, so diff --git a/crates/newyork/src/heap_opt.rs b/crates/newyork/src/heap_opt.rs index 7f5cbb05..f9efcbf2 100644 --- a/crates/newyork/src/heap_opt.rs +++ b/crates/newyork/src/heap_opt.rs @@ -591,28 +591,9 @@ impl HeapAnalysis { } } - /// Flags the free-memory-pointer slot as possibly unbounded when a raw byte write of - /// `length` bytes to `destination` (a copy opcode's destination or an external call's - /// return range) can clobber it. - /// - /// A write that can overwrite the FMP slot `[0x40, 0x60)` replaces the free-memory - /// pointer with arbitrary, possibly out-of-range bytes. Downstream codegen that assumes - /// `FMP < heap_size` (the narrow `mload(0x40)` read and its range proof) would then - /// mis-read the corrupted value, so such a write sets `fmp_could_be_unbounded` — exactly - /// as an untrusted `mstore(0x40, ...)` does. Tainting word 0x40 alone only disables the - /// *native-mode* FMP read; the FMP *range proof* in `to_llvm` is gated on - /// `fmp_could_be_unbounded`, so that flag must be set too or the proof silently - /// truncates the clobbered value. - /// - /// The check is kept deliberately narrow to avoid a code-size regression: a write of - /// statically zero length never covers the slot (solc passes a zero return length to - /// `call` when it fetches the return data with `returndatacopy`), and only a static - /// destination+length that provably overlap the slot, a static destination *inside* the - /// FMP word with a dynamic length (whose first byte(s) land in the slot), or a dynamic - /// destination that is not provably free-pointer-relative flag unboundedness. A static - /// destination *outside* the word (proxy `calldatacopy(0, 0, size)`, OZ's FMP-relative - /// ABI-decode copies to `mload(0x40) >= 0x80`) is left to `has_dynamic_accesses` / the - /// native-mode guards. + /// Flags the free memory pointer as possibly unbounded when a raw write of `length` + /// bytes to `destination` can cover `[0x40, 0x60)`. A zero length never covers it, + /// and neither does a dynamic destination that is free pointer relative. fn flag_write_covering_fmp(&mut self, destination: &Value, length: &Value) { let destination_start = self.extract_static_offset(destination); let len = self.extract_static_offset(length); @@ -630,8 +611,7 @@ impl HeapAnalysis { /// Taints the destination of a copy opcode (`calldatacopy`, `codecopy`, /// `returndatacopy`, …), which writes big-endian bytes that a later native - /// (little-endian) `mload` must not byte-reverse, and flags the free-memory-pointer - /// slot as possibly unbounded when the copy can clobber it. + /// (little-endian) `mload` must not byte-reverse. /// /// When the length is statically known, every word the copy covers is tainted /// — not just the start word — so a multi-word copy can't leave a later word a @@ -2016,8 +1996,6 @@ mod tests { ); } - /// Builds an `mcopy(destination, 0x80, length)` with the operands bound to value IDs 10 - /// (`destination`), 11 (`0x80`) and 12 (`length`); `setup` binds the destination. fn object_with_mcopy(setup: Vec, length: u64) -> Object { use crate::ir::ValueId; let mut statements = setup; @@ -2031,9 +2009,6 @@ mod tests { object_with_code(statements, vec![]) } - /// Builds a `staticcall` returning `return_length` bytes into `return_offset`, with the - /// operands bound to value IDs 10 (`return_offset`), 11 (`return_length`) and 12 (zero); - /// `setup` binds the return offset. fn object_with_external_call(setup: Vec, return_length: u64) -> Object { use crate::ir::{CallKind, ValueId}; let mut statements = setup; @@ -2053,16 +2028,12 @@ mod tests { object_with_code(statements, vec![]) } - /// An `mcopy` onto the FMP word replaces the pointer with arbitrary bytes, so it must - /// disable the range proof like every other copy does. #[test] fn mcopy_onto_fmp_word_flags_unbounded() { let results = object_with_mcopy(vec![literal(10, 0x40)], 0x20).analyze_heap(); assert!(results.fmp_could_be_unbounded()); } - /// An `mcopy` to a destination that is not provably free-pointer-relative can land on - /// the FMP word. #[test] fn mcopy_dynamic_destination_flags_unbounded() { use crate::ir::ValueId; @@ -2076,17 +2047,12 @@ mod tests { assert!(results.fmp_could_be_unbounded()); } - /// An external call whose return range covers the FMP word writes the callee's return - /// data over the pointer. #[test] fn external_call_return_onto_fmp_word_flags_unbounded() { let results = object_with_external_call(vec![literal(10, 0x40)], 0x20).analyze_heap(); assert!(results.fmp_could_be_unbounded()); } - /// solc fetches return data with `returndatacopy` and passes a zero return length to the - /// call itself; a zero-length return range writes nothing and must not disable the FMP - /// optimization even when its offset is dynamic. #[test] fn external_call_zero_length_return_stays_bounded() { use crate::ir::ValueId;