How to Fix: Cranelift: Panic `attempt to negate with overflow` on s390x
The crash is triggered by a classic two’s-complement trap: on s390x, a code path inside Cranelift tries to negate the minimum signed integer value, and Rust correctly panics with attempt to negate with overflow. In practice, this shows up when a .clif test feeds a value such as 0x8000_0000_0000_0000 into legalization, constant folding, or backend lowering logic that assumes every signed integer has a positive counterpart.
Understanding the Root Cause
The immediate reason for the panic is simple: the signed 64-bit minimum value, i64::MIN, equals -9223372036854775808. In two’s-complement arithmetic, its positive form cannot be represented in the same type. That means any direct operation equivalent to -i64::MIN overflows.
For this issue, the important detail is that s390x code generation often needs to reason about immediates, offsets, masks, and transformed arithmetic forms. If a backend optimization or instruction-selection rule does one of the following, it becomes unsafe:
- Computes an absolute value using direct negation.
- Rewrites subtraction into addition with a negated constant.
- Converts a signed immediate into its positive magnitude before encoding.
- Assumes a negative constant can always be inverted safely.
The provided test case includes v10 = iconst.i64 0x8000_0000_0000_0000, which is exactly the problematic boundary value. Even if the original IR is valid, a backend transformation may later hit a pattern like:
let x: i64 = i64::MIN;
let y = -x; // panic in debug-checked Rust paths
In Rust, this panic is expected behavior when overflow checks are enabled. So the real bug is not in the test case itself, but in Cranelift backend logic that fails to guard the minimum signed value before negating it.
Common hotspots where this appears in compiler backends include:
- Instruction legalization for unsupported forms of
isub,iadd_imm, or compare-immediate sequences. - Pattern matching that rewrites
a - Cintoa + (-C). - Addressing-mode selection where displacements or scaled offsets are normalized by sign.
- Immediate encoding helpers that test ranges after sign transformations.
On s390x, these paths deserve extra scrutiny because the backend may choose between multiple instruction forms depending on immediate size, signedness, and legal encodings.
Step-by-Step Solution
The fix is to remove all unchecked signed negation from the relevant lowering path and replace it with logic that explicitly handles i64::MIN.
1. Reproduce the failure locally
cargo test -p cranelift-codegen s390x -- --nocapture
If the issue is tied to a specific file test, run the targeted case instead:
cargo test -p cranelift-filetests -- --nocapture
You can also isolate the problematic IR in a dedicated .clif file similar to:
test compile
target s390x
function u1:0() -> i64 system_v {
block0:
v8 = iconst.i8 0
v10 = iconst.i64 0x8000_0000_0000_0000
; reduced repro continues here
}
2. Locate the negation path in the s390x backend
Search the backend and shared lowering code for patterns like:
-imm
0 - imm
imm.abs()
checked_neg()
wrapping_neg()
overflowing_neg()
Also inspect transformations involving:
isub
iadd_imm
icmp_imm
iconst
The dangerous pattern usually looks like this:
fn rewrite_imm(imm: i64) -> i64 {
-imm
}
That must be replaced with a safe branch.
3. Guard the boundary value explicitly
If the code only needs to know whether a negated form is representable, use checked negation:
fn try_negate_imm(imm: i64) -> Option<i64> {
imm.checked_neg()
}
Then handle the None case by selecting a different lowering strategy:
match imm.checked_neg() {
Some(negated) => {
// use the optimized instruction form
lower_as_add_with_imm(negated);
}
None => {
// imm == i64::MIN, use a fallback that does not negate
lower_via_register_materialization(imm);
}
}
This is usually the cleanest compiler fix because it preserves correctness without relying on wraparound behavior.
4. Prefer fallback lowering for unrepresentable immediates
When i64::MIN appears, the backend should materialize the constant in a register and use a register-register operation instead of forcing an immediate rewrite.
fn lower_sub_or_add_with_imm(dst: Value, src: Value, imm: i64) {
if let Some(negated) = imm.checked_neg() {
emit_add_imm(dst, src, negated);
} else {
let tmp = emit_iconst_i64(imm);
emit_sub_rr(dst, src, tmp);
}
}
This avoids both the panic and any accidental semantic change.
5. Do not replace the panic with wrapping arithmetic unless semantics require it
A tempting patch is:
let negated = imm.wrapping_neg();
That prevents the crash, but it can silently produce the wrong instruction selection logic. In compiler backends, wrapping arithmetic is only valid when the transformation itself is mathematically correct under modulo semantics. For immediate rewriting and encoding decisions, that is often false.
Use wrapping_neg only if the surrounding algorithm is intentionally defined in modular arithmetic. Otherwise, prefer a fallback path.
6. Add a regression test
Create or extend a filetest covering the exact minimum signed 64-bit constant on s390x:
test compile
target s390x
function %min_negation_panic() -> i64 system_v {
block0:
v0 = iconst.i64 0x8000_0000_0000_0000
v1 = iconst.i64 1
v2 = isub v1, v0
return v2
}
The goal of the test is not just successful parsing, but ensuring compilation completes without panic in the backend.
7. Validate with targeted and full test runs
cargo test -p cranelift-codegen s390x
cargo test -p cranelift-filetests
cargo test --workspace
If available in your environment, also run the backend-specific test matrix for s390x to catch related immediate-lowering regressions.
8. If you are preparing the upstream patch
Keep the commit message focused on both the symptom and the compiler invariant being restored. A strong example:
s390x: avoid negating i64::MIN during immediate lowering
Guard signed immediate negation in the s390x lowering path.
When the value is i64::MIN, fall back to register materialization
instead of rewriting through a negated immediate. This fixes the
panic "attempt to negate with overflow" for .clif test cases using
0x8000_0000_0000_0000.
Common Edge Cases
Fixing the obvious i64::MIN case is necessary, but a few neighboring problems can still slip through if the patch is too narrow.
- Other integer widths: If the same helper is generic over
i8,i16,i32, andi64, thenMINfor every signed width must be handled, not just 64-bit values. - Shared lowering helpers: The bug may originate in architecture-independent code and only surface first on s390x. If so, patch the shared utility instead of only the backend caller.
- Compare rewrites: Converting comparisons against negative immediates can trigger the same overflow if the code normalizes ranges through negation.
- Immediate range checks: Some logic negates first and then checks whether the result fits a smaller encoding window. That branch still needs protection.
- Debug vs release behavior: A panic may show up reliably in checked builds, while release builds may optimize differently. Do not treat the absence of a release panic as proof of correctness.
- Constant folding interactions: If mid-level IR simplification already transforms expressions involving
i64::MIN, verify the backend still receives a legal and correctly typed value.
A good hardening strategy is to centralize signed-immediate negation in one helper and forbid ad hoc direct negation across lowering code.
fn negate_if_representable(imm: i64) -> Option<i64> {
imm.checked_neg()
}
Then update all callers to branch on Option instead of assuming success.
FAQ
Why does only 0x8000_0000_0000_0000 trigger this panic?
Because it is i64::MIN, the only signed 64-bit value whose positive counterpart cannot be represented in the same type. Every other negative i64 can be negated safely.
Would using wrapping_neg() fix the problem?
It removes the panic, but not necessarily the bug. wrapping_neg() changes the arithmetic model to modular wraparound. That is only correct if the compiler transformation is valid under those semantics. For instruction selection and immediate rewriting, a fallback lowering path is usually the correct fix.
Is this an s390x-only bug?
It is exposed by the s390x backend in this issue, but the underlying mistake is generic: any compiler code that directly negates a signed minimum value is vulnerable. Review shared helpers and other backends for the same pattern.
The durable fix is straightforward: treat signed minimum values as a first-class edge case, never negate them blindly, and fall back to a lowering strategy that preserves correctness without overflow.