fix(fuse): gate the unsound shared+rebase auto path (#326)#329
Merged
Conversation
`--memory auto` silently selected shared memory + address rebasing for grow-free multi-memory inputs. That is UNSOUND (#326): `convert_memarg` only rebases the static memarg offset (and bulk-memory ops); the dynamic address operand of ordinary i32.load/i32.store is never rebased. Real components address heap/shadow-stack via computed pointers, so shared fusion silently overlaps their private memory — wrong results, no trap, no error. Confirmed in `rewriter.rs` (convert_memarg; needs_rebase_locals gated to Memory{Copy,Fill, Init}) and reproduced end-to-end. Gate now (LS-D-1 — emit correct output or none, never plausible-but-wrong): - `resolve_auto_memory_strategy` no longer selects shared+rebase; Auto always resolves to multi-memory (sound). A `log::warn!` explains the gate. - Explicit `--memory shared --address-rebase` remains an opt-in but now warns loudly that it is unsound for computed-pointer access. - Corrected the docstrings that claimed the shared path was "statically sound". Tests: - `auto_selects_shared_for_growfree_inputs` → `auto_gates_shared_for_growfree_ inputs_326` (asserts multi + 2 separate memories = the correctness #326 protects); `ls_m_7_auto_reprobes_after_add_component` re-probes via memory count (2 → 3) since both resolutions are now multi. - `test_304_identity_direct_adapter_is_inlined` now requests `SharedMemory` explicitly — the #304 inlining feature runs on the shared path, which Auto no longer selects; adapter inlining is orthogonal to the memory-access unsoundness #326 gates. Interim safety gate; correct runtime dynamic-address rebasing in rewriter.rs (Tier-5) is the follow-up "fix properly" step. Safety docs (ADR-4, loss-scenarios, safety-requirements) still describe the old auto→shared default and should be reconciled in a traceability follow-up. Refs #326. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LS-N verification gate✅ 58/58 approved LS entries verified
Approved Failed LS entries(none) Missing regression tests(none) Updated automatically by |
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.
What
Stops
--memory autofrom silently selecting shared-memory + address rebasing — the default path that silently corrupts memory across fused components (#326).autonow resolves to multi-memory (sound); explicit--memory shared --address-rebaseremains an opt-in but warns loudly.This is the "gate now" half of the agreed plan; correct runtime dynamic-address rebasing in
rewriter.rs(Tier-5) is the follow-up "fix properly" step.Root cause (confirmed in-code + reproduced)
convert_memarg(rewriter.rs) rebases only the static memarg offset (and bulk-memory ops);needs_rebase_localsfires only forMemory{Copy,Fill,Init}. So the dynamic address operand of ordinaryi32.load/i32.storeis never rebased. Real components address heap/shadow-stack via computed pointers ⇒ shared fusion silently overlaps their private memory — wrong result, no trap, no error. (--memory multifuses the same input correctly.)Change
resolve_auto_memory_strategy: the grow-free/≥2-memory branch now selects MultiMemory (was SharedMemory+rebase), with alog::warn!explaining the gate. LS-D-1: emit correct output or none, never plausible-but-wrong.--memory shared --address-rebase: proceeds but emits a loud unsoundness warning.Tests
auto_selects_shared_for_growfree_inputs→auto_gates_shared_for_growfree_inputs_326: assertsauto → multi+ 2 separate memories (the isolation fuse --memory auto/shared silently corrupts memory across components: load/store dynamic addresses are not rebased (no memory.grow; distinct from #172/#299) #326 protects).ls_m_7_auto_reprobes_after_add_component: re-probes via memory count (2 → 3) since both resolutions are now multi.test_304_identity_direct_adapter_is_inlined: now requestsSharedMemoryexplicitly — the Fusion emits identity trampolines that never inline (dead inline_adapters flag); de-export fusion-vestigial symbols #304 inlining feature runs on the shared path (orthogonal to the memory-access unsoundness), whichautono longer selects.Scope / follow-ups
loss-scenarios.yaml,safety-requirements.yamlstill describe the oldauto→shareddefault — a traceability follow-up should update them (flagged, not done here).auto(fuse: drop vestigial cabi_realloc / canonical-ABI adapter once the inter-component boundary is internalized (unblocks --memory shared) #298/fuse --memory shared --address-rebase rejects components with memory.grow (default wasip2 allocator) — blocks single-address-space MCU lowering (jess/gale Pixhawk) #299/gale) now needs explicit--memory shareduntil the rewriter fix lands.Refs #326.
🤖 Generated with Claude Code