diff --git a/CHANGELOG.md b/CHANGELOG.md index d0a947d..9274f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,16 @@ - Updated `LLVM` from `21.1.8` to LLVM `22.1.4` ### Added + - Support for solc v0.8.35. +### Fixed + +- A constant folding bug causing incorrect translation of a signed remainder edge case. The affected pattern (Solidity % or Yul smod): + - 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). + ## v1.1.0 Supported `polkadot-sdk` rev: `2603` diff --git a/crates/integration/codesize.json b/crates/integration/codesize.json index e5aab6d..878d2fc 100644 --- a/crates/integration/codesize.json +++ b/crates/integration/codesize.json @@ -1,7 +1,7 @@ { "Baseline": 870, "Computation": 2418, - "DivisionArithmetics": 9274, + "DivisionArithmetics": 9368, "ERC20": 17090, "Events": 1662, "FibonacciIterative": 1427, diff --git a/crates/integration/contracts/AddModZeroProbe.yul b/crates/integration/contracts/AddModZeroProbe.yul new file mode 100644 index 0000000..121b7be --- /dev/null +++ b/crates/integration/contracts/AddModZeroProbe.yul @@ -0,0 +1,21 @@ +/// Probe: `addmod(a, b, 0)` — LLVM `urem ..., 0` is UB; EVM defines ADDMOD +/// with modulus = 0 as 0. The stdlib `__addmod` runtime function guards this. +/// Question is whether LLVM constant-folds through the guard at O3. +object "AddModZeroProbe" { + code { + let size := datasize("AddModZeroProbe_deployed") + codecopy(0, dataoffset("AddModZeroProbe_deployed"), size) + return(0, size) + } + object "AddModZeroProbe_deployed" { + code { + let tag := calldataload(0) + let a := calldataload(32) + let b := calldataload(64) + // 0 modulus, obfuscated so revive can't pre-fold + let modulus := sub(shl(8, 1), shl(8, 1)) + mstore(0, xor(addmod(a, b, modulus), tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/ByteOobProbe.yul b/crates/integration/contracts/ByteOobProbe.yul new file mode 100644 index 0000000..76988fb --- /dev/null +++ b/crates/integration/contracts/ByteOobProbe.yul @@ -0,0 +1,19 @@ +/// Probe: `byte(32, x)` — byte index 32 is out of range. EVM returns 0. +/// revive's `byte` builds a branchless mask; the question is whether the +/// truncate-to-i8 of a constant 32 + shifts produces any UB intermediate. +object "ByteOobProbe" { + code { + let size := datasize("ByteOobProbe_deployed") + codecopy(0, dataoffset("ByteOobProbe_deployed"), size) + return(0, size) + } + object "ByteOobProbe_deployed" { + code { + let tag := calldataload(0) + let value := calldataload(32) + let idx := shl(5, 1) // = 32 + mstore(0, xor(byte(idx, value), tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/DivBothConst.yul b/crates/integration/contracts/DivBothConst.yul new file mode 100644 index 0000000..5799961 --- /dev/null +++ b/crates/integration/contracts/DivBothConst.yul @@ -0,0 +1,30 @@ +/// Both-constant DIV variants. The selector index is read from calldata[0..32] +/// and dispatched to a branch with two literal operands. Compiled directly via +/// revive's Yul-to-LLVM path (bypassing solc's Yul optimizer) so the literals +/// reach LLVM intact and exercise revive's "two literal operands" codegen path. +object "DivBothConst" { + code { + let size := datasize("DivBothConst_deployed") + codecopy(0, dataoffset("DivBothConst_deployed"), size) + return(0, size) + } + object "DivBothConst_deployed" { + code { + let which := calldataload(0) + // `tag` is XORed into the result so that any poison/undef LLVM + // produces from a UB-triggering const-fold propagates and diverges + // from EVM, instead of coincidentally returning 0 and masking it. + let tag := calldataload(32) + let r := 0 + switch which + case 0 { r := div(5, 5) } + case 1 { r := div(5, 1) } + case 2 { r := div(0, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) } + case 3 { r := div(5, 2) } + case 4 { r := div(1, 0) } + default { revert(0, 0) } + mstore(0, xor(r, tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/DivisionArithmeticsConst.sol b/crates/integration/contracts/DivisionArithmeticsConst.sol new file mode 100644 index 0000000..ab493cf --- /dev/null +++ b/crates/integration/contracts/DivisionArithmeticsConst.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8; + +/// Half-constant variants of the operations in `DivisionArithmetics.sol`: +/// one operand reaches the compiler as a literal, the other comes from +/// calldata. solc cannot fold an op when one side is a parameter, so the +/// final bytecode contains a real div/sdiv/mod/smod with one constant +/// operand — exercising revive's "one literal operand" codegen path. +/// +/// Split into four contracts (one per opcode) so each compiled blob stays +/// well under polkavm-linker's debug line-program limit. A single 58-function +/// contract overflowed it in debug builds. +/// +/// Two-constant cases are covered by Yul fixtures (see +/// `*BothConst.yul`) to bypass solc's Yul optimizer. + +contract DivConst { + function divRhsZero(uint256 n) public pure returns (uint256 r) { assembly { r := div(n, 0) } } + function divRhsOne(uint256 n) public pure returns (uint256 r) { assembly { r := div(n, 1) } } + function divRhsTwo(uint256 n) public pure returns (uint256 r) { assembly { r := div(n, 2) } } + function divRhsFive(uint256 n) public pure returns (uint256 r) { assembly { r := div(n, 5) } } + function divRhsMax(uint256 n) public pure returns (uint256 r) { + uint256 c = type(uint256).max; + assembly { r := div(n, c) } + } + + function divLhsZero(uint256 d) public pure returns (uint256 r) { assembly { r := div(0, d) } } + function divLhsOne(uint256 d) public pure returns (uint256 r) { assembly { r := div(1, d) } } + function divLhsTwo(uint256 d) public pure returns (uint256 r) { assembly { r := div(2, d) } } + function divLhsFive(uint256 d) public pure returns (uint256 r) { assembly { r := div(5, d) } } + function divLhsMax(uint256 d) public pure returns (uint256 r) { + uint256 c = type(uint256).max; + assembly { r := div(c, d) } + } +} + +contract SdivConst { + function sdivRhsZero(int256 n) public pure returns (int256 r) { assembly { r := sdiv(n, 0) } } + function sdivRhsOne(int256 n) public pure returns (int256 r) { assembly { r := sdiv(n, 1) } } + function sdivRhsNegOne(int256 n) public pure returns (int256 r) { + int256 c = -1; + assembly { r := sdiv(n, c) } + } + function sdivRhsTwo(int256 n) public pure returns (int256 r) { assembly { r := sdiv(n, 2) } } + function sdivRhsNegTwo(int256 n) public pure returns (int256 r) { + int256 c = -2; + assembly { r := sdiv(n, c) } + } + function sdivRhsFive(int256 n) public pure returns (int256 r) { assembly { r := sdiv(n, 5) } } + function sdivRhsNegFive(int256 n) public pure returns (int256 r) { + int256 c = -5; + assembly { r := sdiv(n, c) } + } + function sdivRhsMin(int256 n) public pure returns (int256 r) { + int256 c = type(int256).min; + assembly { r := sdiv(n, c) } + } + function sdivRhsMinPlusOne(int256 n) public pure returns (int256 r) { + int256 c = type(int256).min + 1; + assembly { r := sdiv(n, c) } + } + function sdivRhsMax(int256 n) public pure returns (int256 r) { + int256 c = type(int256).max; + assembly { r := sdiv(n, c) } + } + + function sdivLhsZero(int256 d) public pure returns (int256 r) { assembly { r := sdiv(0, d) } } + function sdivLhsOne(int256 d) public pure returns (int256 r) { assembly { r := sdiv(1, d) } } + function sdivLhsNegOne(int256 d) public pure returns (int256 r) { + int256 c = -1; + assembly { r := sdiv(c, d) } + } + function sdivLhsTwo(int256 d) public pure returns (int256 r) { assembly { r := sdiv(2, d) } } + function sdivLhsNegTwo(int256 d) public pure returns (int256 r) { + int256 c = -2; + assembly { r := sdiv(c, d) } + } + function sdivLhsFive(int256 d) public pure returns (int256 r) { assembly { r := sdiv(5, d) } } + function sdivLhsNegFive(int256 d) public pure returns (int256 r) { + int256 c = -5; + assembly { r := sdiv(c, d) } + } + function sdivLhsMin(int256 d) public pure returns (int256 r) { + int256 c = type(int256).min; + assembly { r := sdiv(c, d) } + } + function sdivLhsMinPlusOne(int256 d) public pure returns (int256 r) { + int256 c = type(int256).min + 1; + assembly { r := sdiv(c, d) } + } + function sdivLhsMax(int256 d) public pure returns (int256 r) { + int256 c = type(int256).max; + assembly { r := sdiv(c, d) } + } +} + +contract ModConst { + function modRhsZero(uint256 n) public pure returns (uint256 r) { assembly { r := mod(n, 0) } } + function modRhsOne(uint256 n) public pure returns (uint256 r) { assembly { r := mod(n, 1) } } + function modRhsTwo(uint256 n) public pure returns (uint256 r) { assembly { r := mod(n, 2) } } + function modRhsFive(uint256 n) public pure returns (uint256 r) { assembly { r := mod(n, 5) } } + function modRhsMax(uint256 n) public pure returns (uint256 r) { + uint256 c = type(uint256).max; + assembly { r := mod(n, c) } + } + + function modLhsZero(uint256 d) public pure returns (uint256 r) { assembly { r := mod(0, d) } } + function modLhsOne(uint256 d) public pure returns (uint256 r) { assembly { r := mod(1, d) } } + function modLhsTwo(uint256 d) public pure returns (uint256 r) { assembly { r := mod(2, d) } } + function modLhsFive(uint256 d) public pure returns (uint256 r) { assembly { r := mod(5, d) } } + function modLhsMax(uint256 d) public pure returns (uint256 r) { + uint256 c = type(uint256).max; + assembly { r := mod(c, d) } + } +} + +contract SmodConst { + function smodRhsZero(int256 n) public pure returns (int256 r) { assembly { r := smod(n, 0) } } + function smodRhsOne(int256 n) public pure returns (int256 r) { assembly { r := smod(n, 1) } } + function smodRhsNegOne(int256 n) public pure returns (int256 r) { + int256 c = -1; + assembly { r := smod(n, c) } + } + function smodRhsTwo(int256 n) public pure returns (int256 r) { assembly { r := smod(n, 2) } } + function smodRhsNegTwo(int256 n) public pure returns (int256 r) { + int256 c = -2; + assembly { r := smod(n, c) } + } + function smodRhsFive(int256 n) public pure returns (int256 r) { assembly { r := smod(n, 5) } } + function smodRhsNegFive(int256 n) public pure returns (int256 r) { + int256 c = -5; + assembly { r := smod(n, c) } + } + function smodRhsMin(int256 n) public pure returns (int256 r) { + int256 c = type(int256).min; + assembly { r := smod(n, c) } + } + function smodRhsMax(int256 n) public pure returns (int256 r) { + int256 c = type(int256).max; + assembly { r := smod(n, c) } + } + + function smodLhsZero(int256 d) public pure returns (int256 r) { assembly { r := smod(0, d) } } + function smodLhsOne(int256 d) public pure returns (int256 r) { assembly { r := smod(1, d) } } + function smodLhsNegOne(int256 d) public pure returns (int256 r) { + int256 c = -1; + assembly { r := smod(c, d) } + } + function smodLhsTwo(int256 d) public pure returns (int256 r) { assembly { r := smod(2, d) } } + function smodLhsNegTwo(int256 d) public pure returns (int256 r) { + int256 c = -2; + assembly { r := smod(c, d) } + } + function smodLhsFive(int256 d) public pure returns (int256 r) { assembly { r := smod(5, d) } } + function smodLhsNegFive(int256 d) public pure returns (int256 r) { + int256 c = -5; + assembly { r := smod(c, d) } + } + function smodLhsMin(int256 d) public pure returns (int256 r) { + int256 c = type(int256).min; + assembly { r := smod(c, d) } + } + function smodLhsMax(int256 d) public pure returns (int256 r) { + int256 c = type(int256).max; + assembly { r := smod(c, d) } + } +} diff --git a/crates/integration/contracts/ExpZeroZeroProbe.yul b/crates/integration/contracts/ExpZeroZeroProbe.yul new file mode 100644 index 0000000..1114e50 --- /dev/null +++ b/crates/integration/contracts/ExpZeroZeroProbe.yul @@ -0,0 +1,18 @@ +/// Probe: `exp(0, 0)` — EVM defines `0^0 = 1`. revive's stdlib `__exp` checks +/// `exp == 0` first and returns 1; the body never runs for this case. +object "ExpZeroZeroProbe" { + code { + let size := datasize("ExpZeroZeroProbe_deployed") + codecopy(0, dataoffset("ExpZeroZeroProbe_deployed"), size) + return(0, size) + } + object "ExpZeroZeroProbe_deployed" { + code { + let tag := calldataload(0) + let base := sub(shl(8, 1), shl(8, 1)) // = 0 + let exponent := sub(shl(8, 1), shl(8, 1)) // = 0 + mstore(0, xor(exp(base, exponent), tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/ModBothConst.yul b/crates/integration/contracts/ModBothConst.yul new file mode 100644 index 0000000..444ef26 --- /dev/null +++ b/crates/integration/contracts/ModBothConst.yul @@ -0,0 +1,32 @@ +/// Both-constant MOD variants. Selector index is read from calldata[0..32] +/// and dispatched to a branch with two literal operands. Compiled directly via +/// revive's Yul-to-LLVM path (bypassing solc's Yul optimizer) so the literals +/// reach LLVM intact and exercise revive's "two literal operands" codegen path. +object "ModBothConst" { + code { + let size := datasize("ModBothConst_deployed") + codecopy(0, dataoffset("ModBothConst_deployed"), size) + return(0, size) + } + object "ModBothConst_deployed" { + code { + let which := calldataload(0) + // `tag` is XORed into the result so that any poison/undef LLVM + // produces from a UB-triggering const-fold propagates and diverges + // from EVM, instead of coincidentally returning 0 and masking it. + let tag := calldataload(32) + let r := 0 + switch which + case 0 { r := mod(5, 5) } + case 1 { r := mod(5, 1) } + case 2 { r := mod(0, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) } + case 3 { r := mod(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) } + case 4 { r := mod(5, 2) } + case 5 { r := mod(2, 5) } + case 6 { r := mod(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 0) } + default { revert(0, 0) } + mstore(0, xor(r, tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/MulModZeroProbe.yul b/crates/integration/contracts/MulModZeroProbe.yul new file mode 100644 index 0000000..f3bf7bc --- /dev/null +++ b/crates/integration/contracts/MulModZeroProbe.yul @@ -0,0 +1,18 @@ +/// Probe: `mulmod(a, b, 0)` — sibling of `AddModZeroProbe`. EVM = 0. +object "MulModZeroProbe" { + code { + let size := datasize("MulModZeroProbe_deployed") + codecopy(0, dataoffset("MulModZeroProbe_deployed"), size) + return(0, size) + } + object "MulModZeroProbe_deployed" { + code { + let tag := calldataload(0) + let a := calldataload(32) + let b := calldataload(64) + let modulus := sub(shl(8, 1), shl(8, 1)) + mstore(0, xor(mulmod(a, b, modulus), tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/SarOverflowProbe.yul b/crates/integration/contracts/SarOverflowProbe.yul new file mode 100644 index 0000000..d5e0ec0 --- /dev/null +++ b/crates/integration/contracts/SarOverflowProbe.yul @@ -0,0 +1,19 @@ +/// Probe: `sar(256, INT_MIN)` — LLVM `ashr i256 x, 256` is UB; EVM defines SAR +/// with count >= 256 as 0 (positive) or -1 (negative, sign-fill). Tests the +/// negative branch by using INT_MIN as the dividend. +object "SarOverflowProbe" { + code { + let size := datasize("SarOverflowProbe_deployed") + codecopy(0, dataoffset("SarOverflowProbe_deployed"), size) + return(0, size) + } + object "SarOverflowProbe_deployed" { + code { + let tag := calldataload(0) + let value := shl(255, 1) // INT_MIN + let count := shl(8, 1) + mstore(0, xor(sar(count, value), tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/SdivBothConst.yul b/crates/integration/contracts/SdivBothConst.yul new file mode 100644 index 0000000..ae5b6fb --- /dev/null +++ b/crates/integration/contracts/SdivBothConst.yul @@ -0,0 +1,42 @@ +/// Both-constant SDIV variants. Selector index is read from calldata[0..32] +/// and dispatched to a branch with two literal operands. Compiled directly via +/// revive's Yul-to-LLVM path (bypassing solc's Yul optimizer) so the literals +/// reach LLVM intact and exercise revive's "two literal operands" codegen path. +/// +/// Case 11 — sdiv(INT_MIN, -1) — is the signed-overflow edge case that is UB +/// in LLVM `sdiv` and must be guarded in revive's codegen. +object "SdivBothConst" { + code { + let size := datasize("SdivBothConst_deployed") + codecopy(0, dataoffset("SdivBothConst_deployed"), size) + return(0, size) + } + object "SdivBothConst_deployed" { + code { + let which := calldataload(0) + // `tag` is XORed into the result so that any poison/undef LLVM + // produces from a UB-triggering const-fold (e.g. sdiv(INT_MIN,-1)) + // propagates and diverges from EVM, instead of coincidentally + // returning 0 and masking the bug. + let tag := calldataload(32) + let r := 0 + switch which + case 0 { r := sdiv(5, 5) } + case 1 { r := sdiv(5, 1) } + case 2 { r := sdiv(0, 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) } + case 3 { r := sdiv(0, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) } + case 4 { r := sdiv(5, 2) } + case 5 { r := sdiv(5, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) } + case 6 { r := sdiv(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe) } + case 7 { r := sdiv(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb, 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb) } + case 8 { r := sdiv(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb, 2) } + case 9 { r := sdiv(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 0x8000000000000000000000000000000000000000000000000000000000000000) } + case 10 { r := sdiv(1, 0) } + case 11 { r := sdiv(0x8000000000000000000000000000000000000000000000000000000000000000, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) } + case 12 { r := sdiv(0x8000000000000000000000000000000000000000000000000000000000000001, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) } + default { revert(0, 0) } + mstore(0, xor(r, tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/SdivIntMinNegOneBug.yul b/crates/integration/contracts/SdivIntMinNegOneBug.yul new file mode 100644 index 0000000..c7663e9 --- /dev/null +++ b/crates/integration/contracts/SdivIntMinNegOneBug.yul @@ -0,0 +1,27 @@ +/// Dedicated fixture exercising `sdiv(INT_MIN, -1)` — the signed-overflow UB +/// case for division, sibling to the `smod` bug in paritytech/revive#524. +/// +/// LLVM `sdiv` is UB when the dividend is `INT_MIN` and the divisor is `-1` +/// (the mathematical result `-INT_MIN` overflows). EVM SDIV defines this as +/// `INT_MIN`. revive's `__revive_signed_division` runtime function is supposed +/// to guard this; the issue says sdiv is already guarded but having a test +/// here protects against regressions and surfaces any future divergence. +/// +/// See `SmodIntMinNegOneBug.yul` for the rationale behind the Yul expression +/// operands and the lack of a `switch` dispatcher. +object "SdivIntMinNegOneBug" { + code { + let size := datasize("SdivIntMinNegOneBug_deployed") + codecopy(0, dataoffset("SdivIntMinNegOneBug_deployed"), size) + return(0, size) + } + object "SdivIntMinNegOneBug_deployed" { + code { + let tag := calldataload(0) + let int_min := shl(255, 1) + let neg_one := sar(58, shl(58, sub(0, 1))) + mstore(0, xor(sdiv(int_min, neg_one), tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/ShlOverflowProbe.yul b/crates/integration/contracts/ShlOverflowProbe.yul new file mode 100644 index 0000000..f413b2c --- /dev/null +++ b/crates/integration/contracts/ShlOverflowProbe.yul @@ -0,0 +1,20 @@ +/// Probe: `shl(256, x)` — LLVM `shl i256 x, 256` is UB; EVM defines SHL with +/// count >= 256 as 0. revive's `shift_left` adds a switch+phi guard; this +/// fixture stresses whether it survives O3 const-folding. +object "ShlOverflowProbe" { + code { + let size := datasize("ShlOverflowProbe_deployed") + codecopy(0, dataoffset("ShlOverflowProbe_deployed"), size) + return(0, size) + } + object "ShlOverflowProbe_deployed" { + code { + let tag := calldataload(0) + let value := calldataload(32) + // count = 256, obfuscated so revive can't fold pre-LLVM + let count := shl(8, 1) + mstore(0, xor(shl(count, value), tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/ShrOverflowProbe.yul b/crates/integration/contracts/ShrOverflowProbe.yul new file mode 100644 index 0000000..344d11e --- /dev/null +++ b/crates/integration/contracts/ShrOverflowProbe.yul @@ -0,0 +1,18 @@ +/// Probe: `shr(256, x)` — LLVM `lshr i256 x, 256` is UB; EVM defines SHR with +/// count >= 256 as 0. +object "ShrOverflowProbe" { + code { + let size := datasize("ShrOverflowProbe_deployed") + codecopy(0, dataoffset("ShrOverflowProbe_deployed"), size) + return(0, size) + } + object "ShrOverflowProbe_deployed" { + code { + let tag := calldataload(0) + let value := calldataload(32) + let count := shl(8, 1) + mstore(0, xor(shr(count, value), tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/SignExtendOobProbe.yul b/crates/integration/contracts/SignExtendOobProbe.yul new file mode 100644 index 0000000..d7e1bbc --- /dev/null +++ b/crates/integration/contracts/SignExtendOobProbe.yul @@ -0,0 +1,19 @@ +/// Probe: `signextend(32, x)` — byte index 32 is out of range. EVM returns +/// the value unchanged (no extension). stdlib `__signextend` guards via +/// `icmp uge 31`. Tests that the guard survives O3. +object "SignExtendOobProbe" { + code { + let size := datasize("SignExtendOobProbe_deployed") + codecopy(0, dataoffset("SignExtendOobProbe_deployed"), size) + return(0, size) + } + object "SignExtendOobProbe_deployed" { + code { + let tag := calldataload(0) + let value := calldataload(32) + let numbyte := shl(5, 1) // = 32, obfuscated + mstore(0, xor(signextend(numbyte, value), tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/SmodBothConst.yul b/crates/integration/contracts/SmodBothConst.yul new file mode 100644 index 0000000..733a4e5 --- /dev/null +++ b/crates/integration/contracts/SmodBothConst.yul @@ -0,0 +1,46 @@ +/// Both-constant SMOD variants. Selector index is read from calldata[0..32] +/// and dispatched to a branch with two literal operands. Compiled directly via +/// revive's Yul-to-LLVM path (bypassing solc's Yul optimizer) so the literals +/// reach LLVM intact and exercise revive's "two literal operands" codegen path. +/// +/// Case 15 — smod(INT_MIN, -1) — surfaces the bug reported in +/// paritytech/revive#524: LLVM constant-folds `srem(INT_MIN, -1)` to poison +/// because the operation is signed-overflow UB in LLVM, even though EVM SMOD +/// defines it as 0. +object "SmodBothConst" { + code { + let size := datasize("SmodBothConst_deployed") + codecopy(0, dataoffset("SmodBothConst_deployed"), size) + return(0, size) + } + object "SmodBothConst_deployed" { + code { + let which := calldataload(0) + // `tag` is XORed into every result so that any poison/undef LLVM + // produces from a UB-triggering const-fold propagates and diverges + // from EVM, rather than coincidentally collapsing to 0 (which is + // also EVM SMOD's defined result for INT_MIN op -1). + let tag := calldataload(32) + switch which + case 0 { mstore(0, xor(smod(5, 5), tag)) } + case 1 { mstore(0, xor(smod(5, 1), tag)) } + case 2 { mstore(0, xor(smod(0, 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff), tag)) } + case 3 { mstore(0, xor(smod(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff), tag)) } + case 4 { mstore(0, xor(smod(5, 2), tag)) } + case 5 { mstore(0, xor(smod(2, 5), tag)) } + case 6 { mstore(0, xor(smod(5, 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb), tag)) } + case 7 { mstore(0, xor(smod(5, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff), tag)) } + case 8 { mstore(0, xor(smod(5, 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe), tag)) } + case 9 { mstore(0, xor(smod(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb, 2), tag)) } + case 10 { mstore(0, xor(smod(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe, 5), tag)) } + case 11 { mstore(0, xor(smod(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb, 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb), tag)) } + case 12 { mstore(0, xor(smod(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff), tag)) } + case 13 { mstore(0, xor(smod(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb, 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe), tag)) } + case 14 { mstore(0, xor(smod(0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe, 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffb), tag)) } + case 15 { mstore(0, xor(smod(0x8000000000000000000000000000000000000000000000000000000000000000, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff), tag)) } + case 16 { mstore(0, xor(smod(0, 0), tag)) } + default { revert(0, 0) } + return(0, 32) + } + } +} diff --git a/crates/integration/contracts/SmodIntMinNegOneBug.yul b/crates/integration/contracts/SmodIntMinNegOneBug.yul new file mode 100644 index 0000000..508bf4b --- /dev/null +++ b/crates/integration/contracts/SmodIntMinNegOneBug.yul @@ -0,0 +1,41 @@ +/// Dedicated fixture for the `smod(INT_MIN, -1)` LLVM-UB bug reported in +/// paritytech/revive#524. +/// +/// The bug is that LLVM constant-folds `srem(INT_MIN, -1)` to poison (signed +/// overflow UB), which then propagates through subsequent operations and +/// often collapses to 0. EVM SMOD defines this case as 0, so a bare +/// `smod(INT_MIN, -1)` would coincidentally agree; we XOR the result with a +/// runtime calldata word so any divergence between the (defined) EVM value +/// and the (poison-derived) PVM value becomes observable. +/// +/// **Important compilation details that make the bug actually surface here:** +/// +/// 1. Operands are written as Yul expressions (`shl(255, 1)`, `sar(58, shl(58, +/// sub(0,1)))`) rather than raw `0x8000...` / `0xfff...` literals. revive's +/// pipeline constant-evaluates raw literals before LLVM ever sees them, so +/// no `srem` instruction with both literal operands reaches LLVM and the +/// UB-fold opportunity vanishes. Yul expression operands force a real +/// `srem` to appear in LLVM IR. +/// +/// 2. The function has a single execution path (no `switch` over many cases). +/// Multi-case dispatchers exceed LLVM's inlining budget for the runtime +/// `__revive_signed_remainder` function, so the CALL is preserved and +/// evaluated at runtime via RISC-V `rem` (which is defined as 0 for the +/// INT_MIN/-1 pair) — hiding the bug. With a single case the inliner +/// inlines aggressively, exposing the bug. +object "SmodIntMinNegOneBug" { + code { + let size := datasize("SmodIntMinNegOneBug_deployed") + codecopy(0, dataoffset("SmodIntMinNegOneBug_deployed"), size) + return(0, size) + } + object "SmodIntMinNegOneBug_deployed" { + code { + let tag := calldataload(0) + let int_min := shl(255, 1) + let neg_one := sar(58, shl(58, sub(0, 1))) + mstore(0, xor(smod(int_min, neg_one), tag)) + return(0, 32) + } + } +} diff --git a/crates/integration/src/cases.rs b/crates/integration/src/cases.rs index 771933c..e5198f8 100644 --- a/crates/integration/src/cases.rs +++ b/crates/integration/src/cases.rs @@ -186,6 +186,78 @@ case!("DivisionArithmetics.sol", DivisionArithmetics, sdivCall, division_arithme case!("DivisionArithmetics.sol", DivisionArithmetics, modCall, division_arithmetics_mod, n: U256, d: U256); case!("DivisionArithmetics.sol", DivisionArithmetics, smodCall, division_arithmetics_smod, n: I256, d: I256); +sol!( + contract DivConst { + function divRhsZero(uint256 n) external pure returns (uint256); + function divRhsOne(uint256 n) external pure returns (uint256); + function divRhsTwo(uint256 n) external pure returns (uint256); + function divRhsFive(uint256 n) external pure returns (uint256); + function divRhsMax(uint256 n) external pure returns (uint256); + function divLhsZero(uint256 d) external pure returns (uint256); + function divLhsOne(uint256 d) external pure returns (uint256); + function divLhsTwo(uint256 d) external pure returns (uint256); + function divLhsFive(uint256 d) external pure returns (uint256); + function divLhsMax(uint256 d) external pure returns (uint256); + } + + contract SdivConst { + function sdivRhsZero(int256 n) external pure returns (int256); + function sdivRhsOne(int256 n) external pure returns (int256); + function sdivRhsNegOne(int256 n) external pure returns (int256); + function sdivRhsTwo(int256 n) external pure returns (int256); + function sdivRhsNegTwo(int256 n) external pure returns (int256); + function sdivRhsFive(int256 n) external pure returns (int256); + function sdivRhsNegFive(int256 n) external pure returns (int256); + function sdivRhsMin(int256 n) external pure returns (int256); + function sdivRhsMinPlusOne(int256 n) external pure returns (int256); + function sdivRhsMax(int256 n) external pure returns (int256); + function sdivLhsZero(int256 d) external pure returns (int256); + function sdivLhsOne(int256 d) external pure returns (int256); + function sdivLhsNegOne(int256 d) external pure returns (int256); + function sdivLhsTwo(int256 d) external pure returns (int256); + function sdivLhsNegTwo(int256 d) external pure returns (int256); + function sdivLhsFive(int256 d) external pure returns (int256); + function sdivLhsNegFive(int256 d) external pure returns (int256); + function sdivLhsMin(int256 d) external pure returns (int256); + function sdivLhsMinPlusOne(int256 d) external pure returns (int256); + function sdivLhsMax(int256 d) external pure returns (int256); + } + + contract ModConst { + function modRhsZero(uint256 n) external pure returns (uint256); + function modRhsOne(uint256 n) external pure returns (uint256); + function modRhsTwo(uint256 n) external pure returns (uint256); + function modRhsFive(uint256 n) external pure returns (uint256); + function modRhsMax(uint256 n) external pure returns (uint256); + function modLhsZero(uint256 d) external pure returns (uint256); + function modLhsOne(uint256 d) external pure returns (uint256); + function modLhsTwo(uint256 d) external pure returns (uint256); + function modLhsFive(uint256 d) external pure returns (uint256); + function modLhsMax(uint256 d) external pure returns (uint256); + } + + contract SmodConst { + function smodRhsZero(int256 n) external pure returns (int256); + function smodRhsOne(int256 n) external pure returns (int256); + function smodRhsNegOne(int256 n) external pure returns (int256); + function smodRhsTwo(int256 n) external pure returns (int256); + function smodRhsNegTwo(int256 n) external pure returns (int256); + function smodRhsFive(int256 n) external pure returns (int256); + function smodRhsNegFive(int256 n) external pure returns (int256); + function smodRhsMin(int256 n) external pure returns (int256); + function smodRhsMax(int256 n) external pure returns (int256); + function smodLhsZero(int256 d) external pure returns (int256); + function smodLhsOne(int256 d) external pure returns (int256); + function smodLhsNegOne(int256 d) external pure returns (int256); + function smodLhsTwo(int256 d) external pure returns (int256); + function smodLhsNegTwo(int256 d) external pure returns (int256); + function smodLhsFive(int256 d) external pure returns (int256); + function smodLhsNegFive(int256 d) external pure returns (int256); + function smodLhsMin(int256 d) external pure returns (int256); + function smodLhsMax(int256 d) external pure returns (int256); + } +); + sol!( contract Send { function transfer_self(uint _amount) public payable; diff --git a/crates/integration/src/tests.rs b/crates/integration/src/tests.rs index 6172054..bd3fab2 100644 --- a/crates/integration/src/tests.rs +++ b/crates/integration/src/tests.rs @@ -1,11 +1,13 @@ use std::str::FromStr; use alloy_primitives::*; +use alloy_sol_types::SolCall; use resolc::test_utils::build_yul; use revive_runner::*; use SpecsAction::*; use crate::cases::Contract; +use crate::cases::{DivConst, ModConst, SdivConst, SmodConst}; /// Parameters: /// - The function name of the test @@ -268,6 +270,522 @@ fn signed_remainder() { run_differential(actions); } +/// Build calldata for the both-const Yul fixtures. Each fixture reads the case +/// index from `calldataload(0)` and a 32-byte "tag" from `calldataload(32)` +/// that gets XORed into the returned result. A non-zero tag turns silent +/// poison/undef from a UB-triggering const-fold into an observable divergence +/// from EVM (otherwise the buggy path coincidentally returns 0, which matches +/// EVM SMOD/SDIV's defined result for INT_MIN op -1 and masks the bug). +fn yul_which_calldata(which: u8) -> Vec { + let mut data = vec![0u8; 64]; + data[31] = which; + // tag: 0xdeadbeef padded into the second 32-byte word + data[60..64].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + data +} + +fn instantiate_yul(path: &str, contract: &str) -> Vec { + vec![Instantiate { + origin: TestAddress::Alice, + value: 0, + gas_limit: Some(GAS_LIMIT), + storage_deposit_limit: None, + code: Code::Yul { + path: path.into(), + contract: contract.to_string(), + }, + data: vec![], + salt: OptionalHex::default(), + }] +} + +fn unsigned_const_set() -> (U256, U256, U256, U256) { + (U256::from(1), U256::from(2), U256::from(5), U256::MAX) +} + +fn signed_const_set() -> (I256, I256, I256, I256, I256, I256, I256, I256) { + ( + I256::try_from(1).unwrap(), + I256::try_from(2).unwrap(), + I256::try_from(-2).unwrap(), + I256::try_from(5).unwrap(), + I256::try_from(-5).unwrap(), + I256::MIN, + I256::MIN + I256::ONE, + I256::MAX, + ) +} + +fn div_rhs_const_data(n: U256, d: U256) -> Vec { + let (one, two, five, max) = unsigned_const_set(); + if d == U256::ZERO { + DivConst::divRhsZeroCall::new((n,)).abi_encode() + } else if d == one { + DivConst::divRhsOneCall::new((n,)).abi_encode() + } else if d == two { + DivConst::divRhsTwoCall::new((n,)).abi_encode() + } else if d == five { + DivConst::divRhsFiveCall::new((n,)).abi_encode() + } else if d == max { + DivConst::divRhsMaxCall::new((n,)).abi_encode() + } else { + panic!("no divRhsConst variant for d={d}") + } +} + +fn div_lhs_const_data(n: U256, d: U256) -> Vec { + let (one, two, five, max) = unsigned_const_set(); + if n == U256::ZERO { + DivConst::divLhsZeroCall::new((d,)).abi_encode() + } else if n == one { + DivConst::divLhsOneCall::new((d,)).abi_encode() + } else if n == two { + DivConst::divLhsTwoCall::new((d,)).abi_encode() + } else if n == five { + DivConst::divLhsFiveCall::new((d,)).abi_encode() + } else if n == max { + DivConst::divLhsMaxCall::new((d,)).abi_encode() + } else { + panic!("no divLhsConst variant for n={n}") + } +} + +fn mod_rhs_const_data(n: U256, d: U256) -> Vec { + let (one, two, five, max) = unsigned_const_set(); + if d == U256::ZERO { + ModConst::modRhsZeroCall::new((n,)).abi_encode() + } else if d == one { + ModConst::modRhsOneCall::new((n,)).abi_encode() + } else if d == two { + ModConst::modRhsTwoCall::new((n,)).abi_encode() + } else if d == five { + ModConst::modRhsFiveCall::new((n,)).abi_encode() + } else if d == max { + ModConst::modRhsMaxCall::new((n,)).abi_encode() + } else { + panic!("no modRhsConst variant for d={d}") + } +} + +fn mod_lhs_const_data(n: U256, d: U256) -> Vec { + let (one, two, five, max) = unsigned_const_set(); + if n == U256::ZERO { + ModConst::modLhsZeroCall::new((d,)).abi_encode() + } else if n == one { + ModConst::modLhsOneCall::new((d,)).abi_encode() + } else if n == two { + ModConst::modLhsTwoCall::new((d,)).abi_encode() + } else if n == five { + ModConst::modLhsFiveCall::new((d,)).abi_encode() + } else if n == max { + ModConst::modLhsMaxCall::new((d,)).abi_encode() + } else { + panic!("no modLhsConst variant for n={n}") + } +} + +fn sdiv_rhs_const_data(n: I256, d: I256) -> Vec { + let (one, two, neg_two, five, neg_five, min, min_p1, max) = signed_const_set(); + if d == I256::ZERO { + SdivConst::sdivRhsZeroCall::new((n,)).abi_encode() + } else if d == one { + SdivConst::sdivRhsOneCall::new((n,)).abi_encode() + } else if d == I256::MINUS_ONE { + SdivConst::sdivRhsNegOneCall::new((n,)).abi_encode() + } else if d == two { + SdivConst::sdivRhsTwoCall::new((n,)).abi_encode() + } else if d == neg_two { + SdivConst::sdivRhsNegTwoCall::new((n,)).abi_encode() + } else if d == five { + SdivConst::sdivRhsFiveCall::new((n,)).abi_encode() + } else if d == neg_five { + SdivConst::sdivRhsNegFiveCall::new((n,)).abi_encode() + } else if d == min { + SdivConst::sdivRhsMinCall::new((n,)).abi_encode() + } else if d == min_p1 { + SdivConst::sdivRhsMinPlusOneCall::new((n,)).abi_encode() + } else if d == max { + SdivConst::sdivRhsMaxCall::new((n,)).abi_encode() + } else { + panic!("no sdivRhsConst variant for d={d}") + } +} + +fn sdiv_lhs_const_data(n: I256, d: I256) -> Vec { + let (one, two, neg_two, five, neg_five, min, min_p1, max) = signed_const_set(); + if n == I256::ZERO { + SdivConst::sdivLhsZeroCall::new((d,)).abi_encode() + } else if n == one { + SdivConst::sdivLhsOneCall::new((d,)).abi_encode() + } else if n == I256::MINUS_ONE { + SdivConst::sdivLhsNegOneCall::new((d,)).abi_encode() + } else if n == two { + SdivConst::sdivLhsTwoCall::new((d,)).abi_encode() + } else if n == neg_two { + SdivConst::sdivLhsNegTwoCall::new((d,)).abi_encode() + } else if n == five { + SdivConst::sdivLhsFiveCall::new((d,)).abi_encode() + } else if n == neg_five { + SdivConst::sdivLhsNegFiveCall::new((d,)).abi_encode() + } else if n == min { + SdivConst::sdivLhsMinCall::new((d,)).abi_encode() + } else if n == min_p1 { + SdivConst::sdivLhsMinPlusOneCall::new((d,)).abi_encode() + } else if n == max { + SdivConst::sdivLhsMaxCall::new((d,)).abi_encode() + } else { + panic!("no sdivLhsConst variant for n={n}") + } +} + +fn smod_rhs_const_data(n: I256, d: I256) -> Vec { + let (one, two, neg_two, five, neg_five, min, _min_p1, max) = signed_const_set(); + if d == I256::ZERO { + SmodConst::smodRhsZeroCall::new((n,)).abi_encode() + } else if d == one { + SmodConst::smodRhsOneCall::new((n,)).abi_encode() + } else if d == I256::MINUS_ONE { + SmodConst::smodRhsNegOneCall::new((n,)).abi_encode() + } else if d == two { + SmodConst::smodRhsTwoCall::new((n,)).abi_encode() + } else if d == neg_two { + SmodConst::smodRhsNegTwoCall::new((n,)).abi_encode() + } else if d == five { + SmodConst::smodRhsFiveCall::new((n,)).abi_encode() + } else if d == neg_five { + SmodConst::smodRhsNegFiveCall::new((n,)).abi_encode() + } else if d == min { + SmodConst::smodRhsMinCall::new((n,)).abi_encode() + } else if d == max { + SmodConst::smodRhsMaxCall::new((n,)).abi_encode() + } else { + panic!("no smodRhsConst variant for d={d}") + } +} + +fn smod_lhs_const_data(n: I256, d: I256) -> Vec { + let (one, two, neg_two, five, neg_five, min, _min_p1, max) = signed_const_set(); + if n == I256::ZERO { + SmodConst::smodLhsZeroCall::new((d,)).abi_encode() + } else if n == one { + SmodConst::smodLhsOneCall::new((d,)).abi_encode() + } else if n == I256::MINUS_ONE { + SmodConst::smodLhsNegOneCall::new((d,)).abi_encode() + } else if n == two { + SmodConst::smodLhsTwoCall::new((d,)).abi_encode() + } else if n == neg_two { + SmodConst::smodLhsNegTwoCall::new((d,)).abi_encode() + } else if n == five { + SmodConst::smodLhsFiveCall::new((d,)).abi_encode() + } else if n == neg_five { + SmodConst::smodLhsNegFiveCall::new((d,)).abi_encode() + } else if n == min { + SmodConst::smodLhsMinCall::new((d,)).abi_encode() + } else if n == max { + SmodConst::smodLhsMaxCall::new((d,)).abi_encode() + } else { + panic!("no smodLhsConst variant for n={n}") + } +} + +fn push_call(actions: &mut Vec, dest: TestAddress, data: Vec) { + actions.push(Call { + origin: TestAddress::Alice, + dest, + value: 0, + gas_limit: None, + storage_deposit_limit: None, + data, + }); +} + +#[test] +fn unsigned_division_half_const() { + let mut actions = instantiate("contracts/DivisionArithmeticsConst.sol", "DivConst"); + let (one, two, five, _max) = unsigned_const_set(); + let pairs = [ + (five, five), + (five, one), + (U256::ZERO, U256::MAX), + (five, two), + (one, U256::ZERO), + ]; + for (n, d) in pairs { + push_call( + &mut actions, + TestAddress::Instantiated(0), + div_rhs_const_data(n, d), + ); + push_call( + &mut actions, + TestAddress::Instantiated(0), + div_lhs_const_data(n, d), + ); + } + run_differential(actions); +} + +#[test] +fn unsigned_division_both_const() { + let mut actions = instantiate_yul("contracts/DivBothConst.yul", "DivBothConst"); + let pair_count = 5; + for i in 0..pair_count { + push_call( + &mut actions, + TestAddress::Instantiated(0), + yul_which_calldata(i), + ); + } + run_differential(actions); +} + +#[test] +fn signed_division_half_const() { + let mut actions = instantiate("contracts/DivisionArithmeticsConst.sol", "SdivConst"); + let (one, two, neg_two, five, neg_five, _min, _min_p1, _max) = signed_const_set(); + let pairs = [ + (five, five), + (five, one), + (I256::ZERO, I256::MAX), + (I256::ZERO, I256::MINUS_ONE), + (five, two), + (five, I256::MINUS_ONE), + (I256::MINUS_ONE, neg_two), + (neg_five, neg_five), + (neg_five, two), + (I256::MINUS_ONE, I256::MIN), + (one, I256::ZERO), + (I256::MIN, I256::MINUS_ONE), + (I256::MIN + I256::ONE, I256::MINUS_ONE), + ]; + for (n, d) in pairs { + push_call( + &mut actions, + TestAddress::Instantiated(0), + sdiv_rhs_const_data(n, d), + ); + push_call( + &mut actions, + TestAddress::Instantiated(0), + sdiv_lhs_const_data(n, d), + ); + } + run_differential(actions); +} + +#[test] +fn signed_division_both_const() { + let mut actions = instantiate_yul("contracts/SdivBothConst.yul", "SdivBothConst"); + let pair_count = 13; + for i in 0..pair_count { + push_call( + &mut actions, + TestAddress::Instantiated(0), + yul_which_calldata(i), + ); + } + run_differential(actions); +} + +#[test] +fn unsigned_remainder_half_const() { + let mut actions = instantiate("contracts/DivisionArithmeticsConst.sol", "ModConst"); + let (one, two, five, _max) = unsigned_const_set(); + let pairs = [ + (five, five), + (five, one), + (U256::ZERO, U256::MAX), + (U256::MAX, U256::MAX), + (five, two), + (two, five), + (U256::MAX, U256::ZERO), + ]; + for (n, d) in pairs { + push_call( + &mut actions, + TestAddress::Instantiated(0), + mod_rhs_const_data(n, d), + ); + push_call( + &mut actions, + TestAddress::Instantiated(0), + mod_lhs_const_data(n, d), + ); + } + run_differential(actions); +} + +#[test] +fn unsigned_remainder_both_const() { + let mut actions = instantiate_yul("contracts/ModBothConst.yul", "ModBothConst"); + let pair_count = 7; + for i in 0..pair_count { + push_call( + &mut actions, + TestAddress::Instantiated(0), + yul_which_calldata(i), + ); + } + run_differential(actions); +} + +#[test] +fn signed_remainder_half_const() { + let mut actions = instantiate("contracts/DivisionArithmeticsConst.sol", "SmodConst"); + let (one, two, neg_two, five, neg_five, _min, _min_p1, _max) = signed_const_set(); + let pairs = [ + (five, five), + (five, one), + (I256::ZERO, I256::MAX), + (I256::MAX, I256::MAX), + (five, two), + (two, five), + (five, neg_five), + (five, I256::MINUS_ONE), + (five, neg_two), + (neg_five, two), + (neg_two, five), + (neg_five, neg_five), + (neg_five, I256::MINUS_ONE), + (neg_five, neg_two), + (neg_two, neg_five), + (I256::MIN, I256::MINUS_ONE), + (I256::ZERO, I256::ZERO), + ]; + for (n, d) in pairs { + push_call( + &mut actions, + TestAddress::Instantiated(0), + smod_rhs_const_data(n, d), + ); + push_call( + &mut actions, + TestAddress::Instantiated(0), + smod_lhs_const_data(n, d), + ); + } + run_differential(actions); +} + +#[test] +fn signed_remainder_both_const() { + let mut actions = instantiate_yul("contracts/SmodBothConst.yul", "SmodBothConst"); + let pair_count = 17; + for i in 0..pair_count { + push_call( + &mut actions, + TestAddress::Instantiated(0), + yul_which_calldata(i), + ); + } + run_differential(actions); +} + +/// Surfaces the `smod(INT_MIN, -1)` LLVM-UB const-fold bug from +/// paritytech/revive#524. See `SmodIntMinNegOneBug.yul` for why this specific +/// fixture is shaped the way it is. Expected to FAIL until the bug is fixed. +#[test] +fn signed_remainder_int_min_neg_one_bug() { + let mut actions = instantiate_yul("contracts/SmodIntMinNegOneBug.yul", "SmodIntMinNegOneBug"); + let mut tag = vec![0u8; 32]; + tag[28..32].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + push_call(&mut actions, TestAddress::Instantiated(0), tag); + run_differential(actions); +} + +/// Sibling to `signed_remainder_int_min_neg_one_bug`. Per the issue, sdiv is +/// claimed to be guarded; this test pins that guard so regressions surface. +#[test] +fn signed_division_int_min_neg_one_bug() { + let mut actions = instantiate_yul("contracts/SdivIntMinNegOneBug.yul", "SdivIntMinNegOneBug"); + let mut tag = vec![0u8; 32]; + tag[28..32].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + push_call(&mut actions, TestAddress::Instantiated(0), tag); + run_differential(actions); +} + +/// Build the standard "tag plus extra calldata words" payload used by the +/// single-case probe fixtures. +fn probe_calldata(extra_words: &[U256]) -> Vec { + let mut data = vec![0u8; 32 * (1 + extra_words.len())]; + data[28..32].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef]); + for (i, word) in extra_words.iter().enumerate() { + let off = 32 * (i + 1); + data[off..off + 32].copy_from_slice(&word.to_be_bytes::<32>()); + } + data +} + +fn run_probe(path: &str, contract: &str, extra: &[U256]) { + let mut actions = instantiate_yul(path, contract); + push_call( + &mut actions, + TestAddress::Instantiated(0), + probe_calldata(extra), + ); + run_differential(actions); +} + +#[test] +fn probe_shl_overflow() { + run_probe( + "contracts/ShlOverflowProbe.yul", + "ShlOverflowProbe", + &[U256::from(0x123456789abcdef_u64)], + ); +} + +#[test] +fn probe_shr_overflow() { + run_probe( + "contracts/ShrOverflowProbe.yul", + "ShrOverflowProbe", + &[U256::from(0x123456789abcdef_u64)], + ); +} + +#[test] +fn probe_sar_overflow() { + run_probe("contracts/SarOverflowProbe.yul", "SarOverflowProbe", &[]); +} + +#[test] +fn probe_addmod_zero() { + run_probe( + "contracts/AddModZeroProbe.yul", + "AddModZeroProbe", + &[U256::from(42), U256::from(99)], + ); +} + +#[test] +fn probe_mulmod_zero() { + run_probe( + "contracts/MulModZeroProbe.yul", + "MulModZeroProbe", + &[U256::from(42), U256::from(99)], + ); +} + +#[test] +fn probe_signextend_oob() { + run_probe( + "contracts/SignExtendOobProbe.yul", + "SignExtendOobProbe", + &[U256::MAX], + ); +} + +#[test] +fn probe_byte_oob() { + run_probe("contracts/ByteOobProbe.yul", "ByteOobProbe", &[U256::MAX]); +} + +#[test] +fn probe_exp_zero_zero() { + run_probe("contracts/ExpZeroZeroProbe.yul", "ExpZeroZeroProbe", &[]); +} + #[test] fn ext_code_hash() { let mut actions = instantiate("contracts/ExtCode.sol", "ExtCode"); diff --git a/crates/llvm-context/src/polkavm/context/function/runtime/arithmetics.rs b/crates/llvm-context/src/polkavm/context/function/runtime/arithmetics.rs index fa7c278..7e097ff 100644 --- a/crates/llvm-context/src/polkavm/context/function/runtime/arithmetics.rs +++ b/crates/llvm-context/src/polkavm/context/function/runtime/arithmetics.rs @@ -160,6 +160,15 @@ impl WriteLLVM for Remainder { } /// Implements the signed remainder operator according to the EVM specification. +/// +/// `srem(INT_MIN, -1)` is signed-overflow UB in LLVM even though the +/// mathematical remainder is `0`, and EVM `SMOD` defines this case as `0`. +/// Before performing the srem we therefore swap a `-1` divisor for `1`: +/// `srem(x, 1) == 0 == SMOD(x, -1)` for all x, so the substitution preserves +/// EVM semantics while never feeding the UB pair to LLVM's srem. Unlike a +/// branch-based guard this leaves the srem unconditionally well-defined at +/// the IR level, so constant propagation cannot fold its way into poison +/// regardless of pass ordering. See paritytech/revive#524. pub struct SignedRemainder; impl RuntimeFunction for SignedRemainder { @@ -180,9 +189,24 @@ impl RuntimeFunction for SignedRemainder { let operand_2 = Self::paramater(context, 1).into_int_value(); wrapped_division(context, operand_2, || { + let is_neg_one = context.builder().build_int_compare( + inkwell::IntPredicate::EQ, + operand_2, + context.word_type().const_all_ones(), + "smod_divisor_is_neg_one", + )?; + let safe_divisor = context + .builder() + .build_select( + is_neg_one, + context.word_const(1), + operand_2, + "smod_safe_divisor", + )? + .into_int_value(); Ok(context .builder() - .build_int_signed_rem(operand_1, operand_2, "SMOD")?) + .build_int_signed_rem(operand_1, safe_divisor, "SMOD")?) }) .map(Into::into) } diff --git a/crates/resolc/src/test_utils.rs b/crates/resolc/src/test_utils.rs index d54330a..8b9cb24 100644 --- a/crates/resolc/src/test_utils.rs +++ b/crates/resolc/src/test_utils.rs @@ -483,6 +483,130 @@ fn compile_evm( blob } +/// Compile a Yul source into a PVM blob via revive's Yul-to-LLVM path. +/// +/// `Project::try_from_yul_sources` parses raw Yul with revive's own parser +/// and never invokes solc's Yul optimizer, so literal operands written in the +/// source survive intact all the way to LLVM IR. +pub fn compile_yul_blob(contract_name: &str, yul_source: &str) -> Vec { + compile_yul_blob_with_options(contract_name, yul_source, OptimizerSettings::cycles()) +} + +/// Like [`compile_yul_blob`] but with explicit LLVM optimizer settings. +pub fn compile_yul_blob_with_options( + contract_name: &str, + yul_source: &str, + optimizer_settings: OptimizerSettings, +) -> Vec { + let id = CachedBlob { + contract_name: contract_name.to_owned(), + opt: optimizer_settings.middle_end_as_string(), + solc_optimizer_enabled: false, + solidity: yul_source.to_owned(), + }; + if let Some(blob) = PVM_BLOB_CACHE.lock().unwrap().get(&id) { + return blob.clone(); + } + + check_dependencies(); + inkwell::support::enable_llvm_pretty_stack_trace(); + initialize_llvm(PolkaVMTarget::PVM, crate::DEFAULT_EXECUTABLE_NAME, &[]); + + let path = format!("{contract_name}.yul"); + let mut build = Project::try_from_yul_sources( + BTreeMap::from([( + path.clone(), + SolcStandardJsonInputSource::from(yul_source.to_owned()), + )]), + Default::default(), + None, + &DEBUG_CONFIG, + ) + .expect("yul project should build") + .compile( + &mut vec![], + optimizer_settings, + MetadataHash::Keccak256, + &DEBUG_CONFIG, + Default::default(), + Default::default(), + ) + .expect("yul should compile"); + build.take_and_write_warnings(); + build.check_errors().expect("yul build should succeed"); + let mut build = build.link(Default::default(), &DEBUG_CONFIG); + build.take_and_write_warnings(); + build.check_errors().expect("yul link should succeed"); + + let blob = build + .results + .into_values() + .next() + .expect("at least one contract") + .expect("contract should be built") + .build + .bytecode; + assert_eq!(&blob[..3], b"PVM"); + + PVM_BLOB_CACHE.lock().unwrap().insert(id, blob.clone()); + blob +} + +/// Compile a Yul source into the EVM deploy bytecode via `solc --strict-assembly --bin` +/// **without** `--optimize`, so solc's Yul optimizer never folds literal arithmetic. +/// The resulting EVM bytecode therefore executes the op at runtime with the literal +/// operands on the stack, giving us a faithful EVM oracle even for cases where the +/// op would otherwise be constant-folded at compile time. +pub fn compile_yul_evm_deploy_code(contract_name: &str, yul_source: &str) -> Vec { + let id = CachedBlob { + contract_name: contract_name.to_owned(), + opt: String::new(), + solc_optimizer_enabled: false, + solidity: yul_source.to_owned(), + }; + if let Some(blob) = EVM_BLOB_CACHE.lock().unwrap().get(&id) { + return blob.clone(); + } + + check_dependencies(); + + use std::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(0); + let counter = COUNTER.fetch_add(1, Ordering::Relaxed); + let tmp_path = std::env::temp_dir().join(format!( + "revive-yul-{contract_name}-{}-{counter}.yul", + std::process::id() + )); + std::fs::write(&tmp_path, yul_source).expect("write yul"); + + let output = std::process::Command::new(SolcCompiler::DEFAULT_EXECUTABLE_NAME) + .arg("--strict-assembly") + .arg("--bin") + .arg(&tmp_path) + .output() + .unwrap_or_else(|e| panic!("solc subprocess failed: {e}")); + let _ = std::fs::remove_file(&tmp_path); + assert!( + output.status.success(), + "solc failed compiling {contract_name}: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8(output.stdout).expect("solc output is utf-8"); + let hex_line = stdout + .lines() + .rev() + .find(|line| { + let trimmed = line.trim(); + !trimmed.is_empty() && trimmed.chars().all(|c| c.is_ascii_hexdigit()) + }) + .unwrap_or_else(|| panic!("no hex bytecode in solc output for {contract_name}:\n{stdout}")); + let blob = hex::decode(hex_line.trim()).expect("invalid hex from solc"); + + EVM_BLOB_CACHE.lock().unwrap().insert(id, blob.clone()); + blob +} + /// Compiles the Solidity source code into Yul IR and returns /// the Yul IR code of the contract named `contract_name`. pub fn compile_to_yul( diff --git a/crates/runner/src/lib.rs b/crates/runner/src/lib.rs index 4b18e1b..79ee18d 100644 --- a/crates/runner/src/lib.rs +++ b/crates/runner/src/lib.rs @@ -277,6 +277,16 @@ pub enum Code { #[serde(default)] libraries: SolcStandardJsonInputSettingsLibraries, }, + /// Compile a raw Yul source file and use the blob of `contract`. + /// + /// The PVM blob is produced via revive's Yul-to-LLVM path (bypassing solc's + /// Yul optimizer), and the EVM bytecode is produced via `solc --strict-assembly` + /// without `--optimize` so literal operands reach both runtimes intact. + #[cfg(feature = "resolc")] + Yul { + path: std::path::PathBuf, + contract: String, + }, /// Read the contract blob from disk Path(std::path::PathBuf), /// A contract blob @@ -315,6 +325,16 @@ impl From for pallet_revive::Code { libraries, )) } + #[cfg(feature = "solidity")] + Code::Yul { path, contract } => { + let Ok(source_code) = std::fs::read_to_string(&path) else { + panic!("Failed to read Yul source from {}", path.display()); + }; + pallet_revive::Code::Upload(resolc::test_utils::compile_yul_blob( + &contract, + &source_code, + )) + } Code::Path(path) => pallet_revive::Code::Upload(std::fs::read(path).unwrap()), Code::Bytes(bytes) => pallet_revive::Code::Upload(bytes), Code::Hash(hash) => pallet_revive::Code::Existing(hash), diff --git a/crates/runner/src/specs.rs b/crates/runner/src/specs.rs index e51bc37..63c276a 100644 --- a/crates/runner/src/specs.rs +++ b/crates/runner/src/specs.rs @@ -312,15 +312,6 @@ impl Specs { salt, .. } => { - let Code::Solidity { - path: Some(path), - solc_optimizer, - contract, - libraries, - } = code - else { - panic!("the differential runner requires Code::Solidity source"); - }; assert!( salt.0.is_none(), "salt is not supported in differential mode" @@ -331,17 +322,37 @@ impl Specs { "configuring the origin is not supported in differential mode" ); - let deploy_code = match std::fs::read_to_string(&path) { - Ok(solidity_source) => hex::encode(compile_evm_deploy_code( - &contract, - &solidity_source, - solc_optimizer.unwrap_or(true), + let deploy_code = match code { + Code::Solidity { + path: Some(path), + solc_optimizer, + contract, libraries, - )), - Err(err) => panic!( - "failed to read solidity source\n . path: '{}'\n . error: {:?}", - path.display(), - err + } => match std::fs::read_to_string(&path) { + Ok(solidity_source) => hex::encode(compile_evm_deploy_code( + &contract, + &solidity_source, + solc_optimizer.unwrap_or(true), + libraries, + )), + Err(err) => panic!( + "failed to read solidity source\n . path: '{}'\n . error: {:?}", + path.display(), + err + ), + }, + Code::Yul { path, contract } => match std::fs::read_to_string(&path) { + Ok(yul_source) => { + hex::encode(compile_yul_evm_deploy_code(&contract, &yul_source)) + } + Err(err) => panic!( + "failed to read Yul source\n . path: '{}'\n . error: {:?}", + path.display(), + err + ), + }, + _ => panic!( + "the differential runner requires Code::Solidity or Code::Yul source" ), }; let mut vm = evm