//! `/proc` access: finding the client, locating CardsDLL, and positioned reads //! and writes against `/proc//mem`. //! //! Positioned I/O (`pread`/`pwrite`) is used rather than seek+read: a 64-bit //! virtual address is passed straight through as the file offset, so nothing //! depends on a shared file cursor. use std::fs::{self, File, OpenOptions}; use std::io; use std::os::unix::fs::FileExt; use crate::patch::Memory; use crate::{is_client_comm, parse_cardsdll_base, pid_from_proc_entry}; /// Pids whose `comm` is exactly `FIFA17.exe`. /// /// Sorted ascending so that, when more than one client is somehow running, the /// per-pid log lines come out in a stable order (the Python inherits readdir /// order, which is arbitrary). pub fn find_pids() -> Vec { let mut out = Vec::new(); let Ok(entries) = fs::read_dir("/proc") else { return out; }; for entry in entries.flatten() { let name = entry.file_name(); let Some(pid) = name.to_str().and_then(pid_from_proc_entry) else { continue; }; // A pid can exit between readdir and this read; that is not an error. if let Ok(comm) = fs::read_to_string(format!("/proc/{pid}/comm")) { if is_client_comm(&comm) { out.push(pid); } } } out.sort_unstable(); out } /// Base address of the mapped CardsDLL, or `None` while it is not mapped. pub fn cardsdll_base(pid: u32) -> Option { let maps = fs::read_to_string(format!("/proc/{pid}/maps")).ok()?; parse_cardsdll_base(&maps) } /// [`Memory`] over one live client process. pub struct ProcMem { pid: u32, } impl ProcMem { pub fn new(pid: u32) -> Self { Self { pid } } } impl Memory for ProcMem { fn pid(&self) -> u32 { self.pid } /// A failure here normally means the address is not mapped yet — the packer /// has not unpacked that code — which the watch loop treats as "come back /// next tick", not as a failure worth reporting. Read-only handle. fn read(&self, va: u64, buf: &mut [u8]) -> io::Result<()> { File::open(format!("/proc/{}/mem", self.pid))?.read_exact_at(buf, va) } /// Opened read+write like the Python's `r+b`; write-only is not universally /// accepted for `/proc//mem` across kernels. fn write(&self, va: u64, data: &[u8]) -> io::Result<()> { OpenOptions::new() .read(true) .write(true) .open(format!("/proc/{}/mem", self.pid))? .write_all_at(data, va) } } /// Whether `/proc/` still exists — the launcher-liveness check. pub fn pid_alive(pid: i64) -> bool { // Formatted exactly like the Python so a negative or zero pid behaves the // same way (the path simply does not exist). fs::metadata(format!("/proc/{pid}")).is_ok() } /// Real uid of this process, from the ownership of `/proc/self`. /// /// std exposes no `getuid`, and this crate takes no dependencies; `/proc` is /// mandatory for the patcher anyway, so reading it back is not a new assumption. pub fn current_uid() -> io::Result { use std::os::unix::fs::MetadataExt; Ok(fs::metadata("/proc/self")?.uid()) } #[cfg(test)] mod tests { use super::*; #[test] fn our_own_pid_is_alive_and_pid_zero_is_not() { let me: i64 = fs::read_to_string("/proc/self/stat") .unwrap() .split(' ') .next() .unwrap() .parse() .unwrap(); assert!(pid_alive(me)); // /proc/0 and /proc/-1 never exist, matching the Python's path check. assert!(!pid_alive(0)); assert!(!pid_alive(-1)); } #[test] fn uid_is_readable() { // Only that it resolves; the value is environment-dependent. assert!(current_uid().is_ok()); } #[test] fn find_pids_scan_is_safe_without_a_client() { // Deterministic without a client: the scan must not panic and must only // ever return numeric pids. for pid in find_pids() { assert!(pid > 0); } } #[test] fn positioned_io_round_trips_against_our_own_address_space() { // Patching FIFA is not testable here, but the /proc//mem mechanism // is: read and write this process's own heap through the same code path. let me: u32 = fs::read_to_string("/proc/self/stat") .unwrap() .split(' ') .next() .unwrap() .parse() .unwrap(); let mem = ProcMem::new(me); // black_box throughout: this buffer is mutated by the kernel on our // behalf, never by Rust code, so the compiler must not assume it is // unchanged across the write. let target = std::hint::black_box(vec![0x75u8, 0x0f, 0x11, 0x22]); let va = target.as_ptr() as u64; let mut seen = [0u8; 4]; mem.read(va, &mut seen).unwrap(); assert_eq!(seen, *target); mem.write(va, &[0x7f, 0x0f]).unwrap(); assert_eq!(*std::hint::black_box(&target), [0x7f, 0x0f, 0x11, 0x22]); // An address that is certainly not mapped reads as an error, which the // watch loop treats as "not unpacked yet". assert!(mem.read(0x1000, &mut seen).is_err()); } }