fix(vcr-ra): spill passes honor resolved branch geometry (#606) - #607
Merged
Conversation
The #604 audit follow-up: apply_spill_realloc stages 1-2 (#569 reload forwarding, #576 Belady re-choice), the frame-slot DCE they share a flag with (eliminate_dead_frame_stores), forward_stack_reloads (#390), and reallocate_function (range re-colouring) all run on the optimized path's ALREADY-RESOLVED BOffset/BCondOffset streams, where a branch target is an invisible join and a deletion (or 16<->32-bit width flip) inside a branch->target span makes the pre-resolved displacement overshoot by exactly the changed bytes — the nested(1,) 99-over-55 class. ADVERSARIAL VERDICT — SAFE today, by accident, not by construction. Compile-level fixtures (spill pressure / redundant reloads / dead stores inside a block/br_if/br arm) execute bit-identical to wasmtime on main defaults, because five unrelated accidents disarm every firing shape: 1. bridge reloads target reserved R12, which stages 1-2 exclude; 2. eviction spill slots are fresh-monotonic (no same-slot overwrite for the DCE to prove dead); 3. stage 3 (eliminate_unread_frame_stores) declines wholesale on any numeric branch via its admission scan (the only DESIGNED barrier); 4. spill_on_exhaust's scope gate excludes Branch/CondBranch, so i64 pair reloads (non-reserved regs) never meet real control flow; 5. non-const pressure under control flow declines to the direct selector (#496), whose label-form branches re-resolve after the passes. Any future change to one of those (e.g. reloading into an allocated register instead of R12 — the obvious next perf lever) would silently re-arm the shipped-miscompile class. This adopts the #604 resolved_branch_geometry rule structurally in all five passes: resolved branch TARGETS are segment/scan barriers, and any rewrite overlapping a branch->target span must be exactly byte-size-neutral; unmappable streams decline wholesale. New: resolved_branch_geometry_labels_as_zero — the direct selector emits MIXED streams (label-form control flow + numeric bne+0/udf trap guards); the #604 helper declines those, which gave back real forwarding wins (msgq_put_359). The variant maps Label pseudo-ops as 0 bytes (exactly what resolve_label_branches encodes) so the direct path keeps every win; const-CSE keeps its #604 decline stance untouched. Evidence: - 7 new pass-level adversarial tests (each with a branch-free non-vacuity control): stage-1 freeze + target barrier, DCE freeze + scan barrier, stack-fwd loop-join barrier + 4->2-byte freeze, stage-3 wholesale-decline pin, realloc size-changing in-span decline. - Frozen anchors: bit-identical (frozen_codegen_bytes + escape-hatch gates green, untouched). - Corpus (148 fixtures x default/--relocatable/SYNTH_BASE_CSE=0 vs main): 229/231 .text-identical; the 2 deltas are one size- and count-neutral recolour on msgq_put_359 (udiv r1 -> udiv r2 + retarget) where the trap-guard join now correctly splits a realloc segment — proven by the VCR-RA-003 backward-dataflow validator. - Differential sweep: 52 pass, 0 regressions; sret_decide fails identically with the main binary (its .o is bit-identical, harness env issue); 3 skips need external fixtures (/tmp/ci594.o, /tmp/ci597.o, /tmp/merged.wat). - cargo test -p synth-synthesis -p synth-cli green; fmt + clippy clean. Also adds SYNTH_PATH_DEBUG=1 (env-gated eprintln) exposing which selector path compiled each function — load-bearing for this audit and the next one. Closes #606 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
avrabe
added a commit
that referenced
this pull request
Jul 8, 2026
…path; inline UDF trap replaces the fallthrough Bhs Trap_Handler (#640) --safety-bounds software/mask were SILENT NO-OPS on the optimized codegen path (ir_to_arm) — the path that lowers the bulk of a flight loop's i32 loads/stores compiled byte-identical to `none` while the safety manifest claimed enforcement. Worse, the direct selector's existing check never trapped either: its `Bhs Trap_Handler` label branch resolved to an offset-0 fallthrough no-op in a self-contained image. Policy per mode: - software: inline per-access guard on the optimized path (`ADD R12,addr,#off+size-1; CMP R12,R10; BLO +0; UDF #0`) mirroring the direct selector's end-of-access check — same pre-resolved numeric-guard geometry as the DivS/DivU zero traps, so resolved_branch_geometry (#604/#607) maps it unchanged. Direct selector's 8 access shapes switch from `Bhs Trap_Handler` to the same inline `BLO +0; UDF #0` (#374 bulk-memory pattern), so the trap is real in self-contained images and under --relocatable alike. Base-CSE is disabled under software mode (folded [R11,#imm] accesses would bypass the register-shape guard). - mask: optimized path DECLINES memory-accessing functions to the direct selector (in-place `AND addr,R10` is unsound on the non-single-use optimized path) — honest degradation, never a silent drop. - mpu/none: byte-identical to before (pinned by tests). Estimator: new high-rd `ADD #imm` arm (32-bit ADD.W) in estimate_arm_byte_size — the guard's end-address compute previously fell to the `_ => 2` default and drifted every spanning branch by 2 bytes; pinned in estimator_encoder_agreement (3 new cases). Verification: - scripts/repro/safety_bounds_377_differential.py: wasmtime ground truth vs unicorn on BOTH paths x 13 vectors (in-bounds / exact-boundary / first-OOB / far-OOB across all four memory opcode shapes). origin/main (086968a): 14 MISMATCHES (all 7 OOB vectors leak on both paths). This branch: 26/26 PASS. - 8 new unit tests (guard shape/count, end-offset, mask decline, mpu parity, flag-off byte-identity); estimator agreement green; full workspace 105 suites green; frozen fixtures untouched (none use --safety-bounds; flag-off stream pinned byte-identical). Salvage provenance: work recovered from an interrupted session (agent hit its session limit at 170 tool uses in worktree lane-377); this commit completes verification and lands the on-disk state. Closes #377 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adversarial fixture verdict — SAFE today, by accident
Every adversarial shape executes bit-identical to wasmtime on current main defaults (unicorn Thumb vs wasmtime, linear-memory window compare, both branch directions):
So no shipped miscompile, no patch release needed. But the audit shows the safety is FIVE unrelated accidents, not a design property:
reload_spilltargets reserved R12, which stages 1–2 (spill_forward_segment,collect_spill_pairs) exclude;next_spill_offsetonly grows) — no same-slot overwrite for the frame-slot DCE to prove dead;eliminate_unread_frame_stores) declines wholesale on any numeric branch via its admission scan — the only designed barrier of the five;spill_on_exhaust's scope gate excludesBranch/CondBranch, so i64 pair reloads (non-reserved regs, stages 1–2 visible) never coexist with real control flow — only with fixed-contentbne+0; udftrap-guard spans;b.ndisplacement adapts,e010→e00f, on the firing spill_branch3 shape).Change any one of those (e.g. reload into an allocated register instead of R12 — the obvious next perf lever) and the #604 class comes back silently, default-on.
The fix — #604's rule, adopted structurally
resolved_branch_geometry(targets = barriers, span byte-freeze, decline-on-unmappable) is now enforced in all five rewriting passes on the shared stream, not just const-CSE:apply_spill_reallocstages 1–2eliminate_dead_frame_storesforward_stack_reloads(#390)ldr→2-bytemovdeclined in-spaneliminate_unread_frame_storesreallocate_functionNew
resolved_branch_geometry_labels_as_zero: the direct selector emits mixed streams (label-form CF + numeric trap guards). The #604 helper declines those — which gave back real forwarding wins onmsgq_put_359. The variant mapsLabelas 0 bytes (exactly whatresolve_label_branchesencodes), keeping every direct-path win. const-CSE keeps its #604 decline stance byte-for-byte.Evidence
frozen_codegen_bytes+ both escape-hatch gates green, no re-pins.--relocatable/SYNTH_BASE_CSE=0, new vs main): 229/231 .text-identical. The 2 deltas (msgq_put_359 default+basecse0) are one instruction-count- and byte-size-neutral recolour (udiv r1,r0,r1; mov r1,r1→udiv r2,r0,r1; mov r1,r2) where the trap-guard join now correctly splits a realloc segment; accepted by the VCR-RA-003 backward-dataflow validator. Give-back: zero (the labels-as-zero variant restored all of msgq's forwarding wins; only the recolour choice differs).sret_decide_differentialfails identically with the main binary — its compiled.ois bit-identical old-vs-new (cmpclean), so it is a harness-environment issue, not this PR. Skipped (external fixtures not in repo):call_indirect_594(/tmp/ci594.o),call_indirect_597(/tmp/ci597.o),wake_path(/tmp/merged.wat).cargo test -p synth-synthesis -p synth-cligreen (exit 0);cargo fmt --check+cargo clippy -D warningsclean.Also adds
SYNTH_PATH_DEBUG=1(env-gated) exposing which selector path compiled each function — this audit was impossible to do honestly without it.Closes #606
🤖 Generated with Claude Code