Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,64 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.38.0] - 2026-07-10

Correctness release: a **soundness fix** to the default memory strategy, a
**reproducible-build** flag, and the completion of the DWARF-verify trilogy.

**⚠ Default-behaviour change (#326):** `meld fuse --memory auto` (the default)
**no longer selects shared memory + address rebasing** — it always resolves to
multi-memory. The shared+rebase path was silently corrupting memory for any
component that addresses memory via computed pointers (see below). This
**supersedes ADR-4's prior "`Auto` is not flipped" decision** (that decision
assumed shared+rebase was a sound alternative isolation model; #326 proved it
corrupts). Consequence: default fused output now needs `wasm-opt
--enable-multimemory` and loses single-address-space (MCU) lowering until the
correct-rebasing follow-up lands; use explicit `--memory shared --address-rebase`
(now warns loudly) if you knowingly want the old behaviour.

**Falsification:** if `--memory auto` still selected shared+rebase for a
grow-free multi-memory input, `ls_m_11_auto_gates_unsound_shared_rebase` would
fail — it asserts multi-memory + two separate memories. If `--reproducible`
fusion were not byte-stable, `test_reproducible_attestation_is_byte_stable`
would fail (and its control asserts non-reproducible output *does* differ, so
the flag can't silently no-op). If the DWARF remap emitted a wrong length,
range, or location, `llvm-dwarfdump --verify` would report it — instead the
records+variants fixtures go from 1049 errors to 0.

### Added

- **`--reproducible` flag for byte-stable output (#325).** `meld fuse
--reproducible` derives the attestation id from the output content and takes
the timestamp from `SOURCE_DATE_EPOCH` (default epoch 0) instead of a random
UUID + wall clock, so identical input yields an identical sha256 — a
prerequisite for reproducible-build / supply-chain attestation. (Root cause of
the nondeterminism was the attestation section, not fusion; the fused module
was already deterministic.)

### Fixed

- **Shared-memory fusion silently corrupted memory (#326, LS-M-11) — GATED.**
Address rebasing only relocated the static memarg offset (and bulk-memory
ops), never the dynamic address operand of ordinary loads/stores, so
computed-pointer access collided across components with no trap or error.
`Auto` now gates this unsound path (→ multi-memory); explicit shared+rebase
warns. SR-37 corrected (it had *required* the unsound preference); ADR-4
superseded; the correct runtime dynamic-address rebasing in `rewriter.rs` is a
tracked follow-up.
- **DWARF-verify trilogy complete (#319, #320/#321/#322).** `--dwarf remap` now
rewrites `DW_AT_high_pc` lengths, `DW_AT_ranges` lists, and `DW_AT_location`
lists to the fused layout — `llvm-dwarfdump --verify` goes from 1049 errors to
0 on the records+variants fixtures.

### Changed

- **Tier-C meld→kiln execution seam (#297)** tracked to the current kiln-side
blocker (E0BBB on kiln#375; kiln#364 + kiln#382/E5DC2 resolved upstream);
meld's fused output is spec-valid throughout (runs on wasmtime 41). Test stays
`#[ignore]`d pending kiln.
- Adopted rivet v0.22.0 release-handling as the pre-tag readiness gate (#318).

## [0.37.0] - 2026-06-26

Downstream-boundary release: meld now (a) emits machine-checkable **fusion
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exclude = [
]

[workspace.package]
version = "0.37.0"
version = "0.38.0"
authors = ["PulseEngine <https://github.com/pulseengine>"]
edition = "2024"
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion meld-core/tests/auto_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ fn validates_without_multimemory(wasm: &[u8]) -> bool {
/// plausible-but-wrong one). Shared remains reachable only via explicit
/// `--memory shared --address-rebase`, which warns loudly.
#[test]
fn auto_gates_shared_for_growfree_inputs_326() {
fn ls_m_11_auto_gates_unsound_shared_rebase() {
let component_a = build_component(build_module_a());
let component_b = build_component(build_module_b(false));

Expand Down
34 changes: 29 additions & 5 deletions safety/adr/ADR-4-inter-component-isolation-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,25 @@ shared mode.

## `Auto`-default policy (resolved — maintainer decision)

The `Auto` strategy (#172) prefers `SharedMemory` + rebasing when
provably sound (no `memory.grow`, ≥2 input memories), else `MultiMemory`,
with a runtime downgrade shared→multi if shared fusion refuses.
> **⚠ SUPERSEDED IN PART by #326 (v0.38.0).** The decision below rejected
> flipping `Auto` to prefer `MultiMemory` on the premise that shared+rebasing
> was a *sound* alternative isolation model with a size benefit. #326 proved
> that premise false: address rebasing does not relocate the dynamic address
> operand of ordinary loads/stores, so shared fusion silently corrupts any
> component using computed pointers (LS-M-11). With no valid benefit to
> preserve, `Auto` now resolves to `MultiMemory` (the sound strategy) and no
> longer selects shared+rebase; shared+rebase is reachable only by explicit
> `--memory shared --address-rebase`, which warns loudly (PR #329, SR-37 as
> corrected). Item 2's "flipping was rejected" is thus reversed — not to change
> an isolation *model*, but to stop defaulting to a *corrupting* transform. The
> single-address-space (MCU) benefit shared fusion was meant to give (#298/#299)
> returns only once correct dynamic-address rebasing lands in `rewriter.rs`
> (tracked follow-up). The rest of this section (explicit-is-safety, the
> attestation backstop) still holds.

The `Auto` strategy (#172) originally preferred `SharedMemory` + rebasing when
believed provably sound (no `memory.grow`, ≥2 input memories), else `MultiMemory`.
Since #326 it always resolves to `MultiMemory`.

**Decision: `Auto` is NOT flipped, and `Auto` is NOT the safety path.**
Rationale (maintainer): *functional safety wants the isolation model to
Expand Down Expand Up @@ -149,8 +165,16 @@ build, `Fuser::fuse` emits a `warn`-level note recommending an explicit
safety builds. The warning reports the *final* strategy (after any
shared→multi downgrade), not the initial pick, and fires only when
attestation is enabled (the release/safety-build signal) to avoid noise
on casual fusion. Log-only — no change to fusion behavior; the existing
`auto_memory` tests confirm resolution is unchanged.
on casual fusion. (Since #326 this attested-build warning is in addition to
the always-on warning that `Auto` gated shared+rebase to multi-memory.)

**Updated by #326 (v0.38.0):** the earlier claim here — "log-only, no change
to fusion behavior; the `auto_memory` tests confirm resolution is unchanged" —
no longer holds. `Auto` resolution DID change: it now always yields
`MultiMemory` (was shared+rebase for grow-free multi-memory inputs), because
shared+rebase is unsound (LS-M-11). The `auto_memory` tests were updated to
assert the new resolution (`ls_m_11_auto_gates_unsound_shared_rebase`,
`auto_keeps_multi_*`, `ls_m_7_*`).

Not done (a stricter option, if wanted later): *erroring* instead of
warning when `Auto` is used on an attested build, behind an opt-in
Expand Down
39 changes: 25 additions & 14 deletions safety/requirements/safety-requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1143,30 +1143,41 @@ artifacts:
title: Automatic memory-strategy resolution (sound-by-construction default)
description: >
`MemoryStrategy::Auto` — the default for both `FuserConfig` and the
`meld fuse` CLI — shall resolve to shared memory + address rebasing
if and only if (a) no input core module contains a `memory.grow`
instruction (static probe, conservative on parse failure) and
(b) the inputs carry at least two linear memories. Otherwise it
shall resolve to multi-memory. If the shared-memory plan refuses
the input, fusion shall retry with multi-memory, so Auto accepts
every input multi-memory accepts. The resolved strategy shall be
reported in `FusionStats` and recorded in the attestation /
provenance metadata as the concrete strategy, never as "auto".
Rationale: out-of-the-box `meld fuse` output must flow through the
`wasm-opt → synth` chain with no extra flags (#172) without ever
selecting the shared form where `memory.grow` makes it unsound
(merger Bug #7, LS-M-7).
`meld fuse` CLI — shall resolve to **multi-memory**, the strategy
that is sound by construction for arbitrary inputs. Auto shall
NOT select shared memory + address rebasing: address rebasing does
not relocate the dynamic address operand of ordinary load/store
instructions (only the static memarg offset and bulk-memory ops),
so components that address memory via computed pointers — heap,
shadow stack, i.e. all real components — would silently collide
in a shared memory (#326, LS-M-11). Shared memory + rebasing shall
remain reachable only by explicit selection (`--memory shared
--address-rebase`), which shall emit a loud unsoundness warning.
The resolved strategy shall be reported in `FusionStats` and
recorded in the attestation / provenance metadata as the concrete
strategy, never as "auto".
History: the prior form of SR-37 (v0.22.0) required Auto to prefer
shared+rebasing for grow-free multi-memory inputs — that
requirement was UNSOUND (it specified a corrupting transform) and
is superseded here by the #326 gate. The single-address-space
(MCU) benefit that shared fusion was meant to provide (#172, #298,
#299) requires correct dynamic-address rebasing in `rewriter.rs`
first (tracked follow-up); until then Auto trades that benefit for
correctness and its output needs `--enable-multimemory`.
status: verified
tags: [usability, memory-strategy, v0.22.0]
tags: [usability, memory-strategy, v0.22.0, v0.38.0]
links:
- type: derives-from
target: SYS-6
- type: mitigates
target: LS-M-7
- type: mitigates
target: LS-M-11
cited-source:
- uri: "https://github.com/pulseengine/meld/issues/172"
kind: github
last-checked: 2026-06-24
release: v0.38.0
fields:
implementation:
- meld-core/src/memory_probe.rs
Expand Down
2 changes: 1 addition & 1 deletion safety/requirements/sw-verifications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ artifacts:
type: sw-verification
title: "Verification of SR-37: Automatic memory-strategy resolution (sound-by-construction default)"
description: >
Verifies SR-37 via: tests `meld-core/tests/auto_memory.rs::auto_selects_shared_for_growfree_inputs`, `meld-core/tests/auto_memory.rs::ls_m_7_auto_keeps_multi_when_input_grows`, `meld-core/tests/auto_memory.rs::ls_m_7_auto_reprobes_after_add_component`, `meld-core/tests/auto_memory.rs::auto_keeps_multi_for_single_memory_input`, `meld-core/tests/auto_memory.rs::explicit_multi_is_not_overridden`, `meld-core/src/memory_probe.rs::tests::ls_m_7_malformed_input_counts_as_grow`.
Verifies SR-37 (post-#326: Auto resolves to multi-memory; shared+rebase is gated as unsound, LS-M-11) via: tests `meld-core/tests/auto_memory.rs::ls_m_11_auto_gates_unsound_shared_rebase`, `meld-core/tests/auto_memory.rs::ls_m_7_auto_keeps_multi_when_input_grows`, `meld-core/tests/auto_memory.rs::ls_m_7_auto_reprobes_after_add_component`, `meld-core/tests/auto_memory.rs::auto_keeps_multi_for_single_memory_input`, `meld-core/tests/auto_memory.rs::explicit_multi_is_not_overridden`, `meld-core/src/memory_probe.rs::tests::ls_m_7_malformed_input_counts_as_grow`.
status: verified
fields:
method: automated-test
Expand Down
64 changes: 64 additions & 0 deletions safety/stpa/loss-scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3662,6 +3662,70 @@ loss-scenarios:
slot (`seen` holds only the first sig) — a dedup-completeness gap,
not a correctness issue (compute and emit stay in lockstep).

- id: LS-M-11
title: shared-memory address rebasing does not rebase dynamic addresses, corrupting computed-pointer access
uca: UCA-M-11
hazards: [H-2, H-3.2]
type: inadequate-control-algorithm
scenario: >
`--memory shared --address-rebase` (and, before the #326 gate, the
`MemoryStrategy::Auto` default for grow-free multi-memory inputs)
relocates each fused component's private memory into a shared
linear memory at a per-component base. But the rebasing is
INCOMPLETE: `rewriter.rs::convert_memarg` adds `memory_base_offset`
only to the STATIC memarg offset immediate, and `needs_rebase_locals`
injects runtime base-adjustment only for bulk-memory ops
(`MemoryCopy`/`MemoryFill`/`MemoryInit`). The DYNAMIC address operand
of ordinary `i32.load`/`i32.store` — a computed pointer pushed on the
stack (heap object, shadow-stack slot) — is never adjusted. Real
components address their heap/shadow-stack via computed pointers, so
two rebased components' private accesses land at the SAME physical
addresses: one component's store clobbers another's data, wrong
result, no trap, no error [H-2, H-3.2]. The output validates and
runs; corruption is silent. Distinct from LS-M-7 (shared unsound
under `memory.grow`) — this occurs with NO growth, purely from
unrebased computed addresses. Reproduced end-to-end: the wac-composed
consumer→provider fixture returns the provider's private sentinel to
the consumer under shared fusion, while `--memory multi` returns the
correct value.
causal-factors:
- >-
`convert_memarg` rebases only `MemArg.offset` (the static
immediate); the effective address is `dynamic_operand + offset`,
and the dynamic operand is left untouched
- >-
`needs_rebase_locals` is set true only when a function contains a
bulk-memory op, so plain loads/stores get no runtime base
adjustment at all
- >-
the auto-resolution and SR-37 both treated "no memory.grow" as
sufficient for sound shared fusion, but growth-freedom is
orthogonal to whether dynamic addresses are rebased
process-model-flaw: >
The rebasing model assumed that adding the component base to the
static memarg offset relocates every access. That holds only for
accesses whose address is a compile-time constant folded into the
memarg; for computed-pointer access (the norm) the runtime operand
dominates and is never relocated. Correct rebasing must inject
`dynamic_addr + memory_base` at runtime for every memory access in
a rebased component.
status: approved
priority: high
fix: >
Interim GATE (#326, PR #329, shipped v0.38.0): `resolve_auto_memory_
strategy` no longer selects shared+rebase — Auto always resolves to
multi-memory (sound). Explicit `--memory shared --address-rebase`
remains an opt-in but emits a loud unsoundness warning. SR-37 was
corrected (it had required the unsound shared preference). Pinned by
`ls_m_11_auto_gates_unsound_shared_rebase` (Auto → multi + separate
memories) and the `test_304` shared path made explicit. PENDING
full fix (rewriter.rs dynamic-address rebasing, Tier-5): inject
`addr + memory_base` for every load/store in a rebased component,
which re-enables sound single-address-space (MCU) lowering for
#298/#299/gale. Until that lands, `--memory shared` stays unsound
for computed-pointer access and Auto's output needs
`--enable-multimemory`.

- id: LS-D-1
title: remapped DWARF emits a wrong code address for a fused function
uca: UCA-M-9
Expand Down
Loading