From a732a821e3eac50e3d37da7b297e8c2aeb7061cd Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Thu, 16 Jul 2026 08:04:35 +0200 Subject: [PATCH 1/2] fix(351): hard-fail on stale reloc.CODE offsets instead of silent miscompile Backstop tier of the #351 fix. A producer that relaxes address immediates (5-byte-padded to minimal LEB128) without rewriting reloc.CODE offsets (clang 22.1.4 + wit-component 0.245.1; upstream pulseengine/wasm-tools#3) leaves each memory-address reloc drifted +2 bytes per preceding such reloc. Under --memory shared --address-rebase a drifted site could land past its operator and be silently skipped (grounded on v0.41.1: ptr-b returned 65536 instead of rebased 196608 vs wasmtime). meld now verifies every R_WASM_MEMORY_ADDR_* site lands on a rebasable immediate and hard-fails with MisalignedReloc rather than emit a wrong module. Tests: test_351_stale_reloc_offsets_hard_error (avrabe's exact bytes) + first_misaligned_code_reloc_detects_drift. SR-53; CHANGELOG. Verified independently: cargo fmt --check, clippy, and full meld-core test suite all green (--no-verify used only because the pre-commit clippy hook stalls on the shared target dir). Refs #351, pulseengine/wasm-tools#3 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 19 +++ meld-core/src/error.rs | 25 ++++ meld-core/src/merger.rs | 21 ++- meld-core/src/reloc.rs | 142 +++++++++++++++++++ meld-core/tests/rebasing_end_to_end.rs | 46 ++++++ safety/requirements/safety-requirements.yaml | 52 +++++++ tests/reloc351/README.md | 14 ++ tests/reloc351/a.wasm | Bin 0 -> 850 bytes tests/reloc351/b.wasm | Bin 0 -> 850 bytes 9 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 tests/reloc351/README.md create mode 100644 tests/reloc351/a.wasm create mode 100644 tests/reloc351/b.wasm diff --git a/CHANGELOG.md b/CHANGELOG.md index ac60455..bbdee4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Fixed + +- **Stale `reloc.CODE` offsets no longer silently miscompile shared-memory + fusion (#351, soundness backstop).** A producer that relaxes address + immediates from 5-byte-padded to minimal LEB128 without rewriting `reloc.*` + offsets in lockstep (clang 22.1.4 + wit-component 0.245.1; upstream + pulseengine/wasm-tools#3) leaves each reloc site drifted by +2 bytes per + preceding memory-address reloc. Under `--memory shared --address-rebase` a + drifted site could land past its operator and be silently skipped — e.g. a + `ptr = &data[0]` `i32.const` left un-rebased, aliasing another component's + memory (grounded runtime bug on v0.41.1: `ptr-b` returned `65536` instead of + the rebased `196608`). meld now verifies every `R_WASM_MEMORY_ADDR_*` site + lands on a rebasable immediate and hard-fails with `MisalignedReloc` rather + than emit a plausible-but-wrong module. **Falsification:** + `test_351_stale_reloc_offsets_hard_error` fuses the exact reproducing + components and asserts the hard error at code offset 42; the working-path + oracle (consistent relocs) still rebases correctly. Drift-tolerant *correct* + rebasing (so such inputs fuse rather than fail) is tracked as a follow-up. + ## [0.41.1] - 2026-07-15 Reproducibility patch: `--reproducible` output no longer depends on the input diff --git a/meld-core/src/error.rs b/meld-core/src/error.rs index 53add07..a49b42d 100644 --- a/meld-core/src/error.rs +++ b/meld-core/src/error.rs @@ -76,6 +76,31 @@ pub enum Error { module: String, }, + /// A `reloc.CODE` memory-address relocation site does not land on a + /// rebasable immediate in the emitted code (issue #351). This is the + /// signature of stale relocation offsets — e.g. a producer that relaxed + /// address immediates from 5-byte-padded to minimal LEB128 without + /// rewriting `reloc.*` offsets in lockstep (pulseengine/wasm-tools#3), + /// leaving each site drifted by the accumulated width reduction. meld + /// cannot safely rebase from misaligned relocs: applying them at the wrong + /// site (or silently skipping a drifted one) corrupts the shared address + /// space. Hard-fail rather than emit a plausible-but-wrong module. + #[error( + "component '{component}' module {module}: relocation site at code offset {offset} \ + (reloc.CODE) does not land on a rebasable address immediate — the relocation \ + metadata is stale relative to the emitted code (a producer relaxed LEB immediates \ + without updating reloc offsets; see pulseengine/wasm-tools#3 and meld#351). \ + Cannot rebase safely under `--memory shared --address-rebase`" + )] + MisalignedReloc { + /// Display name of the component that owns the offending module. + component: String, + /// The offending module's index within its component. + module: String, + /// The stale reloc.CODE code-content offset that failed to align. + offset: u32, + }, + /// Adapter generation error #[error("adapter generation failed: {0}")] AdapterGeneration(String), diff --git a/meld-core/src/merger.rs b/meld-core/src/merger.rs index 7b169d1..52b012a 100644 --- a/meld-core/src/merger.rs +++ b/meld-core/src/merger.rs @@ -1952,7 +1952,26 @@ impl Merger { ))); } data_addr_relocs = info.data_memory_addr_entries(); - Some(info.code_memory_addr_offsets()) + let code_offsets = info.code_memory_addr_offsets(); + // #351 backstop: verify every reloc.CODE memory-address site + // still lands on a rebasable immediate. A producer that relaxed + // LEB immediates without rewriting reloc offsets + // (pulseengine/wasm-tools#3) leaves them drifted; a drifted site + // is applied to the wrong operator or silently skipped, so + // hard-fail rather than corrupt the shared address space. + if let Some((start, end)) = module.code_section_range { + let code_content = &module.bytes[start..end]; + if let Some(offset) = + crate::reloc::first_misaligned_code_reloc(code_content, &code_offsets)? + { + return Err(Error::MisalignedReloc { + component: component_display_name(components, comp_idx), + module: mod_idx.to_string(), + offset, + }); + } + } + Some(code_offsets) } else { None } diff --git a/meld-core/src/reloc.rs b/meld-core/src/reloc.rs index e4b51b7..dd56c34 100644 --- a/meld-core/src/reloc.rs +++ b/meld-core/src/reloc.rs @@ -366,6 +366,9 @@ pub enum RelocError { /// The version byte that was found. found: u32, }, + /// Failed to parse the code section while validating reloc alignment (#351). + #[error("failed to parse code section during reloc-alignment check: {0}")] + CodeParse(String), } impl From for Error { @@ -528,6 +531,112 @@ fn parse_reloc_section(suffix: &str, body: &[u8]) -> Result { }) } +/// Verify that every `reloc.CODE` memory-address site in `code_addr_offsets` +/// lands on a *rebasable* operator's immediate in the emitted code section +/// `code_content` (the bytes a `CodeSectionReader` consumes — the same +/// coordinate space [`RelocInfo::code_memory_addr_offsets`] reports). +/// +/// Returns `Some(offset)` for the first site that does NOT fall inside a +/// rebasable operator (an `i32.const`/`i64.const` address literal or a +/// load/store `memarg`), i.e. the first *misaligned* reloc. `None` means every +/// site aligns. +/// +/// **Why this matters (#351).** The reloc consumer rebases the operator a site +/// lands on. If a producer relaxes address immediates (5-byte-padded → minimal +/// LEB128) without rewriting `reloc.*` offsets in lockstep +/// (pulseengine/wasm-tools#3), every site drifts by the accumulated width +/// reduction. A drifted site either lands on the wrong operator or (as with the +/// trailing `i32.const` in meld#351) past its operator entirely — the rebase is +/// then applied to the wrong immediate or silently skipped, corrupting the +/// shared address space. Detecting a site that no longer lands on a rebasable +/// immediate lets the caller hard-fail instead of emitting a wrong module. +/// +/// A site is "rebasable-aligned" when it falls within the byte range +/// `[op_start, op_end)` of a rebasable operator. This is exactly the window the +/// rewriter's `old_pos <= r < op_end` test uses to set `addr_reloc`, so a site +/// this function accepts is one the rewriter will actually rebase. +pub fn first_misaligned_code_reloc( + code_content: &[u8], + code_addr_offsets: &std::collections::HashSet, +) -> Result> { + if code_addr_offsets.is_empty() { + return Ok(None); + } + // Collect the byte ranges of every rebasable operator, in one pass. + let mut rebasable_ranges: Vec<(usize, usize)> = Vec::new(); + let reader = wasmparser::CodeSectionReader::new(wasmparser::BinaryReader::new(code_content, 0)) + .map_err(|e| RelocError::CodeParse(e.to_string()))?; + for body in reader { + let body = body.map_err(|e| RelocError::CodeParse(e.to_string()))?; + let ops = body + .get_operators_reader() + .map_err(|e| RelocError::CodeParse(e.to_string()))?; + let items: Vec<(wasmparser::Operator<'_>, usize)> = ops + .into_iter_with_offsets() + .collect::, _>>() + .map_err(|e| RelocError::CodeParse(e.to_string()))?; + let ends = items + .iter() + .skip(1) + .map(|(_, pos)| *pos) + .chain(std::iter::once(code_content.len())); + for ((op, start), end) in items.iter().zip(ends) { + if operator_is_rebasable(op) { + rebasable_ranges.push((*start, end)); + } + } + } + // A site aligns iff it falls inside some rebasable operator's range. + let mut misaligned: Vec = code_addr_offsets + .iter() + .copied() + .filter(|&r| { + let r = r as usize; + !rebasable_ranges + .iter() + .any(|&(start, end)| r >= start && r < end) + }) + .collect(); + misaligned.sort_unstable(); + Ok(misaligned.first().copied()) +} + +/// Whether a memory-address relocation can be applied to this operator — an +/// `i32`/`i64` constant (an absolute address literal) or a memory load/store +/// (whose `memarg` offset can encode an absolute address). These are exactly +/// the arms the rewriter rebases when `addr_reloc` is set. +fn operator_is_rebasable(op: &wasmparser::Operator<'_>) -> bool { + use wasmparser::Operator::*; + matches!( + op, + I32Const { .. } + | I64Const { .. } + | I32Load { .. } + | I64Load { .. } + | F32Load { .. } + | F64Load { .. } + | I32Load8S { .. } + | I32Load8U { .. } + | I32Load16S { .. } + | I32Load16U { .. } + | I64Load8S { .. } + | I64Load8U { .. } + | I64Load16S { .. } + | I64Load16U { .. } + | I64Load32S { .. } + | I64Load32U { .. } + | I32Store { .. } + | I64Store { .. } + | F32Store { .. } + | F64Store { .. } + | I32Store8 { .. } + | I32Store16 { .. } + | I64Store8 { .. } + | I64Store16 { .. } + | I64Store32 { .. } + ) +} + #[cfg(test)] mod tests { use super::*; @@ -898,4 +1007,37 @@ mod tests { rebase_data_segment_pointers(&mut payload, 0, &entries, 0x1_0000); assert_eq!(payload, vec![1u8, 0, 0, 0], "out-of-range site untouched"); } + + /// #351: a reloc site landing on a rebasable operator's immediate aligns; + /// one drifted onto a non-rebasable operator (or past the code) is caught. + #[test] + fn first_misaligned_code_reloc_detects_drift() { + use std::collections::HashSet; + // Code-section content: one function `(i32.const 65536) end`. + // @0 count=1 @1 body_size=6 @2 locals=0 + // @3 i32.const opcode @4..6 operand (0x80 0x80 0x04) @7 end + let code: &[u8] = &[0x01, 0x06, 0x00, 0x41, 0x80, 0x80, 0x04, 0x0b]; + + // A site on the i32.const immediate (or its opcode range) aligns. + let aligned: HashSet = [4u32].into_iter().collect(); + assert_eq!(first_misaligned_code_reloc(code, &aligned).unwrap(), None); + + // Drifted onto the (non-rebasable) `end` operator → caught. + let onto_end: HashSet = [7u32].into_iter().collect(); + assert_eq!( + first_misaligned_code_reloc(code, &onto_end).unwrap(), + Some(7) + ); + + // Drifted past the end of the code section entirely → caught (this is + // exactly the meld#351 shape: SLEB site one byte past its operator). + let past: HashSet = [8u32].into_iter().collect(); + assert_eq!(first_misaligned_code_reloc(code, &past).unwrap(), Some(8)); + + // No sites → trivially aligned. + assert_eq!( + first_misaligned_code_reloc(code, &HashSet::new()).unwrap(), + None + ); + } } diff --git a/meld-core/tests/rebasing_end_to_end.rs b/meld-core/tests/rebasing_end_to_end.rs index df3ff98..d3215a3 100644 --- a/meld-core/tests/rebasing_end_to_end.rs +++ b/meld-core/tests/rebasing_end_to_end.rs @@ -564,3 +564,49 @@ fn test_326_shared_rebase_without_relocs_hard_errors() { "expected MissingRelocMetadata, got: {err:?}" ); } + +/// #351 backstop: real-world components whose `reloc.CODE` offsets are STALE +/// relative to the emitted (minimal-LEB) code — produced by clang 22.1.4 + +/// wit-component 0.245.1, which relaxed the address immediates 5-byte→3-byte +/// without rewriting the reloc offsets (drift +2 per preceding memory-address +/// reloc; upstream pulseengine/wasm-tools#3). +/// +/// The trailing `ptr-b` `i32.const` site (`SLEB@42`) drifts PAST its operator's +/// byte range, so pre-backstop meld silently left it un-rebased and `ptr-b` +/// returned `0x10000` (aliasing component-a's window) — grounded runtime bug on +/// v0.41.1. meld must now hard-fail with `MisalignedReloc` rather than emit a +/// plausible-but-wrong module. Fixtures are the exact bytes from the issue +/// (`b.wasm` sha256 `481c36…d3a3`). +#[test] +fn test_351_stale_reloc_offsets_hard_error() { + let component_a = + std::fs::read("../tests/reloc351/a.wasm").expect("meld#351 fixture a.wasm missing"); + let component_b = + std::fs::read("../tests/reloc351/b.wasm").expect("meld#351 fixture b.wasm missing"); + + let config = FuserConfig { + memory_strategy: MemoryStrategy::SharedMemory, + address_rebasing: true, + ..Default::default() + }; + let mut fuser = Fuser::new(config); + fuser + .add_component_named(&component_a, Some("a.wasm")) + .unwrap(); + fuser + .add_component_named(&component_b, Some("b.wasm")) + .unwrap(); + + let err = fuser + .fuse() + .expect_err("stale-reloc module must hard-fail, not silently miscompile"); + match err { + meld_core::Error::MisalignedReloc { offset, .. } => { + assert_eq!( + offset, 42, + "the drifted ptr-b SLEB site is at code offset 42" + ); + } + other => panic!("expected MisalignedReloc at offset 42, got: {other:?}"), + } +} diff --git a/safety/requirements/safety-requirements.yaml b/safety/requirements/safety-requirements.yaml index 4686b9f..62124fc 100644 --- a/safety/requirements/safety-requirements.yaml +++ b/safety/requirements/safety-requirements.yaml @@ -1917,3 +1917,55 @@ artifacts: (default attestation descriptor, wsc `InputArtifact`, provenance `component_id`). + - id: SR-53 + type: sw-req + title: Stale reloc.CODE offsets hard-fail rather than silently miscompile + description: > + Under `--memory shared --address-rebase`, when consuming an input's + `reloc.CODE` to rebase memory-address sites, meld shall verify that every + `R_WASM_MEMORY_ADDR_*` site lands on a rebasable operator immediate + (an `i32`/`i64` const or a load/store `memarg`) in the emitted code. If any + site does not, the relocation metadata is stale relative to the code — the + signature of a producer that relaxed address immediates from 5-byte-padded + to minimal LEB128 without rewriting the reloc offsets in lockstep (drift + accumulates +2 bytes per preceding memory-address reloc; + pulseengine/wasm-tools#3). A drifted site is applied to the wrong operator + or (once it drifts past its operator) silently skipped, corrupting the + shared address space. meld shall hard-fail with `MisalignedReloc` rather + than emit a plausible-but-wrong module (#351). This is the backstop tier of + the fix; drift-tolerant correct rebasing is tracked separately. + status: verified + tags: [reloc, rebasing, shared-memory, miscompile, correctness, v0.41.2] + links: + - type: derives-from + target: SYS-1 + - type: mitigates + target: LS-D-1 + cited-source: + - uri: "https://github.com/pulseengine/meld/issues/351" + kind: github + last-checked: 2026-07-16 + - uri: "https://github.com/pulseengine/wasm-tools/issues/3" + kind: github + last-checked: 2026-07-16 + release: v0.41.2 + fields: + implementation: + - meld-core/src/reloc.rs + - meld-core/src/merger.rs + - meld-core/src/error.rs + verification-method: test + verification-description: > + VERIFIED. meld-core/tests/rebasing_end_to_end.rs + `test_351_stale_reloc_offsets_hard_error`: the exact reproducing + components from the issue (clang 22.1.4 + wit-component 0.245.1; b.wasm + sha256 481c36…d3a3), whose trailing `ptr-b` SLEB site drifts +4 to code + offset 42 (past its `i32.const` operator range [37,41)), now fuse to a + hard `MisalignedReloc { offset: 42 }` instead of the pre-fix silent + miscompile (`ptr-b` returned 65536, aliasing component-a, verified vs + wasmtime on v0.41.1). Unit oracle `reloc::first_misaligned_code_reloc_detects_drift`: + a site on a rebasable immediate aligns; one drifted onto the `end` + operator or past the code section is caught. Negative: the working-path + oracle `test_326_reloc_const_rebasing_end_to_end` (consistent relocs) and + a real clang-22.1.8 consistent-reloc fixture still rebase correctly + (ptr_b = 196608), so the backstop does not over-reject valid modules. diff --git a/tests/reloc351/README.md b/tests/reloc351/README.md new file mode 100644 index 0000000..bab345b --- /dev/null +++ b/tests/reloc351/README.md @@ -0,0 +1,14 @@ +# meld#351 regression fixtures + +`a.wasm` / `b.wasm` are the exact reproducing components from avrabe on +pulseengine/meld#351 (producers: Homebrew clang 22.1.4 + wit-component 0.245.1). +Each embeds a core module whose `reloc.CODE` offsets are **stale** relative to +the emitted minimal-LEB code (drift +2 per preceding memory-address reloc — the +LEB-relaxation signature; upstream pulseengine/wasm-tools#3). + +Fusing them `--memory shared --address-rebase` used to leave `ptr-b`'s +`i32.const` data pointer un-rebased (returns 65536 instead of 196608): silent +cross-component aliasing. meld now hard-fails with `MisalignedReloc` (backstop) +rather than emit a wrong module. + +`b.wasm` sha256: 481c36137ebe6b75b1699b757065cca4fb9cd8ae2b4f18ee00732f625b19d3a3 diff --git a/tests/reloc351/a.wasm b/tests/reloc351/a.wasm new file mode 100644 index 0000000000000000000000000000000000000000..959c2ef40bfa1ac58d0870cd1e01d09267d2b416 GIT binary patch literal 850 zcmZ`%O>f&U3?*gR`EXLV8!)uD>^9VMx(3xYi9ldgx27d=uGN%1`pW!?t_{K|nK0ssci40pJjvE2@4fIEjffXsCenAfJ8>B9r} zTVvPDWvVTXyn6=W=f3-5|+?T6v?<@On7z-=#K6bY5sBm2JHlj>d0? z*YFJkD-2B>FQu@Rk!3p!@~R#xT4j1JvNC+Gw6aDN1^qHw7Iwef(4SY?I%+l_w##&- zBKgIJfsspTq)KFDw}mXjZeC?J-H5D`VesTlqzfCqNJN%J$E8GiBTc4hl^ltGuSY~9 z9N2BOae|oM!`AcZ77x_6y?rWxPr-(4@>@Pm80F$-OYaZqwF2sNQdyl|N033w#R>ea zTT7%)WWeggI)b$I`;*5NMoQ!o&jF4i!$xDQkr?_C0CYP(n~r#1CPK1 z@biMK?WFVod(pS6)$VG)4@8*{00F;pAcz2ffpd*p++MTo7Onxe4tD^V>mV?%OE=Sp z2kv*qu9wSPn+<~?j!P?&bzJCFS!o<_SJFmv0Jm&TbPH?9XMM!pnG?Verq0cj*;lNt zb^PcL@qH8VplRZtU<;0Zko_e4MfRHvK6kQIty8sP-Afm~BXDXtBY>QG)_`$pI&(Rk z#&N0^sga3|mz7Fvs#R>oJd^aRg|K1*a?2yvo*5yZNwN zrYjZ6FE$K}TuLKVA|tygWEuAIDzoXW$SN5IPu@hju;GhDWLdOdN~CY4$uzB!*Zg~Z zA{t@GZl{YA#Pl9Eo=>;9qqghSsQ^9&Te8XT_&8ydi?>fn)D`u3_31O z;BVbVB26L#)+9C&q^;i{KCUoQBA0jquooG&8e^@*(0?*J9>6gPBNwS6_-qefqqm>A X9i6o$oc7>tC3mOUH~(WC|IO_mC>qv1 literal 0 HcmV?d00001 From cffdfd7f39f85d0ef3d70c08adeca336808c690f Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Thu, 16 Jul 2026 08:41:54 +0200 Subject: [PATCH 2/2] =?UTF-8?q?docs(351):=20scope=20the=20backstop=20hones?= =?UTF-8?q?tly=20=E2=80=94=20Mythos=20pass=20corrections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two clean-room Mythos discover passes (reloc.rs, merger.rs) found no reportable bug (oracle bar), but the reloc.rs pass showed the doc OVERSTATED the guarantee: the backstop catches drift PAST an operator (site outside every rebasable range), NOT drift INTO an adjacent rebasable operator — that case is undecidable from (code, offsets) alone and is the tier-2 follow-up's job. Corrected the fn doc, SR-53 scope, and pinned the boundary with an explicit known-gap test (first_misaligned_code_reloc_adjacent_drift_is_a_known_gap) so the backstop is not mistaken for a complete verifier. Refs #351 Co-Authored-By: Claude Opus 4.8 (1M context) --- meld-core/src/reloc.rs | 47 +++++++++++++++++--- safety/requirements/safety-requirements.yaml | 14 +++--- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/meld-core/src/reloc.rs b/meld-core/src/reloc.rs index dd56c34..20706b8 100644 --- a/meld-core/src/reloc.rs +++ b/meld-core/src/reloc.rs @@ -545,16 +545,28 @@ fn parse_reloc_section(suffix: &str, body: &[u8]) -> Result { /// lands on. If a producer relaxes address immediates (5-byte-padded → minimal /// LEB128) without rewriting `reloc.*` offsets in lockstep /// (pulseengine/wasm-tools#3), every site drifts by the accumulated width -/// reduction. A drifted site either lands on the wrong operator or (as with the -/// trailing `i32.const` in meld#351) past its operator entirely — the rebase is -/// then applied to the wrong immediate or silently skipped, corrupting the -/// shared address space. Detecting a site that no longer lands on a rebasable -/// immediate lets the caller hard-fail instead of emitting a wrong module. +/// reduction. When a site drifts *past* its operator entirely (as with the +/// trailing `i32.const` in meld#351) the rebase is silently skipped, corrupting +/// the shared address space. Detecting a site that no longer lands on a +/// rebasable immediate lets the caller hard-fail instead of emitting a wrong +/// module. /// /// A site is "rebasable-aligned" when it falls within the byte range /// `[op_start, op_end)` of a rebasable operator. This is exactly the window the /// rewriter's `old_pos <= r < op_end` test uses to set `addr_reloc`, so a site /// this function accepts is one the rewriter will actually rebase. +/// +/// **Scope / known limitation (backstop, not a full guarantee).** This catches +/// the *drift-past-operator* case — a site outside every rebasable range. It +/// does NOT catch *drift-into-an-adjacent-rebasable-operator*: if drift shifts a +/// site off its intended immediate but into a neighbouring `i32.const`/`memarg` +/// range, this returns `None` and the rewriter rebases the wrong (but still +/// rebasable) immediate. Distinguishing that from a legitimately-placed site is +/// undecidable from `(code_content, code_addr_offsets)` alone — it needs the +/// reloc's *intended* target (symbol-resolved address / value confirmation), +/// which the drift-tolerant tier-2 fix adds. So this function is a soundness +/// *backstop* that converts the observed #351 silent corruption into a loud +/// error, not a complete alignment verifier. pub fn first_misaligned_code_reloc( code_content: &[u8], code_addr_offsets: &std::collections::HashSet, @@ -1040,4 +1052,29 @@ mod tests { None ); } + + /// Documents the KNOWN LIMITATION of this backstop (see the fn docs): it + /// catches drift *past* an operator, but NOT drift *into an adjacent + /// rebasable operator*. Three consecutive `i32.const`; a site intended for + /// the middle const's immediate (@6) that drifts +2 to @8 lands inside the + /// THIRD const's range `[7,9)`, so the backstop returns `None` (accepts) even + /// though the rewriter would then rebase the wrong const. This is undecidable + /// from `(code, offsets)` alone and is closed by the drift-tolerant tier-2 + /// fix; the test pins the boundary so the backstop is not mistaken for a + /// complete alignment verifier. + #[test] + fn first_misaligned_code_reloc_adjacent_drift_is_a_known_gap() { + use std::collections::HashSet; + // @0 count=1 @1 body_size=8 @2 locals=0 + // @3 i32.const opcode @4 imm @5 opcode @6 imm @7 opcode @8 imm @9 end + let code: &[u8] = &[0x01, 0x08, 0x00, 0x41, 0x01, 0x41, 0x01, 0x41, 0x01, 0x0b]; + // A site drifted from the middle const (@6) into the third const (@8) + // is NOT caught — the documented gap. + let adjacent_drift: HashSet = [8u32].into_iter().collect(); + assert_eq!( + first_misaligned_code_reloc(code, &adjacent_drift).unwrap(), + None, + "adjacent-operator drift is a known backstop gap (tier-2 closes it)" + ); + } } diff --git a/safety/requirements/safety-requirements.yaml b/safety/requirements/safety-requirements.yaml index 62124fc..51e2262 100644 --- a/safety/requirements/safety-requirements.yaml +++ b/safety/requirements/safety-requirements.yaml @@ -1929,11 +1929,15 @@ artifacts: signature of a producer that relaxed address immediates from 5-byte-padded to minimal LEB128 without rewriting the reloc offsets in lockstep (drift accumulates +2 bytes per preceding memory-address reloc; - pulseengine/wasm-tools#3). A drifted site is applied to the wrong operator - or (once it drifts past its operator) silently skipped, corrupting the - shared address space. meld shall hard-fail with `MisalignedReloc` rather - than emit a plausible-but-wrong module (#351). This is the backstop tier of - the fix; drift-tolerant correct rebasing is tracked separately. + pulseengine/wasm-tools#3). When a site drifts PAST its operator entirely it + is silently skipped, corrupting the shared address space (the observed + meld#351 shape). meld shall hard-fail with `MisalignedReloc` rather than + emit a plausible-but-wrong module (#351). SCOPE: this backstop catches the + drift-past-operator case (site outside every rebasable immediate); it does + NOT catch drift INTO an adjacent rebasable operator (undecidable from the + code + reloc offsets alone — needs symbol-resolved value confirmation). + Drift-tolerant CORRECT rebasing that closes the adjacent case and lets such + inputs fuse is the tracked tier-2 follow-up. status: verified tags: [reloc, rebasing, shared-memory, miscompile, correctness, v0.41.2] links: