Fix __ulongrem stdlib edge case (#531)

The implementation is off-by-one.

---------

Signed-off-by: xermicus <cyrill@parity.io>
This commit is contained in:
xermicus authored and GitHub committed 2026-05-28 17:25:23 +02:00
1 parent c67b95805c
commit ae8033130b
5 files changed
+107 -4

No files matched your search

+1
View File
@@ -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
+58
View File
@@ -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);
}
}
+7
View File
@@ -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);
+37
View File
@@ -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<SpecsAction>) {
.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");
+4 -4
View File
@@ -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: