mirror of
https://github.com/pezkuwichain/revive.git
synced 2026-11-02 17:01:11 +00:00
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:
1 parent
62a62770e3
commit
40e97d11eb
23 files changed
+1336
-21
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user