i64_lowering_doesnt_clobber_params (the PR #100 cargo-fuzz harness) and its companion assert_no_param_clobber_before_localget (crates/synth-synthesis/tests/issue_103_i64_aapcs.rs) check a strictly weaker invariant than the one they exist to enforce — and the gap is exactly the shape of #1048 and the I64Clz/I64Ctz/I64Popcnt clobber found alongside it (#1054).
The gap
The check computes earliest_read — the first LocalGet per param — and flags only writes occurring before it:
Check that no ARM instruction emitted from op_idx == 0..first_local_get writes to any AAPCS param register R0..R{num_params-1} before that param is read.
So a write between two reads is exempt by construction:
local.get $amt ;; earliest_read for $amt is HERE
i64.shl ;; expansion clobbers $amt's home high register — AFTER earliest_read
local.get $amt ;; reads back a mangled value
The invariant encodes "don't clobber before first read". The actual requirement is "don't clobber a param the caller may read again".
Why this matters more than an ordinary coverage gap
This harness was built for this exact class. It caught #103 for I64SetCond, and that issue's audit explicitly enumerated the affected ops — including I64Clz, I64Ctz, I64Popcnt, which #1054 has just found still clobbering (MOV rnhi, #0 landing on the operand's home high register, executed red: clz_reread(0xDEADBEEF00000001) → 0x1, hi limb wiped).
So the sequence is: a fuzz harness exists for the class → it catches one instance → the audit names the remaining ops → the harness cannot see those instances because of how its invariant is phrased → they ship.
Related — the same shape found twice more this session
Three checkers, one pattern: each encodes a property that is easier to state and weaker than the one that matters.
Suggested fix
Strengthen the invariant to last read, not first: no data-flow instruction may write a param register at any point where a later LocalGet of that param exists. Equivalently — and closer to what #1054 built — assert operand preservation over the emitted bytes for the whole pseudo-op family, which is defect-class-shaped rather than fixture-shaped.
Worth re-running the strengthened harness against pre-fix bytes as a potency check: if it does not red on the shipped pre-#1048 Shl expansion and the pre-fix Clz tail, it is still weaker than it reads.
Found while triaging #1036's remaining failures. Refs #1048, #1052, #1054, #103, #100.
i64_lowering_doesnt_clobber_params(the PR #100 cargo-fuzz harness) and its companionassert_no_param_clobber_before_localget(crates/synth-synthesis/tests/issue_103_i64_aapcs.rs) check a strictly weaker invariant than the one they exist to enforce — and the gap is exactly the shape of #1048 and theI64Clz/I64Ctz/I64Popcntclobber found alongside it (#1054).The gap
The check computes
earliest_read— the firstLocalGetper param — and flags only writes occurring before it:So a write between two reads is exempt by construction:
The invariant encodes "don't clobber before first read". The actual requirement is "don't clobber a param the caller may read again".
Why this matters more than an ordinary coverage gap
This harness was built for this exact class. It caught #103 for
I64SetCond, and that issue's audit explicitly enumerated the affected ops — includingI64Clz,I64Ctz,I64Popcnt, which #1054 has just found still clobbering (MOV rnhi, #0landing on the operand's home high register, executed red:clz_reread(0xDEADBEEF00000001)→0x1, hi limb wiped).So the sequence is: a fuzz harness exists for the class → it catches one instance → the audit names the remaining ops → the harness cannot see those instances because of how its invariant is phrased → they ship.
Related — the same shape found twice more this session
# zeroed globals table (inits are 0)— it passes correctly, and only because that fixture's initializers are zero, which is why ARM plain-relocatable path silently drops nonzero GLOBAL INITIALIZERS — no init image, no decline, no documented embedder obligation (4th silent drop of the #1041 class) #1052 (nonzero global inits silently dropped) went unnoticed.Three checkers, one pattern: each encodes a property that is easier to state and weaker than the one that matters.
Suggested fix
Strengthen the invariant to last read, not first: no data-flow instruction may write a param register at any point where a later
LocalGetof that param exists. Equivalently — and closer to what #1054 built — assert operand preservation over the emitted bytes for the whole pseudo-op family, which is defect-class-shaped rather than fixture-shaped.Worth re-running the strengthened harness against pre-fix bytes as a potency check: if it does not red on the shipped pre-#1048
Shlexpansion and the pre-fixClztail, it is still weaker than it reads.Found while triaging #1036's remaining failures. Refs #1048, #1052, #1054, #103, #100.