Skip to content

fix(arm): home i64 params per AAPCS register-pairs — close #518 silent miscompile (#242)#531

Open
avrabe wants to merge 1 commit into
mainfrom
fix/518-i64-param-aapcs-homing
Open

fix(arm): home i64 params per AAPCS register-pairs — close #518 silent miscompile (#242)#531
avrabe wants to merge 1 commit into
mainfrom
fix/518-i64-param-aapcs-homing

Conversation

@avrabe

@avrabe avrabe commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

What

Closes #518: an i64 binop reading an i64 parameter silently miscompiled on both ARM selectors. An i64 param occupies an AAPCS register pair (param0 = R0:R1), but both selectors treated a param as a single i32-width register. Found by footgun/adversarial differential testing; flip-independent, pre-existing.

Root cause + fix, by selector

Bounded: two sub-cases declined LOUDLY (Ok-or-Err, never silent wrong-code)

So no silent wrong-code remains: every i64-param function is either correctly compiled (leaf, register-resident — the common case) or loud-skipped.

Gate (oracle-first → correctness gate)

  • scripts/repro/i64_param_518_differential.py flipped to the correctness gate (EXPECT_MISCOMPILE=False): 11 leaf cases match wasmtime on both paths across the full AAPCS matrix (single i64, (i32,i64), (i64,i64), second-i64-param), PLUS a decline-contract check (i64_param_518_decline.wat): d_past_r3 + d_call loud-skip, d_leaf emitted + executes correctly.
  • Frozen anchors 3/3 byte-identical (control_step 0x00210A55 / flight_algo 0x07FDF307 / divseam — no i64 params).
  • Updated the u64-packed FFI return: emit register-direct field access instead of generic 64-bit shift extraction #94 hi32-extract tests (backend + 5 optimizer_bridge unit tests) to source their i64 from a non-param value — the old i64-param shapes were themselves exercising the now-declined miscompile, masked by size-only assertions. Renamed test_ir_to_arm_i64_add_* to also pin the decline contract.
  • Independently clean-room verified (correctness on both paths with adversarial inputs; bug confirmed real on the parent commit; fmt/clippy/tests/frozen all green).

Refs #242, #503 (the i64-stack-param follow-up the decline points at).

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.11382% with 22 lines in your changes missing coverage. Please review.

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

📢 Thoughts on this report? Let us know!

…t miscompile (#518, #242)

An i64 binop reading an i64 PARAM silently miscompiled on BOTH ARM selectors: an
i64 param occupies an AAPCS register PAIR (param0 = R0:R1), but both selectors
treated a param as a single i32-width register. Found by footgun/adversarial
differential testing; flip-independent, pre-existing.

Root cause + fix, by selector:
  - DIRECT (select_with_stack): `infer_i64_locals` learns i64-ness only from
    LocalSet/Tee, so a read-only i64 param stayed is_i64=false → its implicit hi
    register was unreserved and a following i64.const was allocated INTO it
    (movw r1,#K clobbered R1 = hi of an i64 param in R0:R1). Fixes: (a) seed
    `i64_locals` from `self.params_i64`; (b) reserve the hi half of a live i64
    param in the #193 reservation (live_param_regs); (c) map params to registers
    via the new `aapcs_param_regs` (even-aligned pairs — (i32,i64) -> R0,R2:R3,
    not the sequential R1:R2 the old index_to_reg used). All-i32 signatures map
    identically to index_to_reg, so non-i64-param functions are byte-identical.
  - OPTIMIZED (ir_to_arm): the I64Load param-home guard `num_params >= 2/4`
    conflated register-count with param-count and dropped the param (read from a
    fresh R4:R5 pair). Since ir_to_arm lacks per-param TYPES it cannot compute
    AAPCS pairing — decline any function that reads an i64 param to the direct
    selector (the #359/#188/#507 honest-degradation pattern).

Two i64-param sub-cases are declined LOUDLY (Ok-or-Err, never silent wrong-code),
their correct lowering tracked as follow-ups:
  - an i64 param AAPCS-passed PAST R3 (stack) — the open #503-i64 case;
  - an i64 param in a FRAME-BACKING function (has a call, or the pair-exhaustion
    retry) — the param_slots path sizes an i64 param's slot from a width set that
    excludes params, dropping the high half.

So no silent wrong-code remains: every i64-param function is either correctly
compiled (leaf, register-resident, the common case) or loud-skipped.

Gate: scripts/repro/i64_param_518_differential.py flipped to the correctness gate
(EXPECT_MISCOMPILE=False) — 11 leaf cases match wasmtime on BOTH paths across the
full AAPCS matrix (single i64, (i32,i64), (i64,i64), second-i64-param), PLUS a
decline-contract check (i64_param_518_decline.wat): d_past_r3 + d_call loud-skip,
d_leaf emitted + executes correctly. Frozen anchors 3/3 byte-identical
(control_step 0x00210A55 / flight_algo 0x07FDF307 / divseam — no i64 params).
Updated the #94 hi32-extract byte-size test to source its i64 from a sign-extended
i32 param (a non-param i64 that stays on the optimized path) — the old i64-param
shape was itself exercising the now-declined miscompile, masked by a size-only
assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@avrabe avrabe force-pushed the fix/518-i64-param-aapcs-homing branch from e008e9d to 10cdf20 Compare June 27, 2026 11:24
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.

arm: i64 binop with an i64 param silently miscompiles — param read from wrong registers (both paths)

1 participant