Skip to content

feat(334): MCU-dissolve fixups — coalesce __stack_pointer globals + DCE dead import shim#343

Open
avrabe wants to merge 2 commits into
mainfrom
feat/334-mcu-dissolve
Open

feat(334): MCU-dissolve fixups — coalesce __stack_pointer globals + DCE dead import shim#343
avrabe wants to merge 2 commits into
mainfrom
feat/334-mcu-dissolve

Conversation

@avrabe

@avrabe avrabe commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Makes a meld fuse --memory shared wac-composed multi-provider node dissolve cleanly on an MCU (SR-49) — the "ergonomics gate" gale currently hand-works-around with WAT surgery, and the companion to #326.

A post-merge pass (mcu_dissolve.rs, gated on SharedMemory, run before encoding so DWARF/attestation stay consistent) does two fixups:

  1. Coalesce the N per-component __stack_pointer globals into one shared shadow stack. Leaving them separate is unsound (providers descending from the shared-memory top clobber each other's frames on a cross-component call) and blocks synth's shadow-stack rebasing (synth#707). Keep one survivor, redirect global.get/global.set of the duplicates via the existing rewriter machinery, renumber survivors down, drop extras.
  2. Drop the dead lowered-import shim trampoline's keep-alive export (the numeric "0" export over an unreachable, import-dispatched call_indirect) so synth DCEs it instead of loud-declining (synth#642/#676).

Conservative detection (the risk area)

SP coalescing uses only the authoritative name-section signal (__stack_pointer). An init-value fallback heuristic was rejected during this branch's Mythos pass — it could fuse two unrelated mutable-i32 globals in different components that merely share an init value, silently clobbering one component's writes. Absent the name, meld does not guess. Trampoline drop requires numeric name + strict body shape + unreachable + import dispatch table.

Tests (457 lib + 4 integration green; clippy + fmt clean)

  • stack_pointer_globals_coalesced — 3→2 globals, exactly one SP remains, redirect + renumber verified.
  • shared_stack_pointer_runtime_non_clobberwasmtime: pre-coalesce the nested provider call clobbers the caller's frame; post-coalesce it doesn't.
  • dead_import_trampoline_export_dropped — dead "0" dropped; real/vtable/non-trampoline exports kept.
  • conservative_no_coalesce_when_unsure — single-SP, differing-init, AND unnamed same-init all → 0 coalesced.

Falsification

If SP coalescing regressed, shared_stack_pointer_runtime_non_clobber (frames alias) fails. If detection became unsafe, conservative_no_coalesce_when_unsure (unnamed same-init coalesced) fails. If the trampoline drop hit a live export, dead_import_trampoline_export_dropped (kept exports) fails.

Refs #334, SR-49, SYS-2, SYS-8, synth#707.

🤖 Generated with Claude Code

avrabe and others added 2 commits July 11, 2026 18:08
…able

Plan (traceability leads) for #334: coalesce the N per-component __stack_pointer
globals into one shared shadow stack (separate SPs clobber frames — unsound;
blocks synth#707), and DCE the dead wac lowered-import shim trampoline (exported
'0', mis-typed call_indirect over an import table — synth loud-declines it).
release v0.41.0, derives-from SYS-2/SYS-8. rivet validate PASS.

Refs #334, SR-49.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d-shim DCE)

A post-merge pass (mcu_dissolve.rs, gated on MemoryStrategy::SharedMemory, run
before encoding so DWARF/attestation stay consistent) makes a wac-composed
multi-provider node dissolve cleanly on an MCU (SR-49):

1. Coalesce the N per-component __stack_pointer globals into one shared shadow
   stack. Leaving them separate is UNSOUND (providers descending from the shared
   memory top clobber each other's frames) and blocks synth's shadow-stack
   rebasing (synth#707). Keep one survivor, redirect global.get/set of the
   duplicates via the existing rewriter+IndexMaps.globals machinery, renumber
   survivors down, drop the extras.
2. Drop the dead lowered-import shim trampoline's keep-alive export (a numeric
   "0" export over an unreachable, import-dispatched call_indirect) so synth
   DCEs it instead of loud-declining (synth#642/#676).

Detection is CONSERVATIVE — SP coalescing uses ONLY the authoritative `name`
section signal (`__stack_pointer`). The init-value fallback heuristic was
REJECTED (Mythos #334 review, this branch): two unrelated mutable-i32 globals in
different components sharing an init would be fused → silent cross-component
clobber. Absent the name we do not guess. Trampoline drop requires numeric name
+ strict body shape + unreachable + import dispatch table.

Tests (mcu_dissolve.rs): stack_pointer_globals_coalesced (3→2 globals, redirect
verified), shared_stack_pointer_runtime_non_clobber (wasmtime: two providers'
frames don't alias post-coalesce), dead_import_trampoline_export_dropped
(dead "0" dropped; real/vtable/non-trampoline exports kept),
conservative_no_coalesce_when_unsure (single-SP, differing-init, AND unnamed
same-init → 0 coalesced). 457 lib + 4 integration green; clippy + fmt clean.

Refs #334, SR-49, SYS-2, SYS-8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@avrabe avrabe added the mythos-pass-done Mythos delta-pass completed on Tier-5 file changes; findings (or NO FINDINGS) attached to PR label Jul 11, 2026
@avrabe

avrabe commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

Mythos discover pass (manual) — done, one finding fixed

Adversarial pass over mcu_dissolve.rs (new fusion-critical post-merge pass). Cleared: global-renumber coverage is complete (bodies + export (global); defined-global inits / active data-elem offsets reference only imported globals which never drop), name-section index mapping is correct, trampoline export-drop is conservative and can't strip a live export, and the pass composes with encode/DWARF/attestation (runs before them; no upstream pass captured stale global indices).

Finding [MEDIUM] — fixed in this PR. The original SP-detection had an init-value fallback (when no module names __stack_pointer): fuse any mutable-i32 globals with equal init spanning ≥2 components. That could coalesce two unrelated non-SP globals (an errno/TLS/counter at the same init) → redirect one component's global.set onto the other's storage → silent cross-component corruption. The fallback path was also entirely untested (all fixtures supply a __stack_pointer name). Fix: dropped the fallback — coalescing now uses ONLY the authoritative name-section signal; absent it, meld does not guess (the fused module keeps its several SP globals and synth surfaces the multi-SP condition rather than meld corrupting it). Added falsifying test case conservative_no_coalesce_when_unsure (c): two unnamed same-init globals → 0 coalesced.

Lower items noted (not blocking): mixed named/unnamed inputs under-coalesce (fails safe, no corruption); the whole-body re-rewrite couples to the rewriter's MVP-opcode whitelist (latent — no synthetic body emits a non-whitelisted op today). Both are fail-safe and can be follow-ups if they ever bite.

457 lib + 4 integration green post-fix; clippy + fmt clean. mythos-pass-done.

@github-actions

Copy link
Copy Markdown

LS-N verification gate

59/59 approved LS entries verified

count
Passed (≥1 test, all green) 59
Failed (≥1 test failure) 0
Missing (no ls_*_NN_* test found) 0

Approved loss-scenarios.yaml entries are expected to have a
regression test named ls_<letter>_<num>_* (e.g. LS-A-11
ls_a_11_*). The gate runs each prefix via cargo test --lib --no-fail-fast and aggregates pass/fail/missing.

Failed LS entries

(none)

Missing regression tests

(none)

Updated automatically by tools/post_verification_comment.py.
Source of truth: safety/stpa/loss-scenarios.yaml.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mythos-pass-done Mythos delta-pass completed on Tier-5 file changes; findings (or NO FINDINGS) attached to PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant