Skip to main content

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}