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, /// fd numbers as they appear in the QEMU process — needed for ptrace injection pub vcpu_fds_remote: Vec, } 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//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 { 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::() 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::>() .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 { // 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 = None; let mut vcpu_fds: Vec = Vec::new(); let mut vcpu_fds_remote: Vec = 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::() 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 } }