diff --git a/CHANGELOG.md b/CHANGELOG.md index 8559699..b03b435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +### Added + +- **Explicit DWARF policy** (`DwarfHandling::{Strip, PassThrough}` on + `FuserConfig`, default `Strip`). Phase 1.5 of issue #130 + (witness-mapping epic). Until Phase 2 ships an address-remap pass, + passing input `.debug_*` sections through verbatim produces source- + line attribution that points at wrong instructions in the merged code + section — strictly worse than no DWARF for downstream MC/DC tooling + (`pulseengine/witness`). The new default drops `.debug_*` sections; + `PassThrough` is opt-in for the rare case a caller wants the lossy + prior behaviour. Recorded in attestation `tool_parameters` as + `dwarf_handling = "strip" | "passthrough"`. Verified by + `meld-core/tests/dwarf_strip.rs`. + +### Safety / STPA + +- New approved loss scenario: **LS-CP-4** (DWARF passthrough emits + address-incorrect debug info on fused output, [H-7]). + ## [0.6.0] — Unreleased ### Added diff --git a/meld-core/benches/fusion_benchmarks.rs b/meld-core/benches/fusion_benchmarks.rs index d601710..2d2f9f2 100644 --- a/meld-core/benches/fusion_benchmarks.rs +++ b/meld-core/benches/fusion_benchmarks.rs @@ -75,6 +75,7 @@ fn bench_config() -> FuserConfig { address_rebasing: false, preserve_names: false, custom_sections: CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), } diff --git a/meld-core/src/attestation.rs b/meld-core/src/attestation.rs index 6d480c4..dd32dc5 100644 --- a/meld-core/src/attestation.rs +++ b/meld-core/src/attestation.rs @@ -575,6 +575,7 @@ mod tests { "address_rebasing", "preserve_names", "custom_sections", + "dwarf_handling", "output_format", ]; @@ -584,6 +585,7 @@ mod tests { tool_parameters.insert("address_rebasing".to_string(), serde_json::json!(false)); tool_parameters.insert("preserve_names".to_string(), serde_json::json!(false)); tool_parameters.insert("custom_sections".to_string(), serde_json::json!("merge")); + tool_parameters.insert("dwarf_handling".to_string(), serde_json::json!("strip")); tool_parameters.insert( "output_format".to_string(), serde_json::json!("core-module"), diff --git a/meld-core/src/lib.rs b/meld-core/src/lib.rs index ca36aa4..a3e2d76 100644 --- a/meld-core/src/lib.rs +++ b/meld-core/src/lib.rs @@ -81,6 +81,20 @@ pub struct FuserConfig { /// Custom section handling pub custom_sections: CustomSectionHandling, + /// DWARF (`.debug_*`) section handling. + /// + /// Default `Strip` because meld currently does NOT remap DWARF + /// addresses across the merged code section (issue #130 Phase 2). + /// Passing the input DWARF through verbatim produces *wrong* + /// source-line attribution for every fused address — strictly + /// worse than emitting no DWARF, since downstream tooling + /// (`pulseengine/witness` MC/DC) trusts what it reads. + /// + /// Once Phase 2 ships an address-remapping pass, the default may + /// flip to a remap-based mode. Until then, `Strip` is the only + /// non-corrupting setting; `PassThrough` is opt-in and lossy. + pub dwarf_handling: DwarfHandling, + /// Output format: core module (default) or P2 component pub output_format: OutputFormat, @@ -108,6 +122,7 @@ impl Default for FuserConfig { address_rebasing: false, preserve_names: false, custom_sections: CustomSectionHandling::Merge, + dwarf_handling: DwarfHandling::Strip, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), } @@ -150,6 +165,30 @@ pub enum CustomSectionHandling { Drop, } +/// How to handle DWARF (`.debug_*`) custom sections during fusion. +/// +/// Distinct from [`CustomSectionHandling`] because DWARF sections carry +/// code-section byte offsets that meld does NOT yet remap (issue #130 +/// Phase 2). Passing them through unchanged is strictly worse than +/// dropping them — downstream consumers like `pulseengine/witness` +/// would silently produce wrong source-line attribution. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DwarfHandling { + /// Drop all `.debug_*` sections (default). + /// + /// The fused module carries no DWARF; downstream MC/DC tooling + /// degrades to its no-DWARF fallback. Correct, lossy. + Strip, + + /// Pass DWARF sections through verbatim from each input core + /// module. + /// + /// Addresses inside the sections refer to per-input code-section + /// offsets and will be wrong against the merged code section. + /// Use only if the consumer can tolerate or detect that. + PassThrough, +} + /// Statistics about the fusion process #[derive(Debug, Clone, Default)] pub struct FusionStats { @@ -1348,6 +1387,10 @@ impl Fuser { if !self.config.preserve_names && name == "name" { continue; } + if self.config.dwarf_handling == DwarfHandling::Strip && name.starts_with(".debug_") + { + continue; + } module.section(&wasm_encoder::CustomSection { name: std::borrow::Cow::Borrowed(name), data: std::borrow::Cow::Borrowed(contents), @@ -1469,6 +1512,10 @@ impl Fuser { "custom_sections".to_string(), serde_json::json!(self.custom_sections_label()), ); + tool_parameters.insert( + "dwarf_handling".to_string(), + serde_json::json!(self.dwarf_handling_label()), + ); tool_parameters.insert( "output_format".to_string(), serde_json::json!(self.output_format_label()), @@ -1552,6 +1599,14 @@ impl Fuser { } } + #[cfg(feature = "attestation")] + fn dwarf_handling_label(&self) -> &'static str { + match self.config.dwarf_handling { + DwarfHandling::Strip => "strip", + DwarfHandling::PassThrough => "passthrough", + } + } + #[cfg(feature = "attestation")] fn output_format_label(&self) -> &'static str { match self.config.output_format { diff --git a/meld-core/tests/adapter_safety.rs b/meld-core/tests/adapter_safety.rs index 4284635..661eefa 100644 --- a/meld-core/tests/adapter_safety.rs +++ b/meld-core/tests/adapter_safety.rs @@ -427,6 +427,7 @@ fn test_sr12_adapter_generation_for_string_param() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -813,6 +814,7 @@ fn test_sr13_cabi_realloc_targets_correct_memory() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -1227,6 +1229,7 @@ fn test_sr15_list_copy_length() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -1721,6 +1724,7 @@ fn test_sr16_inner_pointer_fixup_list_string() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -2008,6 +2012,7 @@ fn test_sr17_utf8_to_utf16_string_transcoding() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; diff --git a/meld-core/tests/dwarf_strip.rs b/meld-core/tests/dwarf_strip.rs new file mode 100644 index 0000000..483e8e7 --- /dev/null +++ b/meld-core/tests/dwarf_strip.rs @@ -0,0 +1,117 @@ +//! DWARF strip-by-default policy test (Phase 1.5 of the witness-mapping +//! epic, issue #130). +//! +//! # Why this exists +//! +//! Phase 1 (issue #130 / PR #131) documented that meld passes input +//! `.debug_*` sections through verbatim, so every DWARF address in the +//! fused output points at the wrong instruction. Phase 1.5 makes the +//! policy explicit: +//! +//! - `DwarfHandling::Strip` (default) drops every `.debug_*` section so +//! downstream consumers (e.g. `pulseengine/witness` MC/DC) see no +//! DWARF rather than corrupted DWARF. +//! - `DwarfHandling::PassThrough` is opt-in for the rare case a caller +//! wants the lossy old behaviour for diagnostics. +//! +//! Phase 2 will add an actual address-remap pass; until then, "no +//! DWARF" is strictly safer than "wrong DWARF". + +use meld_core::{DwarfHandling, Fuser, FuserConfig}; + +const DEBUG_INFO_FIXTURE: &str = "../tests/wit_bindgen/fixtures/lists.wasm"; + +const DWARF_SECTION_NAMES: &[&str] = &[ + ".debug_abbrev", + ".debug_info", + ".debug_line", + ".debug_str", + ".debug_line_str", + ".debug_str_offsets", + ".debug_addr", + ".debug_rnglists", + ".debug_loclists", + ".debug_ranges", + ".debug_loc", +]; + +fn fixture_available() -> bool { + if std::path::Path::new(DEBUG_INFO_FIXTURE).is_file() { + true + } else { + eprintln!("skipping: fixture not found at {DEBUG_INFO_FIXTURE}"); + false + } +} + +fn count_dwarf_sections(bytes: &[u8]) -> usize { + let parser = wasmparser::Parser::new(0); + let mut total = 0usize; + for payload in parser.parse_all(bytes) { + let payload = match payload { + Ok(p) => p, + Err(_) => break, + }; + if let wasmparser::Payload::CustomSection(reader) = payload + && DWARF_SECTION_NAMES.contains(&reader.name()) + { + total += 1; + } + } + total +} + +fn fuse_with(handling: DwarfHandling) -> Vec { + let bytes = std::fs::read(DEBUG_INFO_FIXTURE).expect("fixture must read"); + let config = FuserConfig { + dwarf_handling: handling, + ..FuserConfig::default() + }; + let mut fuser = Fuser::new(config); + fuser + .add_component_named(&bytes, Some("lists")) + .expect("fixture parses"); + let (fused, _stats) = fuser.fuse_with_stats().expect("fuse succeeds"); + fused +} + +/// Default `FuserConfig` strips DWARF — the fused module carries zero +/// `.debug_*` sections at the top level. +#[test] +fn default_strips_dwarf() { + if !fixture_available() { + return; + } + let fused = fuse_with(FuserConfig::default().dwarf_handling); + assert_eq!( + count_dwarf_sections(&fused), + 0, + "default DwarfHandling::Strip must produce zero DWARF sections" + ); +} + +/// `DwarfHandling::PassThrough` preserves the (lossy) prior behaviour: +/// DWARF sections are present in the fused output. Their addresses are +/// still wrong against the merged code section — that is the point of +/// the explicit opt-in. +#[test] +fn passthrough_preserves_dwarf() { + if !fixture_available() { + return; + } + let fused = fuse_with(DwarfHandling::PassThrough); + assert!( + count_dwarf_sections(&fused) > 0, + "PassThrough must emit at least one DWARF section when input \ + carries one" + ); +} + +/// Smoke check: `Default::default()` resolves to `DwarfHandling::Strip`, +/// not `PassThrough`. If the default ever flips, this test must be +/// updated together with the LS-CP-N entry. +#[test] +fn default_is_strip() { + let cfg = FuserConfig::default(); + assert_eq!(cfg.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 dc818ae..dae6271 100644 --- a/meld-core/tests/issue_112_mythos_v04.rs +++ b/meld-core/tests/issue_112_mythos_v04.rs @@ -178,6 +178,7 @@ fn fuse_config() -> FuserConfig { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, opaque_resources: Vec::new(), } } diff --git a/meld-core/tests/multi_memory.rs b/meld-core/tests/multi_memory.rs index 95ce4e1..f4d5295 100644 --- a/meld-core/tests/multi_memory.rs +++ b/meld-core/tests/multi_memory.rs @@ -209,6 +209,7 @@ fn test_multi_memory_separate_memories() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Merge, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -260,6 +261,7 @@ fn test_multi_memory_preserves_isolation() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Merge, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; diff --git a/meld-core/tests/nested_component.rs b/meld-core/tests/nested_component.rs index 1156f15..45a210d 100644 --- a/meld-core/tests/nested_component.rs +++ b/meld-core/tests/nested_component.rs @@ -67,6 +67,7 @@ fn test_fuse_composed_p2_component() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; diff --git a/meld-core/tests/realloc_safety.rs b/meld-core/tests/realloc_safety.rs index 8f95e7d..5c21dbe 100644 --- a/meld-core/tests/realloc_safety.rs +++ b/meld-core/tests/realloc_safety.rs @@ -507,6 +507,7 @@ fn ls_a_7_every_realloc_call_has_null_guard() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; diff --git a/meld-core/tests/release_components.rs b/meld-core/tests/release_components.rs index 4c87b46..4f7c8f0 100644 --- a/meld-core/tests/release_components.rs +++ b/meld-core/tests/release_components.rs @@ -72,6 +72,7 @@ fn try_fuse(path: &str, name: &str) -> (bool, usize, String) { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -230,6 +231,7 @@ fn test_fused_output_validates() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -322,6 +324,7 @@ fn test_p1_adapter_detection_with_instances() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -402,6 +405,7 @@ fn test_reasonable_memory_count() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -488,6 +492,7 @@ fn test_write_fused_output_for_runtime() { address_rebasing: false, preserve_names: true, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -553,6 +558,7 @@ fn test_no_duplicate_imports() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -637,6 +643,7 @@ fn test_adapter_generation_for_release_components() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -704,6 +711,7 @@ fn test_adapter_call_site_wiring() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -855,6 +863,7 @@ fn test_no_stale_resource_drop_versions() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -939,6 +948,7 @@ fn test_component_wrap_validates() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: OutputFormat::Component, opaque_resources: Vec::new(), }; diff --git a/meld-core/tests/runtime_from_exports.rs b/meld-core/tests/runtime_from_exports.rs index b24c654..173e3ef 100644 --- a/meld-core/tests/runtime_from_exports.rs +++ b/meld-core/tests/runtime_from_exports.rs @@ -131,6 +131,7 @@ fn test_from_exports_resolution_runtime() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -181,6 +182,7 @@ fn test_from_exports_shared_memory_strategy() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -231,6 +233,7 @@ fn test_from_exports_function_count() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; diff --git a/meld-core/tests/runtime_intra_adapter.rs b/meld-core/tests/runtime_intra_adapter.rs index a93601b..b023222 100644 --- a/meld-core/tests/runtime_intra_adapter.rs +++ b/meld-core/tests/runtime_intra_adapter.rs @@ -212,6 +212,7 @@ fn test_intra_component_three_module_fusion() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; @@ -264,6 +265,7 @@ fn test_intra_component_memory_count() { address_rebasing: false, preserve_names: false, custom_sections: meld_core::CustomSectionHandling::Drop, + dwarf_handling: meld_core::DwarfHandling::Strip, output_format: meld_core::OutputFormat::CoreModule, opaque_resources: Vec::new(), }; diff --git a/meld-core/tests/wit_bindgen_runtime.rs b/meld-core/tests/wit_bindgen_runtime.rs index 94a5101..45661fa 100644 --- a/meld-core/tests/wit_bindgen_runtime.rs +++ b/meld-core/tests/wit_bindgen_runtime.rs @@ -61,6 +61,7 @@ fn fuse_fixture(name: &str, output_format: OutputFormat) -> anyhow::Result + Each input core module's `.debug_*` custom sections are pushed + verbatim into the fused module by `merger.rs` (around line 2010) + and emitted by `lib.rs::encode_output` whenever + `CustomSectionHandling != Drop`. Every DWARF address inside those + sections is a byte offset into the ORIGINAL per-input code section. + The fuser rewrites and renumbers function bodies into a single + merged code section, so every passed-through DWARF address + references the wrong instruction (or nothing). Downstream MC/DC + tooling (`pulseengine/witness`) trusts the `(code-offset -> file, + line)` map produced by `gimli` and therefore attributes branch + decisions to incorrect source lines [H-7]. Wrong attribution is + strictly worse than no attribution because witness has a + no-DWARF fallback that degrades gracefully — the lossy + passthrough silently subverts that fallback. + + Phase 2 of issue #130 will add an address-remap pass over + `.debug_line` / `.debug_info` / `.debug_ranges`. Until then the + only non-corrupting policy is to drop `.debug_*` sections. + + Phase 1.5 lands an explicit `DwarfHandling` enum + (`Strip` default, `PassThrough` opt-in) on `FuserConfig`, + filters `.debug_*` in `lib.rs::encode_output` when `Strip`, and + records the choice in attestation `tool_parameters` so consumers + can detect lossy emissions. Verified by + `meld-core/tests/dwarf_strip.rs`. + hazards: [H-7] + causal-factors: + - >- + `merger.rs` accumulates per-input `.debug_*` sections without + rewriting their internal addresses + - >- + `lib.rs::encode_output` had no `.debug_*`-aware policy; the + only knob was the generic `CustomSectionHandling` switch + - >- + Witness consumes the emitted DWARF and treats it as authoritative + for `(code-offset -> file, line)` mapping + - >- + Strip is correct-but-lossy; PassThrough is corrupting-but-loud; + Phase 2 (remap) is the eventual correct-and-lossless mode + status: approved + priority: high + # ========================================================================== # Additional adapter scenarios (discovered during gap analysis) # ==========================================================================