//! Read-only access to another process's address space. //! //! # The safety property this module exists to guarantee //! //! A live FIFA 17 session may be running while this tool is used. Corrupting it //! costs the user their progress and their patience. So the guarantee here is //! structural, not a matter of being careful: //! //! * `/proc//mem` is opened with [`File::open`], which is `O_RDONLY`. //! There is no [`std::fs::OpenOptions`] anywhere in this crate. //! * [`ProcMem`] exposes `&self` read methods only. It hands out no `&mut File` //! and no raw fd, so no caller outside this module can upgrade the handle. //! * Nothing in the crate calls `ptrace`, sends a signal, or writes to any //! path under `/proc`. //! //! Even if a caller tried to write, the kernel would reject it on an `O_RDONLY` //! descriptor. The type system and the open mode agree, which is the point. //! //! # Why pread and not seek + read //! //! [`FileExt::read_at`] is `pread(2)`: it takes the offset as an argument //! instead of mutating a shared file cursor. That means a `&ProcMem` can be //! shared across threads later without a mutex and without one thread's seek //! corrupting another's read. It also removes a whole class of "forgot to seek" //! bugs. There is never a reason to prefer seek+read here. use std::fs::File; use std::io; use std::os::unix::fs::FileExt; /// The page size we assume when stepping over an unreadable hole. Every x86-64 /// mapping is a multiple of this, so it is a safe granularity for recovery. pub const PAGE: u64 = 4096; /// A read-only handle on a process's memory. pub struct ProcMem { file: File, } /// What a single chunk read produced. pub enum ChunkRead { /// `n` bytes landed in the buffer. May be shorter than requested when the /// read ran into an unmapped hole partway through. Got(usize), /// Nothing readable at this address at all. Hole, } impl ProcMem { /// Open the target read-only. See the module docs for why this is /// `File::open` and must stay that way. pub fn open(pid: i32) -> io::Result { let file = File::open(format!("/proc/{pid}/mem")).map_err(|e| { io::Error::new( e.kind(), format!("opening /proc/{pid}/mem: {e} (same-user or CAP_SYS_PTRACE required)"), ) })?; Ok(Self { file }) } /// Best-effort read. Never fatal: a hole reports [`ChunkRead::Hole`] rather /// than propagating an error, because in a 3 GB sweep unreadable regions are /// the normal case, not an exceptional one. /// /// Guard pages, Wine's special mappings and pages Denuvo has not faulted in /// are all marked readable in `/proc//maps` yet return `EIO` here. The /// caller counts these and reports the total so the user knows the sweep was /// partial. pub fn read_chunk(&self, va: u64, buf: &mut [u8]) -> ChunkRead { match self.file.read_at(buf, va) { Ok(0) | Err(_) => ChunkRead::Hole, Ok(n) => ChunkRead::Got(n), } } /// Strict read for cases where a short read is genuinely an error, such as /// an explicit `futmem read ` the user asked for by hand. pub fn read_exact(&self, va: u64, len: usize) -> io::Result> { let mut buf = vec![0u8; len]; self.file.read_exact_at(&mut buf, va).map_err(|e| { io::Error::new( e.kind(), format!("reading {len} bytes at {va:#x}: {e} (address may be unmapped)"), ) })?; Ok(buf) } /// Read up to `len` bytes, returning however many were actually available. /// Used for printing context around a hit that sits near the end of a region. pub fn read_partial(&self, va: u64, len: usize) -> Vec { let mut buf = vec![0u8; len]; match self.file.read_at(&mut buf, va) { Ok(n) => { buf.truncate(n); buf } Err(_) => Vec::new(), } } }