Skip to content

[Mips] Do not move a load/store across a call in adjustForDelaySlot - #1

Open
eigmax wants to merge 1 commit into
zkm-rustc/21.1-2025-08-01from
fix/delay-slot-no-store-motion-across-calls
Open

[Mips] Do not move a load/store across a call in adjustForDelaySlot#1
eigmax wants to merge 1 commit into
zkm-rustc/21.1-2025-08-01from
fix/delay-slot-no-store-motion-across-calls

Conversation

@eigmax

@eigmax eigmax commented Sep 10, 2026

Copy link
Copy Markdown
Member

Problem

adjustForDelaySlot (fork-only, added with "Fill load store into delay slot") walks forward from every load/store looking for ADDiu base, base, imm, then moves the memory op after it with the offset adjusted so the delay-slot filler can place it in the branch delay slot. The walk advances while canSwapLoadStoreWith(I, N) holds, and that check only compares register operands and mayLoadOrStore(). A jal clobbers registers through its RegMask operand, which the operand scan never looks at, so a store is carried across calls even when the register it stores is caller-saved and dies at the call.

Seen in production (ProjectZKM/Ziren#531): in an LTO'd Rust guest the arkworks Miller loop had

addiu $1, $5, 0xc0        ; next coefficient pointer
sw    $1, 0x4c($17)       ; <- original position
jal   memcpy              ; clobbers $1
jal   <Bn::ell>           ; clobbers $1
addiu $17, $17, 0x58
bnez  $16, loop
 sw   $1, -0xc($17)       ; <- after the pass: stale $1 stored in the delay slot

The iterator pointer stayed on coefficient #0 and then became 1; the next iteration copied its line coefficients from address 1, the Miller loop output became zero, and final_exponentiation(..).unwrap() panicked. Every mainnet block calling the EIP-2537 BLS12-381 pairing precompile failed. The deployed reth guest ELF has four such sites (two in the pairing check, one each in crossbeam_epoch and alloc::sync::Arc).

Not the cause (each verified by rebuilding the failing guest and scanning it): -disable-mips-delay-filler (the pre-pass runs before that check), -disable-post-ra, -enable-misched=false, -regalloc=basic, -C opt-level=2, -inst-same-cost, MipsOptimizeLoadStoreImm, MipsLoopReduceHiLo.

Fix

canSwapLoadStoreWith now refuses to swap with a call, terminator, branch, side-effecting or inline-asm instruction, and with any instruction whose RegMask clobbers a physical register the load/store reads.

Verification

  • Static scan of the produced ELF for "$1 read after a jal/jalr before being written, including the next branch's delay slot": failing guest 5 hits, two re-laid-out correct builds 0 hits, production reth guest 4 hits. The scan script is in the linked issue; a toolchain built from this branch should produce 0 hits on those guests.
  • The failing guest with this fix compiled in has not been run yet: it needs a toolchain rebuild (ProjectZKM/toolchain clone.sh/build.sh), then pair_exec on the captured block-25940305 pairing inputs must return the native result (pairing check = 1) instead of panicking.

No lit test is included: the pattern needs a store whose value register is caller-saved, two calls, and an ADDiu of the base register in one block. I can add a MIR test once the tree builds here.

adjustForDelaySlot walks forward from a load/store looking for the
`ADDiu base, base, imm` that lets the memory op sink into the branch delay
slot with an adjusted offset.  canSwapLoadStoreWith only compared register
operands and mayLoadOrStore(), so a store was carried across `jal`
instructions: a call clobbers through its RegMask operand, which the
operand scan never sees.  A store whose value register is caller-saved
($1/$at in the reported case) then wrote whatever the callees left there.

Observed in an LTO'd Rust guest (arkworks Miller loop): the iterator
pointer `sw $1, 0x4c($17)` was moved past `jal memcpy`, `jal ell` and
`addiu $17, $17, 0x58`, becoming `sw $1, -0xc($17)` in the `bnez` delay
slot, so the next iteration copied its line coefficients from address 1
and the pairing returned zero.  Every EIP-2537 pairing block failed in
production.

Refuse to swap with calls, terminators, branches, side-effecting or
inline-asm instructions, and with any RegMask that clobbers a register
the load/store reads.

See ProjectZKM/Ziren#531.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant