diff --git a/meld-core/src/dwarf.rs b/meld-core/src/dwarf.rs index 2b9351f..f1fe3cd 100644 --- a/meld-core/src/dwarf.rs +++ b/meld-core/src/dwarf.rs @@ -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 `` unit rides the SAME // write as the remapped original units, so every cross-section // offset is computed in one shared offset space (appending @@ -903,6 +907,169 @@ fn correct_die_locations>( } } +/// #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>( + write_dwarf: &mut gimli::write::Dwarf, + read_dwarf: &gimli::read::Dwarf, + 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, v: &gimli::read::AttributeValue| -> Vec { + 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"".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 = + 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"".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 = 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 diff --git a/meld-core/tests/adapter_dwarf_e2e.rs b/meld-core/tests/adapter_dwarf_e2e.rs index 0dbf9e3..163a8aa 100644 --- a/meld-core/tests/adapter_dwarf_e2e.rs +++ b/meld-core/tests/adapter_dwarf_e2e.rs @@ -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" + ); + } +}