Spectral/src/qemu.rs

150 lines
5.3 KiB
Rust

use std::fs;
use std::os::unix::io::RawFd;
// pidfd_open(2) — Linux 5.6+
// pidfd_getfd(2) — Linux 5.6+
// Neither is in libc yet so we syscall directly.
const SYS_PIDFD_OPEN: libc::c_long = 434;
const SYS_PIDFD_GETFD: libc::c_long = 438;
pub struct QemuVm {
pub pid: u32,
pub vm_fd: RawFd,
pub vcpu_fds: Vec<RawFd>,
/// fd numbers as they appear in the QEMU process — needed for ptrace injection
pub vcpu_fds_remote: Vec<RawFd>,
}
impl Drop for QemuVm {
fn drop(&mut self) {
unsafe {
libc::close(self.vm_fd);
for fd in &self.vcpu_fds {
libc::close(*fd);
}
}
}
}
impl QemuVm {
/// SIGSTOP the QEMU process and spin until every thread in /proc/<pid>/task/
/// shows State: T (stopped) or t (tracing stop).
///
/// Checking the main thread alone is insufficient: vCPU threads run inside
/// KVM_RUN (a blocking ioctl) and must exit before they enter group-stop and
/// release vcpu->mutex. Spinning until all TIDs are stopped ensures the mutex
/// is free before the injected ioctl tries to acquire it.
///
/// Previous bug: the check used `l.contains('t')` which is always true because
/// "State:" itself contains the letter 't' — freeze() returned immediately.
pub fn freeze(&self) {
unsafe { libc::kill(self.pid as libc::pid_t, libc::SIGSTOP); }
let task_dir = format!("/proc/{}/task", self.pid);
loop {
let Ok(dir) = fs::read_dir(&task_dir) else { break };
let all_stopped = dir
.filter_map(|e| e.ok())
.all(|e| {
let path = format!("{}/{}/status",
task_dir, e.file_name().to_string_lossy());
let Ok(s) = fs::read_to_string(&path) else { return true };
// Match "State:\tT ..." or "State:\tt ..." only — not the word "State" itself.
s.lines().any(|l| l.starts_with("State:\tT") || l.starts_with("State:\tt"))
});
if all_stopped { break; }
std::hint::spin_loop();
}
}
/// SIGCONT to resume.
pub fn thaw(&self) {
unsafe { libc::kill(self.pid as libc::pid_t, libc::SIGCONT); }
}
}
pub fn find_windows_vm() -> Option<QemuVm> {
for entry in fs::read_dir("/proc").ok()? {
let Ok(entry) = entry else { continue };
let pid_str = entry.file_name();
let Ok(pid) = pid_str.to_str().unwrap_or("").parse::<u32>() else { continue };
if !is_qemu_windows_process(pid) {
continue;
}
if let Some(vm) = steal_kvm_fds(pid) {
return Some(vm);
}
}
None
}
fn is_qemu_windows_process(pid: u32) -> bool {
let cmdline_path = format!("/proc/{}/cmdline", pid);
let Ok(cmdline) = fs::read(&cmdline_path) else { return false };
// cmdline is null-delimited; treat as bytes and look for qemu + windows indicators
let s = cmdline.split(|&b| b == 0)
.filter_map(|a| std::str::from_utf8(a).ok())
.collect::<Vec<_>>()
.join(" ");
// must be a qemu-system-x86_64 process running something Windows-flavoured
s.contains("qemu-system-x86_64") && (s.contains("windows") || s.contains("win11") || s.contains("win10"))
}
fn steal_kvm_fds(pid: u32) -> Option<QemuVm> {
// open a pidfd so we can duplicate fds out of the target process
// SAFETY: raw syscall, pid is a valid u32 we read from /proc
let pidfd = unsafe { libc::syscall(SYS_PIDFD_OPEN, pid as libc::pid_t, 0u32) };
if pidfd < 0 {
return None;
}
let pidfd = pidfd as RawFd;
let mut vm_fd: Option<RawFd> = None;
let mut vcpu_fds: Vec<RawFd> = Vec::new();
let mut vcpu_fds_remote: Vec<RawFd> = Vec::new();
let fd_dir = format!("/proc/{}/fd", pid);
for entry in fs::read_dir(&fd_dir).ok()? {
let Ok(entry) = entry else { continue };
let Ok(fd_num) = entry.file_name().to_str().unwrap_or("").parse::<RawFd>() else { continue };
let link_path = format!("/proc/{}/fd/{}", pid, fd_num);
let Ok(target) = fs::read_link(&link_path) else { continue };
let target = target.to_string_lossy();
if target == "anon_inode:kvm-vm" {
let dup = dup_fd(pidfd, fd_num);
if dup >= 0 { vm_fd = Some(dup); }
} else if target.starts_with("anon_inode:kvm-vcpu:") {
let dup = dup_fd(pidfd, fd_num);
if dup >= 0 {
vcpu_fds.push(dup);
vcpu_fds_remote.push(fd_num);
}
}
}
unsafe { libc::close(pidfd) };
let vm_fd = vm_fd?;
if vcpu_fds.is_empty() {
unsafe { libc::close(vm_fd) };
return None;
}
// Sort both vecs together by remote fd number so index 0 == vcpu 0
// (QEMU opens vcpu fds in vcpu-index order, so lowest fd == vcpu 0)
let mut pairs: Vec<(RawFd, RawFd)> = vcpu_fds_remote.into_iter().zip(vcpu_fds).collect();
pairs.sort_by_key(|&(remote, _)| remote);
let (vcpu_fds_remote, vcpu_fds): (Vec<_>, Vec<_>) = pairs.into_iter().unzip();
Some(QemuVm { pid, vm_fd, vcpu_fds, vcpu_fds_remote })
}
fn dup_fd(pidfd: RawFd, target_fd: RawFd) -> RawFd {
// SAFETY: pidfd and target_fd are valid fds obtained above
unsafe { libc::syscall(SYS_PIDFD_GETFD, pidfd, target_fd, 0u32) as RawFd }
}