From 7d197874cb693a725a1ba0d6317ccb555f3493e2 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 19 Aug 2026 07:33:43 +0200 Subject: [PATCH 1/5] fix(386): warn about un-rebasable addresses only when an input lacks relocs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--memory shared --address-rebase` warned "UNSOUND" unconditionally — including for `--emit-relocs` inputs, where it is not. That text predates the reloc CONSUMER (#326→#340): a reloc-covered input is rebased at the SOURCE point (every reloc-flagged i32.const address relocated at its origin), so a pointer computed from it is correct by construction — proven by rebasing_end_to_end::test_326_reloc_const_rebasing_end_to_end, which executes on wasmtime. It fired on exactly the path meld's MCU line is built on: --pack-rebase (SR-57) and --share-stack (SR-66) both REQUIRE shared+rebase, and the falcon supplier validated that pipeline on real components (#370, an executing F100 image at 51% of budget). So every gale production build was told its validated flow is unsound. In a functional-safety tool that is worse than noise — it trains users to ignore warnings, including the precise one that matters. The warning was also redundant: address_strategy.rs already warns per-module, only on the no-reloc path, naming the actual residual (#339) and the remedy; and the genuinely dangerous case (no relocs WITH direct load/store) is already a hard error there (path-F). So it added no coverage, only noise. - lib.rs: gate the warning on `any_component_lacks_reloc_metadata()`, using the same `reloc::has_reloc_metadata` probe `resolve_address_plan` gates on (so the warning cannot disagree with the strategy that follows it); reword to describe the real risk. - Doc accuracy: the PUBLIC `MemoryStrategy::Auto` rustdoc said "Both hold -> SharedMemory with address rebasing" but Auto never picks shared — corrected, along with the module-level docs and the internal rationale (which still cited the pre-consumer #326 reasoning). Removed the dangling reference to `warn_if_unsound_rebasing` (no such function). Auto's message demoted warn->info and now points at the explicit MCU invocation. - Behavior unchanged: strategy selection, fusion output and the per-module address_strategy warnings are untouched. This is messaging accuracy only. Test: tests/shared_rebase_warning_386.rs — reloc-covered inputs fuse QUIETLY (no UNSOUND, no no-reloc warning); an input without relocs still warns. Proven RED->GREEN by revert (pre-fix it fails, capturing the spurious warning verbatim). Full suite 851/0, clippy --all-targets -D warnings clean, fmt clean. Item 3 of #386 (should Auto upgrade when EVERY input is reloc-covered?) is left open — a defaults question, with ADR-4 arguing for explicit selection. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WrmMqf1iuTS1UBEes5TmdC --- meld-core/src/lib.rs | 138 +++++++---- meld-core/tests/shared_rebase_warning_386.rs | 247 +++++++++++++++++++ 2 files changed, 334 insertions(+), 51 deletions(-) create mode 100644 meld-core/tests/shared_rebase_warning_386.rs diff --git a/meld-core/src/lib.rs b/meld-core/src/lib.rs index 8693cee..9f0f35e 100644 --- a/meld-core/src/lib.rs +++ b/meld-core/src/lib.rs @@ -35,10 +35,10 @@ //! //! ## Memory Strategy //! -//! - **Auto** (default): Resolves to shared memory + address rebasing when -//! no input module contains `memory.grow` and the inputs carry two or -//! more memories; multi-memory otherwise. See [`MemoryStrategy::Auto`] -//! and issue #172. +//! - **Auto** (default): always resolves to multi-memory — the strategy that +//! is sound for every input. Shared memory is never auto-selected; it is an +//! explicit choice with an input contract (`--emit-relocs` on every input, or +//! PIC). See [`MemoryStrategy::Auto`], issue #172 and #386. //! - **Multi-memory**: Each component keeps its own linear memory. //! Cross-component pointer-passing calls use adapters with `cabi_realloc` //! and `memory.copy`. Downstream tools need multi-memory support @@ -224,22 +224,24 @@ pub enum MemoryStrategy { /// space (MCU) targets have no lowering for it (issue #172). MultiMemory, - /// Resolve to `SharedMemory` + address rebasing when that is - /// provably sound, `MultiMemory` otherwise (default; issue #172). + /// Pick the memory strategy automatically (default; issue #172). /// - /// Resolution happens once, at the start of fusion, from two - /// static facts about the inputs: - /// 1. No input module contains a `memory.grow` instruction - /// (`memory_probe`) — growth is what breaks shared memory. - /// 2. The inputs carry two or more linear memories — with at most - /// one, multi-memory output is already single-memory, so the - /// rebasing path adds risk without benefit. + /// **Auto always resolves to [`MultiMemory`](Self::MultiMemory)** — the + /// strategy that is sound for every input. It never selects shared memory: + /// shared + rebasing is sound only under an input contract (`--emit-relocs` + /// on every input, or PIC) that `Auto` does not require callers to have met, + /// and ADR-4 wants an attested build to state its memory strategy + /// explicitly rather than inherit one. /// - /// Both hold → `SharedMemory` with address rebasing. Otherwise → - /// `MultiMemory`. If the shared-memory plan itself refuses the - /// input (`Error::MemoryStrategyUnsupported`), fusion retries as - /// `MultiMemory`, so `Auto` never fails on input that the - /// multi-memory strategy accepts. + /// Resolution still inspects the inputs to explain the choice: a + /// `memory.grow` anywhere (`memory_probe`) rules shared memory out outright + /// (growth invalidates a fixed layout), and with fewer than two memories the + /// multi-memory output is already single-memory. + /// + /// For a single-address-space (MCU) target, select it explicitly: + /// `--memory shared --address-rebase` (plus `--pack-rebase` / + /// `--share-stack`). Whether `Auto` should upgrade when EVERY input is + /// reloc-covered is an open defaults question (#386). Auto, } @@ -531,35 +533,67 @@ impl Fuser { } return result; } - // #326: explicit `--memory shared --address-rebase` is an opt-in to an - // unsound transform — address rebasing does not rebase computed memory - // addresses, so it silently corrupts real components. `auto` no longer - // selects this path; when a caller requests it explicitly, warn loudly - // rather than corrupt silently (LS-D-1). The build still proceeds. + // #386: warn about computed-pointer rebasing ONLY when it can actually + // apply — i.e. when some input lacks reloc metadata. + // + // The original blanket warning predates the reloc CONSUMER (#326→#340). + // A reloc-covered input is rebased at the SOURCE point: every + // reloc-flagged `i32.const` address is relocated at its origin, so a + // pointer computed from it is correct by construction (proven by + // `rebasing_end_to_end::test_326_reloc_const_rebasing_end_to_end`, which + // executes on wasmtime). Warning "UNSOUND" on that path is inaccurate — + // and it is the path `--pack-rebase` (SR-57) and `--share-stack` (SR-66) + // are built on, supplier-validated on real components (#370). A + // safety tool that cries wolf on its supported path trains users to + // ignore the warnings that matter. + // + // The residual risk lives entirely on the NO-reloc path, where it is + // already reported precisely and per-module by + // `address_strategy::resolve_address_plan` (an absolute address used as + // a VALUE, #339) — and the genuinely dangerous case (no relocs WITH + // direct load/store) is already a hard error there (path-F). if self.config.memory_strategy == MemoryStrategy::SharedMemory && self.config.address_rebasing + && self.any_component_lacks_reloc_metadata() { log::warn!( - "memory strategy: explicit shared-memory + address rebasing is UNSOUND \ - for components that access memory via computed pointers (#326) — the \ - dynamic address operand of ordinary loads/stores is not rebased, so \ - per-component memory can silently collide. Prefer `--memory multi` \ - unless every input addresses memory only via static offsets." + "memory strategy: shared memory + address rebasing with at least one \ + input that carries NO relocation metadata. Absolute addresses in such \ + a module cannot be rebased, so it may silently alias another \ + component's memory (#326/#339). Rebuild every input with \ + `--emit-relocs`, or use `--memory multi`." ); } self.fuse_with_stats_resolved() } + /// #386: true when at least one input core module carries no `linking`/ + /// `reloc.*` metadata — the only condition under which shared-memory + /// rebasing can leave an absolute address un-rebased. Uses the same probe + /// (`reloc::has_reloc_metadata`) that `resolve_address_plan` gates on, so + /// the warning cannot disagree with the strategy that follows it. + fn any_component_lacks_reloc_metadata(&self) -> bool { + self.components.iter().any(|component| { + component + .core_modules + .iter() + .any(|module| !reloc::has_reloc_metadata(&module.custom_sections)) + }) + } + /// Resolve `MemoryStrategy::Auto` against the added components. /// - /// Auto always selects **multi-memory** — it is the sound strategy. Shared - /// memory + address rebasing was previously auto-selected for grow-free, - /// multi-memory inputs, but that path is **unsound** (#326): rebasing does - /// not relocate the dynamic address operand of ordinary loads/stores, so - /// components addressing memory via computed pointers silently collide. - /// Until correct dynamic rebasing lands, Auto never picks shared+rebase; - /// it remains reachable only via explicit `--memory shared --address-rebase` - /// (which warns loudly). Any user-supplied `address_rebasing` value is + /// Auto always selects **multi-memory** — the strategy that is sound for + /// every input, with no producer-side contract. Shared memory + rebasing was + /// auto-selected for grow-free multi-memory inputs until #326: before the + /// reloc CONSUMER landed, rebasing could not relocate an absolute address at + /// all, so a computed pointer silently collided. That consumer since shipped + /// (#326→#340) and reloc-covered inputs now rebase soundly at the source + /// point — but `Auto` cannot assume the caller met that input contract, and + /// ADR-4 wants an attested build to choose explicitly. So Auto still never + /// picks shared+rebase; it stays reachable via explicit + /// `--memory shared --address-rebase` (which warns only when an input lacks + /// reloc metadata, #386). Any user-supplied `address_rebasing` value is /// overridden — Auto owns both knobs. fn resolve_auto_memory_strategy(&mut self) { let mut memory_count = 0usize; @@ -593,22 +627,24 @@ impl Fuser { self.config.memory_strategy = MemoryStrategy::MultiMemory; self.config.address_rebasing = false; } else { - // #326: address rebasing does NOT rebase the dynamic address - // operand of ordinary loads/stores (only the static memarg offset - // and bulk-memory ops) — so shared-memory fusion silently corrupts - // any component that addresses memory via a computed pointer (heap, - // shadow stack — i.e. all real components). Until correct dynamic - // rebasing lands, `auto` must NOT silently pick shared+rebase. - // Fall back to multi-memory, which is sound (LS-D-1: emit correct - // output, never a plausible-but-wrong one). Explicit - // `--memory shared --address-rebase` remains available as an - // opt-in, and warns loudly (see `warn_if_unsound_rebasing`). - log::warn!( + // `auto` does not pick shared+rebase. Historically (#326) because + // rebasing could not relocate a computed pointer at all; since the + // reloc CONSUMER landed (#326→#340) that is no longer true for + // reloc-covered inputs (they rebase at the source point), so the + // remaining reasons are: a NO-reloc input can still leave an absolute + // address un-rebased (#339), and ADR-4 wants an attested build to + // choose its memory strategy EXPLICITLY rather than inherit one. + // Multi-memory is sound for every input (LS-D-1: emit correct output, + // never a plausible-but-wrong one). Whether `auto` should upgrade + // when EVERY input is reloc-covered is an open defaults question + // (#386). Explicit `--memory shared --address-rebase` remains + // available, and warns when any input lacks reloc metadata. + log::info!( "memory strategy auto: {memory_count} memories, no memory.grow — \ - shared-memory fusion would apply here, but address rebasing is \ - unsound for computed memory addresses (#326); selecting \ - multi-memory instead. Use `--memory shared --address-rebase` to \ - override explicitly (unsound; corrupts computed-pointer access)." + selecting multi-memory (sound for every input; `auto` never picks \ + shared memory, see #386). For a single-address-space (MCU) target, \ + select it explicitly: `--memory shared --address-rebase` — sound \ + when every input is built with `--emit-relocs`." ); self.config.memory_strategy = MemoryStrategy::MultiMemory; self.config.address_rebasing = false; diff --git a/meld-core/tests/shared_rebase_warning_386.rs b/meld-core/tests/shared_rebase_warning_386.rs new file mode 100644 index 0000000..455e329 --- /dev/null +++ b/meld-core/tests/shared_rebase_warning_386.rs @@ -0,0 +1,247 @@ +//! #386 — the shared+rebase "no relocation metadata" warning fires only when it +//! actually applies. +//! +//! `--memory shared --address-rebase` used to warn "UNSOUND" unconditionally. +//! That predates the reloc CONSUMER (#326→#340): a reloc-covered input is rebased +//! at the SOURCE point (every reloc-flagged `i32.const` address is relocated at +//! its origin), so a pointer computed from it is correct by construction — proven +//! by `rebasing_end_to_end::test_326_reloc_const_rebasing_end_to_end`, which +//! executes on wasmtime. Warning on that path is inaccurate, and it is the path +//! `--pack-rebase` (SR-57) and `--share-stack` (SR-66) are built on and that the +//! falcon supplier validated on real components (#370). A safety tool that cries +//! wolf on its supported path trains users to ignore the warnings that matter. +//! +//! Contract pinned here: reloc-covered inputs fuse QUIETLY; an input WITHOUT +//! reloc metadata still warns (the residual #339 risk is real there). + +use std::sync::Mutex; + +use meld_core::{Fuser, FuserConfig, MemoryStrategy}; +use wasm_encoder::{ + CodeSection, Component, ConstExpr, CustomSection, DataSection, DataSegment, DataSegmentMode, + ExportKind, ExportSection, Function, FunctionSection, Instruction, MemArg, MemorySection, + MemoryType, Module, ModuleSection, TypeSection, ValType, +}; + +/// The substring unique to the #386 warning (distinct from the per-module +/// `address_strategy` warning, which names a component/module instead). +const WARN_MARKER: &str = "at least one input"; + +static CAPTURED: Mutex> = Mutex::new(Vec::new()); +static LOGGER: CaptureLogger = CaptureLogger; + +struct CaptureLogger; + +impl log::Log for CaptureLogger { + fn enabled(&self, _: &log::Metadata<'_>) -> bool { + true + } + fn log(&self, record: &log::Record<'_>) { + if record.level() == log::Level::Warn { + CAPTURED.lock().unwrap().push(format!("{}", record.args())); + } + } + fn flush(&self) {} +} + +const DATA_ADDR: i32 = 0x100; + +fn write_uleb(out: &mut Vec, mut v: u32) { + loop { + let mut byte = (v & 0x7f) as u8; + v >>= 7; + if v != 0 { + byte |= 0x80; + } + out.push(byte); + if v == 0 { + break; + } + } +} + +fn find_i32const_reloc_offsets(module_bytes: &[u8], flag_value: i32) -> Vec { + let mut code_start = None; + let mut offsets = Vec::new(); + for payload in wasmparser::Parser::new(0).parse_all(module_bytes) { + match payload.expect("payload") { + wasmparser::Payload::CodeSectionStart { range, .. } => code_start = Some(range.start), + wasmparser::Payload::CodeSectionEntry(body) => { + let cs = code_start.expect("code section start seen first"); + for item in body + .get_operators_reader() + .expect("operators") + .into_iter_with_offsets() + { + let (op, pos) = item.expect("operator"); + if let wasmparser::Operator::I32Const { value } = op + && value == flag_value + { + offsets.push((pos - cs + 1) as u32); + } + } + } + _ => {} + } + } + offsets +} + +fn build_reloc_code_body(offsets: &[u32]) -> Vec { + let mut body = Vec::new(); + write_uleb(&mut body, 3); + write_uleb(&mut body, offsets.len() as u32); + for &off in offsets { + body.push(4u8); // R_WASM_MEMORY_ADDR_SLEB + write_uleb(&mut body, off); + write_uleb(&mut body, 0); + body.push(0u8); + } + body +} + +fn shared_memory_section() -> MemorySection { + let mut memory = MemorySection::new(); + memory.memory(MemoryType { + minimum: 1, + maximum: Some(2), + memory64: false, + shared: true, + page_size_log2: None, + }); + memory +} + +/// A component that reads a sentinel through a reloc-flagged absolute address. +/// `with_relocs` decides whether the `linking` + `reloc.CODE` sections are +/// emitted — i.e. whether meld can rebase that address. +fn build_component(tag: &str, sentinel: u8, export_memory: bool, with_relocs: bool) -> Vec { + let memarg = MemArg { + offset: 0, + align: 0, + memory_index: 0, + }; + + let add_sections = |module: &mut Module| { + let mut types = TypeSection::new(); + types.ty().function([], [ValType::I32]); + + let mut functions = FunctionSection::new(); + functions.function(0); + + let mut exports = ExportSection::new(); + exports.export(&format!("read_{tag}"), ExportKind::Func, 0); + if export_memory { + exports.export("memory", ExportKind::Memory, 0); + } + + let mut code = CodeSection::new(); + let mut read = Function::new([]); + read.instruction(&Instruction::I32Const(DATA_ADDR)); + read.instruction(&Instruction::I32Load8U(memarg)); + read.instruction(&Instruction::End); + code.function(&read); + + let mut data = DataSection::new(); + data.segment(DataSegment { + mode: DataSegmentMode::Active { + memory_index: 0, + offset: &ConstExpr::i32_const(DATA_ADDR), + }, + data: [sentinel], + }); + + module + .section(&types) + .section(&functions) + .section(&shared_memory_section()) + .section(&exports) + .section(&code) + .section(&data); + }; + + let mut module = Module::new(); + add_sections(&mut module); + + if with_relocs { + let mut dry = Module::new(); + add_sections(&mut dry); + let offsets = find_i32const_reloc_offsets(&dry.finish(), DATA_ADDR); + assert_eq!( + offsets.len(), + 1, + "one address literal to flag in read_{tag}" + ); + let reloc_code = build_reloc_code_body(&offsets); + module.section(&CustomSection { + name: "linking".into(), + data: vec![0x02].into(), + }); + module.section(&CustomSection { + name: "reloc.CODE".into(), + data: reloc_code.into(), + }); + } + + let mut component = Component::new(); + component.section(&ModuleSection(&module)); + component.finish() +} + +fn fuse_shared_rebase(a: Vec, b: Vec) -> Result, String> { + let config = FuserConfig { + memory_strategy: MemoryStrategy::SharedMemory, + address_rebasing: true, + ..Default::default() + }; + let mut fuser = Fuser::new(config); + fuser.add_component_named(&a, Some("comp-a")).unwrap(); + fuser.add_component_named(&b, Some("comp-b")).unwrap(); + fuser.fuse().map_err(|e| e.to_string()) +} + +fn drain() -> Vec { + std::mem::take(&mut *CAPTURED.lock().unwrap()) +} + +/// Both phases live in ONE test so the process-global capture buffer is not +/// raced by parallel test threads. +#[test] +fn shared_rebase_warns_only_when_an_input_lacks_relocs() { + log::set_logger(&LOGGER).expect("install capture logger"); + log::set_max_level(log::LevelFilter::Warn); + + // Phase 1 — every input is reloc-covered: meld can rebase every address at + // its source, so the fuse must be QUIET (the #386 regression: it used to + // print "UNSOUND" here, on the very path pack-rebase/share-stack ship). + drain(); + fuse_shared_rebase( + build_component("a", 0xA1, true, true), + build_component("b", 0xB2, false, true), + ) + .expect("reloc-covered shared+rebase fusion"); + let warnings = drain(); + assert!( + !warnings.iter().any(|w| w.contains(WARN_MARKER)), + "reloc-covered inputs must not draw the no-reloc warning, got: {warnings:?}" + ); + assert!( + !warnings.iter().any(|w| w.contains("UNSOUND")), + "reloc-covered shared+rebase is not unsound — no UNSOUND warning, got: {warnings:?}" + ); + + // Phase 2 — one input carries no reloc metadata: its absolute address cannot + // be rebased, so the warning is real and must fire. (This input does a direct + // load, so path-F also hard-errors — the warning fires first, which is the + // observability contract being pinned.) + drain(); + let _ = fuse_shared_rebase( + build_component("a", 0xA1, true, true), + build_component("b", 0xB2, false, false), + ); + let warnings = drain(); + assert!( + warnings.iter().any(|w| w.contains(WARN_MARKER)), + "an input without reloc metadata must draw the warning, got: {warnings:?}" + ); +} From 3f5e69c39b27adbd88dd2fdde31f75b46e466265 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 19 Aug 2026 11:16:11 +0200 Subject: [PATCH 2/5] feat(386): report the available shared-memory build instead of escalating to it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part (a) of the #386 defaults decision. `auto` keeps selecting multi-memory — the strategy that is sound for every input with no producer contract — but now says what is available: - every input reloc-covered -> "these inputs support a single-address-space (MCU) build: select it explicitly with `--memory shared --address-rebase` (+ --pack-rebase / --share-stack)". - some input not reloc-covered -> names the missing precondition (`--emit-relocs`) instead. Deliberately a HINT, not an escalation. `has_reloc_metadata` proves reloc PRESENCE, not COVERAGE (undecidable in general, #339), so auto-switching to the strategy that carries an input contract would rest on a proxy — and it would escalate risk automatically and change artifact layout for existing builds, both of which a functional-safety tool should not do silently. ADR-4: explicit, not auto. The tool informs; the caller chooses. Full suite 851/0, clippy clean, fmt clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WrmMqf1iuTS1UBEes5TmdC --- meld-core/src/lib.rs | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/meld-core/src/lib.rs b/meld-core/src/lib.rs index 9f0f35e..5797787 100644 --- a/meld-core/src/lib.rs +++ b/meld-core/src/lib.rs @@ -639,13 +639,33 @@ impl Fuser { // when EVERY input is reloc-covered is an open defaults question // (#386). Explicit `--memory shared --address-rebase` remains // available, and warns when any input lacks reloc metadata. - log::info!( - "memory strategy auto: {memory_count} memories, no memory.grow — \ - selecting multi-memory (sound for every input; `auto` never picks \ - shared memory, see #386). For a single-address-space (MCU) target, \ - select it explicitly: `--memory shared --address-rebase` — sound \ - when every input is built with `--emit-relocs`." - ); + // + // #386: `auto` will NOT escalate to shared even when the inputs look + // ready for it — `has_reloc_metadata` proves reloc PRESENCE, not + // reloc COVERAGE (coverage is undecidable in general, #339), so an + // automatic switch to the strategy with an input contract would rest + // on a proxy. Instead the tool reports what is available and lets the + // caller choose (ADR-4: explicit, not auto). + if self.any_component_lacks_reloc_metadata() { + log::info!( + "memory strategy auto: {memory_count} memories, no memory.grow — \ + selecting multi-memory (sound for every input). A \ + single-address-space (MCU) build is available via \ + `--memory shared --address-rebase`, but at least one input \ + carries no relocation metadata — rebuild every input with \ + `--emit-relocs` first." + ); + } else { + log::info!( + "memory strategy auto: {memory_count} memories, no memory.grow, \ + every input carries relocation metadata — selecting \ + multi-memory (`auto` never escalates to shared memory on its \ + own, #386). These inputs support a single-address-space (MCU) \ + build: select it explicitly with \ + `--memory shared --address-rebase` (add `--pack-rebase` to \ + compact it, `--share-stack` to share one shadow stack)." + ); + } self.config.memory_strategy = MemoryStrategy::MultiMemory; self.config.address_rebasing = false; } From 899e088b72b6cd57ea9df44336503d0a5ae72d73 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 19 Aug 2026 11:24:54 +0200 Subject: [PATCH 3/5] =?UTF-8?q?feat(SR-68/#386):=20--profile=20safety=20?= =?UTF-8?q?=E2=80=94=20refuse=20to=20infer=20safety-relevant=20properties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-7 decided meld has a dual identity (generic RFC-46 reference fuser AND sealed-safety product, both first-class) and specified TWO ATTESTED PROFILES. Nobody had built the signal, so ADR-4 ("explicit, not auto") could only warn. Why a profile rather than "strict when attested": `attestation` defaults TRUE (lib.rs) and `--memory` defaults to `auto` (CLI), so "attested + Auto" is the DEFAULT invocation — erroring there would fail plain `meld fuse a.wasm b.wasm` for everyone and break the ecosystem identity ADR-7 declared first-class. The profile is the explicit signal that separates the two. - `Profile::{Ecosystem (default), Safety}` + `FuserConfig::profile` + CLI `--profile `. - `Error::SafetyProfileViolation`. - `check_safety_profile()` runs BEFORE fusion (a profile violation is a build configuration error: it must not depend on input shape, nor leave a partially produced artifact). First enforced property = the MEMORY STRATEGY (ADR-4): Auto selects the inter-component isolation model, which decides whether a fault in one component can reach another's state, so a safety build must state it. Error names both the violation and the remedy. - Ecosystem behavior is UNCHANGED — nothing breaks. The profile GATES, it never transforms: a build that passes under Safety emits byte-identical output to the same explicit invocation under Ecosystem (pinned by a test using `reproducible: true` so the comparison is fusion output, not the attestation nonce). Scope held deliberately: only ADR-4 is enforced. ADR-7's other binding requirements (per-boundary strategy declared/attested; silent-downgrade = hard error) are documented as intended future consumers of this seam, not silently half-done. Tests: tests/safety_profile_386.rs (4) — refuses Auto naming the remedy; accepts explicit; ecosystem still infers (the default path); gates-but-does-not-transform. Live CLI verified both directions. rivet SR-68 + SWV-80 (PASS). `meld docs profiles`. Workspace: clippy --workspace --all-targets -D warnings clean, fmt clean, tests 855/0. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WrmMqf1iuTS1UBEes5TmdC --- meld-cli/docs/concept-profiles.md | 44 +++++++ meld-cli/src/docs.rs | 5 + meld-cli/src/main.rs | 36 +++++- meld-core/benches/fusion_benchmarks.rs | 1 + meld-core/src/error.rs | 8 ++ meld-core/src/lib.rs | 74 +++++++++++ meld-core/tests/adapter_safety.rs | 14 +++ meld-core/tests/component_provenance.rs | 1 + meld-core/tests/cross_component_call.rs | 1 + meld-core/tests/drop_realloc.rs | 3 + meld-core/tests/dwarf_passthrough.rs | 3 + meld-core/tests/dwarf_remap_witness.rs | 1 + meld-core/tests/golden_e2e.rs | 1 + meld-core/tests/issue_112_mythos_v04.rs | 1 + meld-core/tests/multi_memory.rs | 3 + .../tests/multiply_instantiated_runtime.rs | 1 + meld-core/tests/nested_component.rs | 1 + meld-core/tests/p3_bridge_runtime.rs | 1 + meld-core/tests/pack_rebase_370.rs | 4 + meld-core/tests/pic_extended_const_353.rs | 1 + meld-core/tests/realloc_safety.rs | 1 + meld-core/tests/rebasing_end_to_end.rs | 7 ++ meld-core/tests/release_components.rs | 10 ++ meld-core/tests/runtime_from_exports.rs | 3 + meld-core/tests/runtime_intra_adapter.rs | 2 + meld-core/tests/safety_profile_386.rs | 116 ++++++++++++++++++ meld-core/tests/segidx_remap.rs | 1 + meld-core/tests/shared_everything_topology.rs | 1 + meld-core/tests/shared_overlap_370.rs | 1 + meld-core/tests/value_address_warn_339.rs | 1 + meld-core/tests/wit_bindgen_runtime.rs | 1 + safety/requirements/safety-requirements.yaml | 60 +++++++++ safety/requirements/sw-verifications.yaml | 19 +++ 33 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 meld-cli/docs/concept-profiles.md create mode 100644 meld-core/tests/safety_profile_386.rs diff --git a/meld-cli/docs/concept-profiles.md b/meld-cli/docs/concept-profiles.md new file mode 100644 index 0000000..cf51cbf --- /dev/null +++ b/meld-cli/docs/concept-profiles.md @@ -0,0 +1,44 @@ +# Build profiles — ecosystem and safety + +meld has two identities, and both are first-class: the generic Component-Model +fuser anyone can use, and a tool you can put in a functional-safety toolchain. +`--profile` says which one this build is. + +The profiles do **not** fuse differently. A build that succeeds under +`--profile safety` produces byte-identical output to the same explicit invocation +under `--profile ecosystem`. What changes is **how much meld is allowed to decide +for you**. + +## `--profile ecosystem` (default) + +Convenience defaults apply. Where a property is unset, meld picks a sensible, +sound value and tells you what it picked. `--memory auto` selects a memory +strategy for you; advisory checks warn. + +## `--profile safety` + +Every safety-relevant property must be **stated**, not inferred. Inferring one is +a hard error instead of a warning. + +Enforced today: + +- **The memory strategy must be explicit.** `--memory auto` is refused. The + memory strategy selects the inter-component isolation model — one shared + address space, or one memory per component — which decides whether a fault in + one component can reach another component's state. That is not a decision a + build should inherit silently. Pass `--memory multi` (isolation preserved) or + `--memory shared` (single address space; add `--address-rebase`, and build + every input with `--emit-relocs`). + +The reasoning is the same one behind meld's other loud failures: for a property +that changes what the artifact *guarantees*, a wrong-but-plausible default is +worse than a stopped build. You can always state the value you want — the profile +only insists that you state it. + +## Why a profile rather than "strict when attested" + +Attestation is on by default, and so is `--memory auto`. Enforcing on +"attested build" would therefore fail the ordinary `meld fuse a.wasm b.wasm` +invocation, which would break the ecosystem identity to serve the safety one. +The profile is the explicit signal that separates them, so neither identity is +compromised. diff --git a/meld-cli/src/docs.rs b/meld-cli/src/docs.rs index 7f3b99d..75f3380 100644 --- a/meld-cli/src/docs.rs +++ b/meld-cli/src/docs.rs @@ -35,6 +35,11 @@ pub const TOPICS: &[Topic] = &[ "Static fusion — what meld does", "concept-fusion.md" ), + topic!( + "profiles", + "Build profiles — ecosystem and safety", + "concept-profiles.md" + ), topic!( "memory-strategies", "Memory strategies — auto, multi, shared", diff --git a/meld-cli/src/main.rs b/meld-cli/src/main.rs index 6330918..4e19533 100644 --- a/meld-cli/src/main.rs +++ b/meld-cli/src/main.rs @@ -18,7 +18,9 @@ use anyhow::{Context, Result, anyhow}; use clap::{Parser, Subcommand}; -use meld_core::{DwarfHandling, Fuser, FuserConfig, FusionStats, MemoryStrategy, OutputFormat}; +use meld_core::{ + DwarfHandling, Fuser, FuserConfig, FusionStats, MemoryStrategy, OutputFormat, Profile, +}; use std::fs; use std::path::Path; use std::time::Instant; @@ -65,6 +67,16 @@ enum Commands { #[arg(long, default_value = "auto")] memory: String, + /// Build profile: 'ecosystem' (default) or 'safety'. Under 'safety' + /// every safety-relevant property must be stated explicitly — inferring + /// one is a hard error rather than a warning (ADR-7's sealed-safety + /// profile; ADR-4 "explicit, not auto"). Today that means `--memory` + /// must be given: 'auto' is refused. A build that passes under 'safety' + /// produces byte-identical output to the same explicit invocation under + /// 'ecosystem' — the profile only decides what may be inferred. + #[arg(long, default_value = "ecosystem")] + profile: String, + /// Rebase memory addresses for shared memory (experimental). /// Only valid with --memory shared; "auto" decides it itself. #[arg(long)] @@ -241,11 +253,13 @@ fn main() -> Result<()> { opaque_rep, pack_rebase, share_stack, + profile, }) => { fuse_command( inputs, output, memory, + profile, address_rebase, pack_rebase, share_stack, @@ -325,6 +339,7 @@ fn fuse_command( inputs: Vec, output: String, memory: String, + profile: String, address_rebase: bool, pack_rebase: bool, share_stack: bool, @@ -344,6 +359,24 @@ fn fuse_command( env!("CARGO_PKG_VERSION") ); + // Parse build profile (ADR-7: ecosystem vs sealed-safety). + let profile = match profile.as_str() { + "ecosystem" => Profile::Ecosystem, + "safety" => { + println!( + "Safety profile: every safety-relevant property must be stated \ + explicitly (ADR-4: explicit, not auto)" + ); + Profile::Safety + } + other => { + return Err(anyhow!( + "Invalid --profile: {}. Use 'ecosystem' or 'safety'", + other + )); + } + }; + // Parse memory strategy let memory_strategy = match memory.as_str() { "auto" => { @@ -453,6 +486,7 @@ fn fuse_command( }; let config = FuserConfig { + profile, memory_strategy, attestation: !no_attestation, reproducible, diff --git a/meld-core/benches/fusion_benchmarks.rs b/meld-core/benches/fusion_benchmarks.rs index 7089431..f82f9c6 100644 --- a/meld-core/benches/fusion_benchmarks.rs +++ b/meld-core/benches/fusion_benchmarks.rs @@ -77,6 +77,7 @@ fn bench_config() -> FuserConfig { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/src/error.rs b/meld-core/src/error.rs index 380b972..7e09189 100644 --- a/meld-core/src/error.rs +++ b/meld-core/src/error.rs @@ -121,6 +121,14 @@ pub enum Error { #[error("canonical ABI error: {0}")] CanonicalAbi(String), + /// A safety-profile requirement was not met (`--profile safety`). + /// + /// Under the sealed-safety profile (ADR-7) meld refuses to *infer* a + /// safety-relevant property that the build should have *declared*. The + /// ecosystem profile keeps the same condition as an advisory warning. + #[error("safety profile: {0}")] + SafetyProfileViolation(String), + /// Same core module instantiated more than once in a component #[error( "component {component_idx} instantiates core module {module_idx} more than once (multiply-instantiated modules are not yet supported)" diff --git a/meld-core/src/lib.rs b/meld-core/src/lib.rs index 5797787..0387924 100644 --- a/meld-core/src/lib.rs +++ b/meld-core/src/lib.rs @@ -82,6 +82,11 @@ use wasm_encoder::Module as EncodedModule; /// Configuration for the fusion process #[derive(Debug, Clone)] pub struct FuserConfig { + /// Which of ADR-7's two attested profiles this build targets. Under + /// [`Profile::Safety`] advisory safety checks become hard errors — see + /// [`Profile`]. Default: [`Profile::Ecosystem`]. + pub profile: Profile, + /// Memory strategy for fused output pub memory_strategy: MemoryStrategy, @@ -192,6 +197,7 @@ pub struct FuserConfig { impl Default for FuserConfig { fn default() -> Self { Self { + profile: Profile::Ecosystem, memory_strategy: MemoryStrategy::Auto, attestation: true, reproducible: false, @@ -245,6 +251,40 @@ pub enum MemoryStrategy { Auto, } +/// Which of ADR-7's two attested profiles this build targets. +/// +/// ADR-7 decided meld has a **dual identity**: the generic RFC-46 reference +/// fuser *and* a sealed-safety product, both first-class. The difference is not +/// the fusion algorithm — it is how much the build is allowed to *infer*. A +/// safety build must **declare** its safety-relevant properties; an ecosystem +/// build may let meld pick sensible defaults. +/// +/// The profile therefore only ever turns advisory checks into hard errors. It +/// never changes what a successful fusion produces: a build that passes under +/// [`Safety`](Self::Safety) emits byte-identical output to the same explicit +/// invocation under [`Ecosystem`](Self::Ecosystem). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Profile { + /// Open / ecosystem profile (default). Advisory safety checks warn. + #[default] + Ecosystem, + + /// Sealed-safety profile. Every safety-relevant property must be stated + /// explicitly; inferring one is an [`Error::SafetyProfileViolation`] rather + /// than a warning. + /// + /// Enforced today: + /// - **ADR-4 — memory strategy must be explicit.** `MemoryStrategy::Auto` + /// picks the inter-component isolation model, which is a safety-relevant + /// property; a safety build must name `--memory`. + /// + /// Intended future consumers (ADR-7 binding requirements, not yet + /// implemented): per-boundary call-lowering/address strategy declared and + /// attested, and silent-downgrade-is-a-hard-error at any boundary whose + /// declared strategy's preconditions are unmet. + Safety, +} + /// Output format for the fused binary #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum OutputFormat { @@ -495,6 +535,11 @@ impl Fuser { ); } } + // ADR-7 sealed-safety profile: refuse to INFER a safety-relevant + // property that the build should have DECLARED. Checked before any work + // so the build fails fast and never produces a half-inferred artifact. + self.check_safety_profile()?; + if self.config.memory_strategy == MemoryStrategy::Auto { self.resolve_auto_memory_strategy(); let result = if self.config.memory_strategy == MemoryStrategy::SharedMemory { @@ -567,6 +612,35 @@ impl Fuser { self.fuse_with_stats_resolved() } + /// ADR-7 / ADR-4: enforce the sealed-safety profile's "declare, don't infer" + /// rule. Returns `Ok(())` unmodified under [`Profile::Ecosystem`], where the + /// same conditions remain advisory warnings. + /// + /// Today one property is enforced — the memory strategy (ADR-4). It selects + /// the inter-component isolation model (one shared address space vs. one + /// memory per component), which decides whether a fault in one component can + /// reach another's state. A safety build must state it. + /// + /// Deliberately checked BEFORE fusion: a profile violation is a build + /// configuration error, so it must not depend on the inputs and must not + /// leave a partially-produced artifact. + fn check_safety_profile(&self) -> Result<()> { + if self.config.profile != Profile::Safety { + return Ok(()); + } + if self.config.memory_strategy == MemoryStrategy::Auto { + return Err(Error::SafetyProfileViolation( + "`--memory auto` infers the inter-component isolation model, which is a \ + safety-relevant property (ADR-4: explicit, not auto). State it explicitly: \ + `--memory multi` (one memory per component — isolation preserved) or \ + `--memory shared` (one address space; add `--address-rebase`, and build every \ + input with `--emit-relocs`)." + .to_string(), + )); + } + Ok(()) + } + /// #386: true when at least one input core module carries no `linking`/ /// `reloc.*` metadata — the only condition under which shared-memory /// rebasing can leave an absolute address un-rebased. Uses the same probe diff --git a/meld-core/tests/adapter_safety.rs b/meld-core/tests/adapter_safety.rs index 02936bb..8635779 100644 --- a/meld-core/tests/adapter_safety.rs +++ b/meld-core/tests/adapter_safety.rs @@ -436,6 +436,7 @@ fn test_sr12_adapter_generation_for_string_param() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -827,6 +828,7 @@ fn test_sr13_cabi_realloc_targets_correct_memory() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -1246,6 +1248,7 @@ fn test_sr15_list_copy_length() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -1745,6 +1748,7 @@ fn test_sr16_inner_pointer_fixup_list_string() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -2048,6 +2052,7 @@ fn test_sr17_utf8_to_utf16_string_transcoding() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -2125,6 +2130,7 @@ fn test_361_same_memory_utf8_to_utf16_transcoding() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -2201,6 +2207,7 @@ fn test_sr17_utf8_to_utf16_supplementary_plane_transcoding() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -2729,6 +2736,7 @@ fn test_sr17_utf16_to_utf8_supplementary_plane_transcoding() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -2806,6 +2814,7 @@ fn test_sr17_utf16_to_utf8_lone_high_surrogate_replacement() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -2873,6 +2882,7 @@ fn test_sr17_utf16_to_utf8_midstring_lone_surrogate_replacement() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -2965,6 +2975,7 @@ fn test_sr17_utf16_to_utf8_malformed_surrogate_matrix() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -3103,6 +3114,7 @@ fn ls_p_20_test_sr17_utf8_to_utf16_malformed_matrix() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -3181,6 +3193,7 @@ fn test_sr17_latin1_to_utf16_transcoding() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -3667,6 +3680,7 @@ fn fuse_run_i32(caller: &[u8], callee: &[u8], label: &str) -> i32 { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/component_provenance.rs b/meld-core/tests/component_provenance.rs index b1209e6..41e4e07 100644 --- a/meld-core/tests/component_provenance.rs +++ b/meld-core/tests/component_provenance.rs @@ -321,6 +321,7 @@ fn opt_out_via_config_drops_the_section() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: CustomSectionHandling::Merge, output_format: OutputFormat::CoreModule, diff --git a/meld-core/tests/cross_component_call.rs b/meld-core/tests/cross_component_call.rs index 667167e..518a7da 100644 --- a/meld-core/tests/cross_component_call.rs +++ b/meld-core/tests/cross_component_call.rs @@ -183,6 +183,7 @@ fn test_304_identity_direct_adapter_is_inlined() { address_rebasing: true, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }); fuser diff --git a/meld-core/tests/drop_realloc.rs b/meld-core/tests/drop_realloc.rs index b17744d..24f9917 100644 --- a/meld-core/tests/drop_realloc.rs +++ b/meld-core/tests/drop_realloc.rs @@ -677,6 +677,7 @@ fn shared_rebase_config() -> FuserConfig { address_rebasing: true, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, attestation: false, reproducible: false, component_provenance: false, @@ -783,6 +784,7 @@ fn adapter_site_keeps_realloc_and_hard_errors_on_grow() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, attestation: false, reproducible: false, component_provenance: false, @@ -871,6 +873,7 @@ fn non_shared_fuse_preserves_realloc() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, attestation: false, reproducible: false, component_provenance: false, diff --git a/meld-core/tests/dwarf_passthrough.rs b/meld-core/tests/dwarf_passthrough.rs index cdfe6cc..7466487 100644 --- a/meld-core/tests/dwarf_passthrough.rs +++ b/meld-core/tests/dwarf_passthrough.rs @@ -170,6 +170,7 @@ fn fuse_passthrough(input: &[u8]) -> Vec { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: CustomSectionHandling::Merge, output_format: OutputFormat::CoreModule, @@ -196,6 +197,7 @@ fn fuse_remap(input: &[u8]) -> Vec { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: CustomSectionHandling::Merge, output_format: OutputFormat::CoreModule, @@ -217,6 +219,7 @@ fn fuse_with_drop(input: &[u8]) -> Vec { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: CustomSectionHandling::Drop, output_format: OutputFormat::CoreModule, diff --git a/meld-core/tests/dwarf_remap_witness.rs b/meld-core/tests/dwarf_remap_witness.rs index fdaff40..3329ce4 100644 --- a/meld-core/tests/dwarf_remap_witness.rs +++ b/meld-core/tests/dwarf_remap_witness.rs @@ -56,6 +56,7 @@ fn fuse_remap(input: &[u8]) -> Vec { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: CustomSectionHandling::Merge, output_format: OutputFormat::CoreModule, diff --git a/meld-core/tests/golden_e2e.rs b/meld-core/tests/golden_e2e.rs index 45101fc..f97cb1c 100644 --- a/meld-core/tests/golden_e2e.rs +++ b/meld-core/tests/golden_e2e.rs @@ -193,6 +193,7 @@ fn fuse_many( address_rebasing, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: CustomSectionHandling::Drop, dwarf_handling: DwarfHandling::Strip, diff --git a/meld-core/tests/issue_112_mythos_v04.rs b/meld-core/tests/issue_112_mythos_v04.rs index 2b11a2b..60104d6 100644 --- a/meld-core/tests/issue_112_mythos_v04.rs +++ b/meld-core/tests/issue_112_mythos_v04.rs @@ -180,6 +180,7 @@ fn fuse_config() -> FuserConfig { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/multi_memory.rs b/meld-core/tests/multi_memory.rs index c98d963..665c2af 100644 --- a/meld-core/tests/multi_memory.rs +++ b/meld-core/tests/multi_memory.rs @@ -211,6 +211,7 @@ fn test_multi_memory_separate_memories() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Merge, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -267,6 +268,7 @@ fn test_multi_memory_preserves_isolation() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Merge, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -344,6 +346,7 @@ fn test_multi_memory_preserves_isolation_three_components() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Merge, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/multiply_instantiated_runtime.rs b/meld-core/tests/multiply_instantiated_runtime.rs index 7d09d9b..22b804f 100644 --- a/meld-core/tests/multiply_instantiated_runtime.rs +++ b/meld-core/tests/multiply_instantiated_runtime.rs @@ -48,6 +48,7 @@ fn base_config() -> FuserConfig { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/nested_component.rs b/meld-core/tests/nested_component.rs index b217015..c896e41 100644 --- a/meld-core/tests/nested_component.rs +++ b/meld-core/tests/nested_component.rs @@ -69,6 +69,7 @@ fn test_fuse_composed_p2_component() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/p3_bridge_runtime.rs b/meld-core/tests/p3_bridge_runtime.rs index e981b43..3436e95 100644 --- a/meld-core/tests/p3_bridge_runtime.rs +++ b/meld-core/tests/p3_bridge_runtime.rs @@ -338,6 +338,7 @@ fn fuse_pair() -> Vec { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/pack_rebase_370.rs b/meld-core/tests/pack_rebase_370.rs index 18de1c0..a886152 100644 --- a/meld-core/tests/pack_rebase_370.rs +++ b/meld-core/tests/pack_rebase_370.rs @@ -373,6 +373,7 @@ fn pack_rebase_passive_init_does_not_clobber() { memory_strategy: MemoryStrategy::SharedMemory, pack_rebase: true, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; let mut fuser = Fuser::new(config); @@ -500,6 +501,7 @@ fn pack_rebase_nodata_module_does_not_alias() { memory_strategy: MemoryStrategy::SharedMemory, pack_rebase: true, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; let mut fuser = Fuser::new(config); @@ -693,6 +695,7 @@ fn fuse_arena(a: &[u8], b: &[u8], c: &[u8]) -> Vec { memory_strategy: MemoryStrategy::SharedMemory, pack_rebase: true, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; let mut fuser = Fuser::new(config); @@ -959,6 +962,7 @@ fn run_imported_arena(marker: Option) { memory_strategy: MemoryStrategy::SharedMemory, pack_rebase: true, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; let mut fuser = Fuser::new(config); diff --git a/meld-core/tests/pic_extended_const_353.rs b/meld-core/tests/pic_extended_const_353.rs index 9e39c09..b442b1a 100644 --- a/meld-core/tests/pic_extended_const_353.rs +++ b/meld-core/tests/pic_extended_const_353.rs @@ -28,6 +28,7 @@ fn base_config() -> FuserConfig { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/realloc_safety.rs b/meld-core/tests/realloc_safety.rs index d0c6982..ab663b9 100644 --- a/meld-core/tests/realloc_safety.rs +++ b/meld-core/tests/realloc_safety.rs @@ -509,6 +509,7 @@ fn ls_a_7_every_realloc_call_has_null_guard() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/rebasing_end_to_end.rs b/meld-core/tests/rebasing_end_to_end.rs index 3c1d852..f55bde8 100644 --- a/meld-core/tests/rebasing_end_to_end.rs +++ b/meld-core/tests/rebasing_end_to_end.rs @@ -142,6 +142,7 @@ fn test_address_rebasing_end_to_end() { address_rebasing: true, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; @@ -234,6 +235,7 @@ fn test_298_real_wit_bindgen_component_blocks_shared_rebase() { address_rebasing: true, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; let mut fuser = Fuser::new(config); @@ -460,6 +462,7 @@ fn test_326_reloc_const_rebasing_end_to_end() { address_rebasing: true, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; let mut fuser = Fuser::new(config); @@ -555,6 +558,7 @@ fn test_326_shared_rebase_without_relocs_hard_errors() { address_rebasing: true, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; let mut fuser = Fuser::new(config); @@ -598,6 +602,7 @@ fn test_351_stale_reloc_offsets_hard_error() { address_rebasing: true, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; let mut fuser = Fuser::new(config); @@ -809,6 +814,7 @@ fn test_301_embedder_arena_seam_survives_fusion_differential() { address_rebasing: true, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; let mut fuser = Fuser::new(config); @@ -981,6 +987,7 @@ fn test_301_embedder_arena_seam_nonzero_base_requires_relocs() { address_rebasing: true, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; let mut fuser = Fuser::new(config); diff --git a/meld-core/tests/release_components.rs b/meld-core/tests/release_components.rs index 613fb7d..6b3c720 100644 --- a/meld-core/tests/release_components.rs +++ b/meld-core/tests/release_components.rs @@ -74,6 +74,7 @@ fn try_fuse(path: &str, name: &str) -> (bool, usize, String) { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -237,6 +238,7 @@ fn test_fused_output_validates() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -334,6 +336,7 @@ fn test_p1_adapter_detection_with_instances() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -419,6 +422,7 @@ fn test_reasonable_memory_count() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -510,6 +514,7 @@ fn test_write_fused_output_for_runtime() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: true, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -580,6 +585,7 @@ fn test_no_duplicate_imports() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -669,6 +675,7 @@ fn test_adapter_generation_for_release_components() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -741,6 +748,7 @@ fn test_adapter_call_site_wiring() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -897,6 +905,7 @@ fn test_no_stale_resource_drop_versions() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -986,6 +995,7 @@ fn test_component_wrap_validates() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/runtime_from_exports.rs b/meld-core/tests/runtime_from_exports.rs index 98ec8d2..a782c4d 100644 --- a/meld-core/tests/runtime_from_exports.rs +++ b/meld-core/tests/runtime_from_exports.rs @@ -133,6 +133,7 @@ fn test_from_exports_resolution_runtime() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -188,6 +189,7 @@ fn test_from_exports_shared_memory_strategy() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -243,6 +245,7 @@ fn test_from_exports_function_count() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/runtime_intra_adapter.rs b/meld-core/tests/runtime_intra_adapter.rs index 569c6b2..b6c0432 100644 --- a/meld-core/tests/runtime_intra_adapter.rs +++ b/meld-core/tests/runtime_intra_adapter.rs @@ -214,6 +214,7 @@ fn test_intra_component_three_module_fusion() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, @@ -271,6 +272,7 @@ fn test_intra_component_memory_count() { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/safety_profile_386.rs b/meld-core/tests/safety_profile_386.rs new file mode 100644 index 0000000..4c97432 --- /dev/null +++ b/meld-core/tests/safety_profile_386.rs @@ -0,0 +1,116 @@ +//! ADR-7 sealed-safety profile (#386) — `--profile safety` refuses to INFER a +//! safety-relevant property that the build should have DECLARED. +//! +//! ADR-7 gave meld a dual identity: the generic RFC-46 reference fuser AND a +//! sealed-safety product. The difference is not the fusion algorithm, it is how +//! much the build may infer. ADR-4 ("explicit, not auto") says a functional-safety +//! build must state its memory strategy — the inter-component isolation model +//! decides whether a fault in one component can reach another's state. Until now +//! that was only a `log::warn!`, and it could not be promoted to an error because +//! `attestation` defaults ON and `--memory` defaults to `auto`, so "attested + +//! Auto" is the DEFAULT invocation, not a safety corner. The profile is the +//! signal that separates them. +//! +//! Contract pinned here: +//! 1. safety profile + `Auto` → hard error naming the remedy +//! 2. safety profile + explicit → fuses normally +//! 3. ecosystem profile + `Auto` → unchanged (no error) — the default path +//! 4. a safety build's output is byte-identical to the same explicit +//! invocation under ecosystem (the profile gates, it does not transform) + +use meld_core::{Fuser, FuserConfig, MemoryStrategy, Profile}; +use wasm_encoder::{ + CodeSection, Component, ExportKind, ExportSection, Function, FunctionSection, Instruction, + Module, ModuleSection, TypeSection, ValType, +}; + +/// A minimal, memory-free component — the profile check is a build-configuration +/// gate, so it must not depend on input shape. +fn build_component(tag: &str) -> Vec { + let mut types = TypeSection::new(); + types.ty().function([], [ValType::I32]); + + let mut functions = FunctionSection::new(); + functions.function(0); + + let mut exports = ExportSection::new(); + exports.export(&format!("f_{tag}"), ExportKind::Func, 0); + + let mut code = CodeSection::new(); + let mut f = Function::new([]); + f.instruction(&Instruction::I32Const(7)); + f.instruction(&Instruction::End); + code.function(&f); + + let mut module = Module::new(); + module + .section(&types) + .section(&functions) + .section(&exports) + .section(&code); + + let mut component = Component::new(); + component.section(&ModuleSection(&module)); + component.finish() +} + +fn fuse(profile: Profile, memory_strategy: MemoryStrategy) -> Result, String> { + let config = FuserConfig { + profile, + memory_strategy, + // Reproducible so the byte-identity assertion below compares fusion + // output rather than a random attestation id / timestamp. + reproducible: true, + ..Default::default() + }; + let mut fuser = Fuser::new(config); + fuser + .add_component_named(&build_component("a"), Some("comp-a")) + .unwrap(); + fuser + .add_component_named(&build_component("b"), Some("comp-b")) + .unwrap(); + fuser.fuse().map_err(|e| e.to_string()) +} + +#[test] +fn safety_profile_refuses_to_infer_the_memory_strategy() { + let err = fuse(Profile::Safety, MemoryStrategy::Auto) + .expect_err("safety profile must refuse an inferred memory strategy"); + assert!( + err.contains("safety profile"), + "error must identify the profile violation, got: {err}" + ); + assert!( + err.contains("--memory"), + "error must name the remedy (state --memory explicitly), got: {err}" + ); +} + +#[test] +fn safety_profile_accepts_an_explicit_memory_strategy() { + fuse(Profile::Safety, MemoryStrategy::MultiMemory) + .expect("safety profile accepts an explicitly stated memory strategy"); +} + +#[test] +fn ecosystem_profile_still_infers() { + // The default path must be untouched — this is what makes the profile + // non-breaking (`attestation` defaults ON and `--memory` defaults to auto, + // so this combination is the ordinary invocation). + fuse(Profile::Ecosystem, MemoryStrategy::Auto) + .expect("ecosystem profile keeps inferring the memory strategy"); +} + +#[test] +fn profile_gates_but_does_not_transform() { + // A build that passes under `safety` must emit exactly what the same + // explicit invocation emits under `ecosystem`. The profile decides what may + // be inferred; it must never change the artifact. + let safety = fuse(Profile::Safety, MemoryStrategy::MultiMemory).expect("safety fuse"); + let ecosystem = fuse(Profile::Ecosystem, MemoryStrategy::MultiMemory).expect("ecosystem fuse"); + assert_eq!( + safety, ecosystem, + "the profile must gate the build, not alter its output" + ); +} diff --git a/meld-core/tests/segidx_remap.rs b/meld-core/tests/segidx_remap.rs index 0d666a9..3dc9fd9 100644 --- a/meld-core/tests/segidx_remap.rs +++ b/meld-core/tests/segidx_remap.rs @@ -51,6 +51,7 @@ fn fuser_config() -> FuserConfig { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Merge, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/shared_everything_topology.rs b/meld-core/tests/shared_everything_topology.rs index c6e95d6..26d1678 100644 --- a/meld-core/tests/shared_everything_topology.rs +++ b/meld-core/tests/shared_everything_topology.rs @@ -28,6 +28,7 @@ fn base_config() -> FuserConfig { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, dwarf_handling: meld_core::DwarfHandling::Strip, diff --git a/meld-core/tests/shared_overlap_370.rs b/meld-core/tests/shared_overlap_370.rs index 10bd793..2137d31 100644 --- a/meld-core/tests/shared_overlap_370.rs +++ b/meld-core/tests/shared_overlap_370.rs @@ -46,6 +46,7 @@ fn fuse_shared(a_off: u32, b_off: u32) -> meld_core::Result> { address_rebasing: false, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, preserve_names: false, custom_sections: CustomSectionHandling::Drop, dwarf_handling: DwarfHandling::Strip, diff --git a/meld-core/tests/value_address_warn_339.rs b/meld-core/tests/value_address_warn_339.rs index 4e899fe..12aa731 100644 --- a/meld-core/tests/value_address_warn_339.rs +++ b/meld-core/tests/value_address_warn_339.rs @@ -121,6 +121,7 @@ fn value_address_no_reloc_accept_warns_and_validates_339() { address_rebasing: true, pack_rebase: false, share_stack: false, + profile: meld_core::Profile::Ecosystem, ..Default::default() }; let mut fuser = Fuser::new(config); diff --git a/meld-core/tests/wit_bindgen_runtime.rs b/meld-core/tests/wit_bindgen_runtime.rs index c175fcb..842f0ad 100644 --- a/meld-core/tests/wit_bindgen_runtime.rs +++ b/meld-core/tests/wit_bindgen_runtime.rs @@ -63,6 +63,7 @@ fn fuse_fixture(name: &str, output_format: OutputFormat) -> anyhow::Result + meld shall provide an explicit build-profile selector (`--profile`, + `FuserConfig::profile`) implementing ADR-7's two attested profiles. Under + `Profile::Safety` (the sealed-safety profile) meld shall REFUSE to infer any + safety-relevant property that the build should have declared, returning + `Error::SafetyProfileViolation` rather than emitting a warning and + proceeding. Under `Profile::Ecosystem` (the default) the same conditions + remain advisory, so the generic RFC-46 identity is unaffected. The profile + shall GATE only: a build that succeeds under `Safety` shall produce + byte-identical output to the same explicit invocation under `Ecosystem`. + The check shall run BEFORE fusion, so a profile violation is a build + configuration error that cannot depend on input shape or leave a partially + produced artifact. + Enforced property (first consumer, ADR-4 "explicit, not auto"): the MEMORY + STRATEGY. `MemoryStrategy::Auto` selects the inter-component isolation model + (one shared address space vs one memory per component), which decides + whether a fault in one component can reach another's state; a safety build + must state `--memory` explicitly. NOTE the profile is required precisely + BECAUSE this cannot be gated on attestation: `attestation` defaults true and + `--memory` defaults to `auto`, so "attested + Auto" is the DEFAULT + invocation, and erroring there would break the ecosystem identity ADR-7 + declared first-class (#386). + Intended future consumers (ADR-7 binding requirements, NOT yet implemented): + per-boundary call-lowering / address strategy declared and attested, and + silent-downgrade-is-a-hard-error where a declared strategy's preconditions + are unmet. + status: implemented + tags: [profile, safety, adr-7, adr-4, memory, configuration] + release: v0.50.0 + links: + - type: derives-from + target: SYS-6 + - type: refines + target: SR-57 + cited-source: + - uri: "https://github.com/pulseengine/meld/issues/386" + kind: github + last-checked: 2026-08-19 + fields: + implementation: + - meld-core/src/lib.rs + verification-method: test + verification-description: > + Verified by meld-core/tests/safety_profile_386.rs (4 tests): + safety_profile_refuses_to_infer_the_memory_strategy (Auto under Safety → + Err naming both the violation and the remedy `--memory`); + safety_profile_accepts_an_explicit_memory_strategy (explicit → fuses); + ecosystem_profile_still_infers (the DEFAULT path is untouched — this is + what makes the profile non-breaking); and profile_gates_but_does_not_ + transform (a Safety build's bytes equal the same explicit Ecosystem + build's, under `reproducible: true` so the comparison is of fusion output + rather than attestation nonce) — pinning that the profile gates rather + than transforms. Documented as the `profiles` topic in `meld docs`. + references: + - "https://github.com/pulseengine/meld/issues/386" + - "meld-core/tests/safety_profile_386.rs" diff --git a/safety/requirements/sw-verifications.yaml b/safety/requirements/sw-verifications.yaml index 2619e82..2cbfbc9 100644 --- a/safety/requirements/sw-verifications.yaml +++ b/safety/requirements/sw-verifications.yaml @@ -1183,3 +1183,22 @@ artifacts: links: - type: verifies target: SR-67 + + - id: SWV-80 + type: sw-verification + title: "Verification of SR-68: sealed-safety build profile" + description: > + Verifies SR-68 via meld-core/tests/safety_profile_386.rs (4 tests): Auto + under Profile::Safety → Err(SafetyProfileViolation) naming the remedy; + explicit strategy under Safety → fuses; Auto under Profile::Ecosystem → + unchanged (the default invocation stays working, which is what makes the + profile non-breaking); and byte-identity between a Safety build and the same + explicit Ecosystem build (reproducible: true), pinning that the profile + GATES rather than transforms. The check runs before fusion, so a violation + cannot depend on input shape. + status: implemented + fields: + method: automated-test + links: + - type: verifies + target: SR-68 From 701698ea53d157d5f2aa79e1fc1a21f0b3e06e07 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 19 Aug 2026 11:58:44 +0200 Subject: [PATCH 4/5] =?UTF-8?q?chore(release):=20v0.50.0=20=E2=80=94=20saf?= =?UTF-8?q?ety=20profile=20+=20accurate=20shared-rebase=20messaging=20(#38?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps workspace 0.49.0 -> 0.50.0 for SR-68 (`--profile safety`) and the #386 messaging/doc accuracy fixes (PR #387). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WrmMqf1iuTS1UBEes5TmdC --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.lock | 4 ++-- Cargo.toml | 2 +- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f854fd..fc5f5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,51 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.50.0] - 2026-08-19 + +### Added +- **`--profile safety`: refuse to infer safety-relevant properties (SR-68, #386)** + — ADR-7 gave meld a dual identity (the generic RFC-46 reference fuser *and* a + sealed-safety product, both first-class) and specified two attested profiles; + this adds the signal that separates them. Under `--profile safety` every + safety-relevant property must be **stated**, and inferring one is a hard error + (`Error::SafetyProfileViolation`) rather than a warning. First enforced + property (ADR-4, "explicit, not auto"): the **memory strategy** — `--memory + auto` is refused, because it selects the inter-component isolation model, which + decides whether a fault in one component can reach another component's state. + The check runs *before* fusion, so a violation is a build-configuration error + that cannot depend on input shape or leave a partial artifact. + + `--profile ecosystem` (the default) is **unchanged** — nothing breaks. The + profile *gates*, it never transforms: a build that succeeds under `safety` + emits byte-identical output to the same explicit invocation under `ecosystem`. + A profile was needed rather than "strict when attested" because `attestation` + defaults on and `--memory` defaults to `auto`, so "attested + auto" is the + ordinary invocation. See `meld docs profiles`. + +### Fixed +- **`--memory shared --address-rebase` no longer cries wolf (#386)** — it warned + "UNSOUND" unconditionally, including for `--emit-relocs` inputs where it is + not, and which is the path `--pack-rebase` (SR-57) and `--share-stack` (SR-66) + are built on and that the falcon supplier validated on real components (#370). + The text predated the reloc *consumer* (#326→#340): a reloc-covered input is + rebased at the source point, so a pointer computed from it is correct by + construction. The warning now fires only when an input actually lacks reloc + metadata — where the residual risk is real and was already reported precisely, + per-module, by the address strategy. +- **`MemoryStrategy::Auto` documentation contradicted the implementation (#386)** + — the public rustdoc described Auto resolving to shared memory + rebasing, + which it has never done since #326. Corrected, along with the module-level docs + and a dangling reference to a function that does not exist. + +### Changed +- `Auto` now **reports** the single-address-space build that the inputs would + support (naming the explicit flags) instead of silently leaving it undiscovered + — and still never escalates to it. Escalating on "inputs look reloc-covered" + is ruled out permanently: the probe proves reloc *presence*, not *coverage* + (undecidable in general, #339), and a safety tool should not move to the + stricter-contract strategy on its own. + ## [0.49.0] - 2026-08-14 ### Added diff --git a/Cargo.lock b/Cargo.lock index 06310d7..12f6df5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1381,7 +1381,7 @@ checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" [[package]] name = "meld-cli" -version = "0.49.0" +version = "0.50.0" dependencies = [ "anyhow", "clap", @@ -1396,7 +1396,7 @@ dependencies = [ [[package]] name = "meld-core" -version = "0.49.0" +version = "0.50.0" dependencies = [ "anyhow", "bitflags", diff --git a/Cargo.toml b/Cargo.toml index afc64dd..aa6ed75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ exclude = [ ] [workspace.package] -version = "0.49.0" +version = "0.50.0" authors = ["PulseEngine "] edition = "2024" license = "Apache-2.0" From 0a887f7f353374ea7ed5c157ff8621123ec12270 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 19 Aug 2026 12:10:12 +0200 Subject: [PATCH 5/5] test(SR-68): compare fusion output without attestation in the gates-not-transforms test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's Coverage job (`cargo test --all-features`) caught a real gap: the byte-identity assertion passed on default features but failed under `--all-features`. Root cause is a KNOWN, documented limitation, not a new defect — under the optional `attestation` (wsc) feature the emitted section carries `HashMap` fields whose serialization ORDER meld cannot control from its side, so byte-identity does not hold there. The existing `test_reproducible_attestation_is_byte_stable` is `#[cfg(not(feature = "attestation"))]` for exactly this reason (lib.rs doc note), and release builds do not enable the feature, so shipped artifacts are unaffected. Fix: set `attestation: false` in the test config so the comparison is of PURE FUSION OUTPUT. Better than cfg-gating the test — it is feature-independent AND compares exactly what the claim says ("the profile gates, it does not transform"), instead of relying on `reproducible` to tame attestation noise. Verified under BOTH configs: default 4/4, `--all-features` 4/4; full `--all-features --workspace` 854/0. LESSON: run `cargo test --all-features` locally, not just `--workspace` — CI runs both and the feature-gated paths are where the surprises are. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WrmMqf1iuTS1UBEes5TmdC --- meld-core/tests/safety_profile_386.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/meld-core/tests/safety_profile_386.rs b/meld-core/tests/safety_profile_386.rs index 4c97432..382baf9 100644 --- a/meld-core/tests/safety_profile_386.rs +++ b/meld-core/tests/safety_profile_386.rs @@ -58,8 +58,17 @@ fn fuse(profile: Profile, memory_strategy: MemoryStrategy) -> Result, St let config = FuserConfig { profile, memory_strategy, - // Reproducible so the byte-identity assertion below compares fusion - // output rather than a random attestation id / timestamp. + // The byte-identity assertion below is about FUSION output, so strip the + // attestation section from the comparison entirely rather than trying to + // tame it: `reproducible` removes the random id + wall clock, but under + // the optional `attestation` (wsc) feature the section also carries + // `HashMap` fields whose serialization ORDER meld cannot control from + // this side (a known, documented limitation — see + // `test_reproducible_attestation_is_byte_stable`, which is + // `cfg(not(feature = "attestation"))` for the same reason; the shipped + // default build has no such maps). With attestation off, this test is + // feature-independent AND compares exactly what it claims to. + attestation: false, reproducible: true, ..Default::default() };