--- title: "Test Windows drivers in seconds, not minutes" description: "Use Windows PE and QEMU as a disposable harness for KMDF testing, fuzzing, and kernel debugging without maintaining a full Windows virtual machine." date: 2026-06-28 category: "technical" tags: ["winpe","windows","internals","ci-cd","kernel","nt","qemu"] published: true paywall: false --- Automated testing of a Windows kernel driver often begins with the heaviest possible test fixture: a complete Windows virtual machine. Every run boots a desktop operating system, starts services the test does not need, restores state, installs a driver, and eventually reaches the few seconds of code that matter. The same overhead appears in driver fuzzing. A crash is expected, but recovering from it may involve reverting a snapshot and waiting for a full guest to become usable again. The feedback loop is measured in minutes when the actual target is a kernel module. I wanted a smaller boundary. For many KMDF tests, the graphical shell and most of user space are irrelevant. What is needed is the NT kernel, the driver stack, a test agent, and a reliable way to collect failures. [Windows PE](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/winpe-intro?view=windows-11) provides that boundary. It boots a minimal Windows environment from a WIM image, usually into a RAM-backed `X:` drive. Each boot starts from the same image, so a crashed or corrupted run can be discarded instead of repaired. ## The harness architecture The setup has four components: 1. A customized `boot.wim` containing the driver, test agent, and required tools. 2. A BCD store configured for unattended boot and kernel debugging. 3. QEMU providing a small, predictable hardware topology. 4. A host-side controller that starts the VM, waits for a result, and collects debugger output. The WIM is the immutable base. Changes made during a test disappear with the VM. Changes that should survive, such as a new driver build or test binary, are injected into the image before the next run. This makes the environment reproducible without relying on a chain of snapshots. The state of a run is defined by the WIM, BCD, QEMU command, and test input. ## Prepare the image and boot policy Mount `boot.wim` with DISM, copy the required files, adjust its offline registry if necessary, and commit the image: ```cmd dism /Mount-Image /ImageFile:C:\winpe\boot.wim /Index:1 /MountDir:C:\winpe\mount rem Copy the driver and test agent, then apply offline configuration. dism /Unmount-Image /MountDir:C:\winpe\mount /Commit ``` The BCD store is separate from the WIM and must be edited separately. For a disposable CI target, recovery screens and boot delays only block automation. I use the following test-only configuration: ```cmd bcdedit /store C:\winpe\media\Boot\BCD /set {default} bootstatuspolicy ignoreallfailures bcdedit /store C:\winpe\media\Boot\BCD /set {default} recoveryenabled no bcdedit /store C:\winpe\media\Boot\BCD /set {bootmgr} timeout 0 bcdedit /store C:\winpe\media\Boot\BCD /set {default} testsigning yes bcdedit /store C:\winpe\media\Boot\BCD /set {default} hypervisorlaunchtype off bcdedit /store C:\winpe\media\Boot\BCD /set {default} isolatedcontext no ``` `testsigning` allows a test-signed driver to load. Disabling the hypervisor and isolated context prevents VBS or HVCI from changing the environment under test. These settings weaken platform security and belong only in a disposable driver-testing image. ## Keep QEMU hardware simple Modern QEMU machine profiles expose a realistic PCIe topology. Realism is not always helpful in a test harness. KDNET needs a supported adapter at a known bus address, and every extra root port makes that mapping harder to reason about. The older i440FX `pc` machine provides a flat PCI bus. I attach an emulated Intel `e1000` adapter at address `0x10`, which becomes device 16 in the decimal `bus.device.function` notation used by the debugger. On a Windows host using WHPX, a minimal command can start like this: ```cmd qemu-system-x86_64.exe ^ -M pc -accel whpx -cpu Skylake-Client-IBRS ^ -m 1024 -vga none -nographic -no-reboot ^ -device e1000,bus=pci.0,addr=0x10 ^ ... ``` The explicit CPU model avoids relying on `-cpu host` under WHPX. The important part for debugging is consistency: keep the machine type, CPU configuration, and PCI address fixed between image preparation and execution. ## Connect WinDbg through KDNET KDNET communicates with a supported network controller below the normal guest networking path. This allows kernel debugging even when the driver under test damages higher networking layers. Configure the target with a host address, port, connection key, and the known PCI location: ```cmd bcdedit /store C:\winpe\media\Boot\BCD /debug {default} on bcdedit /store C:\winpe\media\Boot\BCD /dbgsettings net hostip:10.0.2.2 port:50000 key:1.2.3.4 bcdedit /store C:\winpe\media\Boot\BCD /set {dbgsettings} busparams 0.16.0 ``` The key above is only a placeholder. Generate a real one for the environment. Microsoft recommends using `kdnet.exe` to confirm that the target adapter is supported and to produce the debugger configuration. Adapter choice matters. `e1000` works because the relevant Intel controller is supported by the Windows debugging transport. `virtio-net` or another high-performance virtual adapter will not become a KDNET device merely because WinPE can use it after loading a driver. Boot-time debugger support and ordinary NDIS support are separate concerns. ## Replace the interactive shell with a test agent An automated image does not need Explorer or an interactive command prompt. `winpeshl.ini` can launch the test agent as the WinPE shell: ```ini [LaunchApps] %SYSTEMROOT%\System32\test_agent.exe, "--param1" ``` The agent can install or start the driver, execute the test corpus, send a compact result to the host, and terminate. In the configuration I tested, termination of the main WinPE shell leads to a reboot request. With QEMU's `-no-reboot`, that request ends the VM and returns control to the host runner. This gives the pipeline a simple contract: process exit means the test is over. A kernel crash is captured by WinDbg, a normal result is emitted by the agent, and the next run starts from a fresh image. WinPE normally runs `wpeinit`, which processes initialization settings and may configure networking. If ordinary guest networking is unnecessary, set `false` in the `windowsPE` pass of the answer file. KDNET can continue to use its own supported controller path while DHCP and the regular network stack are skipped. ## Keep a serial fallback Network debugging should not be the only control channel when testing kernel code. Windows Emergency Management Services can expose the Special Administration Console through a serial port. Enable EMS in BCD with `bcdedit /ems on` and configure COM1 with `bcdedit /emssettings emsport:1 emsbaudrate:115200`. QEMU can map that serial device to a host TCP socket or a local pipe. The channel remains useful when normal networking is unavailable and provides a simple way to inspect boot progress before KDNET connects. ## A KDNET pitfall under KVM On Linux hosts, it is tempting to add several Hyper-V enlightenment flags to improve Windows guest performance. In my tests, configurations using flags such as `hv-relaxed` and `hv-vapic` made KDNET unreliable. WinDbg remained at `Waiting to reconnect...` while the guest continued to boot. For this harness, I prefer a clean CPU configuration first and add optimizations only after debugging works consistently. A test VM that boots slightly faster but occasionally loses its debugger is slower in practice. ## The resulting test cycle The final workflow is deliberately small: 1. Inject the current driver and agent into `boot.wim`. 2. Start QEMU with a fixed machine and device layout. 3. Let the agent run one test or fuzzing batch. 4. Capture normal output through the agent or fallback channel. 5. Capture kernel failures through KDNET. 6. Let the guest exit and start the next run from the same clean image. WinPE is not a replacement for testing on a complete Windows installation. Drivers still need coverage across real configurations, security features, devices, and long-running system state. It is a fast first layer for tests that only require the kernel and a controlled user-mode agent. Removing the desktop operating system from the inner loop changes driver testing from VM maintenance into process orchestration. That is the useful part: after a crash, there is nothing to restore. The next clean target is only another boot away.