Skip to content

Commit bb43044

Browse files
avrabeclaude
andauthored
fix(dwarf): remap DW_AT_ranges lists to the fused layout (#319 inc 2) (#321)
* fix(dwarf): remap DW_AT_ranges lists to the fused layout (#319 inc 2) The residual "DIE address ranges are not contained in its parent's ranges" errors after inc 1 came from base-relative .debug_ranges offsets. Root cause: DWARF v4 .debug_ranges entries are offset pairs relative to a base (the CU's DW_AT_low_pc, which is nonzero for wasi-libc CUs). gimli parses them as AddressOrOffsetPair and routes the *offsets* through convert_address, which treats a small offset (e.g. 0x141) as an absolute code address and remaps it to an unrelated output location — so the resolved range escapes its parent (the .debug_ranges analogue of the inc-1 high_pc bug, one level down). The CU base (an address form) is remapped correctly, so consumer = out_base + garbage_offset → e.g. [0x1c439,..) outside the parent [0x133b6,0x1353d). Fix: correct_die_ranges — a post-convert pass that, walking read↔write DIEs in lockstep pre-order, resolves each DIE's absolute input ranges via gimli read `die_ranges` (base already applied), translates both endpoints to the fused layout, and re-emits them as OffsetPair relative to the output CU base. Sub-ranges whose endpoints can't be mapped (dropped/tombstoned code) are dropped; an emptied list deletes DW_AT_ranges — never a wrong or escaping range (LS-D-1). Effect: the "not contained" class is eliminated — records-only 4->0, records+variants 11->0 (cumulative with inc 1: 1049->35 multi, ~510->8 single). The remaining 35/8 are Cause #2 (.debug_loc tombstone vs base-address-selection escape) = inc 3. Test: inc2_die_ranges_stay_within_enclosing_subprogram — fuses records.wasm, re-parses the fused DWARF, asserts every DW_AT_ranges entry sits inside its enclosing subprogram (the invariant that failed). 2 witness + 24 dwarf unit tests pass. Refs: #319. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * harden(dwarf): drop zero-length ranges in correct_die_ranges (#319 inc 2) Mythos delta-pass robustness: a degenerate begin==end OffsetPair is rejected by gimli's range writer (InvalidRange), which would abort the whole DWARF write and fall back to strip. Require strict oe > ob; a zero-length range carries no info, so dropping it is lossless. (No mis-attribution — this is a write-abort/reliability guard.) Refs: #319. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 69c644d commit bb43044

2 files changed

Lines changed: 231 additions & 0 deletions

File tree

meld-core/src/dwarf.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,9 @@ fn rewrite_debug_sections(
575575
// `llvm-dwarfdump --verify` flags as out-of-parent / overlapping. Fix
576576
// the lengths to the fused layout now that `low_pc` is remapped.
577577
correct_high_pc_lengths(&mut write_dwarf, remap);
578+
// #319 inc 2: fix DW_AT_ranges lists (base-relative offsets that gimli
579+
// mis-routes through convert_address) from the read side.
580+
correct_die_ranges(&mut write_dwarf, &read_dwarf, remap);
578581
// #144 inc 3: the synthetic `<meld-adapter>` unit rides the SAME
579582
// write as the remapped original units, so every cross-section
580583
// offset is computed in one shared offset space (appending
@@ -644,6 +647,138 @@ fn correct_high_pc_lengths(write_dwarf: &mut gimli::write::Dwarf, remap: &Addres
644647
}
645648
}
646649

650+
/// Collect a write unit's DIE ids in first-child-first pre-order — the same
651+
/// order `gimli::read`'s `next_dfs` yields, so the two can be walked in
652+
/// lockstep. (Distinct from [`correct_high_pc_lengths`]'s order-agnostic
653+
/// stack walk, which doesn't need to align with the read side.)
654+
fn write_dies_preorder(
655+
unit: &gimli::write::Unit,
656+
id: gimli::write::UnitEntryId,
657+
out: &mut Vec<gimli::write::UnitEntryId>,
658+
) {
659+
out.push(id);
660+
let children: Vec<_> = unit.get(id).children().copied().collect();
661+
for c in children {
662+
write_dies_preorder(unit, c, out);
663+
}
664+
}
665+
666+
/// Rewrite every `DW_AT_ranges` list to the fused layout (#319 inc 2).
667+
///
668+
/// DWARF v4 `.debug_ranges` entries are base-relative offset pairs. gimli
669+
/// parses them as `AddressOrOffsetPair` and routes the *offsets* through
670+
/// `convert_address` — which treats a small offset as an absolute code
671+
/// address and remaps it to an unrelated output location, so the resolved
672+
/// range escapes its parent (the `.debug_ranges` analogue of the inc-1
673+
/// `high_pc`-length bug). Fix it from the read side, where the base is still
674+
/// applied: resolve each DIE's absolute input ranges via `die_ranges`,
675+
/// `translate` both endpoints to the fused layout, and re-emit them as
676+
/// offsets relative to the output CU base. Drop sub-ranges (or the whole
677+
/// list) that can't be mapped — never emit a wrong or escaping range
678+
/// (LS-D-1).
679+
fn correct_die_ranges<R: gimli::read::Reader<Offset = usize>>(
680+
write_dwarf: &mut gimli::write::Dwarf,
681+
read_dwarf: &gimli::read::Dwarf<R>,
682+
remap: &AddressRemap,
683+
) {
684+
use gimli::constants::{DW_AT_low_pc, DW_AT_ranges};
685+
use gimli::write::{Address, AttributeValue, Range, RangeList};
686+
687+
let mut headers = read_dwarf.units();
688+
let mut unit_idx = 0usize;
689+
while let Ok(Some(header)) = headers.next() {
690+
if unit_idx >= write_dwarf.units.count() {
691+
break;
692+
}
693+
let uid = write_dwarf.units.id(unit_idx);
694+
unit_idx += 1;
695+
let unit = match read_dwarf.unit(header) {
696+
Ok(u) => u,
697+
Err(_) => continue,
698+
};
699+
700+
// Output CU base = the write root's already-remapped low_pc; v4
701+
// range entries are emitted relative to it. No usable base → skip.
702+
let out_cu_base = {
703+
let root = write_dwarf.units.get(uid).root();
704+
match write_dwarf.units.get(uid).get(root).get(DW_AT_low_pc) {
705+
Some(AttributeValue::Address(Address::Constant(a))) => *a as u32,
706+
_ => continue,
707+
}
708+
};
709+
710+
// Write DIE ids in the same pre-order the read cursor will walk.
711+
let write_ids = {
712+
let wunit = write_dwarf.units.get(uid);
713+
let mut ids = Vec::new();
714+
write_dies_preorder(wunit, wunit.root(), &mut ids);
715+
ids
716+
};
717+
718+
let mut entries = unit.entries();
719+
let mut wi = 0usize;
720+
while let Ok(Some((_, entry))) = entries.next_dfs() {
721+
if wi >= write_ids.len() {
722+
break;
723+
}
724+
let die_id = write_ids[wi];
725+
wi += 1;
726+
727+
// Only DIEs that carry a range LIST (not low/high — those are the
728+
// inc-1 path). `die_ranges` would otherwise synthesize a single
729+
// range from low/high and we'd wrongly convert a subprogram.
730+
if entry.attr_value(DW_AT_ranges).ok().flatten().is_none() {
731+
continue;
732+
}
733+
734+
let mut ranges = match read_dwarf.die_ranges(&unit, entry) {
735+
Ok(r) => r,
736+
Err(_) => continue,
737+
};
738+
let mut new_ranges = Vec::new();
739+
while let Ok(Some(r)) = ranges.next() {
740+
// `die_ranges` yields absolute input addresses (base applied).
741+
let (Some(ob), Some(oe)) = (
742+
remap.translate(r.begin as u32),
743+
remap.translate(r.end as u32),
744+
) else {
745+
continue; // unmappable (dropped/tombstoned) sub-range → drop
746+
};
747+
// Require a strictly-positive extent: a degenerate `begin ==
748+
// end` OffsetPair is rejected by gimli's range writer
749+
// (`InvalidRange`), which would abort the whole DWARF write
750+
// and strip. A zero-length range carries no information, so
751+
// dropping it is lossless.
752+
if ob >= out_cu_base && oe > ob {
753+
new_ranges.push(Range::OffsetPair {
754+
begin: (ob - out_cu_base) as u64,
755+
end: (oe - out_cu_base) as u64,
756+
});
757+
}
758+
}
759+
760+
if new_ranges.is_empty() {
761+
write_dwarf
762+
.units
763+
.get_mut(uid)
764+
.get_mut(die_id)
765+
.delete(DW_AT_ranges);
766+
} else {
767+
let rid = write_dwarf
768+
.units
769+
.get_mut(uid)
770+
.ranges
771+
.add(RangeList(new_ranges));
772+
write_dwarf
773+
.units
774+
.get_mut(uid)
775+
.get_mut(die_id)
776+
.set(DW_AT_ranges, AttributeValue::RangeListRef(rid));
777+
}
778+
}
779+
}
780+
}
781+
647782
/// Top-level entry point for [`crate::DwarfHandling::Remap`].
648783
///
649784
/// Inspects the input components for `.debug_*` sections and, when

meld-core/tests/dwarf_remap_witness.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,99 @@ fn remapped_subprogram_low_pcs_match_fused_body_starts() {
190190
fused body starts, {tombstoned} dead-code subprograms tombstoned"
191191
);
192192
}
193+
194+
/// A multi-inner-module fixture whose `.debug_ranges` carry inlined
195+
/// subroutines (base-relative offset pairs) — the #319 inc-2 surface.
196+
const RANGES_FIXTURE: &str = "../tests/wit_bindgen/fixtures/records.wasm";
197+
198+
/// #319 inc 2: no `DW_AT_ranges` DIE may escape its enclosing subprogram in
199+
/// the remapped output. `records.wasm` carries inlined subroutines whose
200+
/// `.debug_ranges` are base-relative offset pairs; before the fix gimli
201+
/// mis-routed those *offsets* through `convert_address` (treating a small
202+
/// offset as an absolute address), sending the ranges to unrelated output
203+
/// locations — `llvm-dwarfdump --verify`: "DIE address ranges are not
204+
/// contained in its parent's ranges". This re-parses the fused DWARF and
205+
/// asserts every resolved range sits inside the enclosing subprogram.
206+
#[test]
207+
fn inc2_die_ranges_stay_within_enclosing_subprogram() {
208+
if !std::path::Path::new(RANGES_FIXTURE).is_file() {
209+
eprintln!("skipping: fixture not found at {RANGES_FIXTURE}");
210+
return;
211+
}
212+
let input = std::fs::read(RANGES_FIXTURE).expect("read fixture");
213+
let fused = fuse_remap(&input);
214+
let sections = debug_sections(&fused);
215+
assert!(
216+
sections.contains_key(".debug_info"),
217+
"Remap must emit remapped .debug_info for the ranges fixture"
218+
);
219+
220+
let endian = gimli::LittleEndian;
221+
let load = |id: gimli::SectionId| -> Result<gimli::EndianSlice<'_, gimli::LittleEndian>, gimli::Error> {
222+
let data = sections.get(id.name()).map(|v| v.as_slice()).unwrap_or(&[]);
223+
Ok(gimli::EndianSlice::new(data, endian))
224+
};
225+
let dwarf = gimli::Dwarf::load(load).expect("load output dwarf");
226+
227+
let mut checked = 0usize;
228+
let mut units = dwarf.units();
229+
while let Some(header) = units.next().expect("unit header") {
230+
let unit = dwarf.unit(header).expect("parse unit");
231+
let mut entries = unit.entries();
232+
let mut depth = 0isize;
233+
// Stack of enclosing subprogram ranges: (die_depth, low, end).
234+
let mut encl: Vec<(isize, u64, u64)> = Vec::new();
235+
while let Some((delta, entry)) = entries.next_dfs().expect("dfs walk") {
236+
depth += delta;
237+
while matches!(encl.last(), Some(&(d, _, _)) if d >= depth) {
238+
encl.pop();
239+
}
240+
if entry.tag() == gimli::constants::DW_TAG_subprogram
241+
&& let Some(gimli::AttributeValue::Addr(low)) = entry
242+
.attr_value(gimli::constants::DW_AT_low_pc)
243+
.expect("low_pc")
244+
&& low != TOMBSTONE
245+
&& let Some(hp) = entry
246+
.attr_value(gimli::constants::DW_AT_high_pc)
247+
.expect("high_pc")
248+
{
249+
let end = match hp {
250+
gimli::AttributeValue::Udata(len) => low + len,
251+
gimli::AttributeValue::Addr(a) => a,
252+
_ => low,
253+
};
254+
encl.push((depth, low, end));
255+
}
256+
// A DIE carrying a range LIST, checked against its enclosing
257+
// subprogram (the class that escaped in #319).
258+
if entry
259+
.attr_value(gimli::constants::DW_AT_ranges)
260+
.expect("ranges attr")
261+
.is_some()
262+
&& let Some(&(_, plow, pend)) = encl.last()
263+
{
264+
let mut ranges = dwarf.die_ranges(&unit, entry).expect("die_ranges");
265+
while let Some(r) = ranges.next().expect("range") {
266+
// Skip tombstone / dead-code sentinel entries.
267+
if r.begin >= 0xffff_fffe {
268+
continue;
269+
}
270+
assert!(
271+
r.begin >= plow && r.end <= pend,
272+
"inc2: DW_AT_ranges [{:#x},{:#x}) escapes enclosing \
273+
subprogram [{plow:#x},{pend:#x}) — base-relative range \
274+
offset was remapped as an absolute address (#319)",
275+
r.begin,
276+
r.end
277+
);
278+
checked += 1;
279+
}
280+
}
281+
}
282+
}
283+
assert!(
284+
checked > 0,
285+
"expected at least one DW_AT_ranges DIE inside a subprogram to check"
286+
);
287+
eprintln!("inc2: {checked} DW_AT_ranges entries all contained in their subprogram");
288+
}

0 commit comments

Comments
 (0)