Skip to content

feat(fuzz): cargo-fuzz harnesses for ARM instruction selection (#82)#100

Open
avrabe wants to merge 2 commits into
fix/i32-wrap-r0-clobberfrom
feat/issue-82-cargo-fuzz-harnesses
Open

feat(fuzz): cargo-fuzz harnesses for ARM instruction selection (#82)#100
avrabe wants to merge 2 commits into
fix/i32-wrap-r0-clobberfrom
feat/issue-82-cargo-fuzz-harnesses

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 10, 2026

Closes #82 (scaffolds the fuzz directory and the smoke job; long-budget
nightly campaigns and capstone-differential are left as follow-ups on
the same issue).

Why

synth's V&V coverage matrix has property-tests, Rocq proofs, and Z3
translation validation, but no cargo-fuzz. ARM-codegen edges — encoding
corner cases, instruction-mix edge cases, odd register allocations — are
exactly where silent mis-compilations live and where fuzzing pays the
most.

Concrete evidence: issue #93 (silicon-blocking) escaped every existing
check because the bug — optimizer_bridge::wasm_to_ir silently mapping
three i32↔i64 conversion ops to Opcode::Nop via a _ => fallback —
left an IR that looked perfectly well-formed to type-based checkers. A
single-op fuzz harness would have flagged it in seconds.

What

Adds fuzz/ (workspace-excluded), with libfuzzer-sys + arbitrary, and
four targeted harnesses. Each harness inverts a bug class — not a
single bug instance — into a fuzz-detectable invariant.

Harness Bug class What it asserts
wasm_ops_lower_or_error lowering panic / unencodable instr optimize_full + ir_to_arm and select_with_stack each return Ok(_)/Err(_); emitted ArmOps round-trip through ArmEncoder::encode.
wasm_to_ir_roundtrip_op_coverage silent op drop (issue #93) Every value-producing wasm op contributes ≥ 1 live IR instruction. Issue-#93 conversion ops are skipped until PR #97 lands; the skip block is documented inline.
i64_lowering_doesnt_clobber_params AAPCS clobber (issues #85, #86) For every param p < num_params, no ARM instr emitted from a wasm op preceding LocalGet(p) writes R{p}.
encoder_no_panic encoder panic on a valid ArmOp ArmEncoder::encode returns Ok(_)/Err(_) on randomly-parametrised but well-typed ArmOp across ARM32 / Thumb-2 / Thumb-2+VFP.

Each FuzzOp lowers deterministically to one synth_core::WasmOp, so
libfuzzer crashes carry replayable WASM op sequences — drop the saved
artifact into a unit test and the regression is pinned.

CI smoke gate

.github/workflows/fuzz-smoke.yml runs each harness for 60 s on every
PR + push to main, on the same self-hosted Linux pool as the rest of
synth CI ([self-hosted, linux, x64, rust-cpu]). Matrix:

  • wasm_ops_lower_or_error
  • wasm_to_ir_roundtrip_op_coverage
  • i64_lowering_doesnt_clobber_params
  • encoder_no_panic

Crash artifacts (corpus + minimised input) are uploaded on failure with
30-day retention.

Verification

  • cargo build --workspace clean (synth-fuzz is workspace-excluded; main workspace unaffected).
  • cd fuzz && cargo build clean — all four harnesses compile under stable Rust.
  • cd fuzz && cargo clippy --all-targets -- -D warnings clean.
  • cd fuzz && cargo fmt --check clean.
  • Per-harness cargo +nightly fuzz run X -- -max_total_time=60 — runtime validation is left to the CI smoke job (cargo-fuzz needs nightly + libfuzzer ASan, which is not available in the dev sandbox this PR was authored in).

Notes

How to run locally

rustup toolchain install nightly
cargo install cargo-fuzz
cd fuzz
cargo +nightly fuzz run wasm_to_ir_roundtrip_op_coverage -- -max_total_time=300

Trace: NFR-002
Refs: #82, #93

🤖 Generated with Claude Code

avrabe added a commit that referenced this pull request May 11, 2026
Unblocks every open PR (#96, #97, #99, #100, #101) that was failing on `No space left on device` during z3-sys's C++ build on smithy runners.
@avrabe avrabe force-pushed the feat/issue-82-cargo-fuzz-harnesses branch from bacc0e0 to 9c339bb Compare May 11, 2026 05:30
@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@avrabe avrabe closed this May 11, 2026
@avrabe avrabe reopened this May 11, 2026
@avrabe avrabe force-pushed the feat/issue-82-cargo-fuzz-harnesses branch from 9c339bb to 781bfab Compare May 11, 2026 18:34
avrabe added a commit that referenced this pull request May 12, 2026
Closes #103. Audited 24 i64 ops that previously fell through to `select_default` which hardcoded R0..R3 — all 24 now allocate destination via `alloc_consecutive_pair` / `alloc_temp_safe` with the popped halves in `extra_avoid`.

Categories: 10 comparisons (Eq/Ne/Lt/Le/Gt/Ge × Signed/Unsigned), 5 arithmetic (Mul/Div/Rem × Signed/Unsigned), 2 rotations (Rotl/Rotr), 3 unary bit (Clz/Ctz/Popcnt), 3 sign-extend (Extend8/16/32 Signed), 1 conversion (I32WrapI64).

Same class as v0.1.1 #86 (I64Const) — found by PR #100's `i64_lowering_doesnt_clobber_params` fuzz harness. +3 regression tests.
@avrabe avrabe force-pushed the feat/issue-82-cargo-fuzz-harnesses branch from 781bfab to 31376ca Compare May 12, 2026 04:28
@avrabe avrabe force-pushed the feat/issue-82-cargo-fuzz-harnesses branch 2 times, most recently from d033349 to cfd9efc Compare May 14, 2026 04:56
avrabe and others added 2 commits May 14, 2026 08:02
Adds a workspace-excluded fuzz crate under `fuzz/` with four targeted
libfuzzer harnesses for the WASM → IR → ARM lowering pipeline. Each
harness inverts a specific bug class that has actually shipped (or
nearly shipped) into a fuzz-detectable invariant:

  * wasm_ops_lower_or_error            — panic / unencodable-instr class
  * wasm_to_ir_roundtrip_op_coverage   — silent op-drop class (issue #93)
  * i64_lowering_doesnt_clobber_params — AAPCS-clobber class (#85, #86)
  * encoder_no_panic                   — encoder panic on valid ArmOp

Layout: `fuzz/` is excluded from the main workspace via
`[workspace] members = []` so the libfuzzer-sys ASan toolchain
requirement does not leak into stable workspace builds.

`fuzz/src/common.rs` defines a curated Arbitrary-derived `FuzzOp`
mirror of `synth_core::WasmOp` (i32/i64 arithmetic + comparison +
conversions + locals) that lowers deterministically into `WasmOp`,
so any libfuzzer crash carries a replayable WASM op sequence.

`wasm_to_ir_roundtrip_op_coverage` would have caught issue #93 by
itself: it asserts that every value-producing wasm op leaves at least
one IR instruction after `optimize_full` (with all optimizations
disabled). The three issue-#93 conversion ops (`I64ExtendI32U/S`,
`I32WrapI64`) are *skipped* until PR #97 lands; the skip block is
inline-documented and removing it after #97 restores full coverage
and re-arms the regression check.

CI smoke job (`.github/workflows/fuzz-smoke.yml`) runs each harness
for 60 seconds on every PR, on the same self-hosted Linux pool as
the rest of synth CI, with crash artifacts uploaded on failure.
Long-budget nightly runs are out of scope for this PR.

Trace: NFR-002
Trace: issue-82

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #100's i64_lowering_doesnt_clobber_params harness was over-broadly
flagging any write to R{p} before LocalGet(p) — including legitimate
writes the wasm program explicitly requested via LocalSet(p)/LocalTee(p).
This produced false-positive crashes that masked the real I32WrapI64
intermediate-op-pin bug (now fixed in #111).

The carve-out only suppresses writes whose source wasm op is exactly
LocalSet(p) or LocalTee(p) writing param p — every other write before
LocalGet(p) is still flagged. The harness retains its full ability to
catch genuine compiler-emitted AAPCS clobbers.
@avrabe avrabe force-pushed the feat/issue-82-cargo-fuzz-harnesses branch from cfd9efc to 2051f63 Compare May 14, 2026 06:03
@avrabe avrabe changed the base branch from main to fix/i32-wrap-r0-clobber May 14, 2026 06:03
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.

Add cargo-fuzz targets for ARM-backend instruction selection

1 participant