RQ-61-DANGLE (#1102): a retained function relocating against a DECLINED function refuses loudly on every backend - #1104
Merged
Conversation
…ED function refuses loudly on every backend A retained function calling a function this compile declined shipped an object with exit 0 that could NEVER link: the undefined symbol names a function the module itself DEFINES, so no linker input can resolve it (measured: `ld.lld: undefined symbol: synth_func_0` on the minimal rv32 repro and on gale's multi-export gpio shape). #952 keys on declined REQUESTED EXPORTS and #1013 lives only in the aarch64 ELF builder — an INTERNAL decline referenced by a retained export slipped past both. ARM CHARACTERISED, NOT ASSUMED FINE: `arm-none-eabi-readelf -sW -r` on the unfixed binary shows ARM Thumb-2 AND A32 relocatable objects carrying `func_N` as a GLOBAL SHN_UNDEF with the R_ARM_THM_CALL retained, exit 0 — the SAME defect. The "ARM has no .symtab" report was a probe artifact: the ARM builder emits the symtab section with an EMPTY name string, so a probe by section NAME misses what a probe by section TYPE finds. Without --relocatable the dangling reloc even counted as an external reference and silently flipped the output to ET_REL. THE FIX: one backend-agnostic gate in `compile_all_exports`, after the #952 export gate and before any ELF builder runs — the driver is the one site where the two facts already meet (`skipped_funcs`, now carrying wasm indices, and `compiled_funcs[].relocations`), where the four backends' ELF paths are three separate crates. A retained relocation whose symbol is a skipped function's index label (`func_{idx}` from the ARM/A32/ aarch64 selectors, `synth_func_{idx}` from RV32 — direct calls are always index-labelled) bails with an error naming EVERY dangling caller->callee edge. No object is written. Deliberately NOT waived by --allow-skipped-exports: that flag accepts a PARTIAL object (a requested export absent, the corpus-sweep shape), not an UNLINKABLE one — and aarch64's #1013 refusal was already unconditional; this is the same policy applied where it was missing. Deliberately NOT a stub/trap body and NOT a dropped call: both would turn an unlinkable object into a WRONG one. Fallout, each deliberate: - a64_dangling_reloc_decline_1013.rs: the refusal now fires at the driver, so the asserted message is #1102's; the builder's #851 Err stays as defense-in-depth. Exit-1/no-panic/no-object contract unchanged. - skipped_export_exit_952.rs: its "helper-only skips stay exit 0" negative control was, measured, THIS defect — the fixture's retained `f` carried a dangling `func_1` (GLOBAL UNDEF). Restated: a decline that leaves NO dangling reference stays exit 0 (new cascade fixture, under --allow-skipped-exports); the old fixture is now a RED case in dangling_declined_callee_1102.rs. Red-first both directions: - Loud: baseline exits 0 on the minimal module, the multi-export shape, ARM/A32 f64-helper shape; fixed binary exits 1 naming the class and every edge, leaves no partial object (8 tests, all four backend legs + flag-no-waiver + two negative controls). - Silent: 835 (fixture,leg) pairs — scripts/repro/*.wat + in-tree .wasm x 5 legs (arm-m3-reloc, a32-r5-reloc, rv32-reloc, aarch64-reloc, arm-m4f-image) — baseline vs fixed: 666 byte-identical, 166 fail-identically, 0 DIFFERING, 3 rv32 pairs newly-declined (aarch64_f32_unsupported_554, popcnt_r11_clobber_1021, recursive_shadow_stack), each PROVEN unlinkable-before by an UNDEF `synth_func_N` in the baseline object; no CI job compiles any of the three on rv32. HONEST RESIDUAL: the gate matches direct-call index labels only — a declined function referenced solely from a funcref TABLE entry (call_indirect) is outside this gate and keeps its pre-existing behaviour. Refs #1102, refs #952, refs #1013. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… the #952 negative control was shipping it The status flip rides ON THIS BRANCH (R4 is first-parent-evaluated: an id-naming delivery commit with no acknowledgement reddens main the moment it merges). `verified-by` records the probe that settled the ARM open question — the baseline's ARM/A32 objects carry the dangling GLOBAL SHN_UNDEF `func_N` (the 'no .symtab' report was a probe-by-section-NAME artifact; the ARM builder names its symtab section with an empty string), the 835-pair / 0-differing byte-identity sweep, the 3 rv32 pairs newly-declined and proven unlinkable-before, and the honest residual (funcref-table references to a declined function are outside the gate). Gate after this commit: status-evidence 0 failures, claim_check 52/52. Refs #1102. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
RQ-61-DANGLE (#1102) — an unlinkable object shipped with exit 0
A retained function calling a function this compile declined shipped an object with exit 0 that could never link — the undefined symbol names a function the module itself defines, so no linker input can resolve it. #952 keys on declined requested exports; #1013 lives only in the aarch64 ELF builder. An internal decline referenced by a retained export slipped past both.
The fix: one backend-agnostic driver gate
In
compile_all_exports, after the #952 export gate and before any ELF builder runs: a retained function's relocation whose symbol is a skipped function's index label (func_{idx}on ARM/A32/aarch64,synth_func_{idx}on RV32 — direct calls are always index-labelled) bails with a#1102error naming every dangling caller→callee edge. No object is written.Why the driver, not the ELF builders: the two facts already meet there — the driver owns
skipped_funcs(now carrying wasm indices) andcompiled_funcs[].relocationsfor all four backends, whose ELF paths are three separate crates (rv32 inmain.rs, ARM insynth-backend, aarch64 in its own crate; only aarch64's had a symbol-placement view). One gate, one message, all backends; aarch64's builderErr(#851) stays as defense-in-depth for un-placed symbols that are not skip-related.--allow-skipped-exportsdoes NOT waive it (pinned by a test): that flag accepts a partial object (a requested export absent — the corpus-sweep shape), which is categorically different from an unlinkable one. aarch64's #1013 refusal was already unconditional; this is the same policy applied where it was missing. No stub, no trap body, no dropped call — both would turn an unlinkable object into a wrong one.ARM characterised — same defect, and the probe that settled it
arm-none-eabi-readelf -sW -ron the unfixed binary, module every backend declines (#1093 block-type):--relocatablesynth_func_0GLOBAL UNDEF,ld.lldrefuses#1102--relocatablefunc_0GLOBAL UNDEF + retainedR_ARM_THM_CALL#1102#1102#1102#1102, builder guard retained)The "ARM relocatable objects have no
.symtab" report was a probe artifact: the ARM builder emits its symtab section with an empty name string, so a probe by section NAME misses what a probe by section TYPE (readelf -sW) finds. ARM was not fine; it was the same defect.Red-first, both directions
Loud: baseline exits 0 on the minimal module, gale's multi-export shape (both callers named in the refusal), and the ARM/A32 f64-helper shape; fixed binary exits 1 on all, leaves no partial object. New
dangling_declined_callee_1102.rs: 8 tests — four backend legs, flag-no-waiver, two negative controls.Silent (the one that matters more): 835 (fixture,leg) pairs —
scripts/repro/*.wat+ in-tree.wasm× 5 legs — baseline vs fixed binary:Each of the 3 newly-declined pairs (
aarch64_f32_unsupported_554,popcnt_r11_clobber_1021,recursive_shadow_stack— rv32 leg only) was proven unlinkable before the change: the baseline object carries an UNDEFsynth_func_Nfor a module-defined function. No CI job compiles any of the three on rv32 (the popcnt differential is Thumb-2-only, the shadow-stack fixture is consumed by scry analysis, the f32 fixture by an aarch64 test).Deliberate test fallout
a64_dangling_reloc_decline_1013.rs: the refusal now fires at the driver, so the message assertion moved to#1102; the exit-1/no-panic/no-object contract is unchanged, and the header documents that seeing#851again means the driver gate was removed — a real signal.skipped_export_exit_952.rs: its "helper-only skips stay exit 0" negative control was, measured, this defect — the fixture's retainedfcarried a danglingfunc_1GLOBAL UNDEF. Restated: a decline that leaves no dangling reference (full cascade +--allow-skipped-exports) stays exit 0 with the object emitted; the old fixture is now a RED case in the new test file.Honest residual
The gate matches direct-call index labels only. A declined function referenced solely from a funcref table entry (
call_indirectelem segment) is outside this gate and keeps its pre-existing behaviour — same class, different reference kind, left for its own increment rather than widened here without a repro.Gates
cargo fmt --check/clippy --workspace --all-targets -D warnings/cargo test --workspace(156 suites green, incl. the three touched test files) /claim_check52/52 /status_evidence0 failures.BRANCH_POPULATIONuntouched (main.rsis not on the MC/DC scored surface). Localrivet validate(0.32.0, newer than the CI pin): 40 errors with and without this change — pre-existing version drift, none mine. Status recordRQ-61-DANGLE→implementedwithverified-by, riding on this branch (R4 first-parent rule).Refs #1102.
🤖 Generated with Claude Code
https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L