Skip to content

fix(opt): i64 ops hardcoding R0..R3 cause AAPCS param clobber (#103)#106

Merged
avrabe merged 1 commit into
mainfrom
fix/issue-103-i64setcond-aapcs
May 12, 2026
Merged

fix(opt): i64 ops hardcoding R0..R3 cause AAPCS param clobber (#103)#106
avrabe merged 1 commit into
mainfrom
fix/issue-103-i64setcond-aapcs

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 11, 2026

Fixes #103.

Summary

The cargo-fuzz harness i64_lowering_doesnt_clobber_params (PR #100) caught I64SetCond emitting rd: R0, rn_lo: R0, rn_hi: R1, rm_lo: R2, rm_hi: R3 before LocalGet(0) reads R0 — clobbering an AAPCS param register. The fix is the same class as PR #86's i64-const fix in optimizer_bridge, just applied to the --no-optimize path.

Root cause

In instruction_selector::select_with_stack (the --no-optimize lowering path), the wildcard fallthrough handed every unhandled i64 op off to select_default, which hardcodes:

  • rn_lo: R0, rn_hi: R1 (first operand)
  • rm_lo: R2, rm_hi: R3 (second operand)
  • rd: R0 (result)

These are the AAPCS param registers — clobbered before LocalGet(0..3) has a chance to read them.

Audit

Every i64 op in select_with_stack audited. The following previously fell through to select_default and have now received explicit handlers (24 ops in total):

Category Ops
Comparisons I64Eq, I64Ne, I64LtS, I64LtU, I64LeS, I64LeU, I64GtS, I64GtU, I64GeS, I64GeU
Arithmetic I64Mul, I64DivS, I64DivU, I64RemS, I64RemU
Rotations I64Rotl, I64Rotr
Unary bit I64Clz, I64Ctz, I64Popcnt
Sign-extend I64Extend8S, I64Extend16S, I64Extend32S
Conversion I32WrapI64

Already-handled i64 ops (I64Add, I64Sub, I64And/Or/Xor, I64Shl/ShrU/ShrS, I64Const, I64ExtendI32S/U, I64Load*, I64Store*, I64Eqz) were already correct.

Each new handler:

  1. Pops the actual operand register pair(s) from the wasm stack tracker.
  2. Allocates the destination via alloc_temp_safe (i32 result) or alloc_consecutive_pair (i64 result), passing the popped halves in extra_avoid so the destination doesn't overlap any operand half before the encoded sequence reads it.
  3. Emits the corresponding ArmOp with stack-tracked register references.

Regression test

New file crates/synth-synthesis/tests/issue_103_i64_aapcs.rs (385 lines, 3 tests):

  • issue_103_i64_lt_s_does_not_clobber_r0 — the exact I64SetCond hardcodes R0..R3, clobbers param registers (found by #100 fuzz harness) #103 repro (i64 LtS before LocalGet(0)). On main this panics with: AAPCS clobber: ARM instr at wasm line 2 writes param reg R0 before LocalGet(0) at line 4. Op: I64SetCond { rd: R0, rn_lo: R0, rn_hi: R1, rm_lo: R2, rm_hi: R3, cond: LT }. Passes with this fix.
  • issue_103_all_i64_ops_preserve_params — class-level audit covering all 24 ops at num_params ∈ {1, 2, 3, 4}.
  • i64_eqz_still_preserves_params — sanity check that the already-handled I64Eqz path is not regressed.

The test framework rebuilds the same AAPCS invariant PR #100's fuzz harness asserts: no ARM instruction may write a param register before that param's earliest LocalGet.

Test plan

  • cargo test -p synth-synthesis --test issue_103_i64_aapcs — 3/3 pass with fix, 2/3 fail (the two new tests) on main.
  • cargo test --workspace --exclude synth-verify — clean (synth-verify excluded because of unrelated z3-sys download failure on the dev host).
  • cargo clippy --workspace --all-targets --exclude synth-verify -- -D warnings — clean.
  • cargo fmt --check — clean.
  • cd fuzz && cargo fuzz run i64_lowering_doesnt_clobber_params -- -max_total_time=120 — fuzz infra not yet on main (PR feat(fuzz): cargo-fuzz harnesses for ARM instruction selection (#82) #100 still in CI); the static regression test in this PR encodes the same invariant.

Test count delta

+3 tests in synth-synthesis.

Files referenced as out-of-scope

Per task instructions, did not touch:

🤖 Generated with Claude Code

The cargo-fuzz harness `i64_lowering_doesnt_clobber_params` (PR #100)
caught I64SetCond emitting `rd: R0, rn_lo: R0, rn_hi: R1, rm_lo: R2,
rm_hi: R3` before LocalGet(0) reads R0 — clobbering an AAPCS param
register. Root cause: in `instruction_selector::select_with_stack`
(the `--no-optimize` lowering path), the wildcard fallthrough handed
every unhandled i64 op off to `select_default`, which hardcodes R0:R1
/ R2:R3 for operand pairs and R0 for the result. Same class as PR #86's
i64-const fix in `optimizer_bridge`, just for the no-optimize path.

Audited every i64 op in `select_with_stack` and added explicit handlers
for those that previously fell through:

- Comparisons: I64Eq, I64Ne, I64LtS, I64LtU, I64LeS, I64LeU,
  I64GtS, I64GtU, I64GeS, I64GeU
- Arithmetic: I64Mul, I64DivS, I64DivU, I64RemS, I64RemU
- Rotations: I64Rotl, I64Rotr
- Unary bit: I64Clz, I64Ctz, I64Popcnt
- Sign-extends: I64Extend8S, I64Extend16S, I64Extend32S
- Conversion: I32WrapI64

Each pops the actual operand register pair(s) from the wasm stack and
allocates the destination via `alloc_temp_safe` (i32 result) or
`alloc_consecutive_pair` (i64 result), with the popped halves passed
in `extra_avoid` to keep them live until the encoded sequence reads them.

Regression test `issue_103_i64_aapcs.rs` covers the exact #103 repro plus
all 24 audited ops at num_params=1..4. Fails on main with the documented
clobber message, passes with this fix.

Test count delta: +3 tests in synth-synthesis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

❌ Patch coverage is 85.45455% with 32 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-synthesis/src/instruction_selector.rs 85.45% 32 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe avrabe merged commit 4743121 into main May 12, 2026
9 checks passed
@avrabe avrabe deleted the fix/issue-103-i64setcond-aapcs branch May 12, 2026 04:14
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.

I64SetCond hardcodes R0..R3, clobbers param registers (found by #100 fuzz harness)

1 participant