Spectral/src/mem.rs
2026-04-11 20:23:27 -04:00

111 lines
4 KiB
Rust

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=<mmap addr>)
pub struct MemRegion {
pub gpa_base: u64,
pub hva_base: u64,
pub size: u64,
}
pub struct GuestMem {
pub regions: Vec<MemRegion>,
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<GuestMem> {
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<MemRegion> = 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/<pid>/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<u64, String> {
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<u32, String> {
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<MemRegion> {
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()?))
}