111 lines
4 KiB
Rust
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()?))
|
|
}
|