Skip to content

fix(riscv): #655 — mask bounds the EFFECTIVE address (offset folded before the AND, final byte clamped); i64 loads/stores gain the guard - #658

Merged
avrabe merged 1 commit into
mainfrom
fix/655-rv32-mask-order
Jul 8, 2026
Merged

fix(riscv): #655 — mask bounds the EFFECTIVE address (offset folded before the AND, final byte clamped); i64 loads/stores gain the guard#658
avrabe merged 1 commit into
mainfrom
fix/655-rv32-mask-order

Conversation

@avrabe

@avrabe avrabe commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #655 — the #651 class, RISC-V twin of ARM PR #654.

Sites audited (grep bounds/mask over synth-backend-riscv)

Site Pre-fix state Now
emit_bounds_check Mask arm masked the operand, caller re-added the static offset in the access imm — offset escaped the bound; no final-byte clamp the mask_effective_address semantics, in RV32 instructions
lower_load_word / lower_load_subword / lower_store re-added offset after the mask consume the residual offset returned by the guard (0 under Mask)
lower_i64_load / lower_i64_store NO guard at all — in Software and Mask modes guard the full 8-byte access like the i32 lowerings
emit_bounds_check Software arm already correct: folds offset + access_size - 1, bgeu guard, size on the final byte (size with >= on the last byte ≡ size-1 bound on it) unchanged
Mask value (size vs size-1) already mem_size - 1 (build_options), non-power-of-two declines loudly at compile time — RV32 never had ARM's latent R10-is-SIZE remap pinned by the oracle's exactness rows

The fix

emit_bounds_check now returns (reg, residual_offset). The Mask arm computes

masked = min((operand + offset) & (size-1), size - access_size)
[addi/li+add ea, addr, offset]   ; fold the static offset FIRST
 andi/li+and masked, ea, size-1  ; bound the first byte
[li limit, size-access_size      ; clamp the START so the FINAL byte
 bgeu limit, masked, Lok         ;   (start + access_size - 1) stays inside
 mv masked, limit ; Lok:]        ;   — skipped for byte accesses
 lw/sw …, 0(base+masked)         ; residual offset 0 — nothing re-added

u33 soundness of ADD-then-AND (mirrors #654): ea = operand + offset < 2^33; the RV add computes ea mod 2^32 and every set bit of size-1 < 2^32 lies below bit 32, so the u33 carry is annihilated by the AND either way. Arbitrary 32-bit static offsets are handled exactly — Mask mode now accepts offsets > 2047 that offset_to_imm previously declined.

Red → green

scripts/repro/mask_bounds_655_riscv_differential.py (+ 2 .wat) — unicorn RV32 execution vs the mask-semantics model, with a deterministic non-periodic pattern in the 64 KiB window and an 0xEE sentinel region above it (escapes are detected by value/byte checks, not by an unmapped-page fault); Software mode additionally asserts trap/no-trap.

  • origin/main (22517cb): ORACLE: FAIL (10) — offset-escape load (0xeeeeeeee from the sentinel) + store (sentinel_intact=False), word final-byte overhang, halfword wrap at ea == size, i64 mask escapes (lo and hi), software-mode i64 load and store return instead of trapping, huge-offset compile decline.
  • this branch: ORACLE: PASS — all red rows green, plus in-bounds exactness rows (byte load at size-1, word/halfword/i64 in-bounds) and software in-bounds no-trap rows.

Selector shape tests: rv32_mask_folds_offset_before_and_655 (access imm must be 0; the offset-add precedes the AND), rv32_mask_clamps_final_byte_for_multibyte_only_655 (word clamps, byte doesn't pay), rv32_i64_load_store_guarded_655.

Gates

  • frozen anchors 10/10 (frozen_codegen_bytes, including the RV32 gate — the default RV32 bounds mode is None, which emits nothing; fixtures don't use --safety-bounds, goldens untouched)
  • cargo test --workspace: 107 suites, 0 failures
  • cargo fmt --check + cargo clippy --workspace --all-targets -- -D warnings clean
  • rebased onto origin/main (22517cb, post-v0.34.0)

Refs #651, #654.

🤖 Generated with Claude Code

…s gain the guard

RV32 twin of ARM PR #654 (the #651 class): emit_bounds_check's Mask arm
masked the OPERAND and the caller re-added the static offset in the access
addressing mode — any non-zero offset escaped the bound — and multi-byte
accesses never clamped their FINAL byte. i64.load/i64.store additionally
carried NO guard at all (Software and Mask modes alike).

emit_bounds_check now returns (reg, residual_offset); the Mask arm computes
  masked = min((operand + offset) & (size-1), size - access_size)
(offset folded FIRST, u33 ADD-then-AND soundness documented, final byte
clamped via bgeu+copy, byte accesses skip the clamp) and returns residual
offset 0 so nothing is re-added after the bound. Mask-mode accesses now
accept arbitrary 32-bit static offsets (previously declined > 2047 in
offset_to_imm). The mask value was already size-1 (built in build_options,
non-power-of-two declines loudly at compile time) — pinned by the oracle.

Oracle: scripts/repro/mask_bounds_655_riscv_differential.py (unicorn RV32,
mask-semantics model + sentinel region + software-trap checks) — 10 failures
on origin/main (offset-escape load/store, word overhang, halfword wrap at
ea==size, i64 mask escapes, software-mode i64 no-trap, huge-offset decline),
all green here. Plus 3 selector shape tests (rv32_mask_folds_offset_before_
and_655, rv32_mask_clamps_final_byte_for_multibyte_only_655,
rv32_i64_load_store_guarded_655).

Refs #651, PR #654.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@avrabe
avrabe merged commit 0283278 into main Jul 8, 2026
31 checks passed
@avrabe
avrabe deleted the fix/655-rv32-mask-order branch July 8, 2026 17:59
avrabe added a commit that referenced this pull request Jul 8, 2026
…ER gate + RV32 bounds (#658/#659/#660/#661) (#662)

Pin sweep 0.34.0 -> 0.35.0 + CHANGELOG.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

rv32: emit_bounds_check masks the operand before adding the static offset (#651 class, RISC-V twin)

1 participant