diff --git a/src/eprocess.rs b/src/eprocess.rs new file mode 100644 index 0000000..cafec97 --- /dev/null +++ b/src/eprocess.rs @@ -0,0 +1,120 @@ +use crate::kernel; +use crate::mem::GuestMem; +use crate::offsets; +use crate::paging; + +pub struct Process { + pub eprocess_va: u64, + pub pid: u64, + pub dtb: u64, + pub name: String, +} + +/// Resolve the current EPROCESS via the KPCR on the vCPU whose GS.base is supplied. +/// +/// SAFETY precondition: the vCPU must be in ring 0 (CS.selector & 3 == 0) +/// In ring 3 GS.base points at the TEB, not the KPCR, and the offsets are wrong +pub fn find_initial_eprocess( + mem: &GuestMem, + cr3: u64, + gs_base: u64, +) -> Result { + let kthread = paging::read_virt_u64( + mem, cr3, + gs_base + offsets::KPCR_PRCB + offsets::KPRCB_CURRENT_THREAD, + ).map_err(|e| format!("read KPRCB.CurrentThread: {}", e))?; + + if kthread < 0xffff_8000_0000_0000 { + return Err(format!("CurrentThread {:#x} is not a kernel VA", kthread)); + } + + let eprocess = paging::read_virt_u64(mem, cr3, kthread + offsets::KTHREAD_PROCESS) + .map_err(|e| format!("read KTHREAD.Process: {}", e))?; + + if eprocess < 0xffff_8000_0000_0000 { + return Err(format!("EPROCESS {:#x} is not a kernel VA", eprocess)); + } + + Ok(eprocess) +} + +/// Walk the circular ActiveProcessLinks list starting from `initial_eprocess_va`. +/// +/// Flink points into the *next* EPROCESS's links field, so subtracting +/// EPROC_LINKS recovers the EPROCESS base. The loop ends when Flink brings +/// us back to the starting links entry (full circle) or a bad VA is read. +pub fn walk_eprocess_list( + mem: &GuestMem, + cr3: u64, + initial_eprocess_va: u64, +) -> Vec { + let mut out = Vec::new(); + let mut current = initial_eprocess_va; + + loop { + match read_process(mem, cr3, current) { + Some(p) => out.push(p), + None => break, + } + + let flink = match paging::read_virt_u64(mem, cr3, current + offsets::EPROC_LINKS) { + Ok(v) => v, + Err(_) => break, + }; + + // Flink is a VA pointing at the links field of the next node. + if flink < 0xffff_8000_0000_0000 { + break; + } + + let next = flink - offsets::EPROC_LINKS; + + // Full circle: Flink of last entry == links field of the first entry. + if next == initial_eprocess_va { + break; + } + + current = next; + } + + out +} + +fn read_process(mem: &GuestMem, cr3: u64, eprocess_va: u64) -> Option { + let pid = paging::read_virt_u64(mem, cr3, eprocess_va + offsets::EPROC_PID).ok()?; + let dtb = paging::read_virt_u64(mem, cr3, eprocess_va + offsets::EPROC_DTB).ok()?; + let name = read_name(mem, cr3, eprocess_va + offsets::EPROC_NAME); + + Some(Process { eprocess_va, pid, dtb, name }) +} + +/// Find the System EPROCESS via `PsInitialSystemProcess` in the kernel export table. +/// Doesn't depend on KTHREAD.Process offset — works regardless of build. +/// +/// PsInitialSystemProcess is a PEPROCESS (pointer to EPROCESS), so we read the +/// export VA to get the pointer, then dereference it to get the EPROCESS itself. +pub fn find_initial_eprocess_via_export( + mem: &GuestMem, + cr3: u64, + kernel_base: u64, +) -> Result { + let ps_va = kernel::find_export(mem, cr3, kernel_base, "PsInitialSystemProcess") + .ok_or("PsInitialSystemProcess not found in export table")?; + + let eprocess = paging::read_virt_u64(mem, cr3, ps_va) + .map_err(|e| format!("deref PsInitialSystemProcess at {:#x}: {}", ps_va, e))?; + + if eprocess < 0xffff_8000_0000_0000 { + return Err(format!("PsInitialSystemProcess value {:#x} is not a kernel VA", eprocess)); + } + Ok(eprocess) +} + +fn read_name(mem: &GuestMem, cr3: u64, gva: u64) -> String { + let mut buf = [0u8; 15]; + if paging::read_virt(mem, cr3, gva, &mut buf).is_err() { + return String::from("?"); + } + let len = buf.iter().position(|&b| b == 0).unwrap_or(15); + String::from_utf8_lossy(&buf[..len]).into_owned() +} diff --git a/src/kernel.rs b/src/kernel.rs new file mode 100644 index 0000000..c3c1b13 --- /dev/null +++ b/src/kernel.rs @@ -0,0 +1,109 @@ +use crate::mem::GuestMem; +use crate::paging; + +// Windows KASLR aligns the kernel load address to 2 MiB boundaries. +const STEP_2M: u64 = 0x200_000; +const MAX_SCAN: u64 = 0x4000_0000; // 1 GiB + +/// Find ntoskrnl base from a VA known to be inside it. +/// +/// Two-pass strategy: +/// 1. Coarse: 2 MiB steps over 1 GiB (fast, handles standard KASLR alignment). +/// 2. Fine: 4 KiB steps over 32 MiB (fallback if the base is not 2 MiB-aligned). +/// Each candidate is validated against the `PsInitialSystemProcess` export so we +/// don't mistake a driver or HAL for ntoskrnl. +pub fn find_kernel_base(mem: &GuestMem, cr3: u64, hint_va: u64) -> Option { + if let Some(b) = scan(mem, cr3, hint_va, STEP_2M, MAX_SCAN) { + return Some(b); + } + scan(mem, cr3, hint_va, 0x1000, 0x200_0000) // 4 KiB steps, 32 MiB +} + +fn scan(mem: &GuestMem, cr3: u64, hint_va: u64, step: u64, range: u64) -> Option { + let mut addr = hint_va & !(step - 1); + let floor = hint_va.saturating_sub(range) & !(step - 1); + + while addr >= floor { + if is_pe_header(mem, cr3, addr) && + find_export(mem, cr3, addr, "PsInitialSystemProcess").is_some() + { + return Some(addr); + } + if addr < step { break; } + addr -= step; + } + None +} + +/// Read an x64 IDT gate entry and return the full 64-bit handler VA. +/// +/// x64 IDT entry layout (16 bytes): +/// +0 u16 offset[15:0] +/// +2 u16 selector +/// +4 u8 ist +/// +5 u8 type/attr +/// +6 u16 offset[31:16] +/// +8 u32 offset[63:32] +/// +12 u32 reserved +pub fn read_idt_handler(mem: &GuestMem, cr3: u64, idt_va: u64, idx: u64) -> Option { + let mut e = [0u8; 16]; + paging::read_virt(mem, cr3, idt_va + idx * 16, &mut e).ok()?; + let lo = u16::from_le_bytes([e[0], e[1]]) as u64; + let mid = u16::from_le_bytes([e[6], e[7]]) as u64; + let hi = u32::from_le_bytes([e[8], e[9], e[10], e[11]]) as u64; + Some((hi << 32) | (mid << 16) | lo) +} + +fn is_pe_header(mem: &GuestMem, cr3: u64, va: u64) -> bool { + let mut mz = [0u8; 2]; + if paging::read_virt(mem, cr3, va, &mut mz).is_err() { return false; } + if mz != [0x4D, 0x5A] { return false; } + + let pe_off = match paging::read_virt_u32(mem, cr3, va + 0x3C) { + Ok(v) if v < 0x1000 => v as u64, + _ => return false, + }; + + let mut sig = [0u8; 4]; + if paging::read_virt(mem, cr3, va + pe_off, &mut sig).is_err() { return false; } + sig == [0x50, 0x45, 0x00, 0x00] +} + +/// Resolve a named export from the PE image at `base_va`. +/// For data exports (like PsInitialSystemProcess) this is the variable's VA; +/// the caller must dereference it to get the pointed-to value. +/// +/// IMAGE_OPTIONAL_HEADER64 starts at pe_off + 4 (PE sig) + 20 (IMAGE_FILE_HEADER). +/// DataDirectory[0] (export) is at optional_header + 0x70. +pub fn find_export(mem: &GuestMem, cr3: u64, base_va: u64, target: &str) -> Option { + let pe_off = paging::read_virt_u32(mem, cr3, base_va + 0x3C).ok()? as u64; + + let export_rva = paging::read_virt_u32(mem, cr3, base_va + pe_off + 0x18 + 0x70).ok()? as u64; + if export_rva == 0 { return None; } + let export_va = base_va + export_rva; + + let num_names = paging::read_virt_u32(mem, cr3, export_va + 0x18).ok()? as u64; + let names_rva = paging::read_virt_u32(mem, cr3, export_va + 0x20).ok()? as u64; + let ords_rva = paging::read_virt_u32(mem, cr3, export_va + 0x24).ok()? as u64; + let funcs_rva = paging::read_virt_u32(mem, cr3, export_va + 0x1C).ok()? as u64; + + for i in 0..num_names { + let name_rva = paging::read_virt_u32(mem, cr3, base_va + names_rva + i * 4).ok()? as u64; + if !name_matches(mem, cr3, base_va + name_rva, target) { continue; } + + let mut ord_bytes = [0u8; 2]; + paging::read_virt(mem, cr3, base_va + ords_rva + i * 2, &mut ord_bytes).ok()?; + let ordinal = u16::from_le_bytes(ord_bytes) as u64; + + let func_rva = paging::read_virt_u32(mem, cr3, base_va + funcs_rva + ordinal * 4).ok()? as u64; + return Some(base_va + func_rva); + } + None +} + +fn name_matches(mem: &GuestMem, cr3: u64, va: u64, target: &str) -> bool { + let mut buf = [0u8; 64]; + if paging::read_virt(mem, cr3, va, &mut buf).is_err() { return false; } + let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + &buf[..len] == target.as_bytes() +} diff --git a/src/main.rs b/src/main.rs index d27bd1d..ad41006 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ mod asm; +mod eprocess; +mod kernel; mod kvm; mod mem; +mod offsets; mod paging; mod ptrace_inject; mod qemu; @@ -10,31 +13,82 @@ fn main() { println!("[+] pid={} vm_fd={} vcpu_fds={:?} remote={:?}", vm.pid, vm.vm_fd, vm.vcpu_fds, vm.vcpu_fds_remote); - vm.freeze(); - let sregs = kvm::get_sregs_injected(vm.pid, vm.vcpu_fds_remote[0]) - .expect("KVM_GET_SREGS injection failed"); - let regs = kvm::get_regs_injected(vm.pid, vm.vcpu_fds_remote[0]) - .expect("KVM_GET_REGS injection failed"); - vm.thaw(); - println!("[+] vcpu0 CR3={:#018x}", sregs.cr3); - println!("[+] vcpu0 RIP={:#018x} RSP={:#018x}", regs.rip, regs.rsp); - let (eax, _, _, _) = asm::cpuid(0, 0); println!("[+] CPUID max_leaf={:#x} svm={}", eax, asm::svm_supported()); + // Try each vCPU looking for one in ring 0 (kernel mode). + // Windows KVAS gives user-mode vCPUs a shadow CR3 with no kernel mapping; + // the kernel scan only works from a ring-0 CR3. + // inject_ioctl does PTRACE_DETACH after each call which resumes the main + // thread — re-freeze before the next injection to put it back into group-stop. + let mut found = None; + for i in 0..vm.vcpu_fds_remote.len() { + vm.freeze(); + let Ok(sregs) = kvm::get_sregs_injected(vm.pid, vm.vcpu_fds_remote[i]) else { continue }; + let cpl = sregs.cs.selector & 3; + if cpl != 0 { + println!("[~] vCPU{} CPL={} (user mode), skipping", i, cpl); + continue; + } + vm.freeze(); + let Ok(regs) = kvm::get_regs_injected(vm.pid, vm.vcpu_fds_remote[i]) else { continue }; + found = Some((sregs, regs, i)); + break; + } + vm.thaw(); + + let (sregs, regs, vcpu_idx) = match found { + Some(v) => v, + None => { + println!("[-] no vCPU found in ring 0"); + return; + } + }; + + println!("[+] vCPU{} ring0: CR3={:#018x} RIP={:#018x} RSP={:#018x}", + vcpu_idx, sregs.cr3, regs.rip, regs.rsp); + let guest = mem::GuestMem::attach(vm.pid).expect("failed to map guest RAM"); for r in &guest.regions { println!("[+] mem region GPA {:#010x}..{:#010x} (HVA {:#010x})", r.gpa_base, r.gpa_base + r.size, r.hva_base); } - // Smoke-test the page table walker: translate CR3 itself (it's a GPA, not - // a GVA, so we translate an arbitrary kernel VA to prove the walk works) - // Real EPROCESS walk comes next let cr3 = sregs.cr3; - println!("[+] CR3 (guest PML4 GPA) = {:#018x}", cr3); - match paging::translate(&guest, cr3, sregs.cr3) { - Ok(gpa) => println!("[+] translate(CR3 as GVA) -> GPA {:#018x}", gpa), - Err(e) => println!("[-] translate: {}", e), + // Primary hint: RIP (usually inside ntoskrnl). + // Fallback: IDT entry 0 = KiDivideErrorFault, which is always in ntoskrnl + // and is typically within the first few MiB of the image. + let idt_handler = kernel::read_idt_handler(&guest, cr3, sregs.idt.base, 0); + println!("[+] IDT base={:#018x} entry[0]={:#018x}", + sregs.idt.base, + idt_handler.unwrap_or(0)); + + let hints: &[u64] = &[ + regs.rip, + idt_handler.unwrap_or(0), + ]; + let base_opt = hints.iter() + .filter(|&&h| h >= 0xffff_8000_0000_0000) + .find_map(|&h| kernel::find_kernel_base(&guest, cr3, h)); + + match base_opt { + None => println!("[-] kernel base scan failed"), + Some(base) => { + println!("[+] kernel base = {:#018x}", base); + match eprocess::find_initial_eprocess_via_export(&guest, cr3, base) { + Err(e) => println!("[-] PsInitialSystemProcess: {}", e), + Ok(ep) => { + println!("[+] System EPROCESS = {:#018x}", ep); + let procs = eprocess::walk_eprocess_list(&guest, cr3, ep); + println!("[+] {} processes:", procs.len()); + println!(" {:>6} {:>18} {:>18} name", + "PID", "EPROCESS", "DTB"); + for p in &procs { + println!(" {:>6} {:#018x} {:#018x} {}", + p.pid, p.eprocess_va, p.dtb, p.name); + } + } + } + } } } diff --git a/src/offsets.rs b/src/offsets.rs new file mode 100644 index 0000000..73f2059 --- /dev/null +++ b/src/offsets.rs @@ -0,0 +1,18 @@ +// Windows 11 22H2 build 22621.x x64 +// Derived from public PDB symbols via WinDbg: +// dt nt!_KPCR; dt nt!_KPRCB; dt nt!_KTHREAD; dt nt!_EPROCESS + +// KPCR → embedded KPRCB → CurrentThread +pub const KPCR_PRCB: u64 = 0x180; // _KPCR.Prcb (embedded _KPRCB) +pub const KPRCB_CURRENT_THREAD: u64 = 0x008; // _KPRCB.CurrentThread + +// KTHREAD → owning process +// _KTHREAD.Process (Ptr64 → _KPROCESS = _EPROCESS base on x64) +pub const KTHREAD_PROCESS: u64 = 0x220; + +// _EPROCESS / embedded _KPROCESS fields +pub const EPROC_DTB: u64 = 0x028; // _KPROCESS.DirectoryTableBase +pub const EPROC_PID: u64 = 0x440; // _EPROCESS.UniqueProcessId +pub const EPROC_LINKS: u64 = 0x448; // _EPROCESS.ActiveProcessLinks (Flink) +pub const EPROC_NAME: u64 = 0x5A8; // _EPROCESS.ImageFileName[15] +pub const EPROC_VAD_ROOT: u64 = 0x7D8; // _EPROCESS.VadRoot (RTL_AVL_TREE) diff --git a/src/ptrace_inject.rs b/src/ptrace_inject.rs index adbf24a..db8b83d 100644 --- a/src/ptrace_inject.rs +++ b/src/ptrace_inject.rs @@ -26,8 +26,59 @@ fn mem_read(pid: u32, addr: u64, buf: &mut [u8]) -> std::io::Result<()> { f.read_exact(buf) } +/// Search the target process's executable mappings for a `syscall` instruction +/// (0x0f 0x05) and return its virtual address. +/// +/// Writing shellcode to the stack is blocked by the NX (no-execute) bit: +/// attempting to execute it raises SIGSEGV, which QEMU's signal handler converts +/// to SIGKILL (status=0x9 from waitpid). Using an existing gadget in already- +/// executable memory avoids this entirely. +/// +/// The vdso is tried first — it's tiny, always present, and always contains +/// `syscall` — then all other executable regions are searched. +fn find_syscall_gadget(pid: u32) -> Option { + let maps = std::fs::read_to_string(format!("/proc/{}/maps", pid)).ok()?; + let mut mem = File::open(format!("/proc/{}/mem", pid)).ok()?; + + // Collect executable regions; float the vdso to the front. + let mut vdso: Vec<(u64, u64)> = Vec::new(); + let mut rest: Vec<(u64, u64)> = Vec::new(); + + for line in maps.lines() { + let cols: Vec<&str> = line.splitn(6, ' ').collect(); + if cols.len() < 2 { continue; } + if !cols[1].contains('x') { continue; } + + let mut parts = cols[0].split('-'); + let Ok(start) = u64::from_str_radix(parts.next().unwrap_or(""), 16) else { continue }; + let Ok(end) = u64::from_str_radix(parts.next().unwrap_or(""), 16) else { continue }; + if end <= start { continue; } + + if cols.get(5).map_or(false, |p| p.trim() == "[vdso]") { + vdso.push((start, end)); + } else { + rest.push((start, end)); + } + } + + for (start, end) in vdso.into_iter().chain(rest) { + // Cap per-region scan to 4 MiB so startup stays fast. + let size = ((end - start) as usize).min(4 << 20); + let mut buf = vec![0u8; size]; + if mem.seek(SeekFrom::Start(start)).is_err() { continue; } + if mem.read_exact(&mut buf).is_err() { continue; } + + for i in 0..buf.len().saturating_sub(1) { + if buf[i] == 0x0f && buf[i + 1] == 0x05 { + return Some(start + i as u64); + } + } + } + None +} + /// Inject `ioctl(remote_fd, ioctl_nr, ptr)` into the main QEMU thread (TID=PID) -/// using a `syscall; int3` gadget placed below the SysV red zone. +/// using an existing `syscall` gadget found in QEMU's already-executable memory. /// /// **Caller must call freeze() (SIGSTOP) before this function.** /// SIGSTOP guarantees every vCPU thread has exited KVM_RUN and called vcpu_put(), @@ -35,13 +86,17 @@ fn mem_read(pid: u32, addr: u64, buf: &mut [u8]) -> std::io::Result<()> { /// The injected ioctl then runs inside QEMU's mm so the kernel mm-check passes, /// and can acquire vcpu->mutex (which is free) to read the register state. /// -/// PTRACE_ATTACH is used instead of PTRACE_SEIZE+PTRACE_INTERRUPT because: -/// - The vCPU threads block SIGTRAP in their signal mask, making int3 unreliable. -/// - PTRACE_CONT on PTRACE_SEIZE'd threads in group-stop immediately re-stops. -/// - PTRACE_ATTACH on the main thread (which has a normal signal mask and is -/// already group-stopped) gives a clean ptrace-stop that PTRACE_CONT resumes -/// correctly, escaping the group-stop for that one thread while the rest stay -/// stopped. +/// PTRACE_SINGLESTEP is used instead of a `syscall; int3` shellcode gadget because: +/// - The stack is NX (no-execute); writing code there and jumping to it raises +/// SIGSEGV, which QEMU's signal handler escalates to SIGKILL. +/// - PTRACE_SINGLESTEP sets the TF (Trap Flag) and executes exactly one +/// instruction — the `syscall` — then stops, giving the same "stop after ioctl" +/// semantics without touching executable memory. +/// +/// PTRACE_SEIZE is used instead of PTRACE_ATTACH for the same reasons as before: +/// it reports the existing group-stop immediately so waitpid returns right away, +/// and PTRACE_CONT data=0 on the seized thread escapes group-stop for that one +/// thread while the rest stay stopped. pub fn inject_ioctl( pid: u32, remote_fd: RawFd, @@ -52,10 +107,9 @@ pub fn inject_ioctl( let null = 0usize as *mut c_void; let tid = pid as pid_t; - // PTRACE_ATTACH sends SIGSTOP internally; on an already group-stopped thread - // this does NOT generate a new ptrace-stop event, so the subsequent waitpid - // blocks forever. PTRACE_SEIZE instead reports the *existing* group-stop as - // an immediate notification — waitpid returns right away. + let gadget_addr = find_syscall_gadget(pid) + .ok_or("no syscall gadget found in target executable mappings")?; + let r = unsafe { libc::ptrace(PTRACE_SEIZE as _, tid, null, null) }; if r != 0 { return Err(format!("PTRACE_SEIZE: {}", std::io::Error::last_os_error()).into()); @@ -77,14 +131,9 @@ pub fn inject_ioctl( return Err(format!("PTRACE_GETREGS: {}", std::io::Error::last_os_error()).into()); } - // Scratch layout below the 128-byte SysV AMD64 red zone (descending): - // gadget_addr : 0f 05 cc (syscall; int3) — 8-byte aligned - // struct_addr : zeroed ioctl struct — 16-byte aligned - // rsp is left unchanged: neither `syscall` nor `int3` touch the user stack. - let gadget_addr = (orig.rsp - 128 - 8) & !7u64; - let struct_addr = (gadget_addr - struct_size as u64) & !15u64; - - mem_write(pid, gadget_addr, &[0x0f, 0x05, 0xcc])?; + // Place the ioctl output struct below the SysV AMD64 red zone. + // The stack is writable (just not executable), so this is fine for data. + let struct_addr = (orig.rsp - 128 - struct_size as u64) & !15u64; mem_write(pid, struct_addr, &vec![0u8; struct_size])?; let mut regs = orig; @@ -92,7 +141,7 @@ pub fn inject_ioctl( regs.rdi = remote_fd as u64; regs.rsi = ioctl_nr; regs.rdx = struct_addr; - regs.rip = gadget_addr; + regs.rip = gadget_addr; // existing executable `syscall` instruction let r = unsafe { libc::ptrace(PTRACE_SETREGS as _, tid, null, ®s as *const _ as *mut c_void) @@ -102,19 +151,25 @@ pub fn inject_ioctl( return Err(format!("PTRACE_SETREGS: {}", std::io::Error::last_os_error()).into()); } - // PTRACE_CONT with data=0 suppresses the pending SIGSTOP for this thread, - // resuming it while every other thread stays in group-stop. - // Loop until rip passes the 2-byte `syscall` instruction (gadget_addr+2). - // Stops at gadget_addr itself are signal-delivery-stops before the syscall; - // suppress each and retry. + // PTRACE_SINGLESTEP sets TF and executes exactly one instruction. + // data=0 on the first call suppresses the pending SIGSTOP, escaping group-stop. + // After `syscall` completes and sysret restores RFLAGS (with TF set), the + // CPU raises #DB and the thread stops with rip = gadget_addr + 2. + // If a different signal interrupts before the instruction executes (rip still + // at gadget_addr), suppress it and retry. let mut ret_regs: libc::user_regs_struct = unsafe { std::mem::zeroed() }; loop { - unsafe { libc::ptrace(libc::PTRACE_CONT, tid, null, null) }; + unsafe { libc::ptrace(libc::PTRACE_SINGLESTEP, tid, null, null) }; unsafe { libc::waitpid(tid, &mut status, libc::__WALL) }; + if libc::WIFEXITED(status) || libc::WIFSIGNALED(status) { + return Err(format!("QEMU exited during injection (status={:#x})", status).into()); + } unsafe { libc::ptrace(PTRACE_GETREGS as _, tid, null, &mut ret_regs as *mut _ as *mut c_void) }; - if ret_regs.rip >= gadget_addr + 2 { break; } + if ret_regs.rip == gadget_addr + 2 { + break; // syscall completed; rip now points past the 2-byte `syscall` + } if ret_regs.rip != gadget_addr { unsafe { libc::ptrace(libc::PTRACE_DETACH, tid, null, null) }; return Err(format!( @@ -122,6 +177,8 @@ pub fn inject_ioctl( ret_regs.rip, gadget_addr ).into()); } + // rip == gadget_addr: a signal fired before the instruction; suppressed + // by passing data=0 in the next PTRACE_SINGLESTEP. } let syscall_ret = ret_regs.rax as i64; diff --git a/src/qemu.rs b/src/qemu.rs index 12037b6..464938e 100644 --- a/src/qemu.rs +++ b/src/qemu.rs @@ -27,19 +27,31 @@ impl Drop for QemuVm { } impl QemuVm { - /// SIGSTOP the QEMU process and spin until /proc//status shows State: T. - /// Required before KVM_GET_REGS / KVM_GET_SREGS — the kernel returns EIO - /// if the vCPU thread is actively inside KVM_RUN when you call those ioctls. - /// SIGSTOP is async; the threads only exit KVM_RUN once the kernel delivers it. + /// 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 status_path = format!("/proc/{}/status", self.pid); + let task_dir = format!("/proc/{}/task", self.pid); loop { - let Ok(s) = fs::read_to_string(&status_path) else { break }; - // "State:\tT (stopped)" or "State:\tt (tracing stop)" - if s.lines().any(|l| l.starts_with("State:") && (l.contains('T') || l.contains('t'))) { - break; - } + 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(); } } diff --git a/src/stubs.s b/src/stubs.s index fecc8dc..09e6a87 100644 --- a/src/stubs.s +++ b/src/stubs.s @@ -62,3 +62,40 @@ wrmsr: shrq $32, %rdx # high 32 bits wrmsr ret +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +## +## +# +# +# +# +## +# +# +# +# kurva \ No newline at end of file