From fd462faaaac7cf8082e1b1ed3063993acd81cbcd Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Thu, 17 Jul 2025 10:58:23 -0400 Subject: [PATCH 1/2] Use debugger rendez-vous to obtain memory mappings --- Cargo.lock | 1 + Cargo.toml | 1 + src/lib.rs | 6 + src/linux/maps_reader.rs | 277 +++++++++++++++++++++++- src/linux/mem_reader.rs | 95 ++++++-- src/linux/minidump_writer/errors.rs | 12 +- src/linux/minidump_writer/mod.rs | 112 +++++++--- tests/linux_minidump_writer.rs | 23 -- tests/linux_minidump_writer_failspot.rs | 40 ++++ 9 files changed, 493 insertions(+), 74 deletions(-) create mode 100644 tests/linux_minidump_writer_failspot.rs diff --git a/Cargo.lock b/Cargo.lock index d27a5b3..0a2781b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1466,6 +1466,7 @@ dependencies = [ "minidump-common", "minidump-unwind", "nix", + "plain", "procfs-core", "scroll 0.12.0", "serde", diff --git a/Cargo.toml b/Cargo.toml index b37595a..3aa32f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ thiserror = "2.0" libc = "0.2" goblin = "0.9.2" memmap2 = "0.9" +plain = { version = "0.2.3", default-features = false } [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] nix = { version = "0.29", default-features = false, features = [ diff --git a/src/lib.rs b/src/lib.rs index 67181b4..a415494 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,8 @@ +// Because of the nature of this crate, there are lots of times we cast aliased types to `u64` +// Often, on 64-bit platforms, it's already that, so Clippy gets upset at the u64-to-u64 +// conversion. +#![allow(clippy::useless_conversion)] + cfg_if::cfg_if! { if #[cfg(any(target_os = "linux", target_os = "android"))] { mod linux; @@ -28,5 +33,6 @@ failspot::failspot_name! { ThreadName, SuspendThreads, CpuInfoFileOpen, + EnumerateMappingsFromProc, } } diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index 7f6e81e..d05867c 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -1,9 +1,17 @@ use { - super::{auxv::AuxvType, module_reader::ModuleReaderError, serializers::*}, + super::{ + auxv::AuxvType, mem_reader::MemReader, module_reader::ModuleReaderError, serializers::*, + Pid, + }, crate::serializers::*, byteorder::{NativeEndian, ReadBytesExt}, - goblin::elf, + elf::{ + dynamic::{Dyn, DT_DEBUG}, + header::Header as ElfHeader, + program_header::{ProgramHeader, PF_R, PF_W, PF_X, PT_DYNAMIC, PT_LOAD, PT_PHDR}, + }, memmap2::{Mmap, MmapOptions}, + plain::Plain, procfs_core::process::{MMPermissions, MMapPath, MemoryMaps}, std::{ ffi::{OsStr, OsString}, @@ -14,6 +22,11 @@ use { }, }; +#[cfg(not(target_pointer_width = "64"))] +use goblin::elf32 as elf; +#[cfg(target_pointer_width = "64")] +use goblin::elf64 as elf; + pub const LINUX_GATE_LIBRARY_NAME: &str = "linux-gate.so"; pub const DELETED_SUFFIX: &[u8] = b" (deleted)"; @@ -98,6 +111,68 @@ pub enum MapsReaderError { SymlinkError(std::path::PathBuf, std::path::PathBuf), } +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Debug)] +struct r_debug { + r_version: std::ffi::c_int, + r_map: usize, + r_brk: usize, + r_state: u8, + r_ldbase: usize, +} + +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Debug)] +struct link_map { + l_addr: usize, + l_name: usize, + l_ld: usize, + l_next: usize, + l_prev: usize, +} + +#[derive(Debug, thiserror::Error, serde::Serialize)] +pub enum FromDebuggerRendezvousError { + #[error("failed to read program header table")] + #[serde(serialize_with = "serialize_io_error")] + ReadProgramHeaderTableFailed(#[source] std::io::Error), + #[error("program header table missing self-pointer")] + ProgramHeaderTableNoSelf, + #[error("program header table missing dynamic section")] + ProgramHeaderTableNoDynamic, + #[error("failed to read ELF header")] + #[serde(serialize_with = "serialize_io_error")] + ReadElfHeaderFailed(#[source] std::io::Error), + #[error("ELF header had invalid identifer bytes: `{0:?}`")] + InvalidElfSignature([u8; 4]), + #[error("unexpected size for dynamic section `{0}`")] + InvalidDynamicSectionSize(usize), + #[error("dynamic section missing NULL entry")] + DynamicSectionMissingTerminator, + #[error("failed to read dynamic section")] + #[serde(serialize_with = "serialize_io_error")] + ReadDynamicSectionFailed(#[source] std::io::Error), + #[error("failed to find DT_DEBUG entry in dynamic section")] + MissingDebugEntry, + #[error("failed to read debugger rendezvous address")] + #[serde(serialize_with = "serialize_io_error")] + ReadDebuggerRendezvousAddressFailed(#[source] std::io::Error), + #[error("unexpected debugger rendezvous version '{0}'")] + UnexpectedDebuggerRendezvousVersion(i32), + #[error("an error occurred iterating the link_map linked list")] + IterateLinkMapFailed(#[source] Box), + #[error("failed reading link_map entry")] + #[serde(serialize_with = "serialize_io_error")] + ReadLinkMapEntryFailed(#[source] std::io::Error), + #[error("invalid link entry")] + InvalidLinkEntry, + #[error("failed reading module name")] + #[serde(serialize_with = "serialize_io_error")] + ReadModuleNameFailed(#[source] std::io::Error), +} + fn is_mapping_a_path(pathname: Option<&OsStr>) -> bool { match pathname { Some(x) => x.as_bytes().contains(&b'/'), @@ -130,6 +205,187 @@ impl MappingInfo { self.start_address + self.size } + pub fn from_debugger_rendezvous( + pid: Pid, + program_header_table_address: usize, + program_header_count: usize, + ) -> std::result::Result, FromDebuggerRendezvousError> { + use FromDebuggerRendezvousError as E; + + let mut memory_reader = MemReader::new(pid); + + let program_headers: Vec = memory_reader + .read_pod_vec(program_header_table_address, program_header_count) + .map_err(E::ReadProgramHeaderTableFailed)?; + + let program_header_table_virtual_address = program_headers + .iter() + .find_map(|hdr| (hdr.p_type == PT_PHDR).then_some(hdr.p_vaddr)) + .map(|a| usize::try_from(a).unwrap()) + .ok_or(E::ProgramHeaderTableNoSelf)?; + + let (dynamic_segment_virtual_address, dynamic_segment_size) = program_headers + .iter() + .find_map(|hdr| (hdr.p_type == PT_DYNAMIC).then_some((hdr.p_vaddr, hdr.p_memsz))) + .map(|(a, b)| (usize::try_from(a).unwrap(), usize::try_from(b).unwrap())) + .ok_or(E::ProgramHeaderTableNoDynamic)?; + + let elf_header_address = + program_header_table_address - program_header_table_virtual_address; + let dynamic_segment_address = elf_header_address + dynamic_segment_virtual_address; + + // Let's make sure we can locate and parse the ELF header to ensure we didn't somehow read garbage. + let elf_header: ElfHeader = memory_reader + .read_pod(elf_header_address) + .map_err(E::ReadElfHeaderFailed)?; + + validate_elf_signature(&elf_header)?; + + // Check that the dynamic section size is a multiple of the size of a dynamic entry + if dynamic_segment_size % std::mem::size_of::() != 0 { + return Err(E::InvalidDynamicSectionSize(dynamic_segment_size)); + } + + let dynamic_section: Vec = memory_reader + .read_pod_vec( + dynamic_segment_address, + dynamic_segment_size / std::mem::size_of::(), + ) + .map_err(E::ReadDynamicSectionFailed)?; + + if dynamic_section.last() != Some(&Dyn::default()) { + return Err(E::DynamicSectionMissingTerminator); + } + + let debugger_rendezvous_address = dynamic_section + .iter() + .find_map(|d| (u64::from(d.d_tag) == DT_DEBUG).then_some(d.d_val)) + .map(|a| usize::try_from(a).unwrap()) + .ok_or(E::MissingDebugEntry)?; + + let debugger_rendezvous: r_debug = memory_reader + .read_pod(debugger_rendezvous_address) + .map_err(E::ReadDebuggerRendezvousAddressFailed)?; + + if debugger_rendezvous.r_version != 1 { + return Err(E::UnexpectedDebuggerRendezvousVersion( + debugger_rendezvous.r_version, + )); + } + + Self::read_link_map(&mut memory_reader, debugger_rendezvous.r_map) + .map_err(|e| E::IterateLinkMapFailed(Box::new(e))) + } + + fn read_link_map( + memory_reader: &mut MemReader, + link_map_address: usize, + ) -> std::result::Result, FromDebuggerRendezvousError> { + use FromDebuggerRendezvousError as E; + + let mut link: link_map = memory_reader + .read_pod(link_map_address) + .map_err(E::ReadLinkMapEntryFailed)?; + + if link.l_prev != 0 { + return Err(E::InvalidLinkEntry); + } + + let mut result: Vec = Vec::new(); + + loop { + let name = { + let mut buf = Vec::new(); + memory_reader + .read_until(link.l_name, 0, &mut buf) + .map_err(E::ReadModuleNameFailed)?; + buf.pop(); + OsString::from(String::from_utf8(buf).unwrap()) + }; + + let module_elf_header_address = link.l_addr; + let module_elf_header: ElfHeader = memory_reader + .read_pod(module_elf_header_address) + .map_err(E::ReadElfHeaderFailed)?; + validate_elf_signature(&module_elf_header)?; + + let module_program_header_virtual_address = + usize::try_from(module_elf_header.e_phoff).unwrap(); + let module_program_header_address = + module_elf_header_address + module_program_header_virtual_address; + let module_program_header_len = usize::from(module_elf_header.e_phnum); + let module_program_headers: Vec = memory_reader + .read_pod_vec(module_program_header_address, module_program_header_len) + .map_err(E::ReadProgramHeaderTableFailed)?; + + for module_program_header in module_program_headers + .iter() + .filter(|x| x.p_type == PT_LOAD) + { + let segment_virtual_address = + usize::try_from(module_program_header.p_vaddr).unwrap(); + let segment_address = module_elf_header_address + segment_virtual_address; + let segment_len = usize::try_from(module_program_header.p_memsz).unwrap(); + let segment_end_address = segment_address + segment_len; + let segment_offset = usize::try_from(module_program_header.p_offset).unwrap(); + + // Round each of these down or up to the nearest page + let segment_address = segment_address / 4096 * 4096; + let segment_offset = segment_offset / 4096 * 4096; + let segment_end_address = segment_end_address.div_ceil(4096) * 4096; + let segment_len = segment_end_address - segment_address; + + let mut permissions = MMPermissions::empty(); + if module_program_header.p_flags & PF_R != 0 { + permissions.insert(MMPermissions::READ); + } + if module_program_header.p_flags & PF_W != 0 { + permissions.insert(MMPermissions::WRITE); + } + if module_program_header.p_flags & PF_X != 0 { + permissions.insert(MMPermissions::EXECUTE); + } + + if let Some(prev_mapping) = result.last_mut() { + if prev_mapping.end_address() == segment_address + && prev_mapping.name.is_some() + && prev_mapping.name.as_deref() == Some(&name) + { + prev_mapping.system_mapping_info.end_address = segment_end_address; + prev_mapping.size = segment_end_address - prev_mapping.start_address; + prev_mapping.permissions |= permissions; + continue; + } + } + + let mapping_info = MappingInfo { + start_address: segment_address, + size: segment_len, + // When Android relocation packing causes |start_addr| and |size| to + // be modified with a load bias, we need to remember the unbiased + // address range. The following structure holds the original mapping + // address range as reported by the operating system. + system_mapping_info: SystemMappingInfo { + start_address: segment_address, + end_address: segment_end_address, + }, + offset: segment_offset, + permissions, + name: Some(name.clone()), + }; + result.push(mapping_info); + } + + if link.l_next == 0 { + break; + } + link = memory_reader + .read_pod(link.l_next) + .map_err(E::ReadLinkMapEntryFailed)?; + } + Ok(result) + } + pub fn aggregate( memory_maps: MemoryMaps, linux_gate_loc: Option, @@ -248,7 +504,7 @@ impl MappingInfo { .map(&File::open(filename)?)? }; - if mapped_file.is_empty() || mapped_file.len() < elf::header::SELFMAG { + if mapped_file.is_empty() || mapped_file.len() < goblin::elf::header::SELFMAG { return Err(MapsReaderError::MmapSanityCheckFailed); } Ok(mapped_file) @@ -489,6 +745,21 @@ impl PartialEq<(u32, u32, u32, u32)> for SoVersion { } } +unsafe impl Plain for link_map {} +unsafe impl Plain for r_debug {} + +fn validate_elf_signature( + elf_header: &ElfHeader, +) -> std::result::Result<(), FromDebuggerRendezvousError> { + if elf_header.e_ident[0..4] == [0x7f, 0x45, 0x4c, 0x46] { + Ok(()) + } else { + Err(FromDebuggerRendezvousError::InvalidElfSignature( + elf_header.e_ident[0..4].try_into().unwrap(), + )) + } +} + #[cfg(test)] #[cfg(target_pointer_width = "64")] // All addresses are 64 bit and I'm currently too lazy to adjust it to work for both mod tests { diff --git a/src/linux/mem_reader.rs b/src/linux/mem_reader.rs index 4ca9ff3..bae312e 100644 --- a/src/linux/mem_reader.rs +++ b/src/linux/mem_reader.rs @@ -2,7 +2,8 @@ use { super::{minidump_writer::MinidumpWriter, serializers::*, Pid}, - std::sync::OnceLock, + plain::Plain, + std::{io, sync::OnceLock}, }; #[derive(Debug)] @@ -33,10 +34,10 @@ enum Style { } #[derive(Debug, thiserror::Error, serde::Serialize)] -#[error("Copy from process {child} failed (source {src}, offset: {offset}, length: {length})")] +#[error("Copy from process {child} failed (address {address}, offset: {offset}, length: {length})")] pub struct CopyFromProcessError { pub child: Pid, - pub src: usize, + pub address: usize, pub offset: usize, pub length: usize, #[serde(serialize_with = "serialize_nix_error")] @@ -120,18 +121,76 @@ impl MemReader { Ok(output) } - pub fn read(&self, src: usize, dst: &mut [u8]) -> Result { + pub fn read_pod(&mut self, address: usize) -> io::Result { + fn as_bytes_mut(obj: &mut T) -> &mut [u8] { + unsafe { + std::slice::from_raw_parts_mut(obj as *mut _ as *mut u8, std::mem::size_of::()) + } + } + // Safety: All of this is safe to do because `Plain` is an unsafe trait that may only be + // implemented on types that are valid for every possible bit pattern, so there is nothing + // that we could read from the other process that isn't a valid value for our type. + let mut pod_obj: T = unsafe { std::mem::zeroed() }; + let bytes = as_bytes_mut(&mut pod_obj); + self.read_exact(address, bytes)?; + Ok(pod_obj) + } + + pub fn read_pod_vec( + &mut self, + mut address: usize, + count: usize, + ) -> io::Result> { + let mut v = Vec::with_capacity(count); + for _ in 0..count { + v.push(self.read_pod(address)?); + address += std::mem::size_of::(); + } + Ok(v) + } + + pub fn read_until( + &mut self, + mut address: usize, + terminator: u8, + buf: &mut Vec, + ) -> io::Result { + let start_len = buf.len(); + let mut b = [0u8]; + while self.read(address, &mut b).map_err(io::Error::other)? > 0 { + buf.push(b[0]); + if b[0] == terminator { + break; + } + address += 1; + } + Ok(buf.len() - start_len) + } + + pub fn read_exact(&mut self, mut address: usize, mut dst: &mut [u8]) -> io::Result<()> { + while !dst.is_empty() { + let bytes_read = self.read(address, dst).map_err(io::Error::other)?; + if bytes_read == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + address += bytes_read; + dst = &mut dst[bytes_read..]; + } + Ok(()) + } + + pub fn read(&self, address: usize, dst: &mut [u8]) -> Result { if let Some(rs) = self.style.get() { let res = match rs { - Style::VirtualMem => Self::vmem(self.pid, src, dst).map_err(|s| (s, 0)), - Style::File(file) => Self::file(file, src, dst).map_err(|s| (s, 0)), - Style::Ptrace => Self::ptrace(self.pid, src, dst), + Style::VirtualMem => Self::vmem(self.pid, address, dst).map_err(|s| (s, 0)), + Style::File(file) => Self::file(file, address, dst).map_err(|s| (s, 0)), + Style::Ptrace => Self::ptrace(self.pid, address, dst), Style::Unavailable { ptrace, .. } => Err((*ptrace, 0)), }; return res.map_err(|(source, offset)| CopyFromProcessError { child: self.pid.as_raw(), - src, + address, offset, length: dst.len(), source, @@ -141,7 +200,7 @@ impl MemReader { const DOUBLE_INIT_MSG: &str = "somehow MemReader initialized twice"; // Attempt to read in order of speed - let vmem = match Self::vmem(self.pid, src, dst) { + let vmem = match Self::vmem(self.pid, address, dst) { Ok(len) => { self.style.set(Style::VirtualMem).expect(DOUBLE_INIT_MSG); return Ok(len); @@ -150,7 +209,7 @@ impl MemReader { }; let file = match std::fs::File::open(format!("/proc/{}/mem", self.pid)) { - Ok(file) => match Self::file(&file, src, dst) { + Ok(file) => match Self::file(&file, address, dst) { Ok(len) => { self.style.set(Style::File(file)).expect(DOUBLE_INIT_MSG); return Ok(len); @@ -162,7 +221,7 @@ impl MemReader { )), }; - let ptrace = match Self::ptrace(self.pid, src, dst) { + let ptrace = match Self::ptrace(self.pid, address, dst) { Ok(len) => { self.style.set(Style::Ptrace).expect(DOUBLE_INIT_MSG); return Ok(len); @@ -175,7 +234,7 @@ impl MemReader { .expect(DOUBLE_INIT_MSG); Err(CopyFromProcessError { child: self.pid.as_raw(), - src, + address, offset: 0, length: dst.len(), source: ptrace, @@ -183,9 +242,9 @@ impl MemReader { } #[inline] - fn vmem(pid: nix::unistd::Pid, src: usize, dst: &mut [u8]) -> Result { + fn vmem(pid: nix::unistd::Pid, address: usize, dst: &mut [u8]) -> Result { let remote = &[nix::sys::uio::RemoteIoVec { - base: src, + base: address, len: dst.len(), }]; nix::sys::uio::process_vm_readv(pid, &mut [std::io::IoSliceMut::new(dst)], remote) @@ -209,14 +268,14 @@ impl MemReader { #[inline] fn ptrace( pid: nix::unistd::Pid, - src: usize, + address: usize, dst: &mut [u8], ) -> Result { let mut offset = 0; let mut chunks = dst.chunks_exact_mut(std::mem::size_of::()); for chunk in chunks.by_ref() { - let word = nix::sys::ptrace::read(pid, (src + offset) as *mut std::ffi::c_void) + let word = nix::sys::ptrace::read(pid, (address + offset) as *mut std::ffi::c_void) .map_err(|err| (err, offset))?; chunk.copy_from_slice(&word.to_ne_bytes()); offset += std::mem::size_of::(); @@ -225,7 +284,7 @@ impl MemReader { // I don't think there would ever be a case where we would not read on word boundaries, but just in case... let last = chunks.into_remainder(); if !last.is_empty() { - let word = nix::sys::ptrace::read(pid, (src + offset) as *mut std::ffi::c_void) + let word = nix::sys::ptrace::read(pid, (address + offset) as *mut std::ffi::c_void) .map_err(|err| (err, offset))?; last.copy_from_slice(&word.to_ne_bytes()[..last.len()]); } @@ -244,7 +303,7 @@ impl MinidumpWriter { length: usize, ) -> Result, CopyFromProcessError> { let length = std::num::NonZeroUsize::new(length).ok_or(CopyFromProcessError { - src, + address: src, child: pid, offset: 0, length, diff --git a/src/linux/minidump_writer/errors.rs b/src/linux/minidump_writer/errors.rs index d73a9ce..b9b240a 100644 --- a/src/linux/minidump_writer/errors.rs +++ b/src/linux/minidump_writer/errors.rs @@ -2,7 +2,7 @@ use { super::super::{ auxv::AuxvError, dso_debug::SectionDsoDebugError, - maps_reader::MapsReaderError, + maps_reader::{FromDebuggerRendezvousError, MapsReaderError}, minidump_writer::{ app_memory::SectionAppMemoryError, exception_stream::SectionExceptionStreamError, handle_data_stream::SectionHandleDataStreamError, mappings::SectionMappingsError, @@ -216,6 +216,16 @@ pub enum InitError { SuspendNoThreadsLeft(usize), #[error("Crash thread does not reference principal mapping")] PrincipalMappingNotReferenced, + #[error("Failed to read module list through Debugger Rendez-Vous: {0}")] + ReadModuleViaRendezVousFailed(String), + #[error("no program header table address in auxiliary vector")] + MissingProgramHeaderTableAddress, + #[error("no program header count in auxiliary vector")] + MissingProgramHeaderCount, + #[error("failed to suspend main thread")] + SuspendMainThreadFailed(Box), + #[error("failed to obtain mappings from debugger rendez-vous")] + MappingsFromDebuggerRendezvousFailed(FromDebuggerRendezvousError), } #[derive(Debug, thiserror::Error, serde::Serialize)] diff --git a/src/linux/minidump_writer/mod.rs b/src/linux/minidump_writer/mod.rs index 8666e68..8fa3fe7 100644 --- a/src/linux/minidump_writer/mod.rs +++ b/src/linux/minidump_writer/mod.rs @@ -59,7 +59,13 @@ pub mod thread_names_stream; /// The default timeout after a `SIGSTOP` after which minidump writing proceeds /// regardless of the process state -pub const STOP_TIMEOUT: Duration = Duration::from_millis(100); +pub const STOP_TIMEOUT: Duration = if cfg!(target_os = "android") { + // For whatever reason, Android can be terribly slow for stopping processes + // This often leads to our tests failing intermittently + Duration::from_secs(5) +} else { + Duration::from_millis(100) +}; #[cfg(target_pointer_width = "32")] pub const AT_SYSINFO_EHDR: u32 = 33; @@ -276,15 +282,6 @@ impl MinidumpWriter { soft_errors.push(InitError::EnumerateThreadsFailed(Box::new(e))); } - // Same with mappings -- Some information is still better than no information! - if let Err(e) = self.enumerate_mappings() { - soft_errors.push(InitError::EnumerateMappingsFailed(Box::new(e))); - } - - self.page_size = nix::unistd::sysconf(nix::unistd::SysconfVar::PAGE_SIZE)? - .expect("page size apparently unlimited: doesn't make sense.") - as usize; - let threads_count = self.threads.len(); self.suspend_threads(soft_errors.subwriter(InitError::SuspendThreadsErrors)); @@ -293,6 +290,15 @@ impl MinidumpWriter { soft_errors.push(InitError::SuspendNoThreadsLeft(threads_count)); } + // Same with mappings -- Some information is still better than no information! + if let Err(e) = self.enumerate_mappings(&mut soft_errors) { + soft_errors.push(InitError::EnumerateMappingsFailed(Box::new(e))); + } + + self.page_size = nix::unistd::sysconf(nix::unistd::SysconfVar::PAGE_SIZE)? + .expect("page size apparently unlimited: doesn't make sense.") + as usize; + #[cfg(target_os = "android")] { late_process_mappings(self.process_id, &mut self.mappings)?; @@ -753,23 +759,14 @@ impl MinidumpWriter { Ok(()) } - fn enumerate_mappings(&mut self) -> Result<(), InitError> { - // linux_gate_loc is the beginning of the kernel's mapping of - // linux-gate.so in the process. It doesn't actually show up in the - // maps list as a filename, but it can be found using the AT_SYSINFO_EHDR - // aux vector entry, which gives the information necessary to special - // case its entry when creating the list of mappings. - // See http://www.trilithium.com/johan/2005/08/linux-gate/ for more - // information. - let maps_path = format!("/proc/{}/maps", self.process_id); - let maps_file = - std::fs::File::open(&maps_path).map_err(|e| InitError::IOError(maps_path, e))?; - - let maps = procfs_core::process::MemoryMaps::from_read(maps_file) - .map_err(InitError::ReadProcessMapFileFailed)?; - - self.mappings = MappingInfo::aggregate(maps, self.auxv.get_linux_gate_address()) - .map_err(InitError::AggregateMappingsFailed)?; + fn enumerate_mappings( + &mut self, + mut soft_errors: impl WriteErrorList, + ) -> Result<(), InitError> { + let mut mappings = self.enumerate_mappings_via_proc_maps().or_else(|e| { + soft_errors.push(e); + self.enumerate_mappings_via_debugger_rendezvous() + })?; // Although the initial executable is usually the first mapping, it's not // guaranteed (see http://crosbug.com/25355); therefore, try to use the @@ -784,16 +781,73 @@ impl MinidumpWriter { // format assumes the first module is the one that corresponds to the main // executable (as codified in // processor/minidump.cc:MinidumpModuleList::GetMainModule()). - if let Some(entry_mapping_idx) = self.mappings.iter().position(|mapping| { + if let Some(entry_mapping_idx) = mappings.iter().position(|mapping| { (mapping.start_address..mapping.start_address + mapping.size) .contains(&entry_point_loc) }) { - self.mappings.swap(0, entry_mapping_idx); + mappings.swap(0, entry_mapping_idx); } } + + self.mappings = mappings; + Ok(()) } + fn enumerate_mappings_via_debugger_rendezvous( + &mut self, + ) -> Result, InitError> { + let Some(program_header_table_address) = self + .auxv + .get_program_header_address() + .map(|x| usize::try_from(x).unwrap()) + else { + return Err(InitError::MissingProgramHeaderTableAddress); + }; + + let Some(program_header_count) = self + .auxv + .get_program_header_count() + .map(|x| usize::try_from(x).unwrap()) + else { + return Err(InitError::MissingProgramHeaderCount); + }; + + let mappings = MappingInfo::from_debugger_rendezvous( + self.process_id, + program_header_table_address, + program_header_count, + ) + .map_err(InitError::MappingsFromDebuggerRendezvousFailed)?; + + Ok(mappings) + } + + fn enumerate_mappings_via_proc_maps(&mut self) -> Result, InitError> { + // linux_gate_loc is the beginning of the kernel's mapping of + // linux-gate.so in the process. It doesn't actually show up in the + // maps list as a filename, but it can be found using the AT_SYSINFO_EHDR + // aux vector entry, which gives the information necessary to special + // case its entry when creating the list of mappings. + // See http://www.trilithium.com/johan/2005/08/linux-gate/ for more + // information. + let maps_path = format!("/proc/{}/maps", self.process_id); + let maps_file = failspot!(if EnumerateMappingsFromProc { + Err(std::io::Error::other("fake I/O error reading maps file")) + } else { + std::fs::File::open(&maps_path) + }) + .map_err(|e| InitError::IOError(maps_path, e))?; + + let maps = procfs_core::process::MemoryMaps::from_read(maps_file) + .map_err(InitError::ReadProcessMapFileFailed)?; + + let mappings = MappingInfo::aggregate(maps, self.auxv.get_linux_gate_address()) + .map_err(InitError::AggregateMappingsFailed)?; + + Ok(mappings) + } + /// Read thread info from /proc/$pid/status. /// Fill out the |tgid|, |ppid| and |pid| members of |info|. If unavailable, /// these members are set to -1. Returns true if all three members are diff --git a/tests/linux_minidump_writer.rs b/tests/linux_minidump_writer.rs index 0c7a188..306e15a 100644 --- a/tests/linux_minidump_writer.rs +++ b/tests/linux_minidump_writer.rs @@ -785,26 +785,3 @@ fn with_deleted_binary() { // The 'age'/appendix, always 0 on non-windows targets assert_eq!(did.appendix(), 0); } - -#[test] -fn memory_info_list_stream() { - let mut child = start_child_and_wait_for_threads(1); - let pid = child.id() as i32; - - let mut tmpfile = tempfile::Builder::new() - .prefix("memory_info_list_stream") - .tempfile() - .unwrap(); - - // Write a minidump - MinidumpWriterConfig::new(pid, pid) - .write(&mut tmpfile) - .expect("cound not write minidump"); - child.kill().expect("Failed to kill process"); - child.wait().expect("Failed to wait on killed process"); - - // Ensure the minidump has a MemoryInfoListStream present and has at least one entry. - let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); - let list: MinidumpMemoryInfoList = dump.get_stream().expect("no memory info list"); - assert!(list.iter().count() > 1); -} diff --git a/tests/linux_minidump_writer_failspot.rs b/tests/linux_minidump_writer_failspot.rs new file mode 100644 index 0000000..332f386 --- /dev/null +++ b/tests/linux_minidump_writer_failspot.rs @@ -0,0 +1,40 @@ +#![cfg(any(target_os = "linux", target_os = "android"))] + +use { + common::*, + minidump::*, + minidump_writer::{minidump_writer::MinidumpWriterConfig, FailSpotName}, +}; + +mod common; + +#[test] +fn memory_info_list_stream() { + let mut failspot_client = FailSpotName::testing_client(); + failspot_client.set_enabled(FailSpotName::EnumerateMappingsFromProc, false); + memory_info_list_stream_inner(); + failspot_client.set_enabled(FailSpotName::EnumerateMappingsFromProc, true); + memory_info_list_stream_inner(); +} + +fn memory_info_list_stream_inner() { + let mut child = start_child_and_wait_for_threads(1); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("memory_info_list_stream") + .tempfile() + .unwrap(); + + // Write a minidump + MinidumpWriterConfig::new(pid, pid) + .write(&mut tmpfile) + .expect("cound not write minidump"); + child.kill().expect("Failed to kill process"); + child.wait().expect("Failed to wait on killed process"); + + // Ensure the minidump has a MemoryInfoListStream present and has at least one entry. + let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + let list: MinidumpMemoryInfoList = dump.get_stream().expect("no memory info list"); + assert!(list.iter().count() > 1); +} From 87483da5345a92c86c195a56009a95e5c5b3ed52 Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Mon, 8 Sep 2025 15:43:56 -0400 Subject: [PATCH 2/2] Address review from Gabriele --- src/linux/maps_reader.rs | 357 +++++++++++++++++++++++++++------------ 1 file changed, 251 insertions(+), 106 deletions(-) diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index d05867c..e96a680 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -111,25 +111,50 @@ pub enum MapsReaderError { SymlinkError(std::path::PathBuf, std::path::PathBuf), } +/// Shared object loading information for the debugger +/// +/// The Linux dynamic linker fills this info in the Dynamic section of the ELF headers at +/// runtime. It is known as the "debugger rendez-vous" point and is a legacy structure to assist +/// debuggers in locating loaded shared modules. +/// +/// (But we're going to use it for minidump generation purposes.) +/// +/// https://sourceware.org/git/?p=glibc.git;a=blob;f=elf/link.h;h=b645760402514c4839686aaeade20dd5bb7725dd;hb=HEAD#l40 #[allow(non_camel_case_types)] #[repr(C)] #[derive(Debug)] struct r_debug { + /// Version number of the protocol r_version: std::ffi::c_int, + /// Address of the first link_map structure r_map: usize, + /// Address of callback function for module map/unmap r_brk: usize, + /// Argument to r_brk on whether mapping is being added, subtracted, or is done r_state: u8, + /// Base address the linker is loaded at r_ldbase: usize, } +/// Information for a single dynamically loaded module +/// +/// This structure contains important information about a dynamically-loaded module, like its +/// name and virtual address. It is also a node in a doubly-linked list of such modules. +/// +/// https://sourceware.org/git/?p=glibc.git;a=blob;f=elf/link.h;h=b645760402514c4839686aaeade20dd5bb7725dd;hb=HEAD#l101 #[allow(non_camel_case_types)] #[repr(C)] #[derive(Debug)] struct link_map { + /// Base address module was loaded at l_addr: usize, + /// Name of file for module l_name: usize, + /// Address of the Dynamic section l_ld: usize, + /// Address of next node in list l_next: usize, + /// Address of previous node in list l_prev: usize, } @@ -210,182 +235,265 @@ impl MappingInfo { program_header_table_address: usize, program_header_count: usize, ) -> std::result::Result, FromDebuggerRendezvousError> { - use FromDebuggerRendezvousError as E; - let mut memory_reader = MemReader::new(pid); - let program_headers: Vec = memory_reader - .read_pod_vec(program_header_table_address, program_header_count) - .map_err(E::ReadProgramHeaderTableFailed)?; + let program_headers = Self::read_program_headers( + &mut memory_reader, + program_header_table_address, + program_header_count, + )?; - let program_header_table_virtual_address = program_headers - .iter() - .find_map(|hdr| (hdr.p_type == PT_PHDR).then_some(hdr.p_vaddr)) - .map(|a| usize::try_from(a).unwrap()) - .ok_or(E::ProgramHeaderTableNoSelf)?; - - let (dynamic_segment_virtual_address, dynamic_segment_size) = program_headers - .iter() - .find_map(|hdr| (hdr.p_type == PT_DYNAMIC).then_some((hdr.p_vaddr, hdr.p_memsz))) - .map(|(a, b)| (usize::try_from(a).unwrap(), usize::try_from(b).unwrap())) - .ok_or(E::ProgramHeaderTableNoDynamic)?; + let program_header_table_virtual_address = + Self::find_segment_in_program_headers(&program_headers, PT_PHDR)?.0; let elf_header_address = program_header_table_address - program_header_table_virtual_address; - let dynamic_segment_address = elf_header_address + dynamic_segment_virtual_address; // Let's make sure we can locate and parse the ELF header to ensure we didn't somehow read garbage. + Self::read_elf_header(&mut memory_reader, elf_header_address)?; + + let (dynamic_segment_virtual_address, dynamic_segment_size) = + Self::find_segment_in_program_headers(&program_headers, PT_DYNAMIC)?; + let dynamic_segment_address = elf_header_address + dynamic_segment_virtual_address; + + let dynamic_section = Self::read_dynamic_section( + &mut memory_reader, + dynamic_segment_address, + dynamic_segment_size, + )?; + + let debugger_rendezvous_address = Self::get_rendezvous_address(&dynamic_section)?; + let debugger_rendezvous = + Self::read_debugger_rendezvous(&mut memory_reader, debugger_rendezvous_address)?; + + Self::read_link_map(&mut memory_reader, debugger_rendezvous.r_map) + .map_err(|e| FromDebuggerRendezvousError::IterateLinkMapFailed(Box::new(e))) + } + + fn read_program_headers( + memory_reader: &mut MemReader, + program_header_table_address: usize, + program_header_count: usize, + ) -> std::result::Result, FromDebuggerRendezvousError> { + memory_reader + .read_pod_vec(program_header_table_address, program_header_count) + .map_err(FromDebuggerRendezvousError::ReadProgramHeaderTableFailed) + } + + fn find_segment_in_program_headers( + program_headers: &[ProgramHeader], + segment_type: u32, + ) -> std::result::Result<(usize, usize), FromDebuggerRendezvousError> { + program_headers + .iter() + .find_map(|hdr| (hdr.p_type == segment_type).then_some((hdr.p_vaddr, hdr.p_memsz))) + .map(|(a, b)| (usize::try_from(a).unwrap(), usize::try_from(b).unwrap())) + .ok_or(FromDebuggerRendezvousError::ProgramHeaderTableNoDynamic) + } + + fn read_elf_header( + memory_reader: &mut MemReader, + elf_header_address: usize, + ) -> std::result::Result { let elf_header: ElfHeader = memory_reader .read_pod(elf_header_address) - .map_err(E::ReadElfHeaderFailed)?; + .map_err(FromDebuggerRendezvousError::ReadElfHeaderFailed)?; validate_elf_signature(&elf_header)?; + Ok(elf_header) + } + + fn read_dynamic_section( + memory_reader: &mut MemReader, + dynamic_segment_address: usize, + dynamic_segment_size: usize, + ) -> std::result::Result, FromDebuggerRendezvousError> { // Check that the dynamic section size is a multiple of the size of a dynamic entry if dynamic_segment_size % std::mem::size_of::() != 0 { - return Err(E::InvalidDynamicSectionSize(dynamic_segment_size)); + return Err(FromDebuggerRendezvousError::InvalidDynamicSectionSize( + dynamic_segment_size, + )); } - let dynamic_section: Vec = memory_reader + let dynamic_section = memory_reader .read_pod_vec( dynamic_segment_address, dynamic_segment_size / std::mem::size_of::(), ) - .map_err(E::ReadDynamicSectionFailed)?; + .map_err(FromDebuggerRendezvousError::ReadDynamicSectionFailed)?; if dynamic_section.last() != Some(&Dyn::default()) { - return Err(E::DynamicSectionMissingTerminator); + return Err(FromDebuggerRendezvousError::DynamicSectionMissingTerminator); } - let debugger_rendezvous_address = dynamic_section + Ok(dynamic_section) + } + + fn get_rendezvous_address( + dynamic_section: &[Dyn], + ) -> std::result::Result { + dynamic_section .iter() .find_map(|d| (u64::from(d.d_tag) == DT_DEBUG).then_some(d.d_val)) .map(|a| usize::try_from(a).unwrap()) - .ok_or(E::MissingDebugEntry)?; + .ok_or(FromDebuggerRendezvousError::MissingDebugEntry) + } + fn read_debugger_rendezvous( + memory_reader: &mut MemReader, + debugger_rendezvous_address: usize, + ) -> std::result::Result { let debugger_rendezvous: r_debug = memory_reader .read_pod(debugger_rendezvous_address) - .map_err(E::ReadDebuggerRendezvousAddressFailed)?; - + .map_err(FromDebuggerRendezvousError::ReadDebuggerRendezvousAddressFailed)?; if debugger_rendezvous.r_version != 1 { - return Err(E::UnexpectedDebuggerRendezvousVersion( - debugger_rendezvous.r_version, - )); + return Err( + FromDebuggerRendezvousError::UnexpectedDebuggerRendezvousVersion( + debugger_rendezvous.r_version, + ), + ); } + Ok(debugger_rendezvous) + } - Self::read_link_map(&mut memory_reader, debugger_rendezvous.r_map) - .map_err(|e| E::IterateLinkMapFailed(Box::new(e))) + fn read_c_string( + memory_reader: &mut MemReader, + address: usize, + ) -> std::result::Result { + let mut buf = Vec::new(); + memory_reader + .read_until(address, 0, &mut buf) + .map_err(FromDebuggerRendezvousError::ReadModuleNameFailed)?; + buf.pop(); + Ok(String::from_utf8(buf).unwrap()) } fn read_link_map( memory_reader: &mut MemReader, link_map_address: usize, ) -> std::result::Result, FromDebuggerRendezvousError> { - use FromDebuggerRendezvousError as E; - - let mut link: link_map = memory_reader - .read_pod(link_map_address) - .map_err(E::ReadLinkMapEntryFailed)?; - - if link.l_prev != 0 { - return Err(E::InvalidLinkEntry); - } + let mut link_maps = LinkMaps::new(link_map_address); let mut result: Vec = Vec::new(); - loop { - let name = { - let mut buf = Vec::new(); - memory_reader - .read_until(link.l_name, 0, &mut buf) - .map_err(E::ReadModuleNameFailed)?; - buf.pop(); - OsString::from(String::from_utf8(buf).unwrap()) - }; + while let Some(link) = link_maps.next(memory_reader)? { + let name = OsString::from(Self::read_c_string(memory_reader, link.l_name)?); let module_elf_header_address = link.l_addr; - let module_elf_header: ElfHeader = memory_reader - .read_pod(module_elf_header_address) - .map_err(E::ReadElfHeaderFailed)?; - validate_elf_signature(&module_elf_header)?; + let module_elf_header = + Self::read_elf_header(memory_reader, module_elf_header_address)?; let module_program_header_virtual_address = usize::try_from(module_elf_header.e_phoff).unwrap(); let module_program_header_address = module_elf_header_address + module_program_header_virtual_address; let module_program_header_len = usize::from(module_elf_header.e_phnum); - let module_program_headers: Vec = memory_reader - .read_pod_vec(module_program_header_address, module_program_header_len) - .map_err(E::ReadProgramHeaderTableFailed)?; + + let module_program_headers = Self::read_program_headers( + memory_reader, + module_program_header_address, + module_program_header_len, + )?; for module_program_header in module_program_headers .iter() .filter(|x| x.p_type == PT_LOAD) { - let segment_virtual_address = - usize::try_from(module_program_header.p_vaddr).unwrap(); - let segment_address = module_elf_header_address + segment_virtual_address; - let segment_len = usize::try_from(module_program_header.p_memsz).unwrap(); - let segment_end_address = segment_address + segment_len; - let segment_offset = usize::try_from(module_program_header.p_offset).unwrap(); - - // Round each of these down or up to the nearest page - let segment_address = segment_address / 4096 * 4096; - let segment_offset = segment_offset / 4096 * 4096; - let segment_end_address = segment_end_address.div_ceil(4096) * 4096; - let segment_len = segment_end_address - segment_address; - - let mut permissions = MMPermissions::empty(); - if module_program_header.p_flags & PF_R != 0 { - permissions.insert(MMPermissions::READ); - } - if module_program_header.p_flags & PF_W != 0 { - permissions.insert(MMPermissions::WRITE); - } - if module_program_header.p_flags & PF_X != 0 { - permissions.insert(MMPermissions::EXECUTE); - } + let mapping_info = Self::calculate_mapping_info( + module_program_header, + &name, + module_elf_header_address, + ); if let Some(prev_mapping) = result.last_mut() { - if prev_mapping.end_address() == segment_address + if prev_mapping.end_address() == mapping_info.start_address && prev_mapping.name.is_some() - && prev_mapping.name.as_deref() == Some(&name) + && prev_mapping.name == mapping_info.name { - prev_mapping.system_mapping_info.end_address = segment_end_address; - prev_mapping.size = segment_end_address - prev_mapping.start_address; - prev_mapping.permissions |= permissions; + prev_mapping.system_mapping_info.end_address = + mapping_info.system_mapping_info.end_address; + prev_mapping.size = prev_mapping.system_mapping_info.end_address + - prev_mapping.start_address; + prev_mapping.permissions |= mapping_info.permissions; continue; } } - let mapping_info = MappingInfo { - start_address: segment_address, - size: segment_len, - // When Android relocation packing causes |start_addr| and |size| to - // be modified with a load bias, we need to remember the unbiased - // address range. The following structure holds the original mapping - // address range as reported by the operating system. - system_mapping_info: SystemMappingInfo { - start_address: segment_address, - end_address: segment_end_address, - }, - offset: segment_offset, - permissions, - name: Some(name.clone()), - }; result.push(mapping_info); } - - if link.l_next == 0 { - break; - } - link = memory_reader - .read_pod(link.l_next) - .map_err(E::ReadLinkMapEntryFailed)?; } Ok(result) } + fn calculate_mapping_info( + module_program_header: &ProgramHeader, + name: &OsString, + module_elf_header_address: usize, + ) -> MappingInfo { + let segment_virtual_address = usize::try_from(module_program_header.p_vaddr).unwrap(); + let segment_address = module_elf_header_address + segment_virtual_address; + let segment_len = usize::try_from(module_program_header.p_memsz).unwrap(); + let segment_end_address = segment_address + segment_len; + let segment_offset = usize::try_from(module_program_header.p_offset).unwrap(); + + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; + assert!(page_size > 0); + let page_size = usize::try_from(page_size).unwrap(); + + // Round each of these down or up to the nearest page + let segment_address = Self::align_down(segment_address, page_size); + let segment_offset = Self::align_down(segment_offset, page_size); + let segment_end_address = Self::align_up(segment_end_address, page_size); + let segment_len = segment_end_address - segment_address; + + let permissions = + Self::permissions_from_program_header_flags(module_program_header.p_flags); + + MappingInfo { + start_address: segment_address, + size: segment_len, + // When Android relocation packing causes |start_addr| and |size| to + // be modified with a load bias, we need to remember the unbiased + // address range. The following structure holds the original mapping + // address range as reported by the operating system. + system_mapping_info: SystemMappingInfo { + start_address: segment_address, + end_address: segment_end_address, + }, + offset: segment_offset, + permissions, + name: Some(name.clone()), + } + } + + fn permissions_from_program_header_flags(flags: u32) -> MMPermissions { + let mut permissions = MMPermissions::empty(); + if flags & PF_R != 0 { + permissions.insert(MMPermissions::READ); + } + if flags & PF_W != 0 { + permissions.insert(MMPermissions::WRITE); + } + if flags & PF_X != 0 { + permissions.insert(MMPermissions::EXECUTE); + } + permissions + } + + fn align_down(val: usize, align: usize) -> usize { + val / align * align + } + + fn align_up(val: usize, align: usize) -> usize { + let result = val / align * align; + if val % align != 0 { + result + align + } else { + result + } + } + pub fn aggregate( memory_maps: MemoryMaps, linux_gate_loc: Option, @@ -738,6 +846,43 @@ impl SoVersion { } } +#[derive(Debug)] +struct LinkMaps { + address: usize, + first_node: bool, +} + +impl LinkMaps { + fn new(first_address: usize) -> LinkMaps { + LinkMaps { + address: first_address, + first_node: true, + } + } + fn next( + &mut self, + memory_reader: &mut MemReader, + ) -> std::result::Result, FromDebuggerRendezvousError> { + if self.address == 0 { + return Ok(None); + } + + let link: link_map = memory_reader + .read_pod(self.address) + .map_err(FromDebuggerRendezvousError::ReadLinkMapEntryFailed)?; + if self.first_node { + if link.l_prev != 0 { + return Err(FromDebuggerRendezvousError::InvalidLinkEntry); + } + self.first_node = false; + } + + self.address = link.l_next; + + Ok(Some(link)) + } +} + #[cfg(test)] impl PartialEq<(u32, u32, u32, u32)> for SoVersion { fn eq(&self, o: &(u32, u32, u32, u32)) -> bool {