From ae8033130b2f3e897f4b0738de6c2af494f1739e Mon Sep 17 00:00:00 2001 From: xermicus Date: Thu, 28 May 2026 17:25:23 +0200 Subject: [PATCH] Fix `__ulongrem` stdlib edge case (#531) The implementation is off-by-one. --------- Signed-off-by: xermicus --- CHANGELOG.md | 1 + crates/integration/contracts/UlongRem.sol | 58 +++++++++++++++++++++++ crates/integration/src/cases.rs | 7 +++ crates/integration/src/tests.rs | 37 +++++++++++++++ crates/stdlib/stdlib.ll | 8 ++-- 5 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 crates/integration/contracts/UlongRem.sol diff --git a/CHANGELOG.md b/CHANGELOG.md index 9274f1c..fec4d60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - SREM over the two constant operands `type(int256).min` and `-1`. - At least one of the two constants needs folding (i.e. not just `type(int256).min % -1`), and is written specifically such that solc doesn't fold but LLVM does, resulting in UB (signed-overflow). +- Fix `mulmod` returning out-of-range result for large moduli. ## v1.1.0 diff --git a/crates/integration/contracts/UlongRem.sol b/crates/integration/contracts/UlongRem.sol new file mode 100644 index 0000000..1dccfa5 --- /dev/null +++ b/crates/integration/contracts/UlongRem.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8; + +/* runner.json +{ + "differential": true, + "actions": [ + { + "Upload": { + "code": { + "Solidity": { + "contract": "UlongRem" + } + } + } + }, + { + "Instantiate": { + "code": { + "Solidity": { + "contract": "UlongRemTester" + } + } + } + }, + { + "VerifyCall": { + "success": true + } + } + ] +} +*/ + +/// Exercises the stdlib `__ulongrem` Knuth-D helper via mulmod's slow path +/// (modulus >= 2^128). Inputs come over calldata to defeat solc folding. +contract UlongRem { + function bigMulMod(uint256 a, uint256 b, uint256 m) external pure returns (uint256) { + return mulmod(a, b, m); + } +} + +contract UlongRemTester { + constructor() { + UlongRem c = new UlongRem(); + + // PoC from the bug report. Drives the second-iteration trial-quotient + // candidate to exactly 2^128 - 1, the boundary the off-by-one loop bound + // in `__ulongrem` mishandled. Failure mode (pre-fix): PVM returns + // `correct + m`. + uint256 a = 1 << 159; + uint256 b = type(uint256).max - (1 << 54); + uint256 m = (1 << 255) + 1; + uint256 r = c.bigMulMod(a, b, m); + assert(r < m); + } +} diff --git a/crates/integration/src/cases.rs b/crates/integration/src/cases.rs index e5198f8..93bd1dd 100644 --- a/crates/integration/src/cases.rs +++ b/crates/integration/src/cases.rs @@ -353,6 +353,13 @@ sol!( ); case!("Bitwise.sol", Bitwise, opByteCall, bitwise_byte, index: U256, value: U256); +sol!( + contract UlongRem { + function bigMulMod(uint a, uint b, uint m) external pure returns (uint); + } +); +case!("UlongRem.sol", UlongRem, bigMulModCall, ulongrem_big_mulmod, a: U256, b: U256, m: U256); + sol!( contract Storage { function transient(uint value) public returns (uint ret); diff --git a/crates/integration/src/tests.rs b/crates/integration/src/tests.rs index bd3fab2..945042a 100644 --- a/crates/integration/src/tests.rs +++ b/crates/integration/src/tests.rs @@ -66,6 +66,7 @@ test_spec!(function_type, "FunctionType", "FunctionType.sol"); test_spec!(layout_at, "LayoutAt", "LayoutAt.sol"); test_spec!(shift_arithmetic_right, "SAR", "SAR.sol"); test_spec!(add_mod_mul_mod, "AddModMulModTester", "AddModMulMod.sol"); +test_spec!(ulongrem, "UlongRemTester", "UlongRem.sol"); test_spec!(memory_bounds, "MemoryBounds", "MemoryBounds.sol"); test_spec!(selfdestruct, "Selfdestruct", "Selfdestruct.sol"); test_spec!(clz, "CountLeadingZeros", "CountLeadingZeros.sol"); @@ -104,6 +105,42 @@ fn run_differential(actions: Vec) { .run(); } +/// Rust-driven fuzz against the stdlib `__ulongrem` slow path. Each iteration +/// computes (a, b, m) deterministically from the index and dispatches a single +/// `bigMulMod` call via the differential runner — there is no in-contract loop, +/// so PVM never sees more than one mulmod per call dispatch. Any divergence +/// shows up as a returndata mismatch on the specific failing iteration. +#[test] +fn ulongrem_fuzz() { + use alloy_primitives::keccak256; + + let mut actions = instantiate("contracts/UlongRem.sol", "UlongRem"); + + for i in 0u64..256 { + let derive = |tag: &[u8]| -> U256 { + let mut buf = Vec::with_capacity(8 + tag.len()); + buf.extend_from_slice(&i.to_be_bytes()); + buf.extend_from_slice(tag); + U256::from_be_bytes::<32>(keccak256(&buf).0) + }; + let a = derive(b"a"); + let b = derive(b"b"); + // Force modulus into the slow path (m >= 2^255). + let m = derive(b"m") | (U256::from(1u64) << 255); + + actions.push(Call { + origin: TestAddress::Alice, + dest: TestAddress::Instantiated(0), + value: 0, + gas_limit: None, + storage_deposit_limit: None, + data: Contract::ulongrem_big_mulmod(a, b, m).calldata, + }); + } + + run_differential(actions); +} + #[test] fn bitwise_byte() { let mut actions = instantiate("contracts/Bitwise.sol", "Bitwise"); diff --git a/crates/stdlib/stdlib.ll b/crates/stdlib/stdlib.ll index 67c4095..178c2b3 100644 --- a/crates/stdlib/stdlib.ll +++ b/crates/stdlib/stdlib.ll @@ -104,7 +104,7 @@ define private i256 @__ulongrem(i256 %0, i256 %1, i256 %2) #0 { 19: %.056 = phi i256 [ %15, %13 ], [ %25, %.critedge ] %.055 = phi i256 [ %16, %13 ], [ %26, %.critedge ] - %.not62 = icmp ult i256 %.056, 340282366920938463463374607431768211455 + %.not62 = icmp ult i256 %.056, 340282366920938463463374607431768211456 br i1 %.not62, label %20, label %.critedge 20: @@ -117,7 +117,7 @@ define private i256 @__ulongrem(i256 %0, i256 %1, i256 %2) #0 { .critedge: %25 = add i256 %.056, -1 %26 = add i256 %.055, %14 - %.not65 = icmp ult i256 %26, 340282366920938463463374607431768211455 + %.not65 = icmp ult i256 %26, 340282366920938463463374607431768211456 br i1 %.not65, label %19, label %27 27: @@ -135,7 +135,7 @@ define private i256 @__ulongrem(i256 %0, i256 %1, i256 %2) #0 { 36: %.2 = phi i256 [ %33, %27 ], [ %42, %.critedge1 ] %.1 = phi i256 [ %34, %27 ], [ %43, %.critedge1 ] - %.not63 = icmp ult i256 %.2, 340282366920938463463374607431768211455 + %.not63 = icmp ult i256 %.2, 340282366920938463463374607431768211456 br i1 %.not63, label %37, label %.critedge1 37: @@ -148,7 +148,7 @@ define private i256 @__ulongrem(i256 %0, i256 %1, i256 %2) #0 { .critedge1: %42 = add i256 %.2, -1 %43 = add i256 %.1, %14 - %.not64 = icmp ult i256 %43, 340282366920938463463374607431768211455 + %.not64 = icmp ult i256 %43, 340282366920938463463374607431768211456 br i1 %.not64, label %36, label %44 44: