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
167 changes: 167 additions & 0 deletions meld-core/src/dwarf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,10 @@ fn rewrite_debug_sections(
// #319 inc 3: fix DW_AT_location lists (same base-relative offset bug as
// ranges, plus sentinel-derailed decode) from the read side.
correct_die_locations(&mut write_dwarf, &read_dwarf, remap);
// #331: rebuild `.debug_line` line programs with per-row addresses run
// through `translate` (gimli only converts the sequence start + reapplies
// original advances, which overshoot the non-linearly rewritten bodies).
correct_line_programs(&mut write_dwarf, &read_dwarf, remap);
// #144 inc 3: the synthetic `<meld-adapter>` unit rides the SAME
// write as the remapped original units, so every cross-section
// offset is computed in one shared offset space (appending
Expand Down Expand Up @@ -903,6 +907,169 @@ fn correct_die_locations<R: gimli::read::Reader<Offset = usize>>(
}
}

/// #331: rebuild each unit's `.debug_line` line program so every row's address
/// is run through the offset-aware [`AddressRemap::translate`].
///
/// gimli's `write::Dwarf::from` converts a line program by translating only the
/// sequence *start* (`DW_LNE_set_address`) and re-applying each row's ORIGINAL
/// `advance_pc` delta — it models the fused code as a linear shift of the input.
/// But fusion rewrites function bodies non-linearly (call-index renumbering
/// changes LEB widths), so the original deltas overshoot the shorter fused body
/// and the `code`-section end (out-of-section rows, `--verify`-silent). The DIE
/// `high_pc`/ranges/locations are already correct (they go through `translate`);
/// only the line program is wrong.
///
/// The write `LineProgram`'s instructions are private (no in-place edit), so the
/// program is *replaced*. The file/dir table is replicated in the SAME order
/// gimli's converter used (read `file_names` order), so the resulting `FileId`s
/// coincide with those the write DIEs' `DW_AT_decl_file` already reference — no
/// `decl_file` rewrite needed. Rows whose address does not map (dropped /
/// tombstoned code) are omitted rather than emitted at a wrong address (LS-D-1).
fn correct_line_programs<R: gimli::read::Reader<Offset = usize>>(
write_dwarf: &mut gimli::write::Dwarf,
read_dwarf: &gimli::read::Dwarf<R>,
remap: &AddressRemap,
) {
use gimli::write::{Address, LineProgram, LineString};

// Best-effort bytes for a line-program string attribute (name/dir).
let to_bytes = |unit: &gimli::read::Unit<R>, v: &gimli::read::AttributeValue<R>| -> Vec<u8> {
read_dwarf
.attr_string(unit, v.clone())
.ok()
.and_then(|r| r.to_slice().ok().map(|s| s.into_owned()))
.unwrap_or_default()
};

let mut headers = read_dwarf.units();
let mut unit_idx = 0usize;
while let Ok(Some(header)) = headers.next() {
if unit_idx >= write_dwarf.units.count() {
break;
}
let uid = write_dwarf.units.id(unit_idx);
unit_idx += 1;
let unit = match read_dwarf.unit(header) {
Ok(u) => u,
Err(_) => continue,
};
let Some(read_lp) = unit.line_program.clone() else {
continue;
};
let lph = read_lp.header();
let encoding = lph.encoding();
let line_encoding = lph.line_encoding();

// comp_dir / comp_name: source them from the line-program HEADER
// (`directory(0)` / `file(0).path_name()`) EXACTLY as gimli's own
// converter does (gimli::write::line). This is required for FileId
// alignment, not cosmetic: in DWARF v5 gimli pre-seeds `file(0)` as
// `FileId(0)`, so the `file_names[0]` we re-add below must dedup back
// to it (same name + directory) — otherwise the whole file table
// shifts by one and every DIE's `DW_AT_decl_file` (which we do NOT
// rewrite) resolves to the wrong file. Taking `comp_name` from the CU
// `DW_AT_name` (which can differ from `file(0)`) breaks that dedup on
// real `-gdwarf-5` output (#331 Mythos finding). v4 has no such seed,
// so this is harmless there.
let comp_dir = lph
.directory(0)
.map(|d| to_bytes(&unit, &d))
.unwrap_or_else(|| b".".to_vec());
let comp_name = lph
.file(0)
.map(|f| to_bytes(&unit, &f.path_name()))
.unwrap_or_else(|| b"<unknown>".to_vec());

let mut new_lp = LineProgram::new(
encoding,
line_encoding,
LineString::String(comp_dir),
LineString::String(comp_name),
None,
);

// Directories: index 0 is the implicit comp dir (default_directory).
// v5 include_directories[0] is also the comp dir, so skip it.
let mut dir_ids = vec![new_lp.default_directory()];
for (i, d) in lph.include_directories().iter().enumerate() {
if encoding.version >= 5 && i == 0 {
continue; // comp dir, already present as default_directory
}
let name = to_bytes(&unit, d);
dir_ids.push(new_lp.add_directory(LineString::String(name)));
}

// Files: replicate in read order. v4 file index is 1-based; v5 0-based.
let file_base: u64 = if encoding.version >= 5 { 0 } else { 1 };
let mut file_ids: std::collections::BTreeMap<u64, gimli::write::FileId> =
std::collections::BTreeMap::new();
for (i, fe) in lph.file_names().iter().enumerate() {
let read_idx = file_base + i as u64;
let name = to_bytes(&unit, &fe.path_name());
let dir = dir_ids
.get(fe.directory_index() as usize)
.copied()
.unwrap_or_else(|| new_lp.default_directory());
let fid = new_lp.add_file(LineString::String(name), dir, None);
file_ids.insert(read_idx, fid);
}
// A default file for rows whose file index didn't map. If the program
// declared no files at all, synthesize one so `row.file` is always valid.
let default_file = match file_ids.values().next().copied() {
Some(f) => f,
None => {
let dd = new_lp.default_directory();
new_lp.add_file(LineString::String(b"<unknown>".to_vec()), dd, None)
}
};

// Collect + translate rows, grouped into sequences.
let mut rows = read_lp.rows();
let mut seq: Vec<(u64, gimli::write::FileId, u64, u64, bool)> = Vec::new();
let mut seq_start: Option<u64> = None;
while let Ok(Some((_, row))) = rows.next_row() {
if row.end_sequence() {
if let (Some(start), false) = (seq_start, seq.is_empty())
&& let Some(end_out) = remap.translate(row.address() as u32)
{
new_lp.begin_sequence(Some(Address::Constant(start)));
for (out_addr, file, line, col, stmt) in seq.iter().copied() {
let r = new_lp.row();
r.address_offset = out_addr.saturating_sub(start);
r.file = file;
r.line = line;
r.column = col;
r.is_statement = stmt;
new_lp.generate_row();
}
new_lp.end_sequence((end_out as u64).saturating_sub(start));
}
seq.clear();
seq_start = None;
continue;
}
let Some(out_addr) = remap.translate(row.address() as u32) else {
continue; // unmappable row → drop (never a wrong address)
};
if seq_start.is_none() {
seq_start = Some(out_addr as u64);
}
let file = file_ids
.get(&row.file_index())
.copied()
.unwrap_or(default_file);
let line = row.line().map(|l| l.get()).unwrap_or(0);
let col = match row.column() {
gimli::ColumnType::Column(c) => c.get(),
gimli::ColumnType::LeftEdge => 0,
};
seq.push((out_addr as u64, file, line, col, row.is_stmt()));
}

write_dwarf.units.get_mut(uid).line_program = new_lp;
}
}

