From 91afca6d51e6dba415bdf735144c698450a8eb52 Mon Sep 17 00:00:00 2001 From: Simon Chopin Date: Tue, 21 Jul 2026 17:39:22 +0200 Subject: [PATCH 1/7] tests: allow testing on arm64 Android emulators This is notably useful when using an Apple Silicon machine for development. --- .cargo/android-runner.sh | 8 ++++++-- .cargo/config.toml | 6 ++++++ src/linux/dumper_cpu_info/arm.rs | 11 ++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.cargo/android-runner.sh b/.cargo/android-runner.sh index c1fd439c..cfd908bf 100755 --- a/.cargo/android-runner.sh +++ b/.cargo/android-runner.sh @@ -3,7 +3,11 @@ BINARY=$1 shift +# Cargo doesn't expose the target triple to the runner, so derive it from the +# binary path (.../target//...). +TARGET=$(printf '%s' "$BINARY" | sed -E 's#(.*/)?target/([^/]+)/.*#\2#') + # Make sure to run the following to copy the test helper binary over. -# cargo run --target x86_64-linux-android --bin test +# cargo run --target "$TARGET" --bin test adb push "$BINARY" "/data/local/$BINARY" -adb shell "chmod 777 /data/local/$BINARY && env TEST_HELPER=/data/local/target/x86_64-linux-android/debug/test /data/local/$BINARY" "$@" +adb shell "chmod 777 /data/local/$BINARY && env TEST_HELPER=/data/local/target/$TARGET/debug/test /data/local/$BINARY" "$@" diff --git a/.cargo/config.toml b/.cargo/config.toml index e32d5cfd..a5bdfea5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -14,3 +14,9 @@ linker = "x86_64-linux-android30-clang" # By default the linker _doesn't_ generate a build-id, however we want one for our tests. rustflags = ["-C", "link-args=-Wl,--build-id=sha1"] runner = [".cargo/android-runner.sh"] + +[target.aarch64-linux-android] +linker = "aarch64-linux-android30-clang" +# By default the linker _doesn't_ generate a build-id, however we want one for our tests. +rustflags = ["-C", "link-args=-Wl,--build-id=sha1"] +runner = [".cargo/android-runner.sh"] diff --git a/src/linux/dumper_cpu_info/arm.rs b/src/linux/dumper_cpu_info/arm.rs index 1c54b40d..4517599d 100644 --- a/src/linux/dumper_cpu_info/arm.rs +++ b/src/linux/dumper_cpu_info/arm.rs @@ -1,6 +1,7 @@ use { super::{CpuInfoError, ProcessInspector}, crate::minidump_format::*, + failspot::failspot, scroll::Pwrite, std::{ collections::HashSet, @@ -191,9 +192,17 @@ pub fn write_cpu_information( // readable from regular Android applications on later versions // (>= 4.1) of the Android platform. + if failspot!(CpuInfoFileOpen) { + process_inspector.fail_one_syscall_with(libc::EPERM); + } + let cpuinfo_file = match process_inspector.read_file("/proc/cpuinfo") { Ok(x) => x, - Err(_) => { + Err(e) => { + // Surface a failspot-injected failure as a soft error. + if failspot!(CpuInfoFileOpen) { + return Err(CpuInfoError::ReadFileError(e)); + } // Do not return Error here to allow the minidump generation // to happen properly. return Ok(()); From b84c785bfe8c562680c1f8a6b041a5526d4202d1 Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Thu, 17 Jul 2025 10:58:23 -0400 Subject: [PATCH 2/7] Increase SIGSTOP timeout on Android This should hopefully reduce the number of intermittent failures due to slow emulator/hardware. --- src/linux/minidump_writer/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/linux/minidump_writer/mod.rs b/src/linux/minidump_writer/mod.rs index e66ede2a..ca7f4b29 100644 --- a/src/linux/minidump_writer/mod.rs +++ b/src/linux/minidump_writer/mod.rs @@ -52,7 +52,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; From d068539d9d4fe4fcc8e47d92d53fd3d6fe821220 Mon Sep 17 00:00:00 2001 From: Simon Chopin Date: Wed, 22 Jul 2026 17:21:40 +0200 Subject: [PATCH 3/7] Add memory-reading helpers for reading POD values from a process Co-authored-by: Chris Martin --- Cargo.lock | 2 + Cargo.toml | 1 + .../linux/process-backend/src/local/error.rs | 2 + crates/linux/process-backend/src/local/mod.rs | 4 ++ crates/process-reader/Cargo.toml | 1 + crates/process-reader/src/linux/mod.rs | 26 +++++++++ .../process_inspection/process_reader.rs | 58 +++++++++++++++++++ 7 files changed, 94 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 62bc559f..eb44f929 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1370,6 +1370,7 @@ dependencies = [ "minidump", "minidump-common", "minidump-unwind", + "plain", "process-backend", "procfs-core 0.18.0", "scroll 0.12.0", @@ -1686,6 +1687,7 @@ name = "process-reader" version = "0.1.0" dependencies = [ "libc", + "plain", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index e11978f0..f899497d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ memmap2 = "0.9" byteorder = "1.4" error-graph = { version = "0.1.1", features = ["serde"] } failspot = "0.2.0" +plain = { version = "0.2.3", default-features = false } process-backend = { version = "0.1.0", path = "crates/linux/process-backend", features = ["testing"] } # Used for parsing procfs info. diff --git a/crates/linux/process-backend/src/local/error.rs b/crates/linux/process-backend/src/local/error.rs index 79feefc3..5258cad5 100644 --- a/crates/linux/process-backend/src/local/error.rs +++ b/crates/linux/process-backend/src/local/error.rs @@ -50,4 +50,6 @@ pub enum Error { PtracePeekDataFailed(c_int), #[error("ProcessReader returned an error")] ProcessReader(#[source] process_reader::ReadError), + #[error("ProcessReader returned an error during an exact read")] + ProcessReaderExact(#[source] process_reader::ReadExactError), } diff --git a/crates/linux/process-backend/src/local/mod.rs b/crates/linux/process-backend/src/local/mod.rs index c8c7de06..c4221b72 100644 --- a/crates/linux/process-backend/src/local/mod.rs +++ b/crates/linux/process-backend/src/local/mod.rs @@ -398,6 +398,10 @@ impl ProcessReader { pub fn read_at(&self, address: usize, buf: &mut [u8]) -> Result { self.0.read_at(address, buf).map_err(Error::ProcessReader) } + + pub fn read_pod(&self, address: usize) -> Result { + self.0.read_pod(address).map_err(Error::ProcessReaderExact) + } } #[derive(Debug)] diff --git a/crates/process-reader/Cargo.toml b/crates/process-reader/Cargo.toml index 32417fd3..7b917381 100644 --- a/crates/process-reader/Cargo.toml +++ b/crates/process-reader/Cargo.toml @@ -13,4 +13,5 @@ serde = ["dep:serde"] [target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] libc = { workspace = true, default-features = false } +plain = { version = "0.2.3", default-features = false } serde = { version = "1.0.228", default-features = false, features = ["derive"], optional = true } diff --git a/crates/process-reader/src/linux/mod.rs b/crates/process-reader/src/linux/mod.rs index 21f0f00d..292c446a 100644 --- a/crates/process-reader/src/linux/mod.rs +++ b/crates/process-reader/src/linux/mod.rs @@ -87,6 +87,7 @@ use core::{ }; pub use error::{ReadError, ReadExactError}; +pub use plain::Plain; mod error; mod wrapper; @@ -300,6 +301,31 @@ impl ProcessReader { } } + /// Reads a plain-old-data value of type `T` from the target process at + /// `address`. + /// + /// This reads exactly `size_of::()` bytes into a freshly zeroed `T` using + /// [`read_exact_at`](Self::read_exact_at). The [`Plain`] bound guarantees + /// that every possible bit pattern is a valid `T`, so the bytes read from the + /// target process are always a valid value. + /// + /// # Errors + /// + /// Returns [`ReadExactError`] if the underlying exact read cannot fill the + /// value. + pub fn read_pod(&self, address: usize) -> Result { + // Safety: `Plain` is an unsafe trait that may only be implemented on + // types for which every possible bit pattern is valid, so there is + // nothing we could read from the other process that isn't a valid value + // for `T`. + let mut pod: T = unsafe { core::mem::zeroed() }; + let bytes = unsafe { + core::slice::from_raw_parts_mut(ptr::from_mut(&mut pod).cast::(), size_of::()) + }; + self.read_exact_at(address, bytes)?; + Ok(pod) + } + /// Attempts to read bytes from the target process at `address`. /// /// This method copies bytes from `address` in the target process into `buf` diff --git a/src/linux/process_inspection/process_reader.rs b/src/linux/process_inspection/process_reader.rs index d7cc3bd9..ea404de1 100644 --- a/src/linux/process_inspection/process_reader.rs +++ b/src/linux/process_inspection/process_reader.rs @@ -4,6 +4,7 @@ use super::{ }; use crate::module_reader::ProcessModuleMemoryReader; +use plain::Plain; use process_backend::local; pub type ProcessHandle = libc::pid_t; @@ -82,6 +83,63 @@ impl<'a> ProcessReader<'a> { } .map_err(CopyFromProcessError::Backend) } + + /// Read a plain-old-data value of type `T` from the process at `address`. + pub fn read_pod(&self, address: usize) -> Result { + if let Some(forced_backend) = &self.forced_backend { + match forced_backend { + ForcedBackend::Local(process_reader_backend) => { + process_reader_backend.read_pod(address).map_err(Error::Local) + } + } + } else { + match &self.process_inspector.backend { + Backend::Local { + process_reader_backend, + .. + } => process_reader_backend.read_pod(address).map_err(Error::Local), + } + } + .map_err(CopyFromProcessError::Backend) + } + + /// Read `count` consecutive plain-old-data values of type `T` starting at + /// `address`. + pub fn read_pod_vec( + &self, + mut address: usize, + count: usize, + ) -> Result, CopyFromProcessError> { + let mut v = Vec::with_capacity(count); + for _ in 0..count { + v.push(self.read_pod(address)?); + address += std::mem::size_of::(); + } + Ok(v) + } + + /// Read bytes from the process starting at `address` into `buf` up to and + /// including the first `terminator` byte (or until a read returns no bytes). + /// + /// Returns the number of bytes appended to `buf`. + pub fn read_until( + &self, + mut address: usize, + terminator: u8, + buf: &mut Vec, + ) -> Result { + let start_len = buf.len(); + let mut b = [0u8]; + while self.read(address, &mut b)? > 0 { + buf.push(b[0]); + if b[0] == terminator { + break; + } + address += 1; + } + Ok(buf.len() - start_len) + } + /// Find the address at which a module with the given name is loaded in the process. pub fn find_module( &self, From 22bc713867cf0a9fd8b5833a1cacc52f61b2352a Mon Sep 17 00:00:00 2001 From: Simon Chopin Date: Wed, 22 Jul 2026 17:46:13 +0200 Subject: [PATCH 4/7] Add MappingInfo::from_debugger_rendezvous This constructor documents the process's memory mappings by walking the dynamic linker's debugger rendez-vous structure. This will let us enumerate mappings even when /proc//maps is unavailable. Co-authored-by: Chris Martin --- src/lib.rs | 5 + src/linux/maps_reader.rs | 406 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 410 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index d20f1ed3..7d8038fa 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; diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index 57986db1..a6b93924 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -2,11 +2,20 @@ use { super::{ auxv::AuxvType, module_reader::ModuleReaderError, - process_inspection::{self, ProcessInspector}, + process_inspection::{ + self, ProcessInspector, + process_reader::{CopyFromProcessError, ProcessReader}, + }, serializers::*, }, crate::serializers::*, byteorder::{NativeEndian, ReadBytesExt}, + elf::{ + dynamic::{DT_DEBUG, Dyn}, + header::Header as ElfHeader, + program_header::{PF_R, PF_W, PF_X, PT_DYNAMIC, PT_LOAD, PT_PHDR, ProgramHeader}, + }, + plain::Plain, procfs_core::{ FromRead, process::{MMPermissions, MMapPath, MemoryMaps}, @@ -19,6 +28,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)"; @@ -115,6 +129,87 @@ pub enum MapsReaderError { ), } +/// 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: std::ffi::c_int, + /// 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, +} + +#[derive(Debug, thiserror::Error, serde::Serialize)] +pub enum FromDebuggerRendezvousError { + #[error("failed to read program header table")] + ReadProgramHeaderTableFailed(#[source] CopyFromProcessError), + #[error("program header table missing self-pointer")] + ProgramHeaderTableNoSelf, + #[error("program header table missing dynamic section")] + ProgramHeaderTableNoDynamic, + #[error("failed to read ELF header")] + ReadElfHeaderFailed(#[source] CopyFromProcessError), + #[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")] + ReadDynamicSectionFailed(#[source] CopyFromProcessError), + #[error("failed to find DT_DEBUG entry in dynamic section")] + MissingDebugEntry, + #[error("failed to read debugger rendezvous address")] + ReadDebuggerRendezvousAddressFailed(#[source] CopyFromProcessError), + #[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")] + ReadLinkMapEntryFailed(#[source] CopyFromProcessError), + #[error("invalid link entry")] + InvalidLinkEntry, + #[error("failed reading module name")] + ReadModuleNameFailed(#[source] CopyFromProcessError), +} + fn is_mapping_a_path(pathname: Option<&OsStr>) -> bool { match pathname { Some(x) => x.as_bytes().contains(&b'/'), @@ -161,6 +256,263 @@ impl MappingInfo { self.start_address + self.size } + pub fn from_debugger_rendezvous( + process_inspector: &ProcessInspector, + program_header_table_address: usize, + program_header_count: usize, + ) -> std::result::Result, FromDebuggerRendezvousError> { + let memory_reader = process_inspector.process_reader(); + + let program_headers = Self::read_program_headers( + &memory_reader, + program_header_table_address, + program_header_count, + )?; + + 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's make sure we can locate and parse the ELF header to ensure we didn't somehow read garbage. + Self::read_elf_header(&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(&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(&memory_reader, debugger_rendezvous_address)?; + + Self::read_link_map(&memory_reader, debugger_rendezvous.r_map) + .map_err(|e| FromDebuggerRendezvousError::IterateLinkMapFailed(Box::new(e))) + } + + fn read_program_headers( + memory_reader: &ProcessReader<'_>, + 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: &ProcessReader<'_>, + elf_header_address: usize, + ) -> std::result::Result { + let elf_header: ElfHeader = memory_reader + .read_pod(elf_header_address) + .map_err(FromDebuggerRendezvousError::ReadElfHeaderFailed)?; + + validate_elf_signature(&elf_header)?; + + Ok(elf_header) + } + + fn read_dynamic_section( + memory_reader: &ProcessReader<'_>, + 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.is_multiple_of(std::mem::size_of::()) { + return Err(FromDebuggerRendezvousError::InvalidDynamicSectionSize( + dynamic_segment_size, + )); + } + + let dynamic_section = memory_reader + .read_pod_vec( + dynamic_segment_address, + dynamic_segment_size / std::mem::size_of::(), + ) + .map_err(FromDebuggerRendezvousError::ReadDynamicSectionFailed)?; + + if dynamic_section.last() != Some(&Dyn::default()) { + return Err(FromDebuggerRendezvousError::DynamicSectionMissingTerminator); + } + + 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(FromDebuggerRendezvousError::MissingDebugEntry) + } + + fn read_debugger_rendezvous( + memory_reader: &ProcessReader<'_>, + debugger_rendezvous_address: usize, + ) -> std::result::Result { + let debugger_rendezvous: r_debug = memory_reader + .read_pod(debugger_rendezvous_address) + .map_err(FromDebuggerRendezvousError::ReadDebuggerRendezvousAddressFailed)?; + if debugger_rendezvous.r_version != 1 { + return Err( + FromDebuggerRendezvousError::UnexpectedDebuggerRendezvousVersion( + debugger_rendezvous.r_version, + ), + ); + } + Ok(debugger_rendezvous) + } + + fn read_c_string( + memory_reader: &ProcessReader<'_>, + 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: &ProcessReader<'_>, + link_map_address: usize, + ) -> std::result::Result, FromDebuggerRendezvousError> { + let mut link_maps = LinkMaps::new(link_map_address); + + let mut result: Vec = Vec::new(); + + 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 = 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 = 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 mapping_info = Self::calculate_mapping_info( + module_program_header, + &name, + module_elf_header_address, + ); + + if let Some(prev_mapping) = result.last_mut() + && prev_mapping.end_address() == mapping_info.start_address + && prev_mapping.name.is_some() + && prev_mapping.name == mapping_info.name + { + 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; + } + + result.push(mapping_info); + } + } + 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.is_multiple_of(align) { + result + align + } else { + result + } + } + pub fn aggregate( memory_maps: MemoryMaps, linux_gate_loc: Option, @@ -475,6 +827,58 @@ 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: &ProcessReader<'_>, + ) -> 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)) + } +} + +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)] impl PartialEq<(u32, u32, u32, u32)> for SoVersion { fn eq(&self, o: &(u32, u32, u32, u32)) -> bool { From 8757af2d11e852a2509020f89659507e8f247dac Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Thu, 17 Jul 2025 10:58:23 -0400 Subject: [PATCH 5/7] Enumerate mappings after suspending threads At this stage, the main effect of suspend_threads is to attach ptrace, which we'll need when we add the rendez-vous-based fallback. --- src/linux/minidump_writer/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/linux/minidump_writer/mod.rs b/src/linux/minidump_writer/mod.rs index ca7f4b29..6371895a 100644 --- a/src/linux/minidump_writer/mod.rs +++ b/src/linux/minidump_writer/mod.rs @@ -280,11 +280,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 = unsafe { libc::sysconf(libc::_SC_PAGESIZE).try_into().unwrap() }; assert!( self.page_size > 0, @@ -299,6 +294,11 @@ 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() { + soft_errors.push(InitError::EnumerateMappingsFailed(Box::new(e))); + } + #[cfg(target_os = "android")] { late_process_mappings(&self.process_inspector, &mut self.mappings)?; From 22f869f877820b34970817d7972af29df54e0fa7 Mon Sep 17 00:00:00 2001 From: Simon Chopin Date: Wed, 22 Jul 2026 19:20:38 +0200 Subject: [PATCH 6/7] Fall back to debugger rendez-vous when enumerating mappings Co-authored-by: Chris Martin --- src/linux/minidump_writer/errors.rs | 8 ++- src/linux/minidump_writer/mod.rs | 78 ++++++++++++++++++++++------- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/linux/minidump_writer/errors.rs b/src/linux/minidump_writer/errors.rs index 722703ea..a95debcc 100644 --- a/src/linux/minidump_writer/errors.rs +++ b/src/linux/minidump_writer/errors.rs @@ -3,7 +3,7 @@ use { Pid, auxv::AuxvError, dso_debug::SectionDsoDebugError, - maps_reader::MapsReaderError, + maps_reader::{FromDebuggerRendezvousError, MapsReaderError}, minidump_writer::{ exception_stream::SectionExceptionStreamError, handle_data_stream::SectionHandleDataStreamError, mappings::SectionMappingsError, @@ -176,6 +176,12 @@ pub enum InitError { SuspendNoThreadsLeft(usize), #[error("Crash thread does not reference principal mapping")] PrincipalMappingNotReferenced, + #[error("no program header table address in auxiliary vector")] + MissingProgramHeaderTableAddress, + #[error("no program header count in auxiliary vector")] + MissingProgramHeaderCount, + #[error("failed to obtain mappings from debugger rendez-vous")] + MappingsFromDebuggerRendezvousFailed(#[source] FromDebuggerRendezvousError), } #[derive(Debug, thiserror::Error, serde::Serialize)] diff --git a/src/linux/minidump_writer/mod.rs b/src/linux/minidump_writer/mod.rs index 6371895a..0672cd0e 100644 --- a/src/linux/minidump_writer/mod.rs +++ b/src/linux/minidump_writer/mod.rs @@ -295,7 +295,7 @@ impl MinidumpWriter { } // Same with mappings -- Some information is still better than no information! - if let Err(e) = self.enumerate_mappings() { + if let Err(e) = self.enumerate_mappings(&mut soft_errors) { soft_errors.push(InitError::EnumerateMappingsFailed(Box::new(e))); } @@ -699,20 +699,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. - self.mappings = MappingInfo::for_pid( - &self.process_inspector, - self.process_id, - 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 @@ -727,16 +721,66 @@ 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_inspector, + 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 mappings = MappingInfo::for_pid( + &self.process_inspector, + self.process_id, + 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 From d4e1e9eeb34fec10b66a582453fe008d487126a8 Mon Sep 17 00:00:00 2001 From: Chris Martin Date: Thu, 17 Jul 2025 10:58:23 -0400 Subject: [PATCH 7/7] Test the debugger rendez-vous fallback via a proc-maps failspot We move the tests in their own process to make sure we get an isolated failspot instance. --- src/lib.rs | 1 + src/linux/minidump_writer/mod.rs | 6 ++++ tests/linux_minidump_writer.rs | 23 ------------- tests/linux_minidump_writer_failspot.rs | 45 +++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 tests/linux_minidump_writer_failspot.rs diff --git a/src/lib.rs b/src/lib.rs index 7d8038fa..ac75565d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ cfg_if::cfg_if! { ThreadName, SuspendThreads, CpuInfoFileOpen, + EnumerateMappingsFromProc, } } diff --git a/src/linux/minidump_writer/mod.rs b/src/linux/minidump_writer/mod.rs index 0672cd0e..1d78c0e5 100644 --- a/src/linux/minidump_writer/mod.rs +++ b/src/linux/minidump_writer/mod.rs @@ -771,6 +771,12 @@ impl MinidumpWriter { // case its entry when creating the list of mappings. // See http://www.trilithium.com/johan/2005/08/linux-gate/ for more // information. + if failspot!(EnumerateMappingsFromProc) { + return Err(InitError::AggregateMappingsFailed( + std::io::Error::other("fake I/O error reading maps file").into(), + )); + } + let mappings = MappingInfo::for_pid( &self.process_inspector, self.process_id, diff --git a/tests/linux_minidump_writer.rs b/tests/linux_minidump_writer.rs index 576c3a42..465e8eb8 100644 --- a/tests/linux_minidump_writer.rs +++ b/tests/linux_minidump_writer.rs @@ -783,26 +783,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 00000000..d3145832 --- /dev/null +++ b/tests/linux_minidump_writer_failspot.rs @@ -0,0 +1,45 @@ +#![cfg(any(target_os = "linux", target_os = "android"))] + +use { + common::*, + minidump::*, + minidump_writer::{FailSpotName, minidump_writer::MinidumpWriterConfig}, +}; + +mod common; + +#[test] +fn memory_info_list_stream() { + let mut failspot_client = FailSpotName::testing_client(); + + // First enumerate mappings the usual way, via /proc//maps. + failspot_client.set_enabled(FailSpotName::EnumerateMappingsFromProc, false); + memory_info_list_stream_inner(); + + // Then force the /proc//maps path to fail so that mappings are + // reconstructed from the debugger rendez-vous instead. + 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); +}