Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
209 changes: 208 additions & 1 deletion src/file_format/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1074,7 +1074,7 @@ struct SymTab64 {
///
/// By using this function, the user must be aware of the following limitations:
/// - Only allocatable symbols and symbols used by the dynamic linker are exported
/// (.symtab is not loaded in memory at runtime)
/// (.symtab is not loaded in memory at runtime)
/// - Only 64-bit ELFs are supported (an empty iterator will be returned for 32-bit ELFs)
pub fn symbols(
process: &Process,
Expand Down Expand Up @@ -1157,3 +1157,210 @@ pub fn symbols(
})
.fuse()
}

/// The GNU build ID of an ELF module, read from its `NT_GNU_BUILD_ID` note.
/// The linker derives it from the built binary, so it names one exact build.
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct BuildId {
bytes: [u8; 32],
len: u8,
}

impl BuildId {
/// The bytes of the build ID.
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len as usize]
}
}

impl fmt::Debug for BuildId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in self.as_bytes() {
write!(f, "{byte:02x}")?;
}
Ok(())
}
}

/// Reads the GNU build ID from the notes of the ELF module starting at the
/// given address. Returns [`None`] if the module carries no such note, which
/// not every module does. Only little-endian ELFs are supported.
pub fn build_id(process: &Process, module_address: Address) -> Option<BuildId> {
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
#[repr(C)]
struct NoteHeader {
n_namesz: u32,
n_descsz: u32,
n_type: u32,
}

const NT_GNU_BUILD_ID: u32 = 3;

let header = process.read::<Header>(module_address).ok()?;
let info = Info::parse(bytemuck::bytes_of(&header))?;

if info.endian != Endian::Little {
return None;
}

let (e_phoff, e_phentsize, e_phnum) = if info.bitness.is_64() {
let header = process.read::<Elf64>(module_address).ok()?;
(header.e_phoff, header.e_phentsize as u64, header.e_phnum)
} else {
let header = process.read::<Elf32>(module_address).ok()?;
(
header.e_phoff as u64,
header.e_phentsize as u64,
header.e_phnum,
)
};

(0..e_phnum).find_map(|index| {
let at = module_address + e_phoff + e_phentsize.wrapping_mul(index as u64);

let (p_type, p_vaddr, p_filesz) = if info.bitness.is_64() {
let program_header = process.read::<ProgramHeader64>(at).ok()?;
(
program_header.p_type,
program_header.p_vaddr,
program_header.p_filesz,
)
} else {
let program_header = process.read::<ProgramHeader32>(at).ok()?;
(
program_header.p_type,
program_header.p_vaddr as u64,
program_header.p_filesz as u64,
)
};

if SegmentType(p_type) != SegmentType::PT_NOTE {
return None;
}

// A note is its header, the name, then the data, the latter two padded
// to four bytes.
let segment = module_address + p_vaddr;
let mut offset = 0;
// The walk is bounded so corrupt sizes can't turn it into a scan.
for _ in 0..0x10 {
if offset + size_of::<NoteHeader>() as u64 > p_filesz {
return None;
}

let note = process.read::<NoteHeader>(segment + offset).ok()?;
let name = offset + size_of::<NoteHeader>() as u64;
let desc = name + (note.n_namesz as u64).next_multiple_of(4);

if note.n_type == NT_GNU_BUILD_ID
&& note.n_namesz == 4
&& (1..=32).contains(&note.n_descsz)
&& desc + note.n_descsz as u64 <= p_filesz
&& process.read::<[u8; 4]>(segment + name).ok()? == *b"GNU\0"
{
let mut bytes = [0; 32];
process
.read_into_buf(segment + desc, &mut bytes[..note.n_descsz as usize])
.ok()?;
return Some(BuildId {
bytes,
len: note.n_descsz as u8,
});
}

offset = desc + (note.n_descsz as u64).next_multiple_of(4);
}

None
})
}

#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use super::build_id;
use crate::runtime::mock::with_process;

use std::{format, vec, vec::Vec};

const BASE: u64 = 0x7F12_3450_0000;

// The build ID of the 2019.1 mono runtime, a sha1 the linker computed.
const BUILD_ID: [u8; 20] = [
0xE6, 0xAA, 0x00, 0x0A, 0x9A, 0x52, 0x01, 0x63, 0x57, 0x43, 0xC6, 0xA1, 0xB2, 0x78, 0x1E,
0x05, 0x7D, 0x65, 0x1D, 0xAD,
];

fn put(image: &mut [u8], at: usize, bytes: &[u8]) {
image[at..at + bytes.len()].copy_from_slice(bytes);
}

// Builds a minimal mapped ELF by hand from the spec, so the walk is
// checked against the format rather than against itself: the header, a
// load and a note program header, and a note segment holding an ABI tag
// note followed by the build ID note.
fn image(wide: bool) -> Vec<u8> {
let mut image = vec![0; 0x400];
put(&mut image, 0x00, b"\x7fELF");
image[0x04] = if wide { 2 } else { 1 };
image[0x05] = 1;
image[0x06] = 1;
put(&mut image, 0x10, &3_u16.to_le_bytes());
if wide {
put(&mut image, 0x20, &0x40_u64.to_le_bytes());
put(&mut image, 0x36, &56_u16.to_le_bytes());
put(&mut image, 0x38, &2_u16.to_le_bytes());
put(&mut image, 0x40, &1_u32.to_le_bytes());
put(&mut image, 0x78, &4_u32.to_le_bytes());
put(&mut image, 0x88, &0x200_u64.to_le_bytes());
put(&mut image, 0x98, &0x44_u64.to_le_bytes());
} else {
put(&mut image, 0x1C, &0x34_u32.to_le_bytes());
put(&mut image, 0x2A, &32_u16.to_le_bytes());
put(&mut image, 0x2C, &2_u16.to_le_bytes());
put(&mut image, 0x34, &1_u32.to_le_bytes());
put(&mut image, 0x54, &4_u32.to_le_bytes());
put(&mut image, 0x5C, &0x200_u32.to_le_bytes());
put(&mut image, 0x64, &0x44_u32.to_le_bytes());
}
put(&mut image, 0x200, &4_u32.to_le_bytes());
put(&mut image, 0x204, &16_u32.to_le_bytes());
put(&mut image, 0x208, &1_u32.to_le_bytes());
put(&mut image, 0x20C, b"GNU\0");
put(&mut image, 0x220, &4_u32.to_le_bytes());
put(&mut image, 0x224, &20_u32.to_le_bytes());
put(&mut image, 0x228, &3_u32.to_le_bytes());
put(&mut image, 0x22C, b"GNU\0");
put(&mut image, 0x230, &BUILD_ID);
image
}

#[test]
fn reads_the_build_id_from_a_mapped_image() {
for wide in [true, false] {
with_process(&[(BASE, &image(wide))], |process| {
let build_id = build_id(process, BASE.into()).unwrap();
assert_eq!(build_id.as_bytes(), BUILD_ID);
});
}
}

#[test]
fn renders_the_id_as_hex() {
with_process(&[(BASE, &image(true))], |process| {
let build_id = build_id(process, BASE.into()).unwrap();
assert_eq!(
format!("{build_id:?}"),
"e6aa000a9a5201635743c6a1b2781e057d651dad",
);
});
}

#[test]
fn answers_nothing_without_a_note_segment() {
let mut image = image(true);
put(&mut image, 0x78, &0_u32.to_le_bytes());
with_process(&[(BASE, &image)], |process| {
assert!(build_id(process, BASE.into()).is_none());
});
}
}
Loading
Loading