Changelog¶
What changed in each release of ntoseye, newest first. The command-line tool, the Python package on PyPI, and the Rust crate on crates.io share one version number.
v0.46.0 (2026-10-05)¶
ntoseye can now debug inside a Windows Sandbox or Hyper-V VM on Intel hosts, not only read it: break on the guest’s code and data, step its threads, and change its memory and registers.
Added¶
Breakpoints and steps in a Windows Sandbox or Hyper-V VM (experimental, gdb backend):
bain a.partitionview sets a hardware breakpoint or data watch that stops only on that guest’s VPs,/cin the view names one of them, and its/p,/tand condition read that guest.gfrom the view runs the target, and the hit shows the guest’s view on the VP that hit it.t,pandguthere step the guest’s thread, andg <address>runs to an address in the guest.eb,edand the other writes there change the guest’s memory, andrchanges the registers of a VP that a vCPU runs at the stop, such as one that hit a breakpoint. In the SDK,breakpoints.add(..., hardware=True),step(),step_over(),step_out()andrun_to()afterselect_partition()do the same, andBreakpoint.partitionnames the guest. See https://ntoseye.com/vbs/guest-partitions/#breakpoints-in-a-guest-partition.lm a <address>shows only the module that contains an address, and finds a kernel module for a kernel address even while a process is selected, as in WinDbg.
Fixed¶
!dlls,lm,!vadand the SDK’sProcess.modulesname a user-mode module whose loader entry’s name is paged out, instead of showing<unknown>: the name comes from the file that the image’s VAD maps. On a desktop guest, this was about 8% of the user-mode modules.
v0.45.0 (2026-10-03)¶
ntoseye can now debug the Windows hypervisor itself on Intel hosts. It can walk hypervisor partitions and virtual processors, inspect and disassemble guest memory, including WSL2, decode hypercalls and break on them based on the caller, and unwind hypervisor stacks. Kernel module unloads can now stop the debugger too, just like module loads.
Added¶
sxe/sxn/sxd/sxiud[:<module>]stop on, report, or ignore kernel module unloads the wayldhandles loads, with-cto run commands at the stop; the stop comes after the driver’s unload routine, while the module is still listed with its symbols. The SDK hasExceptions.module_eventsandStop.ModuleUnload; DAP reports the stop reasonmodule unload.~accepts the vCPU id it lists in place of a processor number (~p01.03s,~p01.03k,~p01.03r), ignoring case.Registers.get(name, default=None)returns a register’s value, ordefaultwhen the register file (for example an unwound frame) does not hold it.DisassembledInstructionhaslength,mnemonicandoperands; eachOperanddescribes a register, memory, immediate or branch operand, so scripts no longer need to parseasm.!hvpartitionsshows the Windows hypervisor’s partitions as a tree with their privileges and each VP’s processor, VTL, where it left off and its last exit;!hvvpslists a partition’s VPs and the processors that run them.~, the stop line and the MCP trailer (runs partition 0x4 VP 2) name a vCPU that runs a guest partition (Hyper-V VM, WSL2, Sandbox) aspartition 0x4 VP 2; the SDK’s run status has it asrunning_vp. The SDK hasdbg.hypervisor_partitions()andVirtualProcessor.processors. Hypervisor commands need the memory or gdb backend, or kd/kdnet with host memory, and say so otherwise.!hvept <gpa>(or-v <address>for a root virtual address) walks each VTL’s extended page tables and shows the host address, access, page size and memory type;!hveptdifflists the guest physical ranges VTL0 and VTL1 map differently. The SDK hasHypervisorVtl.translate()andVirtualProcessor.ept_differences().!hvvmcs [-msr|-io]shows every field of a VTL’s Enlightened VMCS, the MSRs it intercepts or passes (named, e.g.IA32_EFER), and its intercepted I/O ports. The SDK hasHypervisorVtl.vmcs_fields(),msr_intercepts()andio_intercepts().!hvdreads and!hvudisassembles a guest partition’s memory, virtual (4-level long-mode guests) or with-pphysical. Like!hvept,!hveptdiffand!hvvmcs, they default to the VP the current processor runs. The SDK hasHypervisorVtl.read(),translate_virtual()anddisassemble().!hvcallslists the hypervisor’s hypercall table with TLFS names, rep/variable-header flags, input/output sizes and handlers (-aincludes unimplemented codes);hv!now has names for the hypercall handlers andhv!VmExitEntry, usable ink,u,ln,xand expressions. The SDK hasdbg.hypercalls().A saved VTL state shows the VM exit’s qualification, interruption info and instruction length, and the guest’s general-purpose registers recovered from the hypervisor’s exit entry code, which
r, expressions and stacks use. A state that may describe the previous exit is marked “(may be one exit behind)” and is not selected automatically; a hardware breakpoint onhv!VmExitEntrystops with a current state. The SDK hasSavedVtlState.general_registersandmay_be_stale.At a hypervisor stop, the stop header,
~and.vtlcxrname the guest VP the processor serves and decode the hypercall being handled (code, TLFS name, flags);!hvcallshows its full input, including XMM fast calls, rep lists and extended GVA flush ranges with their addresses, page counts and page sizes. The hypercall page shows as modulehvcall(hvcall!Hypercall,hvcall!VtlReturn64). MCP, DAP thread names and the SDK (Cpu.serving,SavedVtlState.hypercall) show the same.!hvbp <call> [partition [vp]]stops on a hypercall only from the given caller; its condition is evaluated on the caller’s registers and reads the caller’s memory, so$pqwo(rdx)tests a slow call’s input. The SDK hasBreakpoints.add_hypercall(),Breakpoint.hypercall, andCpu.hypercall_caller(), which gives awhen=callback the caller’s registers, decoded call and memory.!hvexit <reason> [partition [vp]]stops on a VM exit by its reason (cpuid,rdmsr,wrmsr,ept_violation, or the number), only from the given caller; its condition sees the caller’s registers at the exit, so!hvexit wrmsr if @rcx==0x6e0stops on writes ofIA32_TSC_DEADLINE. The guest runs far slower while it is set, as every exit is checked, so a guest partition’s VPs barely run and a filter on one rarely stops;!hvbpstops on their hypercalls. The SDK hasBreakpoints.add_exit()andBreakpoint.vm_exit.!hvr [partition vp [vtl]]shows the registers of any hypervisor VP, such as a WSL2 VP blocked inHLTthat no processor runs: those of the vCPU that runs it, of the exit a vCPU handles for it, or that it saved at its last exit. The SDK hasVirtualProcessor.registers()..partition <id>inspects the Windows guest of a hypervisor partition, such as a Windows Sandbox, in place of the target, read-only:lm,!process,dt,uandkread its kernel with its symbols, and its VPs are the threads..partition 1returns to the target, and a partition that runs no Windows kernel, such as WSL2’s, is refused with that reason. The SDK hasDebugger.select_partition()andDebugger.partition; MCP’s trailer says which partition is inspected.Hypervisor stacks unwind from the hypervisor’s own unwind data when a copy of the running
hvix64.exeis in the symbol cache or a local store (.fetchimage /f <file>,Symbols.import_image()add one), and otherwise from function prologs, marked[prolog]. The few functions whose unwind data leaves out their stack allocation, such as the external-interrupt exit handler from build 22621 on, unwind from their prologs too. The SDK’sCpu.backtrace()walks a processor’s own stack, the hypervisor’s when it is halted there, and withvtl=from where a VTL left off as the hypervisor saved it..vtlcxr 1selects the state the hypervisor saved for VTL1, sok,r,uand expressions inspect the secure kernel where it left off (read-only);.vtlcxrreturns to VTL0.In DAP, a vCPU halted in the hypervisor shows the hypervisor’s own frames first, then a label frame, then the saved VTL state’s frames.
Changed¶
Breaking:
Exceptions.module_loadsis replaced byExceptions.module_events, which lists the unload filters (sx* ud) along with the load filters.Breaking: MCP’s
commandtool no longer takesformat, and rejects a call that passes it: it returns the REPL’s text with its[target ...]trailer. For typed results, use the Python SDK.The hypervisor image is named
hvas in WinDbg (hv+0x…instead ofhvix64+0x…), and expressions accepthvin the hypervisor’s context.On the gdb backend, software breakpoints in user space are refused (use
ba e1); single steps into user space use debug-register sites, while a run to a user-space address (pover a call,gu) ends with an error. Previously strayint3s could be left in shared user code.Software breakpoints in the Windows hypervisor’s code are refused with a pointer to
ba e1, instead of left pending forever; NT breakpoints can still be set from the hypervisor’s context.On the gdb backend, a breakpoint whose condition or filter declines many hits a second slows the target much less: a declined hit no longer rewrites the site journal on disk.
On the gdb backend under VBS, single steps (
t,step(), and the walks built on them, such aswtandtrace_calls) are about 15% faster and use less host CPU: replies from the stub are read buffered, and a step no longer selects its vCPU again after the vCPU’s own stop.uwithout an address works as in WinDbg: it starts at the instruction pointer, and continues after the previousuuntil the target runs or another frame, thread, or process is selected.!peb,!teb,!gleand!dllsno longer need.process: without one they decode the current thread’s process, as in WinDbg.
Fixed¶
tafter~Nson kd/kdnet steps processor N instead of leaving it unmoved, and breakpoints on the stopped processor keep hitting afterwards; on ARM64 targets, stepping another processor is refused instead of hanging.Ctrl+C, a DAP
pause/disconnect, or a server termination signal now break in on a step that never stops, instead of the session hanging until the transport times out; walks (ta,pa,wt,step(until=)) end on the first Ctrl+C.A walk (
step(until=),step_over(until=),run_to(step=)) that itstimeout=or Ctrl+C cuts short now returns aStop.Interrupt, asrun_to()andstep_out()do, instead of aStop.Step. AStop.Stephad seemed to say the walk ended on an instruction in NT even when it was broken into in the Windows hypervisor, where the next step was refused.Ctrl+C in the REPL now stops the target when a breakpoint whose hits are declined (condition,
/tfilter, other process) fires constantly.REPL errors from
g,t,r,~and.threadshow their message instead of an internal form such asDebugInfo("…"), andbporbaon a backend that cannot set them names the backend instead of “the current backend does not support this”.In DAP, a Debug Console command after one that moved the context (
.thread,.cxr,!thread) runs instead of failing with “stale frame id” until the client walks the stack again.Code that profile-guided optimization split off from its function, far from every symbol, is named after that function through its chained unwind data (
nt!IopXxxControlFile+0x22bddf) in stacks, disassembly,lnand the SDK’ssymbols.nearest(), instead ofnt+0xb1552for “no symbol found”.wt,p,guand the SDK’sstep_over/step_outno longer follow the wrong thread, wait forever for a thread that exited, or crawl under load on code every thread runs; a walk whose thread exits ends with an error.A gdbserver step or continue that cannot start now reports SIGINT, so gdb’s
next/stepno longer loop.On the gdb backend, a
bponnt!DbgLoadImageSymbols(or the unload functions) now stops when nosxfilter surfaces the event.On the gdb backend, the data address QEMU sends with a vCPU’s next stop when that vCPU’s watchpoint hit lost the race to another vCPU’s stop no longer misclassifies the stop: a stop on an execute breakpoint is no longer mistaken for a plain stop, and a stop on a breakpoint is no longer reported as a data watchpoint’s hit.
On the gdb backend under VBS, a step or a resume from a breakpoint no longer gives up with “letting every vCPU run for 1s did not free it” when it should not. The debugger’s own work between the vCPUs’ runs counted against that second, and the first time a session finds a vCPU in the Windows hypervisor that work takes over half a second. And a vCPU waiting in an interrupt handler failed the step whenever the last run caught the handler in a call into the hypervisor, such as a spin loop’s long-wait notification; the step now ends in the handler, as it does when the last run finds the handler in NT.
On the gdb backend under VBS, a data watchpoint’s hit that another vCPU makes while a step or a resume from a breakpoint lets the other vCPUs run is no longer lost: a step ends on it (
step()returns that stop), and a resume reports it as its stop.On the gdb backend under VBS, a step or a resume from a breakpoint that waits on the other vCPUs no longer lets them run only a few milliseconds at a time when one of them is stopped on a breakpoint: that vCPU ended every run at once, and is now held for every other run.
On the gdb backend under VBS, a step no longer ends in a guest partition’s code (WSL2, a Hyper-V VM) when the Windows hypervisor runs that partition’s VP on the stepped vCPU’s processor while NT waits there, after which the next step failed with “Bad virtual address”. The step waits for NT to run there again, and a step of a vCPU that runs a guest partition’s VP is refused, naming the VP.
Breakpoint.delete()no longer raises for a breakpoint that is already gone (such as a fired one-shot), and a stale handle no longer deletes a newer breakpoint that reuses its id..threadon a thread whose vCPU is in the hypervisor selects where NT left off instead of the hypervisor’s registers..vtl 0after.vtl 1at a hypervisor stop returns to the stop’s view instead of the hypervisor’s registers.VTL1’s saved state is listed even before the secure kernel was looked up.
A saved VTL state is no longer reported as current after the processor has switched to another VTL or VP.
kat a hypervisor stop no longer repeats addresses or lists NT addresses as hypervisor frames; scan frames are always marked[scan].