Skip to content

RQ-61-IMMRANGE (#1072): per-site closure of the six release-invisible immediate-range guards — one genuinely unguarded (aarch64 expected class id), four demonstrated, one unreachable - #1117

Merged
avrabe merged 5 commits into
mainfrom
fix/immrange-1072
Sep 1, 2026

Conversation

@avrabe

@avrabe avrabe commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

RQ-61-IMMRANGE: per-site closure of the six release-invisible immediate-range guards

Refs #1072

debug_assert! is compiled out in release. Per the issue's own list, each of
the six residual sites gets exactly one of: (1) a DEMONSTRATED upstream guard,
(2) promotion to a loud decline, (3) a stated unreachability recorded at the
assert. A grep was treated as a hypothesis throughout: every enforcement claim
below is backed by a tripping test, and every tripping test was
mutation-checked (guard disabled -> test red -> restored).

Per-site verdicts

Site Guarded value Verdict
aarch64 encoder.rs:470 cmp_imm imm12 br_table chain index (1) bounded by the BR_TABLE_MAX_TARGETS (16) loud decline; tripped by br_table_residuals_loud_decline_by_name
aarch64 encoder.rs:470 cmp_imm imm12 dispatch OOB guard's table size (1) bounded by substrate::plan's MAX_TABLE_SLOTS (4095) decline over the whole contiguous region; tripped by oversized_table_declines; a plan Err aborts the aarch64 compile before any bytes ship
aarch64 encoder.rs:470 cmp_imm imm12 type guard's EXPECTED class id (2) was genuinely UNGUARDED — promoted to a loud selector decline. substrate::plan bounds only the SLOT ids (funcref_class_ids); the expected id was bounded nowhere, and the selector comment claiming "the class ids were range-checked by substrate::plan" was half-false — the #946 shape the issue predicted. Red-first release evidence below. Boundary 4095 still lowers, pinned by test
aarch64 encoder.rs:484 add_ext_uxtw_sh shift index-scale shift (3) stated unreachability, recorded at the assert: the SOLE in-tree caller is the dispatch's index scale passing the LITERAL 3 (TABLE_SLOT_BYTES = 8); no module-derived value reaches the parameter
ARM arm_encoder.rs:193/194 (A32) expected_id <= 255 / type_off <= 4095 (1) enforcement DEMONSTRATED, not assumed: the sole Some((expected_id, type_off)) producer in shipped code is resolve_runtime_type_check (instruction_selector.rs), which loud-declines id > 255 and sidecar offset > 4095; test_676_call_indirect_runtime_check_range_declines trips BOTH declines, and disabling either turns it red (mutation-checked). Comments at the sites now cite the guard, the test, and the release failure shape
ARM arm_encoder.rs:3932/3933 (Thumb-2) same pair (1) same producer, same guards, same test — the two encoder sites consume the identical type_check tuple. Comment updated to match

The release failure shape differs by ISA and is now recorded at each site:
the ARM encoders MASK (& 0xFF / & 0xFFF), so an out-of-range value would
silently TRUNCATE — id 256 compares as 0 and a NULL slot (reserved id 0)
would pass the WASM Core 4.4.8 type check. The aarch64 encoder does not mask,
so overflow bits SHIFT into neighbouring instruction fields.

Red-first, in RELEASE, for the promoted site

Input: a generated legal module with 4096 distinct structural signatures
(1 dispatch type + 4094 six-param fillers + $weird), a 3-slot table holding
only class-1 functions plus a null slot, and call_indirect (type $weird)
$weird's first-occurrence class id is 4096. Every upstream gate passes
(plan checks slot ids only).

Before the fix, ./target/release/synth compile -b aarch64 --all-exports
exited 0 and emitted, at the dispatch's type guard:

44: 8b2a4e10  add  x16, x16, w10, uxtw #3
48: b9400211  ldr  w17, [x16]
4c: 7140023f  cmp  w17, #0x0, lsl #12     <-- intended: cmp w17, #4096
50: 54000040  b.eq ...
54: d4200000  brk  #0

0x7140023F: the expected id's bit 12 landed in bit 22, the sh
(shift-LSL-#12) field
of SUBS-immediate, leaving the imm12 field 0. The
type guard compares the loaded slot class id against 0, so a NULL slot
(reserved id 0) PASSES the type check it must trap on and the dispatch
branches into the null slot's trampoline. The debug binary panicked at
encoder.rs:470 on the same input — the exact debug/release divergence the
issue names.

After the fix the same release binary exits 1:

call_indirect type 4095: structural class id 4096 exceeds the type-guard CMP
unsigned 12-bit immediate (at most 4095) — loud-declining (#1072)
Error: #952: 1 of 1 requested export(s) were skipped ...

Acceptance / reach coupling

The new decline fires only for a module with > 4095 distinct structural
signatures whose call_indirect expects a high-class type, on aarch64. No
previously CORRECT accept can newly decline: any module hitting this decline
previously mis-encoded in release (or panicked in debug). Decline-sensitive
oracles re-run on the freshly built fixed binary:

  • aarch64_m2_decline_538.py — 13/13 declined ops loud-declined, PASS
  • aarch64_call_indirect_851_differential.py — 35 checks / 6 exports vs
    wasmtime incl. all three 4.4.8 traps, PASS

No corpus .wat added; EXPECTED_DECLINES untouched.

Also in this PR

  • oversized_slot_class_id_declines: substrate::plan's MAX_CLASS_ID
    slot-id bound was stated but never tripped by a test (the doc-vs-source sweep: 30 mechanically-verified disagreements, 4 of them possible defects (not doc staleness) #946 shape one
    layer down) — added, mutation-checked.
  • The half-false selector comment at the OOB guard corrected.
  • artifacts/release-v0.61/RQ-61-IMMRANGE.yaml flipped to implemented;
    its done-when restated to the issue's own three-outcome closure list (the
    filed wording demanded Err at all six sites, which the issue itself ranks
    below a demonstrated guard and the artifact's own description rules out as
    the RQ-61-MVPANIC class); verified-by records the oracles and mutation
    checks. No new artifact — no ARTIFACT_FLOOR change.

Gates run locally (all exit-code gated)

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace --features riscv
  • cargo build --release --features riscv (the red-first repro's build)
  • python3 scripts/claim_check.py claims.yaml — 54/54
  • python3 scripts/status_evidence_check.py — 0 failures
  • rivet validate — 40 errors, ALL the unloadable-local-externals
    cross-repo class (.rivet/repos/gale has no rivet.yaml in this
    worktree), none about this change; the federated CI job is advisory

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 4 commits September 1, 2026 21:28
Site: encoder.rs:470 debug_assert!(imm12 < 0x1000, "cmp imm12 out of range").
Of its three selector callers, two were already bounded (br_table's chain
index by the BR_TABLE_MAX_TARGETS decline; the OOB guard's table_slots by
substrate::plan's MAX_TABLE_SLOTS decline). The third — the type guard's
EXPECTED structural class id — was bounded nowhere: substrate::plan checks
only the SLOT ids (funcref_class_ids), and the (1) comment claiming "the
class ids were range-checked by substrate::plan" was half-false, the #946
unverified-enforcement shape #1072 names.

Red-first, in RELEASE (debug_assert compiled out): a module with 4096
distinct structural signatures whose call_indirect expects the 4096th
compiled with exit 0 and emitted 0x7140_023F = `cmp w17, #0, lsl #12` —
the id's bit 12 landed in the sh field (bit 22), imm12 read 0, and a NULL
slot (reserved id 0) would PASS the WASM Core 4.4.8 type check it must
trap on. The debug binary panicked at encoder.rs:470 on the same input.

Fix: loud selector decline (expected > substrate::MAX_CLASS_ID), the same
bound plan pins for slot ids; boundary 4095 still lowers, pinned by test.

Refs #1072

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
The MAX_CLASS_ID bound on funcref_class_ids is the upstream guard the
aarch64 dispatch cites for the sidecar values the type guard LOADS; it was
stated and never tripped (#946 shape). Mutation-checked: disabling the
plan check turns this test red.

Refs #1072

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Comment-only, zero byte change. aarch64 cmp_imm: name the three callers'
loud upstream declines and their tripping tests. aarch64 add_ext_uxtw_sh:
reachability verdict — sole caller passes the literal 3, no module-derived
value reaches the shift. ARM A32+Thumb-2 type-check asserts: the
"selector enforces X" claim is now DEMONSTRATED, not assumed — sole Some
producer resolve_runtime_type_check declines id > 255 / offset > 4095,
test_676_call_indirect_runtime_check_range_declines trips both, and both
declines were mutation-checked (disabling either turns the test red).
Release-mode failure shape recorded: the encoder masks truncate (id 256
compares as 0 — a NULL slot would pass the 4.4.8 check), not panic.

Refs #1072

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…cord

status_evidence R4 demanded acknowledgement of the delivery commits.
done-when restated to #1072's own three-outcome closure list (the filed
wording demanded Err at all six sites, which the issue itself ranks below
a demonstrated guard and the artifact's own description rules out as the
RQ-61-MVPANIC class); the release-mode red-first requirement is kept.
verified-by records the exact oracles and mutation checks. Also: clippy
useless_vec in the new substrate test.

Refs #1072

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit 331bbd8 into main Sep 1, 2026
63 checks passed
@avrabe
avrabe deleted the fix/immrange-1072 branch September 1, 2026 20:37
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.

1 participant