feat(codegen): sound null-funcref-slot call_indirect — runtime null check (#664) - #669
Merged
Merged
Conversation
…heck (#664) A table with null (uninitialized) funcref slots no longer poisons the closed-world type check: the verdict relaxes to "every INITIALIZED slot type-equal", and the null-slot trap (WASM Core §4.4.8) is discharged at RUNTIME — the layout contract links null slots as ZERO words and the dispatch null-checks the loaded pointer (cmp ip,#0; bne ok; udf) between the #642 bounds guard and the BLX, on both Thumb-2 and A32. Fully-initialized tables emit null_check=false, keeping the pre-#664 dispatch bytes identical BY CONSTRUCTION (whole-ELF cmp verified on the #642/#650/#594/#597 fixtures x cortex-m3/r5/m7dp x relocatable/self). New CI-gated differential (call_indirect_664_differential.py): sparse 4-slot table (slots 1,3 initialized) — initialized slots match wasmtime, null and OOB indices stop at a UDF; red at compile on origin/main (capability upgrade). Co-Authored-By: Claude Fable 5 <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
…ed rules, 81 bridge Qed (#673) * chore(release): v0.36.0 — unreachable traps + sparse tables + 40 rules + 81 bridge Qed (#668/#669/#670/#671) Pin sweep 0.35.0 -> 0.36.0 + CHANGELOG. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: fold #672 (post-exhaustion quality) into the v0.36.0 changelog — merged ahead of the tag Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
avrabe
added a commit
that referenced
this pull request
Jul 10, 2026
… BusFaults instead of corrupting linmem/globals (#687, VCR-MEM-003) (#694) Today's self-contained Cortex-M image puts the initial SP at the TOP of SRAM growing down toward the R9 globals table and linear memory — a stack overflow silently corrupts them (unicorn red evidence: deep recursion sweeps the linmem canaries and only faults once SP exits SRAM). --stack-layout=low (+ --stack-size, default 4096) reserves the stack at the SRAM BOTTOM: SP init = SRAM start + stack_size, and the ENTIRE RAM-anchored layout — startup R11 linmem base, R9 globals table, RAM auto-scale, AND the optimized path's absolute 0x2000_0100 base (new CompileConfig::linmem_base threaded to the OptimizerBridge) — shifts UP by stack_size as one. Overflow descends past 0x2000_0000 into reserved space and BusFaults on the FIRST errant push — every Cortex-M, no MPU. Applicability: self-contained images ONLY. --relocatable, import-forced ET_REL, and non-Cortex-M backends REFUSE the flag loudly (their linker script/harness owns the layout). Default high stays byte-identical by construction (reserve=0 degenerates every formula; startup MOVW/MOVT R11 encodes the historical fixed bytes) — frozen anchors 10/10 and whole-ELF cmp vs main on three self-contained fixtures confirm. Layout contract documented on build_multi_func_cortex_m_elf, cross- referenced from the #650/#669 R11 table contract (CallIndirectGuards). Oracle (scripts/repro/stack_layout_687_differential.py, CI-wired): RED high: recursion silently clobbers 4/8 linmem canaries BEFORE any fault (fault only at the SRAM floor, SP 0x1FFFFFF0) GREEN low: UC_ERR_WRITE_UNMAPPED below SRAM (SP 0x20000010) with 8/8 canaries intact — the BusFault precedes any linmem damage TRANSPARENT: in-budget calls match wasmtime under BOTH layouts; the existing #649 global-init differential passes unchanged under EXTRA_SYNTH_FLAGS="--stack-layout low" (R9 0x20010000→0x20011000) SHIFT PIN: low canary addresses = high addresses + stack_size exactly VCR-MEM-003 artifact added: the stack-guard ladder (low layout → MPU guard → v8-M PSPLIM), status implemented for the low-layout rung only. Relates #404/#406 (MPU-protectable region model). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
avrabe
added a commit
that referenced
this pull request
Jul 10, 2026
…the type-id sidecar (#676) A HETEROGENEOUS funcref table (mixed signatures — falcon's fused 41-slot dispatch table) can never satisfy the closed-world type check, so every call_indirect through it loud-declined (20 falcon funcs). WASM Core §4.4.8 makes the mismatch a RUNTIME trap, so the sound lowering is the runtime check itself: the object now carries a type-id sidecar (.synth.table_type_ids — one LE u32 STRUCTURAL signature class id per slot, region order; structurally-equal types share one dense 1-based id, the meld 31-decls/25-distinct shape; id 0 reserved for null slots) which the extended R11 layout contract places at R11 + sum(all table sizes)*4, mirroring the pointer region slot for slot. The dispatch inserts, between the #642 bounds guard and the pointer load, on both Thumb-2 and A32: mov ip, idx, lsl #2 ; add ip, r11, ip ; ldr ip, [ip, #type_off] cmp ip, #expected_class_id ; beq ok ; udf The compare subsumes the #664 null trap (id 0 never equals an expected id >= 1), so heterogeneous dispatches emit null_check=false. Encoding ranges decline loudly (sidecar offset > LDR imm12, class id > 255). Homogeneous tables emit type_check=None + no sidecar section — bytes identical BY CONSTRUCTION (the #650 offset-0 / #664 null_check=false trick): whole-ELF cmp verified against origin/main on the #642/#650/#664 fixtures x cortex-m3/r5, frozen anchors 10/10, workspace green. The estimator is untouched (CallIndirect is direct-selector-only, excluded from the #511 agreement oracle). New CI-gated differential (call_indirect_676_differential.py, Thumb-2 + A32): mixed 5-slot table (two classes interleaved + structural-dup type + nulls) — matching-class calls equal wasmtime, wrong-class ("indirect call type mismatch"), null and OOB indices all stop at a UDF; wasmtime's trap REASONS are asserted per category. Non-vacuous red: a build without the check CALLS the wrong-typed function and returns a wrong value. Red at compile on origin/main (capability upgrade). Object-level contract locked in cargo CI (heterogeneous_table_676.rs: sidecar ids [1,2,1,0,0] + no-sidecar for the homogeneous fixtures). Lineage: #642 guards (#646), #650 multi-table (#653), #664 null slots (#669) — this closes the terminal layer of falcon's call_indirect story. Closes #676 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
avrabe
added a commit
that referenced
this pull request
Jul 10, 2026
…the type-id sidecar (#676) A HETEROGENEOUS funcref table (mixed signatures — falcon's fused 41-slot dispatch table) can never satisfy the closed-world type check, so every call_indirect through it loud-declined (20 falcon funcs). WASM Core §4.4.8 makes the mismatch a RUNTIME trap, so the sound lowering is the runtime check itself: the object now carries a type-id sidecar (.synth.table_type_ids — one LE u32 STRUCTURAL signature class id per slot, region order; structurally-equal types share one dense 1-based id, the meld 31-decls/25-distinct shape; id 0 reserved for null slots) which the extended R11 layout contract places at R11 + sum(all table sizes)*4, mirroring the pointer region slot for slot. The dispatch inserts, between the #642 bounds guard and the pointer load, on both Thumb-2 and A32: mov ip, idx, lsl #2 ; add ip, r11, ip ; ldr ip, [ip, #type_off] cmp ip, #expected_class_id ; beq ok ; udf The compare subsumes the #664 null trap (id 0 never equals an expected id >= 1), so heterogeneous dispatches emit null_check=false. Encoding ranges decline loudly (sidecar offset > LDR imm12, class id > 255). Homogeneous tables emit type_check=None + no sidecar section — bytes identical BY CONSTRUCTION (the #650 offset-0 / #664 null_check=false trick): whole-ELF cmp verified against origin/main on the #642/#650/#664 fixtures x cortex-m3/r5, frozen anchors 10/10, workspace green. The estimator is untouched (CallIndirect is direct-selector-only, excluded from the #511 agreement oracle). New CI-gated differential (call_indirect_676_differential.py, Thumb-2 + A32): mixed 5-slot table (two classes interleaved + structural-dup type + nulls) — matching-class calls equal wasmtime, wrong-class ("indirect call type mismatch"), null and OOB indices all stop at a UDF; wasmtime's trap REASONS are asserted per category. Non-vacuous red: a build without the check CALLS the wrong-typed function and returns a wrong value. Red at compile on origin/main (capability upgrade). Object-level contract locked in cargo CI (heterogeneous_table_676.rs: sidecar ids [1,2,1,0,0] + no-sidecar for the homogeneous fixtures). Lineage: #642 guards (#646), #650 multi-table (#653), #664 null slots (#669) — this closes the terminal layer of falcon's call_indirect story. Closes #676 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
avrabe
added a commit
that referenced
this pull request
Jul 10, 2026
…the type-id sidecar (#676) (#696) A HETEROGENEOUS funcref table (mixed signatures — falcon's fused 41-slot dispatch table) can never satisfy the closed-world type check, so every call_indirect through it loud-declined (20 falcon funcs). WASM Core §4.4.8 makes the mismatch a RUNTIME trap, so the sound lowering is the runtime check itself: the object now carries a type-id sidecar (.synth.table_type_ids — one LE u32 STRUCTURAL signature class id per slot, region order; structurally-equal types share one dense 1-based id, the meld 31-decls/25-distinct shape; id 0 reserved for null slots) which the extended R11 layout contract places at R11 + sum(all table sizes)*4, mirroring the pointer region slot for slot. The dispatch inserts, between the #642 bounds guard and the pointer load, on both Thumb-2 and A32: mov ip, idx, lsl #2 ; add ip, r11, ip ; ldr ip, [ip, #type_off] cmp ip, #expected_class_id ; beq ok ; udf The compare subsumes the #664 null trap (id 0 never equals an expected id >= 1), so heterogeneous dispatches emit null_check=false. Encoding ranges decline loudly (sidecar offset > LDR imm12, class id > 255). Homogeneous tables emit type_check=None + no sidecar section — bytes identical BY CONSTRUCTION (the #650 offset-0 / #664 null_check=false trick): whole-ELF cmp verified against origin/main on the #642/#650/#664 fixtures x cortex-m3/r5, frozen anchors 10/10, workspace green. The estimator is untouched (CallIndirect is direct-selector-only, excluded from the #511 agreement oracle). New CI-gated differential (call_indirect_676_differential.py, Thumb-2 + A32): mixed 5-slot table (two classes interleaved + structural-dup type + nulls) — matching-class calls equal wasmtime, wrong-class ("indirect call type mismatch"), null and OOB indices all stop at a UDF; wasmtime's trap REASONS are asserted per category. Non-vacuous red: a build without the check CALLS the wrong-typed function and returns a wrong value. Red at compile on origin/main (capability upgrade). Object-level contract locked in cargo CI (heterogeneous_table_676.rs: sidecar ids [1,2,1,0,0] + no-sidecar for the homogeneous fixtures). Lineage: #642 guards (#646), #650 multi-table (#653), #664 null slots (#669) — this closes the terminal layer of falcon's call_indirect story. Closes #676 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.
Fixes #664. Builds on #650/#653 (multi-table R11 region) and #646 (the #642 closed-world guards).
The gap
call_indirecton a table with any null (uninitialized funcref) slot loud-declined: the #646 closed-world verifier treated an uninitialized slot as breaking the closed world for EVERY type ("the raw code-pointer table has no runtime null/type id to check"). Sound, but incomplete — component-model tables legitimately carry null slots (falcon-flight v1.112's(table 1 41 41)is sparsely populated), and WASM Core §4.4.8 says acall_indirectreaching one simply traps. All 20 of falcon's dispatch functions declined on this.The relaxation (design)
The closed-world type verdict relaxes from "every slot in
[0, size)initialized + type-equal" to "every INITIALIZED slot type-equal"; null slots become trap-on-call at runtime:synth-core):TableGuardsgainshas_null_slots.table_type_rejectno longer all-rejects on an uninitialized slot — it type-checks the initialized image and reports the null flag. A null slot can never produce a live callee of the wrong type, because the null check traps before the branch — so the type relaxation is sound. Heterogeneous initialized slots, unverifiable segments, growable imports: all still reject exactly as before.synth-synthesis):resolve_call_indirect_guardsreturns the flag; both arms (select_with_stack+select_default) thread it asArmOp::CallIndirect { null_check }.null_check, a three-insn null check lands between the pointer load and theBLX(same trap idiom as the thumb-2: call_indirect emits no bounds-check and no type-check — OOB/wrong-type index does an uncontrolled indirect branch instead of trapping (WASM 4.4.8) #642 bounds guard):The null-region guarantee (emission finding)
synth does not emit the funcref table region — the #650 layout contract has the runtime/harness link it at R11. This PR extends that contract (documented on
CallIndirectGuards): every uninitialized slot MUST be linked as a ZERO word. Zero is never a valid code pointer in the region, so the null check turns it into the deterministic trap. The differential harness links per this contract; meld/falcon integration inherits the same one-line rule (a BSS/zero-filled region satisfies it for free).Single-table / fully-initialized byte-identity — BY CONSTRUCTION, then verified
null_check: false(any fully-initialized table) skips the encoder branch entirely — the #650 offset-0 trick. Verified against an origin/main-built binary: whole-ELFcmpidentity, 24/24 — #642/#650/#594/#597 fixtures × cortex-m3/cortex-r5/cortex-m7dp ×--relocatable/self-contained. Frozen anchors 10/10.Oracle (CI-gated:
call-indirect-664-nullslot-oracle)scripts/repro/call_indirect_664_differential.py+call_indirect_664_nullslot.wat: 4-slot table, slots 1,3 initialized (two segments — the falcon shape), 0,2 null. Per case, unicorn-executed synth code (null slots linked as zero, decoys past the region) vs wasmtime:viadeclines (SYMBOL MISSING). Capability upgrade: red = "declines today", green = sound sparse dispatch.viarepro now compiles clean oncortex-m7dp(0 skips).Scope notes
call_indirectwas never lowered on RV32 (pre-existingunsupported wasm oploud-skip) — unchanged; the decoder relaxation is shared, so the RV32 lowering inherits the flag when it lands.CallIndirectstill declines atoptimizer_bridge(thumb-2: call_indirect emits no bounds-check and no type-check — OOB/wrong-type index does an uncontrolled indirect branch instead of trapping (WASM 4.4.8) #642) → stays direct-selector-only, on the test(vcr-oracle): estimator↔encoder agreement oracle for the optimized path (#498, #242) #511 oracle's exclusion list — verified, no estimator change.(ref.null)entries inside a segment still poison contents (unverifiable-contents rule unchanged); this PR addresses slots not covered by any segment — the falcon case.Workspace tests green (108 suites), fmt clean, clippy
-D warningsclean workspace-wide (chunked per-crate + full).🤖 Generated with Claude Code