process_reader/linux/mod.rs
1//! Read memory from another Linux or Android process.
2//!
3//! `process-reader` is a small `no_std` helper crate for copying raw bytes from a
4//! target process into caller-provided buffers. It is intended for crash-reporting
5//! and minidump-writing code that needs to inspect a process without taking a
6//! dependency on the standard library.
7//!
8//! The main entry point is [`ProcessReader`]. A reader can either be created in
9//! automatic mode with [`ProcessReader::new`], or pinned to one of the supported
10//! Linux/Android mechanisms:
11//!
12//! - [`ProcessReader::for_virtual_mem`] uses `process_vm_readv(2)`.
13//! - [`ProcessReader::for_file`] uses `/proc/<pid>/mem`.
14//! - [`ProcessReader::for_ptrace`] uses `ptrace(PTRACE_PEEKDATA)`.
15//!
16//! # Read semantics
17//!
18//! [`ProcessReader::read_at`] attempts to copy bytes from a virtual address in the
19//! target process into the caller's buffer. It returns the number of bytes copied,
20//! in the range `0..=buf.len()`. That number may be smaller than the buffer
21//! length. A short successful read is returned as `Ok(n)`, not as an error, and
22//! callers should only interpret `buf[..n]` as bytes read by the call.
23//!
24//! The target process may modify its memory while it is being read. This crate
25//! does not suspend the target or provide snapshot consistency.
26//!
27//! # Strategy selection
28//!
29//! [`ProcessReader::new`] tries the strategies in this order: `process_vm_readv`,
30//! `/proc/<pid>/mem`, then `ptrace(PTRACE_PEEKDATA)`. The first strategy that
31//! returns `Ok(_)` for a non-empty request selects the strategy for that reader,
32//! even if that successful read is shorter than the requested buffer.
33//! Future reads through the same reader use the selected strategy directly; they
34//! do not fall back to other strategies if the selected strategy later fails for a
35//! different address.
36//!
37//! This keeps the common path small and predictable, but callers should create a
38//! new [`ProcessReader`] if they want to retry automatic strategy selection after
39//! a strategy-specific failure.
40//!
41//! # Error model
42//!
43//! [`ProcessReader::read_at`] returns [`ReadError`] only when the selected strategy
44//! reports an error, or when automatic strategy selection cannot get any strategy
45//! to return a successful read. Short successful reads are reported as `Ok(n)`.
46//!
47//! When exactly one strategy failed, [`core::error::Error::source`] returns that
48//! strategy's lower-level error. When automatic selection fails because every
49//! strategy failed, there is no single root cause, so `source()` returns `None`.
50//! Use [`ReadError::virtual_mem_error`], [`ReadError::file_error`], and
51//! [`ReadError::ptrace_error`] to inspect the individual strategy failures.
52//!
53//! The contents of the destination buffer are unspecified after an error. Some
54//! strategies can fail after writing part of the requested range, and errors do
55//! not report how many bytes were copied before the failure.
56//!
57//! # Platform support
58//!
59//! This crate supports Linux and Android. Other operating systems fail to compile.
60//! The intended Android targets are contemporary Android systems; very old Android
61//! releases are not part of the supported configuration.
62//!
63//! # Example
64//!
65//! ```no_run
66//! # fn example() -> Result<(), process_reader::ReadError> {
67//! use process_reader::ProcessReader;
68//!
69//! let pid = 12345 as libc::pid_t;
70//! let reader = ProcessReader::new(pid);
71//!
72//! let mut bytes = [0u8; 16];
73//! let bytes_read = reader.read_at(0x1000, &mut bytes)?;
74//! let bytes = &bytes[..bytes_read];
75//! # let _ = bytes;
76//! # Ok(())
77//! # }
78//! ```
79
80use self::{error::*, wrapper::*};
81use core::{
82 cell::OnceCell,
83 ffi::{CStr, c_long, c_void},
84 fmt::Write,
85 mem::size_of,
86 ptr,
87};
88
89pub use error::{ReadError, ReadExactError};
90
91mod error;
92mod wrapper;
93
94const PTRACE_PEEKDATA_LEN: usize = size_of::<c_long>();
95
96/// Reads raw bytes from another Linux or Android process.
97///
98/// A `ProcessReader` is bound to a single target process ID. It can either
99/// choose a read strategy automatically with [`ProcessReader::new`], or be
100/// constructed with a fixed strategy using [`ProcessReader::for_virtual_mem`],
101/// [`ProcessReader::for_file`], or [`ProcessReader::for_ptrace`].
102///
103/// The type does not interpret the bytes it reads. It only copies bytes from the
104/// target process into the caller's buffer and reports how many bytes were copied.
105///
106/// # Process state and permissions
107///
108/// The operating system still enforces the usual Linux/Android access checks.
109/// Depending on the chosen strategy, the caller may need suitable ptrace-style
110/// permissions, ownership, dumpability, capabilities, or an already-stopped
111/// tracee.
112///
113/// `ProcessReader` does not suspend the target process, attach to it, detach from
114/// it, or otherwise manage target process lifetime. It also does not provide a
115/// consistent snapshot if the target process mutates memory while it is being
116/// read.
117#[derive(Debug)]
118pub struct ProcessReader {
119 pid: libc::pid_t,
120 style: OnceCell<Style>,
121}
122
123impl ProcessReader {
124 /// Creates a reader that automatically chooses a process-memory read strategy.
125 ///
126 /// The first non-empty call to [`read_at`](Self::read_at) tries the supported
127 /// strategies in this order:
128 ///
129 /// 1. `process_vm_readv(2)`
130 /// 2. `/proc/<pid>/mem`
131 /// 3. `ptrace(PTRACE_PEEKDATA)`
132 ///
133 /// The first strategy that returns `Ok(_)` is cached and used for all
134 /// subsequent reads through this `ProcessReader`. A successful read may be
135 /// shorter than the requested buffer; a short successful read still selects
136 /// the strategy. Subsequent reads do not fall back to another strategy if the
137 /// cached strategy fails.
138 ///
139 /// Empty reads succeed immediately, return `Ok(0)`, and do not select a
140 /// strategy.
141 ///
142 /// # Panics
143 ///
144 /// Panics if `pid < 0`. Passing a negative PID is treated as a caller logic
145 /// error.
146 pub fn new(pid: libc::pid_t) -> Self {
147 Self::assert_valid_pid(pid);
148 Self {
149 pid,
150 style: OnceCell::new(),
151 }
152 }
153
154 /// Creates a reader pinned to the `process_vm_readv(2)` strategy.
155 ///
156 /// Reads performed through the returned reader use only `process_vm_readv`.
157 /// They do not fall back to `/proc/<pid>/mem` or `ptrace`.
158 ///
159 /// This is generally the fastest strategy when the kernel permits it. It can
160 /// also return a short successful read if `process_vm_readv` transfers fewer
161 /// bytes than requested.
162 ///
163 /// # Panics
164 ///
165 /// Panics if `pid < 0`. Passing a negative PID is treated as a caller logic
166 /// error.
167 pub fn for_virtual_mem(pid: libc::pid_t) -> Self {
168 Self::assert_valid_pid(pid);
169 Self {
170 pid,
171 style: OnceCell::from(Style::VirtualMem),
172 }
173 }
174
175 /// Creates a reader pinned to the `/proc/<pid>/mem` strategy.
176 ///
177 /// This constructor opens `/proc/<pid>/mem` immediately and keeps the file
178 /// descriptor open for the lifetime of the returned reader. Reads performed
179 /// through the returned reader use only that file descriptor and do not fall
180 /// back to `process_vm_readv` or `ptrace`.
181 ///
182 /// This strategy currently attempts to fill the whole requested buffer. On
183 /// success, [`read_at`](Self::read_at) returns `Ok(buf.len())`. If the file
184 /// read fails or reaches EOF before the buffer is filled, `read_at` returns an
185 /// error, and the buffer may have been partially overwritten.
186 ///
187 /// # Errors
188 ///
189 /// Returns [`ReadError`] if `/proc/<pid>/mem` could not be opened.
190 ///
191 /// # Panics
192 ///
193 /// Panics if `pid < 0`. Passing a negative PID is treated as a caller logic
194 /// error.
195 pub fn for_file(pid: libc::pid_t) -> Result<Self, ReadError> {
196 Self::assert_valid_pid(pid);
197 let file = Self::open_mem_file_for_pid(pid)
198 .map_err(FileStrategyError::Open)
199 .map_err(|e| ReadError(ReadErrorInner::FileStrategy(e)))?;
200 Ok(Self {
201 pid,
202 style: OnceCell::from(Style::File(file)),
203 })
204 }
205
206 /// Creates a reader pinned to the `ptrace(PTRACE_PEEKDATA)` strategy.
207 ///
208 /// Reads performed through the returned reader use only
209 /// `ptrace(PTRACE_PEEKDATA)`. They do not fall back to `process_vm_readv` or
210 /// `/proc/<pid>/mem`.
211 ///
212 /// This constructor does not call `PTRACE_ATTACH`, `PTRACE_SEIZE`,
213 /// `waitpid`, `PTRACE_CONT`, or `PTRACE_DETACH`. The caller must arrange any
214 /// required ptrace relationship and stopped tracee state before reading.
215 ///
216 /// The requested address does not need to be word-aligned; the implementation
217 /// performs aligned `PTRACE_PEEKDATA` reads internally and copies the requested
218 /// byte range out of those words.
219 ///
220 /// This strategy currently attempts to fill the whole requested buffer. On
221 /// success, [`read_at`](Self::read_at) returns `Ok(buf.len())`. If a ptrace
222 /// read fails before the buffer is filled, `read_at` returns an error, and the
223 /// buffer may have been partially overwritten.
224 ///
225 /// # Panics
226 ///
227 /// Panics if `pid < 0`. Passing a negative PID is treated as a caller logic
228 /// error.
229 pub fn for_ptrace(pid: libc::pid_t) -> Self {
230 Self::assert_valid_pid(pid);
231 Self {
232 pid,
233 style: OnceCell::from(Style::Ptrace),
234 }
235 }
236
237 /// Reads from another process until `buf` is completely filled.
238 ///
239 /// This is a convenience wrapper around [`ProcessReader::read_at`]. Unlike
240 /// [`read_at`](Self::read_at), this method does not return successful short
241 /// reads. It repeatedly calls [`read_at`](Self::read_at), advancing `address`
242 /// and the output buffer by the number of bytes read, until the entire buffer
243 /// has been filled.
244 ///
245 /// If an underlying read fails before the buffer is filled, this method returns
246 /// [`ReadExactError::Read`]. If an underlying read succeeds but returns `0`
247 /// bytes before the buffer is filled, this method returns
248 /// [`ReadExactError::UnexpectedEof`].
249 ///
250 /// On success, all of `buf` has been filled with bytes read from the target
251 /// process.
252 ///
253 /// # Strategy selection
254 ///
255 /// This method uses [`ProcessReader::read_at`] internally, so it follows the
256 /// same strategy-selection rules. In particular, a reader created with
257 /// [`ProcessReader::new`] caches the first strategy that succeeds for a
258 /// non-empty read, even if that read is short. Later reads performed by this
259 /// method continue using the selected strategy.
260 ///
261 /// # Errors
262 ///
263 /// Returns [`ReadExactError::Read`] if [`read_at`](Self::read_at) returns an
264 /// error before the buffer is full.
265 ///
266 /// Returns [`ReadExactError::UnexpectedEof`] if [`read_at`](Self::read_at)
267 /// returns `Ok(0)` before the buffer is full. A zero-length successful read is
268 /// treated as an exact-read failure because this method could not make forward
269 /// progress.
270 ///
271 /// If this method returns an error, `buf` may have been partially overwritten.
272 /// This error type does not report how many bytes were read before the failure.
273 ///
274 /// # Panics
275 ///
276 /// Panics if a successful partial read leaves bytes remaining in `buf`, but
277 /// advancing the read address by the number of bytes read would wrap past the
278 /// end of the address space.
279 pub fn read_exact_at(
280 &self,
281 mut address: usize,
282 mut buf: &mut [u8],
283 ) -> Result<(), ReadExactError> {
284 if buf.is_empty() {
285 return Ok(());
286 }
287
288 loop {
289 let bytes_read = self.read_at(address, buf).map_err(ReadExactError::Read)?;
290 if bytes_read == 0 {
291 return Err(ReadExactError::UnexpectedEof);
292 }
293 if bytes_read == buf.len() {
294 return Ok(());
295 }
296 address = address
297 .checked_add(bytes_read)
298 .expect("requested read will wrap past end of address space");
299 buf = &mut buf[bytes_read..];
300 }
301 }
302
303 /// Attempts to read bytes from the target process at `address`.
304 ///
305 /// This method copies bytes from `address` in the target process into `buf`
306 /// and returns the number of bytes copied. The returned length is in the
307 /// range `0..=buf.len()` and may be smaller than `buf.len()`. A short
308 /// successful read is returned as `Ok(n)`, not as an error, and callers should
309 /// only interpret `buf[..n]` as bytes read by this call.
310 ///
311 /// If `buf` is empty, this method returns `Ok(0)` without performing a system
312 /// call and without selecting a strategy for readers created with
313 /// [`ProcessReader::new`].
314 ///
315 /// The `address` is a virtual address in the target process, not in the
316 /// calling process.
317 ///
318 /// # Strategy behavior
319 ///
320 /// For readers created with [`ProcessReader::new`], the first `Ok(_)` from a
321 /// non-empty read request selects a strategy for the reader. A successful
322 /// read may be shorter than the requested buffer, and a short successful read
323 /// still selects the strategy. Future reads use that selected strategy only.
324 ///
325 /// Readers created with [`ProcessReader::for_virtual_mem`],
326 /// [`ProcessReader::for_file`], or [`ProcessReader::for_ptrace`] always use
327 /// only the requested strategy.
328 ///
329 /// The `process_vm_readv(2)` strategy returns the byte count reported by
330 /// `process_vm_readv`. The `/proc/<pid>/mem` and `ptrace(PTRACE_PEEKDATA)`
331 /// strategies currently attempt to fill the whole buffer and return
332 /// `Ok(buf.len())` on success.
333 ///
334 /// # Errors
335 ///
336 /// Returns [`ReadError`] if the selected strategy reports an error, or if
337 /// automatic strategy selection cannot get any strategy to return success.
338 ///
339 /// A failed read may have partially overwritten `buf`, and the error does not
340 /// report how many bytes were copied before the failure. Callers should not
341 /// rely on the contents of `buf` after an error.
342 pub fn read_at(&self, address: usize, buf: &mut [u8]) -> Result<usize, ReadError> {
343 if buf.is_empty() {
344 return Ok(0);
345 }
346
347 if let Some(style) = self.style.get() {
348 return match style {
349 Style::VirtualMem => {
350 Self::vmem(self.pid, address, buf).map_err(ReadErrorInner::VirtualMemStrategy)
351 }
352 Style::File(file) => Self::file(file, address, buf)
353 .map(|()| buf.len())
354 .map_err(ReadErrorInner::FileStrategy),
355 Style::Ptrace => Self::ptrace(self.pid, address, buf)
356 .map(|()| buf.len())
357 .map_err(ReadErrorInner::PtraceStrategy),
358 }
359 .map_err(ReadError);
360 }
361
362 const DOUBLE_INIT_MSG: &str = "somehow ProcessReader::style initialized twice";
363
364 let vmem_err = match Self::vmem(self.pid, address, buf) {
365 Ok(len) => {
366 self.style.set(Style::VirtualMem).expect(DOUBLE_INIT_MSG);
367 return Ok(len);
368 }
369 Err(e) => e,
370 };
371
372 let file_err = match Self::open_mem_file_for_pid(self.pid) {
373 Ok(file) => match Self::file(&file, address, buf) {
374 Ok(()) => {
375 self.style.set(Style::File(file)).expect(DOUBLE_INIT_MSG);
376 return Ok(buf.len());
377 }
378 Err(e) => e,
379 },
380 Err(e) => FileStrategyError::Open(e),
381 };
382
383 let ptrace_err = match Self::ptrace(self.pid, address, buf) {
384 Ok(()) => {
385 self.style.set(Style::Ptrace).expect(DOUBLE_INIT_MSG);
386 return Ok(buf.len());
387 }
388 Err(e) => e,
389 };
390
391 Err(ReadError(ReadErrorInner::AllStrategies {
392 vmem_err,
393 file_err,
394 ptrace_err,
395 }))
396 }
397 fn assert_valid_pid(pid: libc::pid_t) {
398 assert!(pid >= 0, "pid must be a non-negative process ID");
399 }
400 fn open_mem_file_for_pid(pid: libc::pid_t) -> Result<File, OpenFailed> {
401 // The max length of a string that looks like "/proc/{pid}/mem\0"
402 //
403 // "/proc/" = 6 bytes
404 // "<pid>" = a 32-bit non-negative signed integer. Max: 2147483647 -> 10 bytes
405 // "/mem" = 4 bytes
406 // null terminator = 1 byte
407 const MAX_PROC_MEM_LEN: usize = 6 + 10 + 4 + 1;
408
409 let mut path_c_str = [0u8; MAX_PROC_MEM_LEN];
410 let path_c_str = {
411 let mut writer = ByteSliceWriter::new(&mut path_c_str);
412 write!(writer, "/proc/{pid}/mem\0").unwrap();
413 CStr::from_bytes_until_nul(&path_c_str).unwrap()
414 };
415
416 File::open(path_c_str)
417 }
418 fn vmem(
419 pid: libc::pid_t,
420 address: usize,
421 buf: &mut [u8],
422 ) -> Result<usize, ProcessVmReadvFailed> {
423 let mut local_iov = [libc::iovec {
424 iov_base: buf.as_mut_ptr().cast(),
425 iov_len: buf.len(),
426 }];
427
428 let mut remote_iov = [libc::iovec {
429 iov_base: address as *mut _,
430 iov_len: buf.len(),
431 }];
432
433 let rv = unsafe {
434 libc::process_vm_readv(
435 pid,
436 local_iov.as_mut_ptr(),
437 local_iov.len().try_into().unwrap(),
438 remote_iov.as_mut_ptr(),
439 remote_iov.len().try_into().unwrap(),
440 0,
441 )
442 };
443 if rv == -1 {
444 return Err(ProcessVmReadvFailed(errno()));
445 }
446
447 let bytes_read = usize::try_from(rv).unwrap();
448 Ok(bytes_read)
449 }
450 fn file(fd: &File, position: usize, buf: &mut [u8]) -> Result<(), FileStrategyError> {
451 fd.read_exact_at(position, buf)
452 .map_err(FileStrategyError::Read)
453 }
454 fn ptrace(pid: libc::pid_t, address: usize, buf: &mut [u8]) -> Result<(), PtraceError> {
455 let mut reader = PtraceReader::new(pid, address)?;
456 reader.read_exact(buf)
457 }
458}
459
460struct PtraceReader {
461 pid: libc::pid_t,
462 position: usize,
463 buffer: [u8; PTRACE_PEEKDATA_LEN],
464 buffer_pos: usize,
465}
466
467impl PtraceReader {
468 fn new(pid: libc::pid_t, position: usize) -> Result<Self, PtraceError> {
469 let requested_position = position;
470 let position = requested_position / PTRACE_PEEKDATA_LEN * PTRACE_PEEKDATA_LEN;
471 let buffer = Self::ptrace_peekdata(pid, position)?;
472 let buffer_pos = requested_position - position;
473 Ok(Self {
474 pid,
475 position,
476 buffer,
477 buffer_pos,
478 })
479 }
480 fn read_exact(&mut self, mut buf: &mut [u8]) -> Result<(), PtraceError> {
481 while !buf.is_empty() {
482 let bytes_read = self.read(buf)?;
483 assert!(bytes_read > 0);
484 buf = &mut buf[bytes_read..];
485 }
486 Ok(())
487 }
488 fn read(&mut self, buf: &mut [u8]) -> Result<usize, PtraceError> {
489 let buffered_bytes = self.fill_buf()?;
490
491 let bytes_to_read = usize::min(buf.len(), buffered_bytes.len());
492 buf[0..bytes_to_read].copy_from_slice(&buffered_bytes[0..bytes_to_read]);
493 self.buffer_pos += bytes_to_read;
494
495 Ok(bytes_to_read)
496 }
497 fn fill_buf(&mut self) -> Result<&[u8], PtraceError> {
498 if self.buffer_pos == PTRACE_PEEKDATA_LEN {
499 self.position = self
500 .position
501 .checked_add(PTRACE_PEEKDATA_LEN)
502 .ok_or(PtraceError::AddressOverflow)?;
503
504 self.buffer = Self::ptrace_peekdata(self.pid, self.position)?;
505 self.buffer_pos = 0;
506 }
507 Ok(&self.buffer[self.buffer_pos..PTRACE_PEEKDATA_LEN])
508 }
509 fn ptrace_peekdata(
510 pid: libc::pid_t,
511 position: usize,
512 ) -> Result<[u8; PTRACE_PEEKDATA_LEN], PtraceError> {
513 set_errno(0);
514 let rv = unsafe {
515 // ptrace is vararg, so best to explicitly declare types
516 let addr: *mut c_void = position as *mut _;
517 let data: *mut c_void = ptr::null_mut();
518 libc::ptrace(libc::PTRACE_PEEKDATA, pid, addr, data)
519 };
520 let err = errno();
521 if rv == -1 && err != 0 {
522 return Err(PtraceError::Syscall {
523 errno: err,
524 position,
525 });
526 }
527 Ok(rv.to_ne_bytes())
528 }
529}
530
531#[derive(Debug)]
532enum Style {
533 VirtualMem,
534 File(File),
535 Ptrace,
536}