diff --git a/CHANGELOG.md b/CHANGELOG.md index dd7b31ebb..55e235149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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** diff --git a/symbolic-debuginfo/src/elf.rs b/symbolic-debuginfo/src/elf.rs index 99d33c894..0a48154ee 100644 --- a/symbolic-debuginfo/src/elf.rs +++ b/symbolic-debuginfo/src/elf.rs @@ -593,7 +593,7 @@ impl<'data> ElfObject<'data> { // // See also: 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; }; @@ -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. @@ -730,13 +730,104 @@ 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). + 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, + }; + + // 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; + 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 @@ -917,18 +1008,27 @@ impl<'data> Dwarf<'data> for ElfObject<'data> { } fn raw_section(&self, name: &str) -> Option> { - let (_, section) = self.find_section(name)?; + let (_, _, section) = self.find_section(name)?; Some(section) } fn section(&self, name: &str) -> Option> { - let (compressed, mut section) = self.find_section(name)?; + let (idx, compressed, mut section) = self.find_section(name)?; if compressed { let decompressed = self.decompress_section(§ion.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) } } diff --git a/symbolic-debuginfo/tests/test_objects.rs b/symbolic-debuginfo/tests/test_objects.rs index 70d45b3c1..dab7c3368 100644 --- a/symbolic-debuginfo/tests/test_objects.rs +++ b/symbolic-debuginfo/tests/test_objects.rs @@ -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::, _>>()?; + + 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"))?; diff --git a/symbolic-testutils/fixtures/linux/relocations/README.md b/symbolic-testutils/fixtures/linux/relocations/README.md new file mode 100644 index 000000000..93f134d62 --- /dev/null +++ b/symbolic-testutils/fixtures/linux/relocations/README.md @@ -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`. diff --git a/symbolic-testutils/fixtures/linux/relocations/unlinked.o b/symbolic-testutils/fixtures/linux/relocations/unlinked.o new file mode 100644 index 000000000..1950759bc Binary files /dev/null and b/symbolic-testutils/fixtures/linux/relocations/unlinked.o differ