150 lines
5.3 KiB
Rust
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 }
|
|
}
|