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
+1336 -21

No files matched your search

+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");