diff --git a/src/file_format/elf.rs b/src/file_format/elf.rs index 179bfed..56c3efa 100644 --- a/src/file_format/elf.rs +++ b/src/file_format/elf.rs @@ -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, @@ -1157,3 +1157,274 @@ 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 in the given +/// range. 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, range: (Address, u64)) -> Option { + #[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 (module_address, module_size) = range; + let header = process.read::
(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::(module_address).ok()?; + (header.e_phoff, header.e_phentsize as u64, header.e_phnum) + } else { + let header = process.read::(module_address).ok()?; + ( + header.e_phoff as u64, + header.e_phentsize as u64, + header.e_phnum, + ) + }; + + let program_header = |index: u16| { + let at = module_address + e_phoff + e_phentsize.wrapping_mul(index as u64); + if info.bitness.is_64() { + let header = process.read::(at).ok()?; + Some(( + header.p_type, + header.p_offset, + header.p_vaddr, + header.p_filesz, + )) + } else { + let header = process.read::(at).ok()?; + Some(( + header.p_type, + header.p_offset as u64, + header.p_vaddr as u64, + header.p_filesz as u64, + )) + } + }; + + // A shared object's addresses count from wherever it landed, and an + // executable's are absolute. Either way the load segment at file offset + // zero holds this header, and the module address is where that segment + // landed, so the difference between the two is what every other address + // needs added. + let header_vaddr = (0..e_phnum) + .filter_map(program_header) + .find(|&(p_type, p_offset, ..)| { + SegmentType(p_type) == SegmentType::PT_LOAD && p_offset == 0 + }) + .map(|(_, _, p_vaddr, _)| p_vaddr)?; + let bias = module_address.value().wrapping_sub(header_vaddr); + let module_end = module_address.value().saturating_add(module_size); + + (0..e_phnum) + .filter_map(program_header) + .find_map(|(p_type, _, p_vaddr, p_filesz)| { + if SegmentType(p_type) != SegmentType::PT_NOTE { + return None; + } + + // The segment has to lie inside the module, which is what bounds + // the walk. + let segment = bias.wrapping_add(p_vaddr); + if segment < module_address.value() || segment.checked_add(p_filesz)? > module_end { + return None; + } + let segment = Address::new(segment); + + // A note is its header, the name, then the data, the latter two + // padded to four bytes. + let mut offset = 0; + while offset + size_of::() as u64 <= p_filesz { + let note = process.read::(segment + offset).ok()?; + let name = offset + size_of::() 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(¬e.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 { + 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(), 0x400)).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(), 0x400)).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(), 0x400)).is_none()); + }); + } + + // An executable's segments carry absolute addresses, and one built without + // position independence is mapped exactly where they say. + #[test] + fn reads_the_build_id_from_an_executable_mapped_at_its_own_address() { + const EXECUTABLE_BASE: u64 = 0x40_0000; + let mut image = image(true); + put(&mut image, 0x10, &2_u16.to_le_bytes()); + put(&mut image, 0x50, &EXECUTABLE_BASE.to_le_bytes()); + put(&mut image, 0x88, &(EXECUTABLE_BASE + 0x200).to_le_bytes()); + with_process(&[(EXECUTABLE_BASE, &image)], |process| { + let build_id = build_id(process, (EXECUTABLE_BASE.into(), 0x400)).unwrap(); + assert_eq!(build_id.as_bytes(), BUILD_ID); + }); + } + + #[test] + fn answers_nothing_when_the_note_segment_overruns_the_module() { + let mut image = image(true); + put(&mut image, 0x98, &0x1000_u64.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert!(build_id(process, (BASE.into(), 0x400)).is_none()); + }); + } + + #[test] + fn reads_a_build_id_past_the_sixteenth_note() { + let mut image = image(true); + // Seventeen empty notes ahead of the build ID, all in one segment. + put(&mut image, 0x200, &[0; 0x44]); + let at = 0x200 + 17 * 12; + put(&mut image, at, &4_u32.to_le_bytes()); + put(&mut image, at + 4, &20_u32.to_le_bytes()); + put(&mut image, at + 8, &3_u32.to_le_bytes()); + put(&mut image, at + 12, b"GNU\0"); + put(&mut image, at + 16, &BUILD_ID); + put(&mut image, 0x98, &((at + 36 - 0x200) as u64).to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + let build_id = build_id(process, (BASE.into(), 0x400)).unwrap(); + assert_eq!(build_id.as_bytes(), BUILD_ID); + }); + } +} diff --git a/src/file_format/macho.rs b/src/file_format/macho.rs index 147a24c..a34bed8 100644 --- a/src/file_format/macho.rs +++ b/src/file_format/macho.rs @@ -1,11 +1,15 @@ //! Support for parsing Mach-O format +use core::{fmt, mem}; + #[cfg(feature = "alloc")] use core::iter::FusedIterator; #[cfg(feature = "alloc")] use alloc::collections::BTreeMap; +use bytemuck::{Pod, Zeroable}; + #[cfg(feature = "alloc")] use crate::{string::ArrayCString, Error}; use crate::{Address, PointerSize, Process}; @@ -36,11 +40,8 @@ fn scan_macho_page(process: &Process, range: (Address, u64)) -> Option
let first_page = addr + distance_to_page; for i in 0..((len - distance_to_page) / PAGE_SIZE) { let a = first_page + (i * PAGE_SIZE); - match process.read::(a) { - Ok(MH_MAGIC_64 | MH_CIGAM_64 | MH_MAGIC_32 | MH_CIGAM_32) => { - return Some(a); - } - _ => (), + if let Ok(MH_MAGIC_64 | MH_CIGAM_64 | MH_MAGIC_32 | MH_CIGAM_32) = process.read::(a) { + return Some(a); } } None @@ -71,6 +72,8 @@ fn scan_macho_pages( // Constants for the cmd field of load commands, the type // https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html +/// the uuid +const LC_UUID: u32 = 0x1b; /// link-edit stab symbol table info #[cfg(feature = "alloc")] const LC_SYMTAB: u32 = 0x2; @@ -78,6 +81,101 @@ const LC_SYMTAB: u32 = 0x2; #[cfg(feature = "alloc")] const LC_SEGMENT_64: u32 = 0x19; +/// The UUID of a Mach-O module, from its `LC_UUID` load command. The linker +/// derives it from the built binary, so it names one exact build. +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct Uuid { + /// The bytes of the UUID. + pub bytes: [u8; 16], +} + +impl fmt::Debug for Uuid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, byte) in self.bytes.iter().enumerate() { + if let 4 | 6 | 8 | 10 = i { + f.write_str("-")?; + } + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +/// Reads the UUID from the load commands of the Mach-O module in the given +/// range. Returns [`None`] if the module carries no `LC_UUID` command, or is +/// 32-bit or big-endian. macOS has not run 32-bit software since 2019, and +/// Unity dropped PowerPC, the only big-endian Mac, with Unity 3. +pub fn uuid(process: &Process, range: (Address, u64)) -> Option { + #[derive(Debug, Copy, Clone, Zeroable, Pod)] + #[repr(C)] + struct MachHeader { + magic: u32, + cputype: u32, + cpusubtype: u32, + filetype: u32, + ncmds: u32, + sizeofcmds: u32, + flags: u32, + } + + #[derive(Debug, Copy, Clone, Zeroable, Pod)] + #[repr(C)] + struct LoadCommand { + cmd: u32, + cmdsize: u32, + } + + let page = scan_macho_page(process, range)?; + let header = process.read::(page).ok()?; + + // Anything but the 64-bit little-endian magic is 32-bit or big-endian, + // and nothing here decodes either. The 64-bit header ends with one + // reserved field. + if header.magic != MH_MAGIC_64 { + return None; + } + let commands = page + (mem::size_of::() + mem::size_of::()) as u64; + + // The command table has to lie inside the module, which is what bounds + // the walk. + let (module_address, module_size) = range; + let table_size = header.sizeofcmds as u64; + let module_end = module_address.value().saturating_add(module_size); + if commands + .value() + .checked_add(table_size) + .is_none_or(|end| end > module_end) + { + return None; + } + + let mut offset = 0; + for _ in 0..header.ncmds { + if offset + mem::size_of::() as u64 > table_size { + return None; + } + let command = process.read::(commands + offset).ok()?; + let size = command.cmdsize as u64; + if size < mem::size_of::() as u64 || offset + size > table_size { + return None; + } + + if command.cmd == LC_UUID { + if size < (mem::size_of::() + mem::size_of::()) as u64 { + return None; + } + return process + .read::<[u8; 16]>(commands + offset + mem::size_of::() as u64) + .ok() + .map(|bytes| Uuid { bytes }); + } + + offset += size; + } + + None +} + #[cfg(feature = "alloc")] struct MachOFormatOffsets { number_of_commands: u32, @@ -205,3 +303,114 @@ fn fileoff_to_vmaddr(map: &BTreeMap, fileoff: u64) -> u64 { .map(|(&k, &v)| v + fileoff - k) .unwrap_or(fileoff) } + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::uuid; + use crate::runtime::mock::with_process; + + use std::{format, vec, vec::Vec}; + + const BASE: u64 = 0x1_0000_0000; + + // The UUID of the 2019.4 mono runtime shipped with a real mac player. + const UUID: [u8; 16] = [ + 0xE7, 0x42, 0x0B, 0xC7, 0xA2, 0x6B, 0x33, 0xFA, 0xB5, 0xCD, 0x41, 0xCA, 0xD7, 0xD4, 0x61, + 0x4C, + ]; + + fn put(image: &mut [u8], at: usize, bytes: &[u8]) { + image[at..at + bytes.len()].copy_from_slice(bytes); + } + + // Builds a minimal mapped Mach-O by hand from the loader header, so the + // walk is checked against the format rather than against itself: the + // header declaring its command table, a segment command, and the uuid + // command. + fn image() -> Vec { + let mut image = vec![0; 0x1000]; + put(&mut image, 0x00, &0xFEEDFACF_u32.to_le_bytes()); + put(&mut image, 0x10, &2_u32.to_le_bytes()); + put(&mut image, 0x14, &(0x48 + 24_u32).to_le_bytes()); + let commands = 0x20; + put(&mut image, commands, &0x19_u32.to_le_bytes()); + put(&mut image, commands + 0x4, &0x48_u32.to_le_bytes()); + put(&mut image, commands + 0x48, &0x1B_u32.to_le_bytes()); + put(&mut image, commands + 0x4C, &24_u32.to_le_bytes()); + put(&mut image, commands + 0x50, &UUID); + image + } + + #[test] + fn reads_the_uuid_from_a_mapped_image() { + with_process(&[(BASE, &image())], |process| { + let uuid = uuid(process, (BASE.into(), 0x1000)).unwrap(); + assert_eq!(uuid.bytes, UUID); + }); + } + + #[test] + fn answers_nothing_for_a_32_bit_image() { + let mut image = image(); + put(&mut image, 0x00, &0xFEEDFACE_u32.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert!(uuid(process, (BASE.into(), 0x1000)).is_none()); + }); + } + + #[test] + fn renders_the_uuid_canonically() { + with_process(&[(BASE, &image())], |process| { + let uuid = uuid(process, (BASE.into(), 0x1000)).unwrap(); + assert_eq!(format!("{uuid:?}"), "e7420bc7-a26b-33fa-b5cd-41cad7d4614c"); + }); + } + + #[test] + fn answers_nothing_without_a_uuid_command() { + let mut image = image(); + put(&mut image, 0x20 + 0x48, &0_u32.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert!(uuid(process, (BASE.into(), 0x1000)).is_none()); + }); + } + + #[test] + fn answers_nothing_when_the_commands_overrun_the_module() { + let mut image = image(); + put(&mut image, 0x14, &0x2000_u32.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert!(uuid(process, (BASE.into(), 0x1000)).is_none()); + }); + } + + #[test] + fn reads_the_uuid_past_the_sixty_fourth_command() { + let mut image = vec![0; 0x1000]; + put(&mut image, 0x00, &0xFEEDFACF_u32.to_le_bytes()); + put(&mut image, 0x10, &66_u32.to_le_bytes()); + put(&mut image, 0x14, &(65 * 8 + 24_u32).to_le_bytes()); + // Sixty-five empty commands ahead of the uuid one. + let mut at = 0x20; + for _ in 0..65 { + put(&mut image, at + 4, &8_u32.to_le_bytes()); + at += 8; + } + put(&mut image, at, &0x1B_u32.to_le_bytes()); + put(&mut image, at + 4, &24_u32.to_le_bytes()); + put(&mut image, at + 8, &UUID); + with_process(&[(BASE, &image)], |process| { + let uuid = uuid(process, (BASE.into(), 0x1000)).unwrap(); + assert_eq!(uuid.bytes, UUID); + }); + } + + #[test] + fn answers_nothing_for_a_big_endian_image() { + let mut image = image(); + put(&mut image, 0x00, &0xFEEDFACF_u32.to_be_bytes()); + with_process(&[(BASE, &image)], |process| { + assert!(uuid(process, (BASE.into(), 0x1000)).is_none()); + }); + } +} diff --git a/src/file_format/pe.rs b/src/file_format/pe.rs index 46f63cb..6d5f862 100644 --- a/src/file_format/pe.rs +++ b/src/file_format/pe.rs @@ -95,6 +95,94 @@ struct OptionalCOFFHeader { // There's more but those vary depending on whether it's PE or PE+. } +// The magic at the head of the optional header decides between the PE32 and +// PE32+ layouts. +const OPTIONAL_HEADER_MAGIC_PE32: u16 = 0x10B; +const OPTIONAL_HEADER_MAGIC_PE32_PLUS: u16 = 0x20B; + +/// An entry of the data directory array at the end of the optional header, +/// naming where one of the image's tables lives. +#[derive(Debug, Copy, Clone, Zeroable, Pod)] +#[repr(C)] +struct DataDirectory { + virtual_address: u32, + size: u32, +} + +/// The full PE32 optional header. +#[derive(Debug, Copy, Clone, Zeroable, Pod)] +#[repr(C)] +struct OptionalHeader32 { + magic: u16, + major_linker_version: u8, + minor_linker_version: u8, + size_of_code: u32, + size_of_initialized_data: u32, + size_of_uninitialized_data: u32, + address_of_entry_point: u32, + base_of_code: u32, + base_of_data: u32, + image_base: u32, + section_alignment: u32, + file_alignment: u32, + major_operating_system_version: u16, + minor_operating_system_version: u16, + major_image_version: u16, + minor_image_version: u16, + major_subsystem_version: u16, + minor_subsystem_version: u16, + win32_version_value: u32, + size_of_image: u32, + size_of_headers: u32, + checksum: u32, + subsystem: u16, + dll_characteristics: u16, + size_of_stack_reserve: u32, + size_of_stack_commit: u32, + size_of_heap_reserve: u32, + size_of_heap_commit: u32, + loader_flags: u32, + number_of_rva_and_sizes: u32, + data_directories: [DataDirectory; 16], +} + +/// The full PE32+ optional header, which drops `base_of_data` and widens the +/// image base and the stack and heap sizes. +#[derive(Debug, Copy, Clone, Zeroable, Pod)] +#[repr(C)] +struct OptionalHeader64 { + magic: u16, + major_linker_version: u8, + minor_linker_version: u8, + size_of_code: u32, + size_of_initialized_data: u32, + size_of_uninitialized_data: u32, + address_of_entry_point: u32, + base_of_code: u32, + image_base: u64, + section_alignment: u32, + file_alignment: u32, + major_operating_system_version: u16, + minor_operating_system_version: u16, + major_image_version: u16, + minor_image_version: u16, + major_subsystem_version: u16, + minor_subsystem_version: u16, + win32_version_value: u32, + size_of_image: u32, + size_of_headers: u32, + checksum: u32, + subsystem: u16, + dll_characteristics: u16, + size_of_stack_reserve: u64, + size_of_stack_commit: u64, + size_of_heap_reserve: u64, + size_of_heap_commit: u64, + loader_flags: u32, + number_of_rva_and_sizes: u32, + data_directories: [DataDirectory; 16], +} + #[derive(Debug, Copy, Clone, Zeroable, Pod, Default)] #[repr(C)] struct ExportedSymbolsTableDef { @@ -484,3 +572,295 @@ impl FileVersion { .map(|val| val.file_version) } } + +/// The identity of the debug information of a PE module, as recorded in the +/// module's CodeView debug directory entry. Every build of a module gets a +/// fresh identity, so it names one exact binary: symbol servers key their +/// downloads on the GUID and age pair. +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct DebugId { + /// The GUID of the debug information, in the byte order it is stored in. + pub guid: [u8; 16], + /// The number of times the debug information was written out. + pub age: u32, +} + +impl DebugId { + /// Reads the debug identity from the CodeView entry of the debug directory + /// of the PE module starting at the specified memory address. Returns + /// `None` if the module has no debug directory, no CodeView entry, or + /// debug information in a format older than PDB 7.0. + pub fn read(process: &Process, module_address: impl Into
) -> Option { + #[repr(C)] + #[derive(Debug, Copy, Clone, Zeroable, Pod)] + struct DebugDirectoryEntry { + characteristics: u32, + time_date_stamp: u32, + major_version: u16, + minor_version: u16, + debug_type: u32, + size_of_data: u32, + address_of_raw_data: u32, + pointer_to_raw_data: u32, + } + + #[repr(C)] + #[derive(Debug, Copy, Clone, Zeroable, Pod)] + struct CodeView70 { + signature: [u8; 4], + guid: [u8; 16], + age: u32, + } + + const IMAGE_DIRECTORY_ENTRY_DEBUG: usize = 6; + const IMAGE_DEBUG_TYPE_CODEVIEW: u32 = 2; + + let address: Address = module_address.into(); + + let (coff_header, coff_header_address) = read_coff_header(process, address)?; + + let optional_header_address = coff_header_address + mem::size_of::() as u64; + + let (directories_at, directory_count, size_of_image, directory) = + match process.read::(optional_header_address).ok()? { + OPTIONAL_HEADER_MAGIC_PE32 => { + let header = process + .read::(optional_header_address) + .ok()?; + ( + mem::offset_of!(OptionalHeader32, data_directories), + header.number_of_rva_and_sizes, + header.size_of_image, + header.data_directories[IMAGE_DIRECTORY_ENTRY_DEBUG], + ) + } + OPTIONAL_HEADER_MAGIC_PE32_PLUS => { + let header = process + .read::(optional_header_address) + .ok()?; + ( + mem::offset_of!(OptionalHeader64, data_directories), + header.number_of_rva_and_sizes, + header.size_of_image, + header.data_directories[IMAGE_DIRECTORY_ENTRY_DEBUG], + ) + } + _ => return None, + }; + + // The directories close the optional header, and an image declares how + // many it has, so the debug slot exists only when both that count and + // the header's declared size reach it. + let debug_slot_end = + directories_at + (IMAGE_DIRECTORY_ENTRY_DEBUG + 1) * mem::size_of::(); + if directory_count as usize <= IMAGE_DIRECTORY_ENTRY_DEBUG + || (coff_header.size_of_optional_header as usize) < debug_slot_end + { + return None; + } + + if directory.virtual_address == 0 || directory.size == 0 { + return None; + } + + // Everything the walk touches has to lie inside the image, which is + // what bounds it. + let inside = |offset: u32, size: u32| offset as u64 + size as u64 <= size_of_image as u64; + if !inside(directory.virtual_address, directory.size) { + return None; + } + + let entries = directory.size as usize / mem::size_of::(); + (0..entries) + .map_while(|i| { + // The table is contiguous, so an entry that can't be read ends it. + process + .read::( + address + + directory.virtual_address + + (i * mem::size_of::()) as u64, + ) + .ok() + }) + .filter(|entry| { + entry.debug_type == IMAGE_DEBUG_TYPE_CODEVIEW + && entry.address_of_raw_data != 0 + && entry.size_of_data as usize >= mem::size_of::() + && inside(entry.address_of_raw_data, entry.size_of_data) + }) + .find_map(|entry| { + process + .read::(address + entry.address_of_raw_data) + .ok() + .filter(|codeview| codeview.signature == *b"RSDS") + .map(|codeview| Self { + guid: codeview.guid, + age: codeview.age, + }) + }) + } +} + +impl fmt::Debug for DebugId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // The first three fields of the GUID are stored little-endian and + // render big-endian in the canonical form. + let [a0, a1, a2, a3, b0, b1, c0, c1, d0, d1, d2, d3, d4, d5, d6, d7] = self.guid; + write!( + f, + "{:08x}-{:04x}-{:04x}-{d0:02x}{d1:02x}-{d2:02x}{d3:02x}{d4:02x}{d5:02x}{d6:02x}{d7:02x} (age {})", + u32::from_le_bytes([a0, a1, a2, a3]), + u16::from_le_bytes([b0, b1]), + u16::from_le_bytes([c0, c1]), + self.age, + ) + } +} + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::DebugId; + use crate::runtime::mock::with_process; + + use std::{vec, vec::Vec}; + + const BASE: u64 = 0x7FF6_1000_0000; + + // The 2019.4 mono runtime's GUID, stored as CodeView stores it: the first + // three fields little-endian, the rest in order. + const GUID: [u8; 16] = [ + 0xC7, 0xAA, 0x10, 0x77, 0x5A, 0x31, 0x30, 0x4D, 0xA7, 0x7A, 0x08, 0x07, 0x29, 0x69, 0x66, + 0xF6, + ]; + + fn put(image: &mut [u8], at: usize, bytes: &[u8]) { + image[at..at + bytes.len()].copy_from_slice(bytes); + } + + // Builds a minimal mapped PE image by hand from the spec, so the walk is + // checked against the format rather than against itself: headers at the + // base declaring the image size and all sixteen directories, a two-entry + // debug directory with the CodeView entry second, and the PDB 7.0 record + // it points at. + fn image(wide: bool) -> Vec { + let mut image = vec![0; 0x400]; + put(&mut image, 0x00, b"MZ"); + put(&mut image, 0x3C, &0x80_u32.to_le_bytes()); + put(&mut image, 0x80, b"PE\0\0"); + let size_of_optional_header: u16 = if wide { 0xF0 } else { 0xE0 }; + put(&mut image, 0x94, &size_of_optional_header.to_le_bytes()); + let magic: u16 = if wide { 0x20B } else { 0x10B }; + put(&mut image, 0x98, &magic.to_le_bytes()); + put(&mut image, 0x98 + 0x38, &0x400_u32.to_le_bytes()); + let directories_at = 0x98 + if wide { 0x70 } else { 0x60 }; + put(&mut image, directories_at - 4, &16_u32.to_le_bytes()); + let debug_dd_at = directories_at + 6 * 8; + put(&mut image, debug_dd_at, &0x200_u32.to_le_bytes()); + put(&mut image, debug_dd_at + 4, &(2 * 28_u32).to_le_bytes()); + // Entry 0 is POGO data, entry 1 the CodeView record. + put(&mut image, 0x200 + 0xC, &13_u32.to_le_bytes()); + put(&mut image, 0x21C + 0xC, &2_u32.to_le_bytes()); + put(&mut image, 0x21C + 0x10, &0x30_u32.to_le_bytes()); + put(&mut image, 0x21C + 0x14, &0x300_u32.to_le_bytes()); + put(&mut image, 0x300, b"RSDS"); + put(&mut image, 0x304, &GUID); + put(&mut image, 0x314, &1_u32.to_le_bytes()); + put(&mut image, 0x318, b"mono-2.0-bdwgc.pdb\0"); + image + } + + #[test] + fn reads_the_debug_id_from_a_mapped_image() { + for wide in [true, false] { + with_process(&[(BASE, &image(wide))], |process| { + let debug_id = DebugId::read(process, BASE).unwrap(); + assert_eq!(debug_id.guid, GUID); + assert_eq!(debug_id.age, 1); + }); + } + } + + #[test] + fn renders_the_guid_canonically() { + let debug_id = DebugId { guid: GUID, age: 1 }; + assert_eq!( + std::format!("{debug_id:?}"), + "7710aac7-315a-4d30-a77a-0807296966f6 (age 1)", + ); + } + + #[test] + fn answers_nothing_without_a_debug_directory() { + let mut image = image(true); + put(&mut image, 0x98 + 0xA0, &[0; 8]); + with_process(&[(BASE, &image)], |process| { + assert!(DebugId::read(process, BASE).is_none()); + }); + } + + #[test] + fn answers_nothing_for_debug_information_older_than_pdb_70() { + let mut image = image(true); + put(&mut image, 0x300, b"NB10"); + with_process(&[(BASE, &image)], |process| { + assert!(DebugId::read(process, BASE).is_none()); + }); + } + + #[test] + fn answers_nothing_when_the_directories_end_before_debug() { + let mut image = image(true); + put(&mut image, 0x98 + 0x6C, &6_u32.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert!(DebugId::read(process, BASE).is_none()); + }); + } + + #[test] + fn reads_through_an_optional_header_that_ends_at_the_debug_slot() { + let mut image = image(true); + put(&mut image, 0x98 + 0x6C, &7_u32.to_le_bytes()); + put(&mut image, 0x94, &(0x70 + 7 * 8_u16).to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert_eq!(DebugId::read(process, BASE).unwrap().guid, GUID); + }); + } + + #[test] + fn answers_nothing_for_a_codeview_entry_too_short_for_its_record() { + let mut image = image(true); + put(&mut image, 0x21C + 0x10, &0x10_u32.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert!(DebugId::read(process, BASE).is_none()); + }); + } + + #[test] + fn answers_nothing_for_a_debug_directory_outside_the_image() { + let mut image = image(true); + put(&mut image, 0x98 + 0x38, &0x200_u32.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert!(DebugId::read(process, BASE).is_none()); + }); + } + + #[test] + fn reads_a_codeview_entry_past_the_sixteenth() { + let mut image = image(true); + image.resize(0x800, 0); + put(&mut image, 0x98 + 0x38, &0x800_u32.to_le_bytes()); + // Eighteen entries, the CodeView one last, its record moved clear of + // the table. + put(&mut image, 0x138 + 4, &(18 * 28_u32).to_le_bytes()); + put(&mut image, 0x21C, &[0; 28]); + put(&mut image, 0x3DC + 0xC, &2_u32.to_le_bytes()); + put(&mut image, 0x3DC + 0x10, &0x30_u32.to_le_bytes()); + put(&mut image, 0x3DC + 0x14, &0x600_u32.to_le_bytes()); + put(&mut image, 0x600, b"RSDS"); + put(&mut image, 0x604, &GUID); + put(&mut image, 0x614, &1_u32.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert_eq!(DebugId::read(process, BASE).unwrap().guid, GUID); + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index e178811..403a41f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,6 +127,9 @@ #[cfg(feature = "alloc")] extern crate alloc; +#[cfg(all(test, not(target_family = "wasm")))] +extern crate std; + mod primitives; mod runtime; diff --git a/src/runtime/mock.rs b/src/runtime/mock.rs new file mode 100644 index 0000000..baf18e9 --- /dev/null +++ b/src/runtime/mock.rs @@ -0,0 +1,74 @@ +//! A fake host for tests: definitions of the wasm imports the runtime layer +//! links against, backed by in-memory images so readers can run on the host. + +use core::{cell::RefCell, num::NonZeroU64}; + +use std::vec::Vec; + +use crate::Process; + +std::thread_local! { + static MEMORY: RefCell)>> = const { RefCell::new(Vec::new()) }; +} + +/// Runs a test against a process whose memory holds the given regions, each an +/// address and the bytes starting there. Reads outside every region fail. +pub fn with_process(regions: &[(u64, &[u8])], test: impl FnOnce(&Process) -> R) -> R { + MEMORY.with(|memory| { + *memory.borrow_mut() = regions + .iter() + .map(|&(address, bytes)| (address, bytes.to_vec())) + .collect(); + }); + let process = Process::attach("mock").expect("the mock always attaches"); + test(&process) +} + +#[no_mangle] +extern "C" fn process_attach(_name_ptr: *const u8, _name_len: usize) -> Option { + NonZeroU64::new(1) +} + +#[no_mangle] +extern "C" fn process_detach(_process: u64) {} + +#[no_mangle] +extern "C" fn process_read(_process: u64, address: u64, buf_ptr: *mut u8, buf_len: usize) -> bool { + MEMORY.with(|memory| { + memory.borrow().iter().any(|(start, bytes)| { + let Some(offset) = address.checked_sub(*start) else { + return false; + }; + let Ok(offset) = usize::try_from(offset) else { + return false; + }; + if !offset + .checked_add(buf_len) + .is_some_and(|end| end <= bytes.len()) + { + return false; + } + // SAFETY: The runtime layer passes a buffer valid for buf_len + // bytes, and the range is checked to lie inside the region. + unsafe { + core::ptr::copy_nonoverlapping(bytes.as_ptr().add(offset), buf_ptr, buf_len); + } + true + }) + }) +} + +#[cfg(test)] +mod tests { + use super::with_process; + + #[test] + fn reads_come_from_the_regions() { + with_process(&[(0x1000, &[1, 2, 3, 4])], |process| { + assert_eq!(process.read::(0x1000_u64).unwrap(), 0x04030201); + assert_eq!(process.read::<[u8; 2]>(0x1002_u64).unwrap(), [3, 4]); + assert!(process.read::(0x0FFF_u64).is_err()); + assert!(process.read::(0x1001_u64).is_err()); + }); + } +} diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 21164a1..742dbec 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -5,6 +5,8 @@ mod memory_range; mod process; mod sys; +#[cfg(all(test, not(target_family = "wasm")))] +pub(crate) mod mock; pub mod settings; pub mod timer;