Skip to content

RQ-62-TABLEDANGLE (#1102 residual): refuse a funcref table naming a DECLINED function on the host-linked paths - #1138

Merged
avrabe merged 2 commits into
mainfrom
fix/tabledangle-1102
Sep 3, 2026
Merged

RQ-62-TABLEDANGLE (#1102 residual): refuse a funcref table naming a DECLINED function on the host-linked paths#1138
avrabe merged 2 commits into
mainfrom
fix/tabledangle-1102

Conversation

@avrabe

@avrabe avrabe commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

RQ-62-TABLEDANGLE — the funcref-table residual of #1102

Refs #1102

#1102's gate matches direct-call relocation index labels, which is complete for
direct calls (verified in #1116). An elem segment naming a declined function
puts it in the table with no direct call site, so no relocation carries its
index label and the gate sees nothing.

Red-first, measured per backend (unfixed binary, main @ b552fb4)

Fixture: a dynamic-index call_indirect over (elem $good $bad) where
$bad declines on every backend (decoder-marked v128 op). The dynamic index
matters: with i32.const 0 the ARM relocatable path folds the dispatch into a
direct call and the table never materializes.

path exit object dangling UNDEF link (arm-none-eabi-ld / builder)
ARM Thumb-2 --relocatable 0 ships none links CLEAN — R11 table region is embedder-populated; the declined function's code+symbol are in NO object, so slot 1 is unpopulatable. THE LIVE HOLE.
A32 cortex-r5 --relocatable 0 ships none identical shape
ARM --cortex-m self-contained 1 none n/a already refuses: #275 broken-dispatch-table bail (slot-precise, test-pinned)
RV32 (esp32c3 / rv32imac) 1 none n/a call_indirect itself loud-declines → export skipped → #952
aarch64 1 none n/a substrate table's b func_N trampoline hits the ELF builder's #851/#1013 refusal

Note the table shape differs from #1102's direct-call shape: the object is NOT
unlinkable — it links clean, and the defect is a live dispatch table slot whose
function exists in no object anywhere. Same class (exit 0 for a module-defined
function synth silently dropped), different symptom.

The fix

One driver-level gate beside the #1102 one (the site where skipped_funcs,
compiled_funcs and the decoded funcref slots already meet for all four
backends): a RETAINED function performing call_indirect plus a funcref slot
naming a skipped function refuses the compile, naming the dead slot(s).

Controls

Gates

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace --features riscv
  • python3 scripts/claim_check.py claims.yaml, scripts/status_evidence_check.py
  • New test: crates/synth-cli/tests/tabledangle_1102_elem.rs (8 legs)

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 2 commits September 3, 2026 07:44
…ECLINED function on the host-linked paths (#1138)

Red-first, measured per backend on the unfixed binary (a dynamic-index
call_indirect over (elem $good $bad) where $bad declines everywhere):

- ARM Thumb-2 --relocatable: exit 0, object ships, arm-none-eabi-ld links
  it CLEAN — the R11 table region is embedder-populated and the declined
  function's code and symbol are in NO object, so slot 1 is unpopulatable.
  THE LIVE HOLE.
- A32 cortex-r5 --relocatable: identical shape, exit 0.
- ARM --cortex-m self-contained: already refuses (#275 broken-dispatch-
  table bail, slot-precise, test-pinned) — the new gate stands down there.
- RV32: call_indirect itself loud-declines, the dispatching export is
  skipped, #952 exits 1 — table path unreachable upstream (pinned
  bidirectionally by the new test so an RV32 call_indirect capability
  re-opens the question loudly).
- aarch64: already refused via the ELF builder's #851/#1013 Err on the
  substrate table's b func_N trampoline; the driver gate now fires first
  with the uniform message, the builder refusal staying defense-in-depth.

The gate sits beside the #1102 one (where skipped_funcs, compiled_funcs
and the decoded funcref slots already meet for all four backends), keys on
the decoded funcref-slot image + the wasm op stream — the #1116 'direct-
call relocations are always index-labelled' completeness claim is neither
reused nor extended — and is scoped to a RETAINED call_indirect, so a
skipped elem target nothing dispatches through stays a routine partial-
object skip. NOT waived by --allow-skipped-exports (the #1102 reason).

Sweep-shape control: all 171 scripts/repro fixtures compiled under the
arm_corpus_sweep_973 leg (cortex-m4f --relocatable --all-exports
--embedder-*) — 0 hit the new gate, so EXPECTED_DECLINES is untouched.

Refs #1102

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… status stays proposed pending release verification

Refs #1102

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@avrabe
avrabe force-pushed the fix/tabledangle-1102 branch from bd197bd to 8bf84b5 Compare September 3, 2026 05:44
@codecov

codecov Bot commented Sep 3, 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 858ff8d into main Sep 3, 2026
64 of 65 checks passed
@avrabe
avrabe deleted the fix/tabledangle-1102 branch September 3, 2026 06:03
avrabe added a commit that referenced this pull request Sep 4, 2026
…rse than the artifact predicted (#1140)

#1138 closed the #1102 residual. Flipping with the evidence, because the finding
is sharper than the artifact anticipated and should not live only in a PR body.

THE HOLE WAS A DIFFERENT AND WORSE FAILURE MODE. #1102's form was an object that
could not LINK. This one LINKS CLEAN: ARM Thumb-2 and A32 --relocatable, exit 0,
object written, `UNDEF symbols: NONE`, `arm-none-eabi-ld` links it without
complaint — because the declined function's code AND symbol are in NO OBJECT AT
ALL. The dispatch table gets a slot no embedder can populate, and the call
executes whatever that region holds. #1102 was at least loud at link time; this
was silent all the way to execution. Reproduced independently on pre-fix main
before merging, reading the symtab by SHT_SYMTAB TYPE (the ARM builder names its
symtab with an EMPTY name, so a name-based read wrongly reports "no symtab").

THE DYNAMIC INDEX IS LOAD-BEARING, which is why this survived: with
`i32.const 0` the ARM relocatable path devirtualizes the dispatch into a direct
call and the table never materializes, so a constant-index fixture finds nothing.

Suspicion held for exactly 2 of 5 paths, reported per backend rather than
generalised — the failure this artifact exists to prevent is precisely the
#1102 history of fixing aarch64 (#1013) and leaving RV32 live for a release
because nobody checked the others.

The RV32 leg is pinned BIDIRECTIONALLY, so if RV32 ever gains `call_indirect`
the test goes red and forces the table question to be re-answered rather than
inherited. Sweep control: all 171 repro fixtures compiled under the exact
arm_corpus_sweep_973 leg, 0 hit the new gate, EXPECTED_DECLINES untouched.

Verified on merged main: both previously-silent paths exit 1, write no object,
and name the dead slot precisely ("slot 1 -> function 1").

NOTE ON THIS COMMIT: my first attempt added a SECOND `landed:` key beside the
lane's, and `status_evidence_check` refused it — "duplicate-key defect in a
release file (#1059)". That is #1059's own rule firing on the coordinator, which
is the intended direction. Merged into one field rather than silenced.

Refs #1102


Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

Co-authored-by: Claude Opus 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.

1 participant