Skip to content

Commit 697474d

Browse files
avrabeclaude
andauthored
fix(dwarf): remap DW_AT_location lists to the fused layout (#319 inc 3) (#322)
Final increment — takes the fused DWARF to llvm-dwarfdump --verify clean. The residual Invalid DW_AT_location / Invalid DWARF expressions were the same base-relative-offset bug as inc 2, in .debug_loc location lists. Fix: correct_die_locations (the correct_die_ranges analogue) rebuilds each list from the read side, translating entry endpoints and carrying the location expression bytes verbatim; unmappable entries dropped, emptied list deletes the attribute (LS-D-1). records-only + records+variants both -> No errors (cumulative #319: 1049 -> 0). Test: inc3_location_list_ranges_stay_in_module. Refs: #319. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bb43044 commit 697474d

2 files changed

Lines changed: 194 additions & 0 deletions

File tree

meld-core/src/dwarf.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,9 @@ fn rewrite_debug_sections(
578578
// #319 inc 2: fix DW_AT_ranges lists (base-relative offsets that gimli
579579
// mis-routes through convert_address) from the read side.
580580
correct_die_ranges(&mut write_dwarf, &read_dwarf, remap);
581+
// #319 inc 3: fix DW_AT_location lists (same base-relative offset bug as
582+
// ranges, plus sentinel-derailed decode) from the read side.
583+
correct_die_locations(&mut write_dwarf, &read_dwarf, remap);
581584
// #144 inc 3: the synthetic `<meld-adapter>` unit rides the SAME
582585
// write as the remapped original units, so every cross-section
583586
// offset is computed in one shared offset space (appending
@@ -779,6 +782,127 @@ fn correct_die_ranges<R: gimli::read::Reader<Offset = usize>>(
779782
}
780783
}
781784

785+
/// Rewrite every `DW_AT_location` **location list** to the fused layout
786+
/// (#319 inc 3). Location-list entries carry the same base-relative
787+
/// begin/end offsets as `.debug_ranges` (inc 2), so gimli mis-routes them
788+
/// through `convert_address` — the entry ranges land at garbage output
789+
/// offsets and, combined with the `0xFFFFFFFF`/`0xFFFFFFFE` sentinels,
790+
/// derail the whole list (`llvm-dwarfdump`: "Invalid DW_AT_location" /
791+
/// "Invalid DWARF expressions"). Fix it exactly like [`correct_die_ranges`]:
792+
/// resolve each list from the read side (`attr_locations`, base applied),
793+
/// `translate` the entry endpoints, and re-emit as `OffsetPair` relative to
794+
/// the output CU base — carrying the location **expression bytes verbatim**
795+
/// (WASM location expressions — `DW_OP_WASM_location`, `DW_OP_stack_value` —
796+
/// hold no code addresses; a data `DW_OP_addr` is unchanged under fusion,
797+
/// matching `convert_address`'s data passthrough). Bare exprloc
798+
/// `DW_AT_location`s are not lists (`attr_locations` → `None`) and are left
799+
/// to gimli. Unmappable entries are dropped; an emptied list deletes the
800+
/// attribute (LS-D-1).
801+
fn correct_die_locations<R: gimli::read::Reader<Offset = usize>>(
802+
write_dwarf: &mut gimli::write::Dwarf,
803+
read_dwarf: &gimli::read::Dwarf<R>,
804+
remap: &AddressRemap,
805+
) {
806+
use gimli::constants::{DW_AT_location, DW_AT_low_pc};
807+
use gimli::write::{Address, AttributeValue, Expression, Location, LocationList};
808+
809+
let mut headers = read_dwarf.units();
810+
let mut unit_idx = 0usize;
811+
while let Ok(Some(header)) = headers.next() {
812+
if unit_idx >= write_dwarf.units.count() {
813+
break;
814+
}
815+
let uid = write_dwarf.units.id(unit_idx);
816+
unit_idx += 1;
817+
let unit = match read_dwarf.unit(header) {
818+
Ok(u) => u,
819+
Err(_) => continue,
820+
};
821+
822+
let out_cu_base = {
823+
let root = write_dwarf.units.get(uid).root();
824+
match write_dwarf.units.get(uid).get(root).get(DW_AT_low_pc) {
825+
Some(AttributeValue::Address(Address::Constant(a))) => *a as u32,
826+
_ => continue,
827+
}
828+
};
829+
830+
let write_ids = {
831+
let wunit = write_dwarf.units.get(uid);
832+
let mut ids = Vec::new();
833+
write_dies_preorder(wunit, wunit.root(), &mut ids);
834+
ids
835+
};
836+
837+
let mut entries = unit.entries();
838+
let mut wi = 0usize;
839+
while let Ok(Some((_, entry))) = entries.next_dfs() {
840+
if wi >= write_ids.len() {
841+
break;
842+
}
843+
let die_id = write_ids[wi];
844+
wi += 1;
845+
846+
let loc_attr = match entry.attr_value(DW_AT_location) {
847+
Ok(Some(v)) => v,
848+
_ => continue,
849+
};
850+
// `attr_locations` returns `None` for a bare exprloc (not a list).
851+
let mut locs = match read_dwarf.attr_locations(&unit, loc_attr) {
852+
Ok(Some(l)) => l,
853+
_ => continue,
854+
};
855+
856+
let mut new_locs = Vec::new();
857+
let mut aborted = false;
858+
while let Ok(Some(le)) = locs.next() {
859+
let (Some(ob), Some(oe)) = (
860+
remap.translate(le.range.begin as u32),
861+
remap.translate(le.range.end as u32),
862+
) else {
863+
continue; // unmappable (dropped/tombstoned) entry → drop
864+
};
865+
if ob < out_cu_base || oe <= ob {
866+
continue;
867+
}
868+
let bytes = match le.data.0.to_slice() {
869+
Ok(b) => b.into_owned(),
870+
Err(_) => {
871+
aborted = true;
872+
break;
873+
}
874+
};
875+
new_locs.push(Location::OffsetPair {
876+
begin: (ob - out_cu_base) as u64,
877+
end: (oe - out_cu_base) as u64,
878+
data: Expression::raw(bytes),
879+
});
880+
}
881+
if aborted {
882+
continue; // couldn't read an expression — leave gimli's output
883+
}
884+
if new_locs.is_empty() {
885+
write_dwarf
886+
.units
887+
.get_mut(uid)
888+
.get_mut(die_id)
889+
.delete(DW_AT_location);
890+
} else {
891+
let lid = write_dwarf
892+
.units
893+
.get_mut(uid)
894+
.locations
895+
.add(LocationList(new_locs));
896+
write_dwarf
897+
.units
898+
.get_mut(uid)
899+
.get_mut(die_id)
900+
.set(DW_AT_location, AttributeValue::LocationListRef(lid));
901+
}
902+
}
903+
}
904+
}
905+
782906
/// Top-level entry point for [`crate::DwarfHandling::Remap`].
783907
///
784908
/// Inspects the input components for `.debug_*` sections and, when

meld-core/tests/dwarf_remap_witness.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,73 @@ fn inc2_die_ranges_stay_within_enclosing_subprogram() {
286286
);
287287
eprintln!("inc2: {checked} DW_AT_ranges entries all contained in their subprogram");
288288
}
289+
290+
/// #319 inc 3: no `DW_AT_location` location-list entry may have an
291+
/// out-of-module range in the remapped output. Location-list entries carry
292+
/// base-relative begin/end offsets identical to `.debug_ranges` (inc 2);
293+
/// before the fix gimli mis-routed those offsets through `convert_address`,
294+
/// producing garbage ranges (e.g. `[0x905993c0, …)`) that derailed the
295+
/// list decode (`llvm-dwarfdump`: "Invalid DW_AT_location" / "Invalid DWARF
296+
/// expressions"). This fuses the fixture and asserts every location-list
297+
/// entry's range lies within the fused module's byte length — a garbage
298+
/// remap (≈2.4 GiB begin) fails; a correct fused-code offset (< a few MiB)
299+
/// passes.
300+
#[test]
301+
fn inc3_location_list_ranges_stay_in_module() {
302+
if !std::path::Path::new(RANGES_FIXTURE).is_file() {
303+
eprintln!("skipping: fixture not found at {RANGES_FIXTURE}");
304+
return;
305+
}
306+
let input = std::fs::read(RANGES_FIXTURE).expect("read fixture");
307+
let fused = fuse_remap(&input);
308+
// Generous upper bound: no real fused-code address exceeds the whole
309+
// module size; the pre-fix garbage (~0x905993c0) blows past it.
310+
let bound = fused.len() as u64;
311+
let sections = debug_sections(&fused);
312+
assert!(
313+
sections.contains_key(".debug_info"),
314+
"expected remapped DWARF"
315+
);
316+
317+
let endian = gimli::LittleEndian;
318+
let load = |id: gimli::SectionId| -> Result<gimli::EndianSlice<'_, gimli::LittleEndian>, gimli::Error> {
319+
let data = sections.get(id.name()).map(|v| v.as_slice()).unwrap_or(&[]);
320+
Ok(gimli::EndianSlice::new(data, endian))
321+
};
322+
let dwarf = gimli::Dwarf::load(load).expect("load output dwarf");
323+
324+
let mut checked = 0usize;
325+
let mut units = dwarf.units();
326+
while let Some(header) = units.next().expect("unit header") {
327+
let unit = dwarf.unit(header).expect("parse unit");
328+
let mut entries = unit.entries();
329+
while let Some((_, entry)) = entries.next_dfs().expect("dfs walk") {
330+
let Some(loc) = entry
331+
.attr_value(gimli::constants::DW_AT_location)
332+
.expect("location attr")
333+
else {
334+
continue;
335+
};
336+
// Only location LISTS (a bare exprloc → None).
337+
let Some(mut locs) = dwarf.attr_locations(&unit, loc).expect("attr_locations") else {
338+
continue;
339+
};
340+
while let Some(le) = locs.next().expect("loc entry") {
341+
assert!(
342+
le.range.begin <= le.range.end && le.range.end <= bound,
343+
"inc3: DW_AT_location entry range [{:#x},{:#x}) is out of the \
344+
fused module (len {bound:#x}) — a base-relative location \
345+
offset was remapped as an absolute address (#319)",
346+
le.range.begin,
347+
le.range.end
348+
);
349+
checked += 1;
350+
}
351+
}
352+
}
353+
assert!(
354+
checked > 0,
355+
"expected at least one DW_AT_location list entry to check"
356+
);
357+
eprintln!("inc3: {checked} DW_AT_location list entries all within the fused module");
358+
}

0 commit comments

Comments
 (0)