Skip to content

The i64 param-clobber fuzz harness checks 'before FIRST read' — a write between two reads is exempt by construction (missed #1048 and the Clz/Ctz/Popcnt clobber it had itself named) #1055

Description

@avrabe

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions