From 9f7e49a20ffb479bfcd9a45237587b56592803eb Mon Sep 17 00:00:00 2001 From: Simon Chopin Date: Wed, 22 Jul 2026 17:21:40 +0200 Subject: [PATCH 1/5] linux: add memory-reading helpers for reading POD values from a process Co-authored-by: Chris Martin --- Cargo.lock | 1 + Cargo.toml | 1 + src/linux/process_inspection/mod.rs | 2 + .../process_inspection/process_reader.rs | 119 ++++++++++++++++++ 4 files changed, 123 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 8cdfe142..59a43810 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1374,6 +1374,7 @@ dependencies = [ "minidump", "minidump-common", "minidump-unwind", + "plain", "process-backend", "procfs-core 0.18.0", "scroll 0.12.0", diff --git a/Cargo.toml b/Cargo.toml index 7020432a..f936a1d9 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/src/linux/process_inspection/mod.rs b/src/linux/process_inspection/mod.rs index 97ea5bbb..2fce2c79 100644 --- a/src/linux/process_inspection/mod.rs +++ b/src/linux/process_inspection/mod.rs @@ -268,6 +268,8 @@ pub enum Error { AddressOverflowed, #[error("unexpected end of buffer reached")] UnexpectedEndOfBuffer, + #[error("got an unexpected end of file")] + UnexpectedEndOfFile, } fn set_process_backend_drop_fail_handler() { diff --git a/src/linux/process_inspection/process_reader.rs b/src/linux/process_inspection/process_reader.rs index cae4b7d3..092e2521 100644 --- a/src/linux/process_inspection/process_reader.rs +++ b/src/linux/process_inspection/process_reader.rs @@ -4,6 +4,8 @@ use super::{ }; use crate::module_reader::ProcessModuleMemoryReader; +use plain::Plain; + pub type ProcessHandle = libc::pid_t; #[derive(Debug)] @@ -16,6 +18,123 @@ impl<'a> ProcessReader<'a> { pub fn read(&self, src: usize, dst: &mut [u8]) -> Result { self.0.read_at(src, dst) } + + /// Read memory from the process until the buffer is filled. + /// + /// This is a convenience wrapper around [`ProcessReader::read`]. Unlike + /// [`read`](Self::read), this method does not return successful short + /// reads. It repeatedly calls [`read`](Self::read), advancing `address` + /// and the output buffer by the number of bytes read, until the entire buffer + /// has been filled. + /// + /// If an underlying read fails before the buffer is filled, this method returns + /// that error as is. If an underlying read succeeds but returns `0` + /// bytes before the buffer is filled, this method returns + /// [`CopyFromProcessError::Backend`] containing [`Error::UnexpectedEndOfFile`]. + /// + /// On success, all of `buf` has been filled with bytes read from the target + /// process. + /// + /// # Errors + /// + /// Returns whichever error [`read`](Self::read) would return. + /// + /// In addition, returns [`CopyFromProcessError::Backend`] containing + /// [`Error::UnexpectedEndOfFile`] if an underlying read returns `0` bytes + /// before the buffer is full, and [`CopyFromProcessError::Backend`] containing + /// [`Error::AddressOverflowed`] if advancing the read address by the number of + /// bytes read would wrap past the end of the address space. + /// + /// If this method returns an error, `buf` may have been partially overwritten. + /// The error type does not report how many bytes were read before the failure. + pub fn read_exact( + &self, + mut src: usize, + mut dst: &mut [u8], + ) -> Result<(), CopyFromProcessError> { + if dst.is_empty() { + return Ok(()); + } + + loop { + let bytes_read = self.read(src, dst)?; + if bytes_read == 0 { + return Err(CopyFromProcessError::Backend(Error::UnexpectedEndOfFile)); + } + if bytes_read == dst.len() { + return Ok(()); + } + src = src + .checked_add(bytes_read) + .ok_or(CopyFromProcessError::Backend(Error::AddressOverflowed))?; + dst = &mut dst[bytes_read..]; + } + } + + /// 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`](Self::read_exact). 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 + /// + /// See [`read_exact`](Self::read_exact) for the possible errors. + 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( + core::ptr::from_mut(&mut pod).cast::(), + size_of::(), + ) + }; + self.read_exact(address, bytes)?; + Ok(pod) + } + + /// 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 3c61a1f8f53d8d640ed93b4975d1ef8da41df940 Mon Sep 17 00:00:00 2001 From: Simon Chopin Date: Mon, 24 Aug 2026 16:01:38 +0200 Subject: [PATCH 2/5] linux: dso_debug: don't model r_state as a Rust enum If we're going to read it from a crashing process, we can't really trust its data, which means we can't safely deserialize it into an enum: it's straight UB to build an enum value outside of its discriminant range. --- src/linux/dso_debug.rs | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/linux/dso_debug.rs b/src/linux/dso_debug.rs index 6816bab4..19b1b47b 100644 --- a/src/linux/dso_debug.rs +++ b/src/linux/dso_debug.rs @@ -63,22 +63,6 @@ pub struct LinkMap { l_prev: usize, /* Chain of loaded objects. WAS: `struct link_map *` */ } -// COPY from -/// This state value describes the mapping change taking place when -/// the `r_brk' address is called. -#[derive(Debug, Clone, Default)] -#[allow(non_camel_case_types, unused)] -#[repr(C)] -enum RState { - /// Mapping change is complete. - #[default] - RT_CONSISTENT, - /// Beginning to add a new object. - RT_ADD, - /// Beginning to remove an object mapping. - RT_DELETE, -} - // COPY from #[derive(Debug, Clone, Default)] #[repr(C)] @@ -92,7 +76,10 @@ pub struct RDebug { The debugger can set a breakpoint at this address if it wants to notice shared object mapping changes. */ r_brk: ElfAddr, - r_state: RState, + /// Which mapping change is taking place when `r_brk` is called: 0 + /// (`RT_CONSISTENT`, the change is complete), 1 (`RT_ADD`, beginning to add + /// a new object) or 2 (`RT_DELETE`, beginning to remove an object mapping). + r_state: libc::c_int, r_ldbase: ElfAddr, /* Base address the linker is loaded at. */ } From bee92c857819c16f6ce12482ef244e9e771a987b Mon Sep 17 00:00:00 2001 From: Simon Chopin Date: Mon, 24 Aug 2026 16:02:40 +0200 Subject: [PATCH 3/5] linux: dso_debug: enrich and format the documentation --- src/linux/dso_debug.rs | 72 +++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/src/linux/dso_debug.rs b/src/linux/dso_debug.rs index 19b1b47b..b1a17cfc 100644 --- a/src/linux/dso_debug.rs +++ b/src/linux/dso_debug.rs @@ -1,3 +1,8 @@ +//! The dynamic linker's debugger rendez-vous. +//! +//! This holds the `` structures the linker publishes for debuggers, and +//! the `MD_LINUX_DSO_DEBUG` stream that is written straight from them. + use { super::{ auxv::AuxvDumpInfo, minidump_writer::MinidumpWriter, process_inspection::ProcessInspector, @@ -49,38 +54,67 @@ pub enum SectionDsoDebugError { ), } -// COPY from +/// 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. +/// +/// Only the leading members below are part of the protocol with the debugger — +/// the same format used in SVR4. The linker's own fields follow them, and we +/// never read those. +/// +/// #[derive(Debug, Clone, Default)] #[repr(C)] pub struct LinkMap { - /* These first few members are part of the protocol with the debugger. - This is the same format used in SVR4. */ - l_addr: ElfAddr, /* Difference between the address in the ELF - file and the addresses in memory. */ - l_name: usize, /* Absolute file name object was found in. WAS: `char*` */ - l_ld: usize, /* Dynamic section of the shared object. WAS: `ElfW(Dyn) *` */ - l_next: usize, /* Chain of loaded objects. WAS: `struct link_map *` */ - l_prev: usize, /* Chain of loaded objects. WAS: `struct link_map *` */ + /// Difference between the addresses in the ELF file and the addresses in + /// memory. + l_addr: ElfAddr, + /// Address of the absolute file name the object was found in. + /// WAS: `char *` + l_name: usize, + /// Address of the dynamic section of the shared object. + /// WAS: `ElfW(Dyn) *` + l_ld: usize, + /// Address of the next node in the chain of loaded objects. + /// WAS: `struct link_map *` + l_next: usize, + /// Address of the previous node in the chain of loaded objects. + /// WAS: `struct link_map *` + l_prev: usize, } -// COPY from +/// Shared object loading information for the debugger. +/// +/// The Linux dynamic linker fills this in and points the main program's +/// `DT_DEBUG` dynamic entry at it. 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.) +/// +/// #[derive(Debug, Clone, Default)] #[repr(C)] pub struct RDebug { - r_version: libc::c_int, /* Version number for this protocol. */ - r_map: usize, /* Head of the chain of loaded objects. WAS: `struct link_map *` */ - - /* This is the address of a function internal to the run-time linker, - that will always be called when the linker begins to map in a - library or unmap it, and again when the mapping change is complete. - The debugger can set a breakpoint at this address if it wants to - notice shared object mapping changes. */ + /// Version number for this protocol. + r_version: libc::c_int, + /// Address of the head of the chain of loaded objects. + /// WAS: `struct link_map *` + r_map: usize, + /// Address of a function internal to the run-time linker, that will always + /// be called when the linker begins to map in a library or unmap it, and + /// again when the mapping change is complete. The debugger can set a + /// breakpoint at this address if it wants to notice shared object mapping + /// changes. r_brk: ElfAddr, /// Which mapping change is taking place when `r_brk` is called: 0 /// (`RT_CONSISTENT`, the change is complete), 1 (`RT_ADD`, beginning to add /// a new object) or 2 (`RT_DELETE`, beginning to remove an object mapping). r_state: libc::c_int, - r_ldbase: ElfAddr, /* Base address the linker is loaded at. */ + /// Base address the linker is loaded at. + r_ldbase: ElfAddr, } pub fn write_dso_debug_stream( From cdb158a5e43b0a260ffc769f341dac73291994d4 Mon Sep 17 00:00:00 2001 From: Simon Chopin Date: Tue, 25 Aug 2026 11:59:45 +0200 Subject: [PATCH 4/5] linux: dso_debug: fix entry size type for u32 `goblin::elf::Dyn` is pointer-width agnostic, but the in-memory value is *not*, which makes that type inappropriate as a basis to copy stuff over. Since it's the size used for 64-bit targets, it never showed on those architectures. --- src/linux/dso_debug.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/linux/dso_debug.rs b/src/linux/dso_debug.rs index b1a17cfc..a874a501 100644 --- a/src/linux/dso_debug.rs +++ b/src/linux/dso_debug.rs @@ -18,6 +18,11 @@ use { type Result = std::result::Result; +#[cfg(not(target_pointer_width = "64"))] +use goblin::elf32 as elf; +#[cfg(target_pointer_width = "64")] +use goblin::elf64 as elf; + cfg_if::cfg_if! { if #[cfg(target_pointer_width = "32")] { use goblin::elf::program_header::program_header32::SIZEOF_PHDR; @@ -167,7 +172,7 @@ pub fn write_dso_debug_stream( dyn_addr += base as ElfAddr; - let dyn_size = std::mem::size_of::(); + let dyn_size = std::mem::size_of::(); let mut r_debug = 0usize; let mut dynamic_length = 0usize; @@ -183,14 +188,15 @@ pub fn write_dso_debug_stream( dynamic_length += dyn_size; // goblin::elf::Dyn doesn't have padding bytes - let (head, body, _tail) = unsafe { dyn_data.align_to::() }; + let (head, body, _tail) = unsafe { dyn_data.align_to::() }; assert!(head.is_empty(), "Data was not aligned"); let dyn_struct = &body[0]; - let debug_tag = goblin::elf::dynamic::DT_DEBUG; - if dyn_struct.d_tag == debug_tag { + #[allow(clippy::useless_conversion)] + let d_tag = u64::from(dyn_struct.d_tag); + if d_tag == goblin::elf::dynamic::DT_DEBUG { r_debug = dyn_struct.d_val as usize; - } else if dyn_struct.d_tag == goblin::elf::dynamic::DT_NULL { + } else if d_tag == goblin::elf::dynamic::DT_NULL { break; } } From 5480cc29531f3dd50074419a34a5db6617955b7b Mon Sep 17 00:00:00 2001 From: Simon Chopin Date: Mon, 24 Aug 2026 16:04:49 +0200 Subject: [PATCH 5/5] linux: dso_debug: read the rendez-vous with read_pod This simplifies the code a fair bit. --- src/linux/dso_debug.rs | 67 +++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 44 deletions(-) diff --git a/src/linux/dso_debug.rs b/src/linux/dso_debug.rs index a874a501..41ad5820 100644 --- a/src/linux/dso_debug.rs +++ b/src/linux/dso_debug.rs @@ -14,6 +14,7 @@ use { }, minidump_format::*, }, + plain::Plain, }; type Result = std::result::Result; @@ -75,19 +76,19 @@ pub enum SectionDsoDebugError { pub struct LinkMap { /// Difference between the addresses in the ELF file and the addresses in /// memory. - l_addr: ElfAddr, + pub(crate) l_addr: ElfAddr, /// Address of the absolute file name the object was found in. /// WAS: `char *` - l_name: usize, + pub(crate) l_name: usize, /// Address of the dynamic section of the shared object. /// WAS: `ElfW(Dyn) *` - l_ld: usize, + pub(crate) l_ld: usize, /// Address of the next node in the chain of loaded objects. /// WAS: `struct link_map *` - l_next: usize, + pub(crate) l_next: usize, /// Address of the previous node in the chain of loaded objects. /// WAS: `struct link_map *` - l_prev: usize, + pub(crate) l_prev: usize, } /// Shared object loading information for the debugger. @@ -104,24 +105,29 @@ pub struct LinkMap { #[repr(C)] pub struct RDebug { /// Version number for this protocol. - r_version: libc::c_int, + pub(crate) r_version: libc::c_int, /// Address of the head of the chain of loaded objects. /// WAS: `struct link_map *` - r_map: usize, + pub(crate) r_map: usize, /// Address of a function internal to the run-time linker, that will always /// be called when the linker begins to map in a library or unmap it, and /// again when the mapping change is complete. The debugger can set a /// breakpoint at this address if it wants to notice shared object mapping /// changes. - r_brk: ElfAddr, + pub(crate) r_brk: ElfAddr, /// Which mapping change is taking place when `r_brk` is called: 0 /// (`RT_CONSISTENT`, the change is complete), 1 (`RT_ADD`, beginning to add /// a new object) or 2 (`RT_DELETE`, beginning to remove an object mapping). - r_state: libc::c_int, + pub(crate) r_state: libc::c_int, /// Base address the linker is loaded at. - r_ldbase: ElfAddr, + pub(crate) r_ldbase: ElfAddr, } +// Safety: both are `repr(C)` aggregates of plain integers, so every bit pattern +// we could read out of the target process is a valid value. +unsafe impl Plain for LinkMap {} +unsafe impl Plain for RDebug {} + pub fn write_dso_debug_stream( process_inspector: &dyn ProcessInspector, buffer: &mut Buffer, @@ -175,23 +181,16 @@ pub fn write_dso_debug_stream( let dyn_size = std::mem::size_of::(); let mut r_debug = 0usize; let mut dynamic_length = 0usize; + let memory_reader = process_inspector.process_reader(); // The dynamic linker makes information available that helps gdb find all // DSOs loaded into the program. If this information is indeed available, // dump it to a MD_LINUX_DSO_DEBUG stream. loop { - let dyn_data = MinidumpWriter::copy_from_process( - process_inspector, - dyn_addr as usize + dynamic_length, - dyn_size, - )?; + let dyn_struct: elf::dynamic::Dyn = + memory_reader.read_pod(dyn_addr as usize + dynamic_length)?; dynamic_length += dyn_size; - // goblin::elf::Dyn doesn't have padding bytes - let (head, body, _tail) = unsafe { dyn_data.align_to::() }; - assert!(head.is_empty(), "Data was not aligned"); - let dyn_struct = &body[0]; - #[allow(clippy::useless_conversion)] let d_tag = u64::from(dyn_struct.d_tag); if d_tag == goblin::elf::dynamic::DT_DEBUG { @@ -205,38 +204,18 @@ pub fn write_dso_debug_stream( // loaded DSOs. // Our list of DSOs potentially is different from the ones in the crashing // process. So, we have to be careful to never dereference pointers - // directly. Instead, we use CopyFromProcess() everywhere. + // directly. Instead, we copy every node out of the process. // See for a more detailed discussion of the how the dynamic // loader communicates with debuggers. - - let debug_entry_data = MinidumpWriter::copy_from_process( - process_inspector, - r_debug, - std::mem::size_of::(), - )?; - - // goblin::elf::Dyn doesn't have padding bytes - let (head, body, _tail) = unsafe { debug_entry_data.align_to::() }; - assert!(head.is_empty(), "Data was not aligned"); - let debug_entry = &body[0]; + let debug_entry: RDebug = memory_reader.read_pod(r_debug)?; // Count the number of loaded DSOs let mut dso_vec = Vec::new(); let mut curr_map = debug_entry.r_map; while curr_map != 0 { - let link_map_data = MinidumpWriter::copy_from_process( - process_inspector, - curr_map, - std::mem::size_of::(), - )?; - - // LinkMap is repr(C) and doesn't have padding bytes, so this should be safe - let (head, body, _tail) = unsafe { link_map_data.align_to::() }; - assert!(head.is_empty(), "Data was not aligned"); - let map = &body[0]; - + let map: LinkMap = memory_reader.read_pod(curr_map)?; curr_map = map.l_next; - dso_vec.push(map.clone()); + dso_vec.push(map); } let mut linkmap_rva = u32::MAX;