How to Fix: Cranelift: Wrong result for `select_spectre_guard.i64` with `i128` arguments on riscv64
Cranelift on riscv64 can miscompile select_spectre_guard.i64 when the surrounding data flow forces i128 values through the backend, and the failure usually points to a legalization or register-allocation mismatch rather than a frontend IR bug.
This issue appears in a .clif test where removing function parameters, return values, or stack_store operations makes the failure disappear. That pattern is a strong signal that the bug is triggered by how riscv64 lowers or preserves multi-register i128 values across control-dependent selection logic, especially around select_spectre_guard.i64.
In practice, the fix is not to rewrite the test but to correct the backend handling of i128 operands so that the guarded select operation preserves the intended low/high-half relationship, stack layout, and move sequencing.
Understanding the Root Cause
select_spectre_guard.i64 is not an ordinary select. It exists to preserve speculation safety semantics while producing a scalar result. On riscv64, however, the surrounding IR can still force the compiler to track related i128 values through ABI lowering, stack spills, reloads, or tuple-like reconstruction.
The critical detail is that i128 on a 64-bit target is usually represented as two 64-bit halves. If the backend legalization path treats those halves inconsistently during one of these phases, the final generated code can produce a wrong value even though the original CLIF looks valid:
- Instruction legalization may split an i128 into low/high parts but fail to preserve ordering across a guarded select sequence.
- Register allocation may spill one half and reload it in a different place, exposing a latent bug in move insertion.
- Stack slot handling may interact with endianness or offset computation, especially if several stack_store operations constrain the live ranges.
- ABI lowering for function params/returns may force values into register pairs or stack-backed forms that take a different lowering path from the minimized test.
That explains why the test passes once you remove arguments, returns, or stack traffic: doing so changes the live-range pressure and can avoid the buggy lowering path entirely.
Technically, the likely failure mode is one of these backend mistakes:
- The low 64 bits and high 64 bits of an i128 are swapped during a move or reload.
- One half is selected under the Spectre guard condition while the other half is taken from the wrong source.
- A pseudo-instruction or lowered sequence for select_spectre_guard.i64 clobbers a register still needed by the split i128 value.
- The backend assumes a value is dead after selection, but later stack reconstruction still reads it.
So the root cause is not that select_spectre_guard.i64 directly returns i128; it is that the backend mishandles adjacent i128 state while lowering a guarded select in a constrained riscv64 code path.
Step-by-Step Solution
The most reliable fix is to patch the riscv64 Cranelift backend where split i128 values interact with guarded selection, then lock it down with a regression test.
1. Reproduce the failure with the original CLIF test
cargo test -p cranelift-codegen -- riscv64 select_spectre_guard
If you have a dedicated file for the reproducer, run the filecheck-based test directly through the project’s existing test harness.
2. Inspect the legalized IR and lowered machine form
You want to confirm where the value becomes incorrect: before lowering, during lowering, or after register allocation.
RUST_LOG=cranelift_codegen=trace cargo test -p cranelift-codegen -- riscv64
Focus on these checkpoints:
- The original CLIF around select_spectre_guard.i64.
- The legalized form of any related i128 values.
- Inserted stack_store and reload sequences.
- The final riscv64 instruction stream and register assignments.
3. Audit the split/rebuild path for i128 on riscv64
Search the backend for the code that lowers or manipulates wide integers on riscv64. You are looking for logic equivalent to:
// Pseudocode: split i128 into lo/hi halves consistently during lowering.
fn lower_i128(value: I128Value) -> (Reg, Reg) {
let lo = extract_low64(value);
let hi = extract_high64(value);
(lo, hi)
}
fn rebuild_i128(lo: Reg, hi: Reg) -> I128Value {
concat_i64_to_i128(lo, hi)
}
The fix must ensure the same half ordering is used everywhere:
- extract
- spill
- reload
- copy/move insertion
- return-value reconstruction
If any one stage uses (hi, lo) while another expects (lo, hi), the test will fail only under certain pressure patterns, exactly like this issue.
4. Verify the guarded select does not clobber registers needed by the i128 pair
Check the lowering sequence for select_spectre_guard.i64. If it emits a conditional move, branch sequence, or masking pattern, confirm that temporary registers are not reusing one half of a live i128.
// Pseudocode checklist
// before lowering select_spectre_guard.i64:
// live: i128_lo in x10, i128_hi in x11
// lowering must not use x10/x11 as scratch unless values are moved or marked dead
If scratch allocation is the problem, reserve a dedicated temp or force a copy before the guarded sequence.
5. Fix stack spill/reload ordering for paired values
This class of bug often shows up when the compiler stores two 64-bit halves to the stack and reloads them in the wrong order. Review code that computes stack offsets for wide values.
// Correct idea for a 16-byte i128 stack slot on a 64-bit backend:
store64 slot + 0 <= lo
store64 slot + 8 <= hi
lo = load64 slot + 0
hi = load64 slot + 8
If the actual backend does the reverse, or if one path uses architectural endianness incorrectly for scalar reconstruction, fix that path and re-run the reproducer.
6. Add a regression test that preserves the original pressure
Because the issue disappears when the test is minimized, keep the characteristics that trigger the bug:
- Multiple function arguments
- At least one return path involving i128
- Enough stack_store operations to force spills
- The exact select_spectre_guard.i64 pattern that previously miscompiled
; Keep params, returns, and stack stores intact.
; The goal is to preserve the register-pressure pattern that exposed the bug.
Do not over-minimize the regression test. In backend bugs, keeping the pressure pattern is often more important than making the file short.
7. Validate on the target backend only
Run the relevant test suite for riscv64 and verify the change does not alter behavior on unrelated targets.
cargo test -p cranelift-codegen riscv64
cargo test -p cranelift-filetests
If possible, also compare before/after generated machine code for the failing function to confirm the repaired sequence now preserves the correct i128 halves.
Common Edge Cases
- Register pressure hides the bug: A simplified test may compile correctly because no spills occur. That does not mean the backend is correct.
- Only one half is corrupted: Many failures look random because the low half remains valid while the high half is stale, swapped, or reloaded from the wrong slot.
- ABI-specific behavior: Params and returns can take a different path from local temporaries. A fix that only handles one path may still fail in call boundaries.
- Scratch register reuse: Guarded select lowering may use a temp that overlaps with one register of the split i128.
- Late-stage move insertion: The legalized IR can look correct while the final machine code is wrong due to copies inserted after allocation decisions.
- Endianness assumptions: Even on a little-endian target, reconstruction logic for wide scalars must be consistent with spill layout and extraction helpers.
FAQ
Why does removing stack_store operations make the test pass?
Because stack traffic changes live ranges and often forces spills/reloads. If the bug lives in paired-value stack handling for i128, removing stores can bypass the broken path.
Is select_spectre_guard.i64 itself broken on riscv64?
Usually not in isolation. The bug is more likely in the interaction between the guarded select lowering and nearby i128 legalization, register allocation, or stack reconstruction.
Why is this issue hard to minimize?
Because backend miscompilations often depend on exact pressure conditions. Parameters, returns, and stack stores are not noise here; they are part of the trigger that exposes the wrong code generation.
The practical takeaway is simple: treat this as a riscv64 backend correctness bug involving i128 split-value handling around select_spectre_guard.i64. Fix the half ordering, spill/reload consistency, and scratch-register safety, then preserve the original pressure pattern in a regression test so the bug stays fixed.