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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions meld-core/benches/fusion_benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
Expand Down
2 changes: 2 additions & 0 deletions meld-core/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ mod tests {
"address_rebasing",
"preserve_names",
"custom_sections",
"dwarf_handling",
"output_format",
];

Expand All @@ -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"),
Expand Down
55 changes: 55 additions & 0 deletions meld-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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(),
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions meld-core/tests/adapter_safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
Expand Down Expand Up @@ -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(),
};
Expand Down Expand Up @@ -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(),
};
Expand Down Expand Up @@ -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(),
};
Expand Down Expand Up @@ -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(),
};
Expand Down
117 changes: 117 additions & 0 deletions meld-core/tests/dwarf_strip.rs
Original file line number Diff line number Diff line change
@@ -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<u8> {
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);
}
1 change: 1 addition & 0 deletions meld-core/tests/issue_112_mythos_v04.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}
Expand Down
2 changes: 2 additions & 0 deletions meld-core/tests/multi_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
Expand Down Expand Up @@ -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(),
};
Expand Down
1 change: 1 addition & 0 deletions meld-core/tests/nested_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
Expand Down
1 change: 1 addition & 0 deletions meld-core/tests/realloc_safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
Expand Down
Loading
Loading