diff --git a/Cargo.lock b/Cargo.lock index 25a09b95..f9a40153 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1681,10 +1681,19 @@ version = "0.0.0" dependencies = [ "libc", "log", + "process-reader", "serde", "thiserror 2.0.18", ] +[[package]] +name = "process-reader" +version = "0.0.0" +dependencies = [ + "libc", + "serde", +] + [[package]] name = "procfs-core" version = "0.17.0" diff --git a/Cargo.toml b/Cargo.toml index 8a1e594f..96f871c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "3" members = [ + "crates/process-reader", "crates/linux/process-backend", ] @@ -54,6 +55,7 @@ byteorder = "1.4" error-graph = { version = "0.1.1", features = ["serde"] } failspot = "0.2.0" process-backend = { path = "crates/linux/process-backend", features = ["testing"] } + # Used for parsing procfs info. # default-features is disabled since it pulls in chrono procfs-core = { version = "0.18", default-features = false, features = ["serde1"] } diff --git a/crates/linux/process-backend/Cargo.toml b/crates/linux/process-backend/Cargo.toml index ba920c99..ebc43e8f 100644 --- a/crates/linux/process-backend/Cargo.toml +++ b/crates/linux/process-backend/Cargo.toml @@ -7,9 +7,10 @@ edition.workspace = true rust-version.workspace = true license.workspace = true -[dependencies] +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] libc.workspace = true log.workspace = true +process-reader = { path = "../../process-reader", features = ["serde"] } serde.workspace = true thiserror.workspace = true diff --git a/crates/linux/process-backend/src/local/error.rs b/crates/linux/process-backend/src/local/error.rs index c01c682b..79feefc3 100644 --- a/crates/linux/process-backend/src/local/error.rs +++ b/crates/linux/process-backend/src/local/error.rs @@ -48,4 +48,6 @@ pub enum Error { ProcessVmReadvFailed(c_int), #[error("failed to peek at process data: {0}")] PtracePeekDataFailed(c_int), + #[error("ProcessReader returned an error")] + ProcessReader(#[source] process_reader::ReadError), } diff --git a/crates/linux/process-backend/src/local/mod.rs b/crates/linux/process-backend/src/local/mod.rs index e594283f..c8c7de06 100644 --- a/crates/linux/process-backend/src/local/mod.rs +++ b/crates/linux/process-backend/src/local/mod.rs @@ -1,15 +1,13 @@ -use { - crate::regs::*, - core::{ - cell::RefCell, - ffi::{CStr, c_int, c_long, c_void}, - mem, ptr, - }, - libc::pid_t, - syscall_invoker::SyscallInvoker, +use crate::regs::*; +use core::{ + cell::RefCell, + ffi::{CStr, c_int, c_long, c_void}, + mem, ptr, }; +use libc::pid_t; +use syscall_invoker::SyscallInvoker; -pub use {error::Error, module_reader::MappedModuleMemoryReader}; +pub use self::{error::Error, module_reader::MappedModuleMemoryReader}; mod error; mod module_reader; @@ -34,6 +32,9 @@ impl Backend { syscall_invoker: Default::default(), } } + pub fn process_reader(&self) -> ProcessReader { + ProcessReader(process_reader::ProcessReader::new(self.pid)) + } pub fn stop_process(&self) -> Result<(), Error> { self.standard_syscall(|| unsafe { libc::kill(self.pid, libc::SIGSTOP) }) .map_err(Error::SigStopFailed)?; @@ -138,50 +139,6 @@ impl Backend { Ok(bytes_read) } - pub fn read_process_io_vec(&self, buf: &mut [u8], offset: usize) -> Result { - let mut local_iov = [libc::iovec { - iov_base: buf.as_mut_ptr().cast(), - iov_len: buf.len(), - }]; - - let mut remote_iov = [libc::iovec { - iov_base: offset as *mut _, - iov_len: buf.len(), - }]; - - let bytes_read = self - .standard_syscall(|| unsafe { - libc::process_vm_readv( - self.pid, - local_iov.as_mut_ptr(), - local_iov.len().try_into().unwrap(), - remote_iov.as_mut_ptr(), - remote_iov.len().try_into().unwrap(), - 0, - ) - }) - .map_err(Error::ProcessVmReadvFailed)?; - - Ok(usize::try_from(bytes_read).unwrap()) - } - - pub fn ptrace_peekdata(&self, offset: usize) -> Result<[u8; mem::size_of::()], Error> { - self.special_syscall(|| unsafe { - set_errno(0); - let rv = ptrace( - libc::PTRACE_PEEKDATA, - self.pid, - offset as *mut _, - ptr::null_mut(), - ); - if rv == -1 && errno() != 0 { - return Err(()); - } - Ok(rv.to_ne_bytes()) - }) - .map_err(Error::PtracePeekDataFailed) - } - pub fn get_gen_regs(&self, tid: libc::pid_t) -> Result { self.getregset(tid).or_else(|_| self.getregs(tid)) } @@ -218,6 +175,20 @@ impl Backend { .map_err(Error::PtracePeekUserFailed) } + pub fn process_reader_for_virtual_mem(&self) -> ProcessReader { + ProcessReader(process_reader::ProcessReader::for_virtual_mem(self.pid)) + } + + pub fn process_reader_for_file(&self) -> Result { + process_reader::ProcessReader::for_file(self.pid) + .map(ProcessReader) + .map_err(Error::ProcessReader) + } + + pub fn process_reader_for_ptrace(&self) -> ProcessReader { + ProcessReader(process_reader::ProcessReader::for_ptrace(self.pid)) + } + fn open_file(&self, path: &CStr) -> Result { self.standard_syscall(|| unsafe { libc::open(path.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC, 0) @@ -321,7 +292,7 @@ impl Backend { Ok(()) } - pub fn standard_syscall(&self, f: F) -> Result + fn standard_syscall(&self, f: F) -> Result where F: FnOnce() -> T, T: From + core::cmp::PartialEq, @@ -329,7 +300,7 @@ impl Backend { self.syscall_invoker.borrow_mut().invoke_standard(f) } - pub fn special_syscall(&self, f: F) -> Result + fn special_syscall(&self, f: F) -> Result where F: FnOnce() -> Result, { @@ -420,6 +391,15 @@ impl Drop for DirReader { } } +#[derive(Debug)] +pub struct ProcessReader(process_reader::ProcessReader); + +impl ProcessReader { + pub fn read_at(&self, address: usize, buf: &mut [u8]) -> Result { + self.0.read_at(address, buf).map_err(Error::ProcessReader) + } +} + #[derive(Debug)] struct OwnedFd(c_int); diff --git a/crates/process-reader/Cargo.toml b/crates/process-reader/Cargo.toml new file mode 100644 index 00000000..9ca990a7 --- /dev/null +++ b/crates/process-reader/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "process-reader" +version = "0.0.0" +edition = "2024" +rust-version = "1.88.0" +license = "MIT" +readme = "README.md" + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +libc = { version = "0.2.186", default-features = false } +serde = { version = "1.0.228", default-features = false, features=["derive"], optional = true } + +[features] +serde = ["dep:serde"] \ No newline at end of file diff --git a/crates/process-reader/README.md b/crates/process-reader/README.md new file mode 100644 index 00000000..9909705c --- /dev/null +++ b/crates/process-reader/README.md @@ -0,0 +1,130 @@ +# process-reader + +`process-reader` is a small `no_std` Rust crate for reading raw bytes from another Linux or Android process. + +It is designed for crash-reporting and minidump-writing code that needs to inspect process memory without depending on `std`. The crate copies bytes into a caller-provided buffer; it does not interpret the memory it reads. + +## Platform support + +This crate supports Linux and Android only. Other operating systems fail at compile time. + +The intended Android targets are contemporary Android systems. Very old Android releases are not part of the supported configuration. + +## Reading memory + +The main type is `ProcessReader`: + +```rust +use process_reader::ProcessReader; + +fn read_some_memory( + pid: libc::pid_t, + address: usize, +) -> Result { + let reader = ProcessReader::new(pid); + + let mut bytes = [0u8; 16]; + let bytes_read = reader.read_at(address, &mut bytes)?; + + // Only this prefix was read by the call. It may be shorter than the buffer. + let _bytes = &bytes[..bytes_read]; + + Ok(bytes_read) +} +``` + +`read_at(address, buf)` attempts to read bytes from `address` in the target process and returns the number of bytes copied. The returned length is in the range `0..=buf.len()`. The address is a virtual address in the target process, not in the calling process. + +A successful read may be shorter than `buf.len()`. Short successful reads are returned as `Ok(n)`, not as errors, so callers should only interpret `buf[..n]` as bytes read by the call. + +Passing an empty buffer succeeds immediately, returns `Ok(0)`, and does not perform a system call. + +## Strategies + +`ProcessReader` supports three Linux/Android process-memory mechanisms: + +| Constructor | Strategy | +| --- | --- | +| `ProcessReader::new(pid)` | Automatically choose a strategy on the first non-empty read request | +| `ProcessReader::for_virtual_mem(pid)` | Use only `process_vm_readv(2)` | +| `ProcessReader::for_file(pid)` | Use only `/proc//mem` | +| `ProcessReader::for_ptrace(pid)` | Use only `ptrace(PTRACE_PEEKDATA)` | + +`ProcessReader::new(pid)` tries strategies in this order: + +1. `process_vm_readv(2)` +2. `/proc//mem` +3. `ptrace(PTRACE_PEEKDATA)` + +The first strategy that returns `Ok(_)` for a non-empty read request is cached by that reader. A successful read can be shorter than the requested buffer; a short successful read still selects the strategy. Later reads use the cached strategy directly and do not fall back to another strategy if the cached strategy fails for a different address. + +To force a specific mechanism, use one of the `for_*` constructors. + +### Strategy-specific read lengths + +The public API allows any strategy to return a short successful read. The current implementation has these strategy-specific behaviors: + +- `process_vm_readv(2)` returns the byte count reported by `process_vm_readv`, which may be smaller than the requested buffer. +- `/proc//mem` attempts to fill the whole buffer. On success it returns `Ok(buf.len())`; if it cannot fill the buffer, it returns an error. +- `ptrace(PTRACE_PEEKDATA)` attempts to fill the whole buffer. On success it returns `Ok(buf.len())`; if it cannot fill the buffer, it returns an error. + +## PID invariant + +All constructors require a non-negative process ID. Passing `pid < 0` is considered a caller logic error and will panic. + +## Ptrace behavior + +`ProcessReader::for_ptrace(pid)` only performs `PTRACE_PEEKDATA` reads. + +It does not attach to the target, seize it, wait for it to stop, resume it, or detach from it. The caller must arrange any required ptrace relationship and target stopped state before reading. + +The requested address does not need to be word-aligned. The implementation performs aligned word reads internally and copies out the requested byte range. + +## Errors + +All read failures use `ReadError`. + +Short successful reads are not errors. They are returned as `Ok(n)` from `read_at`, where `n` is the number of bytes copied. + +When a `ReadError` represents one failed strategy, `ReadError::source()` returns that strategy's lower-level error. + +For a reader created with `ProcessReader::new(pid)`, the first read can fail because every strategy failed. That is an aggregate failure rather than a single error chain, so `ReadError::source()` returns `None`. Use the strategy-specific accessors to inspect the individual failures: + +```rust +fn inspect_error(error: &process_reader::ReadError) { + if let Some(error) = error.virtual_mem_error() { + let _ = error; + } + + if let Some(error) = error.file_error() { + let _ = error; + } + + if let Some(error) = error.ptrace_error() { + let _ = error; + } +} +``` + +The destination buffer may be partially overwritten when a read fails. The error does not report how many bytes were copied before the failure, so callers should not rely on the contents of the buffer after an error. + +The crate does not provide snapshot consistency. If the target process mutates memory while it is being read, the caller may observe a mixture of old and new bytes. + +## `no_std` + +The crate is `#![no_std]`. It depends on `libc` with default features disabled. + +## Optional features + +### `serde` + +Enables `Serialize` and `Deserialize` implementations for `ReadError`. + +```toml +[dependencies] +process-reader = { version = "0.0.0", features = ["serde"] } +``` + +## License + +MIT diff --git a/crates/process-reader/src/lib.rs b/crates/process-reader/src/lib.rs new file mode 100644 index 00000000..d40119df --- /dev/null +++ b/crates/process-reader/src/lib.rs @@ -0,0 +1,6 @@ +#![no_std] + +pub use platform::*; + +#[cfg_attr(any(target_os = "linux", target_os = "android"), path = "linux/mod.rs")] +mod platform; diff --git a/crates/process-reader/src/linux/error.rs b/crates/process-reader/src/linux/error.rs new file mode 100644 index 00000000..fd721185 --- /dev/null +++ b/crates/process-reader/src/linux/error.rs @@ -0,0 +1,285 @@ +use core::{ + error::Error as StdError, + ffi::c_int, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +/// An error returned while reading memory from a target process. +/// +/// `ReadError` is intentionally a small public wrapper around private strategy +/// errors. Its [`Display`] implementation describes the high-level failure. Its +/// [`core::error::Error::source`] implementation returns a lower-level source +/// when there is exactly one failed strategy, including failures from forced +/// strategies and failures from a strategy selected by an automatic reader. +/// +/// Short successful reads are not represented by `ReadError`; they are returned +/// from [`crate::ProcessReader::read_at`] as `Ok(n)` where `n` is smaller than +/// the requested buffer length. +/// +/// When [`crate::ProcessReader::new`] tries every strategy and all of them fail, +/// there is no single source error. In that case +/// [`core::error::Error::source`] returns `None`, and callers can inspect each +/// branch with +/// [`ReadError::virtual_mem_error`], [`ReadError::file_error`], and +/// [`ReadError::ptrace_error`]. +/// +/// If the `serde` feature is enabled, this type implements `Serialize` and +/// `Deserialize`. +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ReadError(pub(crate) ReadErrorInner); + +impl ReadError { + /// Returns the error from the `process_vm_readv(2)` strategy, if present. + /// + /// This returns `Some` when a reader forced to or cached on + /// [`crate::ProcessReader::for_virtual_mem`] fails, and when automatic + /// strategy selection fails after attempting `process_vm_readv`. + /// + /// It returns `None` when the error does not contain a `process_vm_readv` + /// failure. + pub fn virtual_mem_error(&self) -> Option<&(dyn StdError + 'static)> { + match &self.0 { + ReadErrorInner::AllStrategies { vmem_err, .. } => Some(vmem_err), + ReadErrorInner::VirtualMemStrategy(e) => Some(e), + _ => None, + } + } + + /// Returns the error from the `/proc//mem` strategy, if present. + /// + /// This returns `Some` when a reader forced to or cached on + /// [`crate::ProcessReader::for_file`] fails, and when automatic strategy + /// selection fails after attempting to use `/proc//mem`. + /// + /// It returns `None` when the error does not contain a `/proc//mem` + /// failure. + pub fn file_error(&self) -> Option<&(dyn StdError + 'static)> { + match &self.0 { + ReadErrorInner::AllStrategies { file_err, .. } => Some(file_err), + ReadErrorInner::FileStrategy(e) => Some(e), + _ => None, + } + } + + /// Returns the error from the `ptrace(PTRACE_PEEKDATA)` strategy, if present. + /// + /// This returns `Some` when a reader forced to or cached on + /// [`crate::ProcessReader::for_ptrace`] fails, and when automatic strategy + /// selection fails after attempting `ptrace(PTRACE_PEEKDATA)`. + /// + /// It returns `None` when the error does not contain a ptrace failure. + pub fn ptrace_error(&self) -> Option<&(dyn StdError + 'static)> { + match &self.0 { + ReadErrorInner::AllStrategies { ptrace_err, .. } => Some(ptrace_err), + ReadErrorInner::PtraceStrategy(e) => Some(e), + _ => None, + } + } +} + +impl Display for ReadError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + use ReadErrorInner as E; + match &self.0 { + E::AllStrategies { .. } => write!(f, "all process reading strategies failed"), + E::VirtualMemStrategy(_) => write!(f, "virtual memory strategy failed"), + E::FileStrategy(_) => write!(f, "file strategy failed"), + E::PtraceStrategy(_) => write!(f, "ptrace strategy failed"), + } + } +} + +impl StdError for ReadError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + use ReadErrorInner as E; + match &self.0 { + E::AllStrategies { .. } => None, + E::VirtualMemStrategy(e) => Some(e), + E::FileStrategy(e) => Some(e), + E::PtraceStrategy(e) => Some(e), + } + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub(crate) enum ReadErrorInner { + AllStrategies { + vmem_err: ProcessVmReadvFailed, + file_err: FileStrategyError, + ptrace_err: PtraceError, + }, + VirtualMemStrategy(ProcessVmReadvFailed), + FileStrategy(FileStrategyError), + PtraceStrategy(PtraceError), +} + +/// Error returned by [`ProcessReader::read_exact_at`]. +/// +/// [`ProcessReader::read_exact_at`] is built on top of +/// [`ProcessReader::read_at`]. It repeatedly performs partial reads until the +/// caller's buffer is full. This error reports why that exact read could not be +/// completed. +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ReadExactError { + /// An underlying call to [`ProcessReader::read_at`] failed before the buffer + /// was completely filled. + /// + /// The wrapped [`ReadError`] describes the strategy that failed, or the set + /// of strategies that failed if automatic strategy selection had not yet + /// chosen a strategy. + Read(ReadError), + /// The reader stopped making progress before the buffer was completely + /// filled. + /// + /// This occurs when [`ProcessReader::read_at`] returns `Ok(0)` while + /// `read_exact_at` still has bytes left to read. + UnexpectedEof, +} + +impl Display for ReadExactError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::Read(_) => write!(f, "an error occurred before filling entire buffer"), + Self::UnexpectedEof => write!(f, "unexpected end-of-file before filling entire buffer"), + } + } +} + +impl StdError for ReadExactError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Self::Read(e) => Some(e), + Self::UnexpectedEof => None, + } + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub(crate) struct ProcessVmReadvFailed(pub(crate) c_int); + +impl Display for ProcessVmReadvFailed { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "process_vm_readv() returned an error code: {}", self.0) + } +} + +impl StdError for ProcessVmReadvFailed {} + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub(crate) enum FileStrategyError { + Open(OpenFailed), + Read(ReadExactAtError), +} + +impl Display for FileStrategyError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::Open(_) => write!(f, "failed to open /proc//mem file"), + Self::Read(_) => write!(f, "failed to read /proc//mem file"), + } + } +} + +impl StdError for FileStrategyError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Self::Open(e) => Some(e), + Self::Read(e) => Some(e), + } + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub(crate) struct OpenFailed(pub(crate) c_int); + +impl Display for OpenFailed { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "open64() returned an error code: {}", self.0) + } +} + +impl StdError for OpenFailed {} + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub(crate) enum ReadExactAtError { + ReadAt(ReadAtFailed), + AddressOverflow, + UnexpectedEof { position: usize }, +} + +impl Display for ReadExactAtError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::ReadAt(_) => write!(f, "I/O error reading file"), + Self::AddressOverflow => write!( + f, + "the given address/length pair overflowed the machine's memory range" + ), + Self::UnexpectedEof { position } => write!( + f, + "unexpected end-of-file encountered at position: {position}" + ), + } + } +} + +impl StdError for ReadExactAtError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Self::ReadAt(e) => Some(e), + Self::AddressOverflow => None, + Self::UnexpectedEof { .. } => None, + } + } +} + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub(crate) enum ReadAtFailed { + Syscall(c_int), + AddressOutOfBounds, +} + +impl Display for ReadAtFailed { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::Syscall(errno) => write!(f, "pread64() returned an error code: {errno}"), + Self::AddressOutOfBounds => { + write!(f, "requested address was out-of-bounds for pread64()") + } + } + } +} + +impl StdError for ReadAtFailed {} + +#[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub(crate) enum PtraceError { + Syscall { errno: c_int, position: usize }, + AddressOverflow, +} + +impl Display for PtraceError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::Syscall { errno, position } => write!( + f, + "ptrace(PTRACE_PEEKDATA) returned an error code at position {position}: {errno}", + ), + Self::AddressOverflow => write!( + f, + "the given address/length pair overflowed the machine's memory range" + ), + } + } +} + +impl StdError for PtraceError {} diff --git a/crates/process-reader/src/linux/mod.rs b/crates/process-reader/src/linux/mod.rs new file mode 100644 index 00000000..21f0f00d --- /dev/null +++ b/crates/process-reader/src/linux/mod.rs @@ -0,0 +1,536 @@ +//! Read memory from another Linux or Android process. +//! +//! `process-reader` is a small `no_std` helper crate for copying raw bytes from a +//! target process into caller-provided buffers. It is intended for crash-reporting +//! and minidump-writing code that needs to inspect a process without taking a +//! dependency on the standard library. +//! +//! The main entry point is [`ProcessReader`]. A reader can either be created in +//! automatic mode with [`ProcessReader::new`], or pinned to one of the supported +//! Linux/Android mechanisms: +//! +//! - [`ProcessReader::for_virtual_mem`] uses `process_vm_readv(2)`. +//! - [`ProcessReader::for_file`] uses `/proc//mem`. +//! - [`ProcessReader::for_ptrace`] uses `ptrace(PTRACE_PEEKDATA)`. +//! +//! # Read semantics +//! +//! [`ProcessReader::read_at`] attempts to copy bytes from a virtual address in the +//! target process into the caller's buffer. It returns the number of bytes copied, +//! in the range `0..=buf.len()`. That number may be smaller than the buffer +//! length. A short successful read is returned as `Ok(n)`, not as an error, and +//! callers should only interpret `buf[..n]` as bytes read by the call. +//! +//! The target process may modify its memory while it is being read. This crate +//! does not suspend the target or provide snapshot consistency. +//! +//! # Strategy selection +//! +//! [`ProcessReader::new`] tries the strategies in this order: `process_vm_readv`, +//! `/proc//mem`, then `ptrace(PTRACE_PEEKDATA)`. The first strategy that +//! returns `Ok(_)` for a non-empty request selects the strategy for that reader, +//! even if that successful read is shorter than the requested buffer. +//! Future reads through the same reader use the selected strategy directly; they +//! do not fall back to other strategies if the selected strategy later fails for a +//! different address. +//! +//! This keeps the common path small and predictable, but callers should create a +//! new [`ProcessReader`] if they want to retry automatic strategy selection after +//! a strategy-specific failure. +//! +//! # Error model +//! +//! [`ProcessReader::read_at`] returns [`ReadError`] only when the selected strategy +//! reports an error, or when automatic strategy selection cannot get any strategy +//! to return a successful read. Short successful reads are reported as `Ok(n)`. +//! +//! When exactly one strategy failed, [`core::error::Error::source`] returns that +//! strategy's lower-level error. When automatic selection fails because every +//! strategy failed, there is no single root cause, so `source()` returns `None`. +//! Use [`ReadError::virtual_mem_error`], [`ReadError::file_error`], and +//! [`ReadError::ptrace_error`] to inspect the individual strategy failures. +//! +//! The contents of the destination buffer are unspecified after an error. Some +//! strategies can fail after writing part of the requested range, and errors do +//! not report how many bytes were copied before the failure. +//! +//! # Platform support +//! +//! This crate supports Linux and Android. Other operating systems fail to compile. +//! The intended Android targets are contemporary Android systems; very old Android +//! releases are not part of the supported configuration. +//! +//! # Example +//! +//! ```no_run +//! # fn example() -> Result<(), process_reader::ReadError> { +//! use process_reader::ProcessReader; +//! +//! let pid = 12345 as libc::pid_t; +//! let reader = ProcessReader::new(pid); +//! +//! let mut bytes = [0u8; 16]; +//! let bytes_read = reader.read_at(0x1000, &mut bytes)?; +//! let bytes = &bytes[..bytes_read]; +//! # let _ = bytes; +//! # Ok(()) +//! # } +//! ``` + +use self::{error::*, wrapper::*}; +use core::{ + cell::OnceCell, + ffi::{CStr, c_long, c_void}, + fmt::Write, + mem::size_of, + ptr, +}; + +pub use error::{ReadError, ReadExactError}; + +mod error; +mod wrapper; + +const PTRACE_PEEKDATA_LEN: usize = size_of::(); + +/// Reads raw bytes from another Linux or Android process. +/// +/// A `ProcessReader` is bound to a single target process ID. It can either +/// choose a read strategy automatically with [`ProcessReader::new`], or be +/// constructed with a fixed strategy using [`ProcessReader::for_virtual_mem`], +/// [`ProcessReader::for_file`], or [`ProcessReader::for_ptrace`]. +/// +/// The type does not interpret the bytes it reads. It only copies bytes from the +/// target process into the caller's buffer and reports how many bytes were copied. +/// +/// # Process state and permissions +/// +/// The operating system still enforces the usual Linux/Android access checks. +/// Depending on the chosen strategy, the caller may need suitable ptrace-style +/// permissions, ownership, dumpability, capabilities, or an already-stopped +/// tracee. +/// +/// `ProcessReader` does not suspend the target process, attach to it, detach from +/// it, or otherwise manage target process lifetime. It also does not provide a +/// consistent snapshot if the target process mutates memory while it is being +/// read. +#[derive(Debug)] +pub struct ProcessReader { + pid: libc::pid_t, + style: OnceCell