use std::fs::{self, File}; use std::os::unix::fs::FileExt; // A contiguous range of guest physical memory backed by a region in QEMU's // address space. gpa_base is where this slot starts in guest physical space // For most single-slot quickemu VMs this will be (gpa_base=0, hva_base=) pub struct MemRegion { pub gpa_base: u64, pub hva_base: u64, pub size: u64, } pub struct GuestMem { pub regions: Vec, mem_fd: File, } impl GuestMem { // For a standard quickemu Windows 11 VM, guest physical memory starts at // GPA 0 and is backed by one or two such mappings pub fn attach(qemu_pid: u32) -> Option { let maps_path = format!("/proc/{}/maps", qemu_pid); let mem_path = format!("/proc/{}/mem", qemu_pid); let maps = fs::read_to_string(&maps_path).ok()?; let mem_fd = File::open(&mem_path).ok()?; let mut regions: Vec = parse_guest_ram_regions(&maps); if regions.is_empty() { return None; } // Assign guest physical addresses in ascending HVA order — the first // region gets GPA 0, successive ones are packed behind it // This is only heuristically correct will be replaced with proper // KVM_SET_USER_MEMORY_REGION slot enumeration once we need NUMA / holes regions.sort_by_key(|r| r.hva_base); let mut cursor: u64 = 0; for r in &mut regions { r.gpa_base = cursor; cursor += r.size; } Some(GuestMem { regions, mem_fd }) } pub fn read_phys(&self, gpa: u64, buf: &mut [u8]) -> Result<(), String> { let region = self.regions.iter().find(|r| { gpa >= r.gpa_base && gpa + buf.len() as u64 <= r.gpa_base + r.size }).ok_or_else(|| format!("GPA {:#x} not covered by any known region", gpa))?; let hva = region.hva_base + (gpa - region.gpa_base); // SAFETY: pread64 on /proc//mem at the HVA offset is how // external processes read another process's virtual memory without // ptrace — the kernel validates the address in the target process self.mem_fd.read_at(buf, hva).map_err(|e| format!("pread GPA {:#x}: {}", gpa, e))?; Ok(()) } pub fn read_u64(&self, gpa: u64) -> Result { let mut buf = [0u8; 8]; self.read_phys(gpa, &mut buf)?; Ok(u64::from_le_bytes(buf)) } pub fn read_u32(&self, gpa: u64) -> Result { let mut buf = [0u8; 4]; self.read_phys(gpa, &mut buf)?; Ok(u32::from_le_bytes(buf)) } } const MIN_REGION_SIZE: u64 = 256 * 1024 * 1024; // 256 MiB fn parse_guest_ram_regions(maps: &str) -> Vec { let mut out = Vec::new(); for line in maps.lines() { // Format: addr_start-addr_end perms offset dev inode [path] let mut parts = line.splitn(6, ' '); let addrs = parts.next().unwrap_or(""); let perms = parts.next().unwrap_or(""); let _offset = parts.next().unwrap_or(""); let _dev = parts.next().unwrap_or(""); let _inode = parts.next().unwrap_or(""); let path = parts.next().unwrap_or("").trim(); // Must be rw-p (private, read-write, not executable — guest RAM pages // are not mapped executable in QEMU's address space). if perms != "rw-p" { continue; } // Anonymous (no backing file) or memfd-backed. // Exclude vvar, vsyscall, stack, heap labels, and QEMU's own segments. let is_anon = path.is_empty(); let is_memfd = path.starts_with("/memfd:"); if !is_anon && !is_memfd { continue; } let (start, end) = parse_addr_range(addrs).unwrap_or((0, 0)); if end <= start { continue; } let size = end - start; if size < MIN_REGION_SIZE { continue; } out.push(MemRegion { gpa_base: 0, hva_base: start, size }); } out } fn parse_addr_range(s: &str) -> Option<(u64, u64)> { let (a, b) = s.split_once('-')?; Some((u64::from_str_radix(a, 16).ok()?, u64::from_str_radix(b, 16).ok()?)) }