From 9f7e49a20ffb479bfcd9a45237587b56592803eb Mon Sep 17 00:00:00 2001 From: Simon Chopin Date: Wed, 22 Jul 2026 17:21:40 +0200 Subject: [PATCH] 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,