Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- Patch potential aborts in the vendored Swift demangler. ([#1054](https://github.com/getsentry/symbolic/pull/1054), [#1059](https://github.com/getsentry/symbolic/pull/1059))
- Do not allocate based on untrusted input in unreal parser. ([#1055](https://github.com/getsentry/symbolic/pull/1055))
- Ensure unwind sections contain any unwind information. ([#1058](https://github.com/getsentry/symbolic/pull/1058))
- Apply ELF relocations to debug sections in relocatable objects, fixing zero-valued file/directory names read from unlinked `.o` files. ([#1060](https://github.com/getsentry/symbolic/pull/1060))

**Dependencies**

Expand Down
113 changes: 107 additions & 6 deletions symbolic-debuginfo/src/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ impl<'data> ElfObject<'data> {
//
// See also: <https://github.com/getsentry/symbolicator/issues/2025>
let has_unwind_info = |name: &str| {
let Some((compressed, section)) = self.find_section(name) else {
let Some((_, compressed, section)) = self.find_section(name) else {
return false;
};

Expand Down Expand Up @@ -683,8 +683,8 @@ impl<'data> ElfObject<'data> {
}

/// Locates and reads a section in an ELF binary.
fn find_section(&self, name: &str) -> Option<(bool, DwarfSection<'data>)> {
for header in &self.elf.section_headers {
fn find_section(&self, name: &str) -> Option<(usize, bool, DwarfSection<'data>)> {
for (idx, header) in self.elf.section_headers.iter().enumerate() {
// The section type is usually SHT_PROGBITS, but some compilers also use
// SHT_X86_64_UNWIND and SHT_MIPS_DWARF. We apply the same approach as elfutils,
// matching against SHT_NOBITS, instead.
Expand Down Expand Up @@ -730,13 +730,105 @@ impl<'data> ElfObject<'data> {
align: header.sh_addralign,
};

return Some((compressed, section));
return Some((idx, compressed, section));
}
}

None
}

/// The (32-bit, 64-bit) "absolute value" relocation type pair for this object's `e_machine`,
/// or `None` if it isn't one this crate applies relocations for.
///
/// Relocation type numbers are only meaningful per-machine: type 1 is `R_X86_64_64` on
/// x86_64, but `R_PPC_ADDR32` on PowerPC and `R_RISCV_32` on RISC-V.
fn absolute_reloc_types(&self) -> Option<(u32, u32)> {
match self.elf.header.e_machine {
elf::header::EM_X86_64 => Some((elf::reloc::R_X86_64_32, elf::reloc::R_X86_64_64)),
elf::header::EM_AARCH64 => {
Some((elf::reloc::R_AARCH64_ABS32, elf::reloc::R_AARCH64_ABS64))
}
_ => None,
}
}

/// Whether any relocation applies to `section_idx`, so callers can skip `into_owned()`'s
/// copy of the section bytes when there is nothing to patch. Keep in sync with
/// `apply_section_relocations`'s own matching criteria below.
fn section_has_applicable_relocations(&self, section_idx: usize) -> bool {
let Some((r32, r64)) = self.absolute_reloc_types() else {
return false;
};

self.elf.shdr_relocs.iter().any(|(rela_idx, relocs)| {
self.elf
.section_headers
.get(*rela_idx)
.is_some_and(|header| header.sh_info as usize == section_idx)
&& relocs
.iter()
.any(|reloc| reloc.r_type == r32 || reloc.r_type == r64)
})
}

/// Applies ELF relocations to a raw debug section's bytes, matching relocation sections
/// to `section_idx` via `sh_info` rather than by name.
///
/// Unlinked relocatable objects (`ET_REL`, plain `.o` files) leave `DW_FORM_line_strp`/
/// `DW_FORM_strp` offsets in `.debug_line`/`.debug_info` as zero placeholders, with a
/// companion relocation recording the real offset into `.debug_line_str`/`.debug_str`
/// (not fixed until link time). `self.elf.shdr_relocs` is already parsed but was never
/// applied, so every such field silently read back as offset 0.
Comment thread
darktorres marked this conversation as resolved.
Outdated
fn apply_section_relocations(&self, section_idx: usize, data: &mut [u8]) {
let Some((r32, r64)) = self.absolute_reloc_types() else {
return;
};

for (rela_idx, relocs) in &self.elf.shdr_relocs {
let Some(rela_header) = self.elf.section_headers.get(*rela_idx) else {
continue;
};
if rela_header.sh_info as usize != section_idx {
continue;
}

for reloc in relocs.iter() {
let width = match reloc.r_type {
t if t == r32 => 4usize,
t if t == r64 => 8usize,
_ => continue,
Comment thread
cursor[bot] marked this conversation as resolved.
};

// Uses `r_addend` directly, assuming a section-relative symbol (value 0),
// true for every compiler's debug-section relocations. `r_addend` is `None`
// only for REL (not RELA) sections, which x86_64/AArch64 never use.
let Some(addend) = reloc.r_addend else {
continue;
};
let offset = reloc.r_offset as usize;
// r_offset is unchecked file input, so guard against overflow rather than
// panicking on a malformed or adversarial object.
let Some(end) = offset.checked_add(width) else {
continue;
};
if end > data.len() {
continue;
}

let value = addend as u64;
Comment thread
sentry[bot] marked this conversation as resolved.
let mut buf = [0u8; 8];
let patched = if self.elf.little_endian {
buf.copy_from_slice(&value.to_le_bytes());
&buf[..width]
} else {
buf.copy_from_slice(&value.to_be_bytes());
&buf[8 - width..]
};
data[offset..end].copy_from_slice(patched);
}
}
}

/// Searches for a GNU build identifier node in an ELF file.
///
/// Depending on the compiler and linker, the build ID can be declared in a
Expand Down Expand Up @@ -917,18 +1009,27 @@ impl<'data> Dwarf<'data> for ElfObject<'data> {
}

fn raw_section(&self, name: &str) -> Option<DwarfSection<'data>> {
let (_, section) = self.find_section(name)?;
let (_, _, section) = self.find_section(name)?;
Some(section)
}

fn section(&self, name: &str) -> Option<DwarfSection<'data>> {
let (compressed, mut section) = self.find_section(name)?;
let (idx, compressed, mut section) = self.find_section(name)?;

if compressed {
let decompressed = self.decompress_section(&section.data)?;
section.data = Cow::Owned(decompressed);
}

if self.elf.header.e_type == elf::header::ET_REL {
// Skip into_owned()'s copy for sections with nothing to patch (the common case).
if self.section_has_applicable_relocations(idx) {
let mut owned = section.data.into_owned();
self.apply_section_relocations(idx, &mut owned);
section.data = Cow::Owned(owned);
}
}

Some(section)
}
}
Expand Down
24 changes: 24 additions & 0 deletions symbolic-debuginfo/tests/test_objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,30 @@ fn test_elf_files() -> Result<(), Error> {
Ok(())
}

#[test]
fn test_elf_unlinked_relocations_resolve_correctly() -> Result<(), Error> {
// Regression test for unapplied ELF relocations leaving DW_FORM_line_strp/strp offsets
// in an unlinked object (ET_REL) reading back as 0. See apply_section_relocations in elf.rs.
let view = ByteView::open(fixture("linux/relocations/unlinked.o"))?;
let object = Object::parse(&view)?;
assert_eq!(object.kind(), ObjectKind::Relocatable);

let session = object.debug_session()?;
let files = session.files().collect::<Result<Vec<_>, _>>()?;

let example = files
.iter()
.find(|f| f.name_str() == "example.c")
.expect("fixture must reference example.c");

// dir_str()/name_str(), not abs_path_str(), so the assertion doesn't depend on the
// fixture's original build machine's absolute compilation directory.
assert_eq!(example.dir_str(), "subdir");
assert_eq!(example.name_str(), "example.c");

Ok(())
}

#[test]
fn test_elf_functions() -> Result<(), Error> {
let view = ByteView::open(fixture("linux/crash.debug"))?;
Expand Down
5 changes: 5 additions & 0 deletions symbolic-testutils/fixtures/linux/relocations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
`unlinked.o` is an unlinked ELF relocatable object (`ET_REL`) compiled from a source file in a
subdirectory, to reproduce a bug where unapplied ELF relocations left `DW_FORM_line_strp`/
`DW_FORM_strp` offsets reading back as 0 (see `apply_section_relocations` in `elf.rs`).

The file was obtained using `mkdir -p subdir && echo 'int example_function(void) { return 0; }' > subdir/example.c && gcc -g -c subdir/example.c -o unlinked.o`.
Binary file not shown.
Loading