Skip to content

Miscompile in __revive_signed_remainder: srem(INT256_MIN, -1) UB #524

@jubnzv

Description

@jubnzv

SignedRemainder lowers Solidity signed % (EVM SMOD) to a bare LLVM srem via build_int_signed_rem, guarded only against a zero divisor by wrapped_division. For divisor -1 it emits srem x, -1, which is UB in LLVM when x == INT256_MIN and constant-folds to poison.

MRE:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
contract C {
    function f(int256 x) external pure returns (int256) {
        // INT_MIN % -1 == 0, so this is the identity `return x;`
        unchecked { return (type(int256).min % ((int256(-1) << 58) >> 58)) ^ x; }
    }
}

Reproduce: compile with resolc -O3 --bin mre.sol and call f(5) on PolkaVM/pallet-revive — it returns 0. The same source compiled with solc --bin-runtime --optimize and run on go-ethereum evm returns 5.

EVM:     f(5) = 0x...05   (correct)
PolkaVM: f(5) = 0x...00   (whole function collapsed to 0)

Expected behavior: f(x) == x for all x (EVM SMOD(INT_MIN, -1) = 0).

Notes:

  • Only signed % (srem); / (sdiv) is guarded and correct.
  • Runtime x % y with (INT_MIN, -1) is correct — the bare srem lowers to RISC-V rem, which defines INT_MIN rem -1 = 0; the bug is only LLVM's constant-fold of srem(constINT_MIN, const-1), so the dividend must also be compile-time constant.
  • The divisor must be a -1 that solc does not pre-fold to a literal (here (int256(-1) << 58) >> 58); writing int256(-1) directly makes solc fold INT_MIN % -1 → 0 itself and the bug disappears.
  • Silent: no diagnostic; PolkaVM bytecode returns the wrong constant.

Git commit: 7fe8d41 (revive)
Build: resolc release, -O3; solc 0.8.35-develop; go-ethereum evm 1.17.2

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions