Fix optimization specific SREM edge case (#527)

Existing test coverage didn't catch a signed remainder overflow
semantics edge case behavior where at least one the operators is being
constant folded by LLVM instead of solc. This PR fixes the SREM
translation bug and adds missing test coverage for division ops with
constants.

Closes #524

---------

Signed-off-by: xermicus <cyrill@parity.io>
This commit is contained in:
xermicus authored and GitHub committed 2026-05-28 09:01:41 +02:00
1 parent 62a62770e3
commit 40e97d11eb
23 files changed
+1327 -12

No files matched your search

+8
View File
@@ -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`
+1 -1
View File
@@ -1,7 +1,7 @@
{
"Baseline": 870,
"Computation": 2418,
"DivisionArithmetics": 9274,
"DivisionArithmetics": 9368,
"ERC20": 17090,
"Events": 1662,
"FibonacciIterative": 1427,
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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) }
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
+72
View File
@@ -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;
+518
View File
@@ -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<u8> {
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<SpecsAction> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<u8> {
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<SpecsAction>, dest: TestAddress, data: Vec<u8>) {
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<u8> {
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");
@@ -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)
}
+124
View File
@@ -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<u8> {
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<u8> {
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<u8> {
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(
+20
View File
@@ -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<Code> 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),
+21 -10
View File
@@ -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,7 +322,13 @@ impl Specs {
"configuring the origin is not supported in differential mode"
);
let deploy_code = match std::fs::read_to_string(&path) {
let deploy_code = match code {
Code::Solidity {
path: Some(path),
solc_optimizer,
contract,
libraries,
} => match std::fs::read_to_string(&path) {
Ok(solidity_source) => hex::encode(compile_evm_deploy_code(
&contract,
&solidity_source,
@@ -343,6 +340,20 @@ impl Specs {
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
.code_blob(deploy_code.as_bytes().to_vec())