Skip to content

fix(bulk-memory): #677 operand clobber + #679 silent-unmasked mask — copy/fill - #695

Merged
avrabe merged 2 commits into
mainfrom
fix/677-679-bulk-memory
Jul 10, 2026
Merged

fix(bulk-memory): #677 operand clobber + #679 silent-unmasked mask — copy/fill#695
avrabe merged 2 commits into
mainfrom
fix/677-679-bulk-memory

Conversation

@avrabe

@avrabe avrabe commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Fixes the two bulk-memory defects in the #374 memory.copy/memory.fill lowering (select_with_stack, shared Thumb-2 + A32 direct path). Closes #677, closes #679.

#677 — copy/fill clobber a local dst/src/len operand reused after the op

The lowering mutated its three popped operand registers in place (dst/src as walking loop pointers, len as the byte buffer). local.get of a register-homed local (AAPCS param r0-r3, promoted local r4-r8) pushes the home register itself, so a local reused after the op read a wild mem_base + <cursor> pointer or the last byte copied.

Fix (mirrors the #193 reservation discipline): new bulk_mutable_operand helper copies a popped operand into a fresh scratch (mov scratch, reg) before the loop mutates it, whenever the register is still live — live param / promoted-local home, duplicate vstack entry, if/block result reg, or aliased to another popped operand (memory.fill (local.get 0) (local.get 0) …). A provably-dead temp is used in place, so the const-operand shapes the #374 differential pins stay byte-identical.

The red differential then exposed the #663-class range-realloc hole underneath: try_reallocate_segment treated a pool register with no range in a segment as free, but such a register can be live-through (a param home the segment never touches). The memcpy backward path's walking-pointer intermediate got recolored onto R0 — the still-live dst local — re-introducing the clobber the selector fix had just removed. Absent pool colours are now blocked with synthetic pinned interference nodes. Identity colouring within the segment's present registers always exists (simultaneously-live ranges carry distinct original registers), so no recoloring the original bytes had is lost; relaxed-exit terminal segments keep their #580 exemptions (only absent R0/R1 blocked past the bx lr). This is a targeted instance-fix of the #663 defect class — #663's loop reproducers remain to be validated separately.

#679--safety-bounds mask silent no-op for bulk memory (attestation integrity)

Bulk memory under mask was emitted byte-identical to none — no fold, no clamp — while safety-manifest.json still attested "safety_bounds": "mask". Fix applies the scalar #651/#654 mask_effective_address wrap-not-trap discipline to the loop setup:

dst &= size-1 ; src &= size-1            ; fold both effective addresses
len = min(len, size - dst, size - src)   ; len bounds the FINAL byte

Every loop access (read and write) lands in [0, size); a wasm-in-bounds op is unchanged; the manifest's mask claim is now backed by the emission — silent-unmasked-while-attested is structurally gone for copy/fill.

Oracles (all local-run; red on v0.37.1 baseline → green here)

gate baseline (origin/main) this PR
bulk_local_clobber_677_differential.py (unicorn vs wasmtime) 2/8 FAIL 8/8 PASS
bulk_mask_679_differential.py pure-bulk byte-diff mask≠none identical (FAIL) differs (300 vs 250 B)
bulk_mask_679_differential.py escape/fold/clamp + containment (R10=4096) FAIL (raw escaped writes) 8/8 PASS
bulk_memory_374_differential.py (#374) 16/16 16/16
frozen_codegen_bytes anchors (fixtures lack bulk memory — verified) 10/10 10/10
safety_bounds_377_differential.py (both paths) 13/13 ×2 13/13 ×2
unreachable_665 / i32_shift_mask_682 PASS PASS
cargo test --workspace, fmt, chunked clippy -D warnings green green

Plus 8 new selector unit tests (677 preservation/aliasing/no-copy-when-dead; 679 fold+clamp presence, mask≠none structural). Both new oracles are CI-wired in the trap-semantics job, reading symbols with a symtab-first/disasm-fallback loader (#489 discipline).

Lineage: #374 (bulk-memory lowering), #654/#651 (scalar mask discipline), #193 (reservation discipline), #663 (realloc defect class), #489 (oracle CI-gating).

🤖 Generated with Claude Code

@avrabe
avrabe force-pushed the fix/677-679-bulk-memory branch 2 times, most recently from 1bf3e1e to 27db225 Compare July 10, 2026 16:57
avrabe and others added 2 commits July 10, 2026 19:04
…d mask profile

Two defects in the #374 memory.copy/memory.fill lowering (select_with_stack),
one lane:

(dst/src as walking loop pointers, len as the byte buffer). LocalGet of a
register-homed local (AAPCS param r0-r3, promoted local r4-r8) pushes the
HOME register itself, so a local reused AFTER the op read a wild
mem_base+cursor pointer or the last byte copied. Fix, mirroring the #193
reservation discipline: `bulk_mutable_operand` copies a popped operand into a
fresh scratch before mutation when it is still live (live param/promoted home,
duplicate vstack entry, if/block result reg, or aliased to another popped
operand of the same op); a provably-dead temp is used in place, keeping the
const-operand shapes byte-identical (#374 differential still 16/16).

The red differential also exposed the #663-class range-realloc hole the fix
then tripped over: `try_reallocate_segment` treated a pool register with NO
range in a segment as free, but such a register can be LIVE-THROUGH (a param
home the segment never touches — the memcpy backward path recolored its
walking-pointer intermediate onto R0, the still-live dst local). Absent pool
colours are now blocked with synthetic pinned interference nodes; identity
colouring within the segment's present registers always exists, so no
recoloring the original bytes had is lost (frozen anchors stay 10/10
bit-identical). Relaxed-exit terminal segments keep the #580 exemptions
(only absent R0/R1 blocked past the bx lr).

was emitted byte-identical to `none` while safety-manifest.json still
attested "mask" (attestation-integrity hole). The lowering now applies the
scalar #651/#654 mask_effective_address wrap-not-trap discipline: dst and src
effective addresses fold with AND (size-1) and len clamps to size-dst /
size-src so the FINAL byte stays in bounds — every loop access lands in
[0, size), wasm-in-bounds ops are unchanged, and the manifest's mask claim is
now backed by the emission (mask ≢ none proven by the pure-bulk byte-diff
gate).

Oracles (all run locally, red on v0.37.1 → green here):
- scripts/repro/bulk_local_clobber_677_differential.py — 2/8 → 8/8 vs
  wasmtime under unicorn (dst/src/len reuse + const control).
- scripts/repro/bulk_mask_679_differential.py — pure-bulk byte-diff
  (identical → differs), manifest coherence, escape/fold/clamp vectors with
  R10=4096 and out-of-bound containment (raw escaped writes → contained).
- scripts/repro/bulk_memory_374_differential.py — 16/16 (unchanged shapes
  byte-identical; script gains SYNTH env override).
- frozen_codegen_bytes 10/10; safety_bounds_377 13/13+13/13; unreachable_665,
  i32_shift_mask_682 PASS; cargo test --workspace green; fmt + clippy -D clean.
- 8 new selector unit tests (677 preservation/aliasing/no-copy-when-dead,
  679 fold+clamp presence, mask≠none structural).

Both oracles are CI-wired in the trap-semantics job (#489 discipline).

Closes #677. Closes #679.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pattern)

- liveness.rs: iter_mut over map → values_mut (rust-1.97 clippy).
- The two #677/#679 differential harnesses read .symtab via get_section_by_name,
  which returns None because synth emits an unnamed SHT_SYMTAB section; switched
  to iterate by sh_type (the established #489 pattern all other harnesses use).
  Both oracles PASS locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@avrabe
avrabe force-pushed the fix/677-679-bulk-memory branch from 27db225 to e0c98f4 Compare July 10, 2026 17:04
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.65396% with 8 lines in your changes missing coverage. Please review.

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

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit eebe612 into main Jul 10, 2026
36 checks passed
@avrabe
avrabe deleted the fix/677-679-bulk-memory branch July 10, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment