Skip to content

Commit

Permalink
Add ElfObject::debug_link (#450)
Browse files Browse the repository at this point in the history
Adds ability to read the debug link info out of ELF files, this is an alternative mechanism to
the debug id to locate debug files.
  • Loading branch information
dureuill committed Nov 22, 2021
1 parent c30e34f commit 6bc23c1
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 2 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

**Features**:

- Add `ElfObject::debug_link` that allows recovering the [debug link](https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html) from an Elf if present. ([#450](https://github.com/getsentry/symbolic/pull/450))

## 8.5.0

**Features**:
Expand Down
151 changes: 151 additions & 0 deletions symbolic-debuginfo/src/elf.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
//! Support for the Executable and Linkable Format, used on Linux.

use std::borrow::Cow;
use std::cell::Cell;
use std::convert::TryInto;
use std::error::Error;
use std::ffi::CStr;
use std::fmt;
use std::io::Cursor;

Expand All @@ -15,6 +18,7 @@ use goblin::{
container::{Container, Ctx},
elf, strtab,
};
use nom::InputTakeAtPosition;
use scroll::Pread;
use thiserror::Error;

Expand Down Expand Up @@ -354,6 +358,22 @@ impl<'data> ElfObject<'data> {
.map(CodeId::from_binary)
}

/// The debug link of this object.
///
/// The debug link is an alternative to the build id for specifying the location
/// of an ELF's debugging information. It refers to a filename that can be used
/// to build various debug paths where debuggers can look for the debug files.
///
/// # Errors
///
/// - None if there is no gnu_debuglink section
/// - DebugLinkError if this section exists, but is malformed
pub fn debug_link(&self) -> Result<Option<DebugLink>, DebugLinkError> {
self.section("gnu_debuglink")
.map(|section| DebugLink::from_data(section.data, self.endianity()))
.transpose()
}

/// The binary's soname, if any.
pub fn name(&self) -> Option<&'data str> {
self.elf.soname
Expand Down Expand Up @@ -881,3 +901,134 @@ impl<'data, 'object> Iterator for ElfSymbolIterator<'data, 'object> {
})
}
}

/// Parsed debug link section.
#[derive(Debug)]
pub struct DebugLink<'data> {
filename: Cow<'data, CStr>,
crc: u32,
}

impl<'data> DebugLink<'data> {
/// Attempts to parse a debug link section from its data.
///
/// The expected format for the section is:
///
/// - A filename, with any leading directory components removed, followed by a zero byte,
/// - zero to three bytes of padding, as needed to reach the next four-byte boundary within the section, and
/// - a four-byte CRC checksum, stored in the same endianness used for the executable file itself.
/// (from <https://sourceware.org/gdb/current/onlinedocs/gdb/Separate-Debug-Files.html#index-_002egnu_005fdebuglink-sections>)
///
/// # Errors
///
/// If the section data is malformed, in particular:
/// - No NUL byte delimiting the filename from the CRC
/// - Not enough space for the CRC checksum
pub fn from_data(
data: Cow<'data, [u8]>,
endianity: Endian,
) -> Result<Self, DebugLinkError<'data>> {
match data {
Cow::Owned(data) => {
let (filename, crc) = Self::from_borrowed_data(&data, endianity)
.map(|(filename, crc)| (filename.to_owned(), crc))
.map_err(|kind| DebugLinkError {
kind,
data: Cow::Owned(data),
})?;
Ok(Self {
filename: Cow::Owned(filename),
crc,
})
}
Cow::Borrowed(data) => {
let (filename, crc) =
Self::from_borrowed_data(data, endianity).map_err(|kind| DebugLinkError {
kind,
data: Cow::Borrowed(data),
})?;
Ok(Self {
filename: Cow::Borrowed(filename),
crc,
})
}
}
}

fn from_borrowed_data(
data: &[u8],
endianity: Endian,
) -> Result<(&CStr, u32), DebugLinkErrorKind> {
// split_at_position is not inclusive, so does not contain the NUL:
// use a cell to remember the last character that was seen, then exit only on the next character (the NUL).
// This is OK because we have another character after the NUL (CRC or padding).
let last_seen: Cell<Option<u8>> = Cell::new(None);
let (crc, filename) = data
.split_at_position(|c| {
let c = last_seen.replace(Some(c));
if let Some(c) = c {
c == b'\0'
} else {
false
}
})
.map_err(|_: nom::Err<()>| DebugLinkErrorKind::MissingNul)?;

// let's be liberal and assume that the padding is correct and all 0s,
// and just check that we have enough remaining length for the CRC.
let crc = crc
.get(crc.len() - 4..)
.ok_or_else(|| DebugLinkErrorKind::MissingCrc {
filename_len_with_nul: filename.len(),
})?;

let crc: [u8; 4] = crc.try_into().map_err(|_| DebugLinkErrorKind::MissingCrc {
filename_len_with_nul: filename.len(),
})?;

let crc = match endianity {
Endian::Little => u32::from_le_bytes(crc),
Endian::Big => u32::from_be_bytes(crc),
};

let filename =
CStr::from_bytes_with_nul(filename).map_err(|_| DebugLinkErrorKind::MissingNul)?;

Ok((filename, crc))
}

/// The debug link filename
pub fn filename(&self) -> &CStr {
&self.filename
}

/// The CRC checksum associated with the debug link file
pub fn crc(&self) -> u32 {
self.crc
}
}

/// Kind of errors that can occur while parsing a debug link section.
#[derive(Debug, Error)]
pub enum DebugLinkErrorKind {
/// No NUL byte delimiting the filename from the CRC
#[error("missing NUL character")]
MissingNul,
/// Not enough space in the section data for the CRC checksum
#[error("missing CRC")]
MissingCrc {
/// Size of the filename part of the section including the NUL character
filename_len_with_nul: usize,
},
}

/// Errors that can occur while parsing a debug link section.
#[derive(Debug, Error)]
#[error("could not parse debug link section")]
pub struct DebugLinkError<'data> {
#[source]
/// The kind of error that occurred.
pub kind: DebugLinkErrorKind,
/// The original data of the debug section.
pub data: Cow<'data, [u8]>,
}
59 changes: 57 additions & 2 deletions symbolic-debuginfo/tests/test_objects.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::fmt;
use std::{ffi::CString, fmt};

use symbolic_common::ByteView;
use symbolic_debuginfo::{FileEntry, Function, Object, SymbolMap};
use symbolic_debuginfo::{elf::ElfObject, FileEntry, Function, Object, SymbolMap};
use symbolic_testutils::fixture;

use similar_asserts::assert_eq;
Expand Down Expand Up @@ -234,6 +234,61 @@ fn test_elf_functions() -> Result<(), Error> {
Ok(())
}

fn elf_debug_crc() -> Result<u32, Error> {
Ok(u32::from_str_radix(
std::fs::read_to_string(fixture("linux/elf_debuglink/gen/debug_info.txt.crc"))?.trim(),
16,
)?)
}

fn check_debug_info(filename: &'static str, debug_info: &'static str) -> Result<(), Error> {
let view = ByteView::open(fixture("linux/elf_debuglink/gen/".to_owned() + filename))?;

let object = ElfObject::parse(&view)?;

let debug_link = object
.debug_link()
.map_err(|err| err.kind)?
.expect("debug link not found");

assert_eq!(debug_link.filename(), CString::new(debug_info)?.as_c_str(),);
assert_eq!(debug_link.crc(), elf_debug_crc()?);

Ok(())
}

#[test]
fn test_elf_debug_link() -> Result<(), Error> {
check_debug_info("elf_with_debuglink", "debug_info.txt")
}

#[test]
fn test_elf_debug_link_none() -> Result<(), Error> {
let view = ByteView::open(fixture("linux/elf_debuglink/gen/elf_without_debuglink"))?;

let object = ElfObject::parse(&view)?;

let debug_link = object.debug_link().map_err(|err| err.kind)?;
assert!(debug_link.is_none(), "debug link unexpectedly found");

Ok(())
}

#[test]
// Test that the size of the debug_info filename doesn't influence the result.
fn test_elf_debug_link_padding() -> Result<(), Error> {
check_debug_info("elf_with1_debuglink", "debug_info1.txt")?;
check_debug_info("elf_with12_debuglink", "debug_info12.txt")?;
check_debug_info("elf_with123_debuglink", "debug_info123.txt")
}

#[test]
// Test with a compressed gnu_debuglink section. This exerts the "Owned" path of the code,
// while without compression the "Borrowed" is exerted.
fn test_elf_debug_link_compressed() -> Result<(), Error> {
check_debug_info("elf_with_compressed_debuglink", "debug_info.txt")
}

#[test]
fn test_mach_executable() -> Result<(), Error> {
let view = ByteView::open(fixture("macos/crash"))?;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
abf39bdc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/bin/bash

# This script was used to generate the fixtures in the "gen" directory, used to test the
# `ElfObject::debug_link` method.

# Pre-requisites:
#
# - gcc
# - eu-elfcompress (elfutils)
# - objcopy (GNU Binary Utilities)
# - crc32 (perl-archive-zip)

OUTPUT=gen

# 0. Clean and remake output directory, switch to it
rm -rf $OUTPUT
mkdir -p $OUTPUT
cd $OUTPUT

# 1. compile our C example. To keep size low, let's compile the simplest program we can write.
gcc -x c -Os -o elf_without_debuglink - << EOF
int main() {
return 0;
}
EOF

# 2. generate some fake debug file. objcopy doesn't require the file to be a proper debug file,
# only that it exists (to compute its CRC and embed it in the section).
# Let's use a simple text file.
echo "Fake debug info" > debug_info.txt

# 3. compute the expected CRC for debug_info.txt.
# This will be used in tests to check we find the correct CRC.
crc32 debug_info.txt > debug_info.txt.crc

# 4. Add the debug info to a copy of our binary
objcopy --add-gnu-debuglink=debug_info.txt elf_{without,with}_debuglink

# 5. To test for the various possible paddings, also add debug info
# with different-sized filenames
cp debug_info{,1}.txt && objcopy --add-gnu-debuglink=debug_info1.txt elf_{without,with1}_debuglink
cp debug_info{,12}.txt && objcopy --add-gnu-debuglink=debug_info12.txt elf_{without,with12}_debuglink
cp debug_info{,123}.txt && objcopy --add-gnu-debuglink=debug_info123.txt elf_{without,with123}_debuglink

# 6. To test the "Owned" case, let's make a copy of the ELF with a compressed section
eu-elfcompress -v --force --name ".gnu_debuglink" -t zlib -o elf_with{_compressed,}_debuglink

# 7. Remove debug info files that aren't actually needed by the tests
rm *.txt

0 comments on commit 6bc23c1

Please sign in to comment.