/// Top-level entry point for [`crate::DwarfHandling::Remap`].
///
/// Inspects the input components for `.debug_*` sections and, when
Expand Down
52 changes: 52 additions & 0 deletions meld-core/tests/adapter_dwarf_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,55 @@ fn fused_module_with_synthetic_dwarf_still_runs() {
assert_eq!(get_a.call(&mut store, ()).unwrap(), 11, "a's start ran");
assert_eq!(get_b.call(&mut store, ()).unwrap(), 22, "b's start ran");
}

/// #331: with multiple DWARF-bearing inputs, every `.debug_line` row address
/// must fall within the fused code section. gimli's line-program conversion
/// translated only the sequence start and re-applied each row's ORIGINAL
/// `advance_pc` delta; fusion rewrites bodies non-linearly (call-index
/// renumbering), so the deltas overshot the code end (verify-silent).
/// `correct_line_programs` re-translates every row through the offset-aware
/// `AddressRemap`, keeping the merged (multi-source) line info in-bounds.
#[test]
fn dwarf_331_line_rows_within_code_section_multi_source() {
// Two real DWARF-bearing release components → a multi-source remap.
let (Ok(rust), Ok(c)) = (
std::fs::read("../tests/wit_bindgen/fixtures/release-0.2.0/hello_rust.wasm"),
std::fs::read("../tests/wit_bindgen/fixtures/release-0.2.0/hello_c_cli.wasm"),
) else {
eprintln!("skipping #331: release fixtures absent");
return;
};

let mut fuser = Fuser::new(FuserConfig {
memory_strategy: MemoryStrategy::MultiMemory,
dwarf_handling: DwarfHandling::Remap,
attestation: false,
reproducible: false,
..Default::default()
});
fuser.add_component_named(&rust, Some("rust")).unwrap();
fuser.add_component_named(&c, Some("c")).unwrap();
let fused = fuser.fuse().expect("fuse two DWARF-bearing components");

// Tight upper bound: the greatest function-body end (code-section-relative),
// derived independently with wasmparser. A `.debug_line` row past this is
// the #331 out-of-section overshoot.
let code_end = function_body_ranges(&fused)
.iter()
.map(|(_, e)| *e)
.max()
.expect("fused output has a code section");

let rows = debug_line_rows(&fused);
assert!(
!rows.is_empty(),
"multi-source DWARF remap must still emit .debug_line rows (line info preserved)"
);
for (addr, file, line) in &rows {
assert!(
*addr <= code_end,
"#331: .debug_line row at {addr:#x} ({file}:{line}) exceeds the fused \
code-section end {code_end:#x} — out-of-section line row"
);
}
}
Loading