Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d764a05
add mock host for tests
ero-qt Aug 28, 2026
41cf503
add pe debug id read
ero-qt Aug 28, 2026
cdd85fe
add elf build id read
ero-qt Aug 28, 2026
7fedd82
add mach-o uuid read
ero-qt Aug 28, 2026
57d2fba
fix metadata handle typo
ero-qt Aug 28, 2026
1b384a0
add memory ranges to mock host
ero-qt Aug 28, 2026
a69bac8
add image route for assembly names
ero-qt Aug 28, 2026
3ed274e
fix il2cpp version detection for unity 2023
ero-qt Aug 28, 2026
881c126
add known il2cpp builds
ero-qt Aug 28, 2026
9946591
fix assembly walk reads for 32 bit targets
ero-qt Aug 28, 2026
36033e7
cut unread class image offset
ero-qt Aug 28, 2026
0fb98ba
add image route for assembly names
ero-qt Aug 28, 2026
60b5616
add known mono builds
ero-qt Aug 28, 2026
92a49f5
add walk parity tests
ero-qt Aug 28, 2026
217ab04
carve shared walk out of both backends
ero-qt Aug 28, 2026
5fbec19
add nested class lookup
ero-qt Aug 28, 2026
97f5d6f
add definition route for generic field counts
ero-qt Aug 28, 2026
6f7ca61
move declaring type offset before parent
ero-qt Aug 28, 2026
9464835
fix static reads for inherited fields
ero-qt Aug 28, 2026
290c9d0
add managed string reads
ero-qt Aug 29, 2026
15e557e
add managed array reads
ero-qt Aug 29, 2026
30a4f08
add managed list reads
ero-qt Aug 29, 2026
58a79a2
add dictionary offset resolution
ero-qt Aug 29, 2026
fd6bb49
add dictionary reads
ero-qt Aug 29, 2026
0f359f5
add hashset reads
ero-qt Aug 29, 2026
6cf714c
add parallel arrays shape for dictionaries and hashsets
ero-qt Aug 29, 2026
7d09664
add reference element reads
ero-qt Aug 29, 2026
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