Building a Kernel Driver: First Blood
A custom Windows kernel driver, two BSODs, and a remote deploy pipeline built in one sitting.
The Problem
I build hardware tools. The X52 Pro HOTAS configurator, SpaceMouse integration, sim wheels — all of them need to present virtual HID devices to Windows. Until now, that meant depending on vJoy and HidHide: two third-party drivers maintained by other people, with their own quirks, their own install headaches, and their own limitations.
vJoy works. But it's old. It doesn't support arbitrary HID descriptors. It requires test signing. And if you want to do anything beyond basic joystick axes and buttons, you're fighting the framework instead of building your product.
So I built my own.
VirtualController
VirtualController is a Windows kernel-mode bus driver. It sits in the kernel as a virtual bus, and when user-mode code tells it to create a device, it spawns a child PDO (Physical Device Object) that Windows sees as a real HID device. Plug in a virtual HOTAS, a virtual SpaceMouse, a virtual racing wheel — whatever you need. Each one gets its own HID descriptor, its own VID/PID, its own friendly name.
The stack:
- VCBus.sys — KMDF kernel driver, the bus enumerator
- VCClient.dll — C API wrapper, talks to the driver via IOCTLs
- vcclient.py — Python ctypes bridge, auto-detects the DLL
- vcconfig.py — Config GUI with device presets
All original code. Nothing borrowed from vJoy, ViGEmBus, or HidHide.
The First BSOD
The driver compiled. The test cert was created. Test signing was enabled. I ran install_driver.bat and the screen went blue.
Bugcheck: 0x0000007E (0xC0000005, 0xfffff8011e3c15a0, ...)
SYSTEM_THREAD_EXCEPTION_NOT_HANDLED — ACCESS_VIOLATION
Access violation in kernel space. The kind of crash that reboots your machine with no warning.
The first instinct was to check the driver source — and there were real bugs in there. The WDF child list was configured with the wrong identification description struct. The scan callback was copying raw kernel pointers into ephemeral buffers. The unload callback had the wrong function signature. All worth fixing.
But none of those were the crash.
Disassembling the Crash
The event log gave me the faulting instruction address. I pulled the minidump, cross-referenced with the driver's disassembly, and found the crash point:
mov r10, qword ptr [WdfFunctions]
call qword ptr [r10+3A0h]
That's a WDF function table dispatch. Index 0x74 = 116 decimal. I looked it up in the WDK headers:
WdfDriverCreateTableIndex = 116,
WdfDriverCreate — the very first WDF call in DriverEntry. The function table was garbage because the WDF loader never ran.
The culprit was one line in the build configuration:
<EntryPointSymbol>DriverEntry</EntryPointSymbol>
In a WDF driver, the PE entry point must be FxDriverEntry, not DriverEntry. The WDF loader library (wdfdriverentry.lib) provides FxDriverEntry, which calls WdfVersionBind to set up the function table, then calls your DriverEntry. By setting the entry point directly to DriverEntry, the WDF initialisation was skipped entirely. Every WDF function call was jumping through a null pointer.
One line. One BSOD.
Don't Test Kernel Drivers on Your Dev Machine
Obvious in hindsight. After the second BSOD (this time on the dev machine), I set up a proper test pipeline. A Surface tablet with test signing enabled, connected via OpenSSH:
# Build on dev machine
./build_driver.bat
# Sign with the test cert
signtool sign /s PrivateCertStore /sha1 <hash> /fd sha256 VCBus.sys
signtool sign /s PrivateCertStore /sha1 <hash> /fd sha256 vcbus.cat
# Push to tablet
scp VCBus.sys vcbus.cat gavin@192.168.0.110:C:\VCBus\
# Install remotely
ssh gavin@192.168.0.110 "pnputil /add-driver C:\VCBus\VCBus.inf /install"
ssh gavin@192.168.0.110 "devcon install C:\VCBus\VCBus.inf Root\VirtualController"
If the tablet BSODs, my dev machine keeps running. Rebuild, re-sign, SCP, try again. The whole cycle takes about thirty seconds.
First Successful Load
After fixing the entry point:
> devcon status Root\VirtualController
ROOT\SYSTEM\0002
Name: VirtualController Bus
Driver is running.
1 matching device(s) found.
> sc query VCBus
SERVICE_NAME: VCBus
STATE: 4 RUNNING
Then the Python test:
=== VirtualController Test ===
Version: 0.1.0
Simulated: False
Driver: available
Creating virtual 6DOF device...
Device ID: 1
Connected: True
That Simulated: False line is the one that matters. The DLL isn't faking it. It's talking to a real kernel driver, through a real device interface, creating a real virtual device in the Windows device tree.
What's Next
The device creation works. The bus driver is running. The next step is getting Windows to enumerate the child HID devices — once hidclass.sys picks up the PDO, input reports will flow and I can start feeding real axis data from the X52 Pro through VirtualController instead of vJoy.
Then it's a matter of writing the right HID descriptors for each device type and swapping the backend in the existing configurator tools.
What I Learned
-
WDF drivers must use
FxDriverEntry, not your ownDriverEntry, as the PE entry point. The WDF loader sets up the function table before calling your code. Skip it and every WDF call crashes. -
WDF child list identification descriptions must start with
WDF_CHILD_IDENTIFICATION_DESCRIPTION_HEADER. Pass the wrong struct and you're corrupting WDF's internal state. -
Never test kernel drivers on your working machine. Set up a cheap test device with SSH and test signing. The deploy cycle is faster than rebooting from a BSOD.
-
The crash offset is your best friend. When a kernel driver BSODs, the faulting address minus the module base gives you the RVA. Cross-reference with the disassembly and the WDF function enum, and you can identify the exact API call that failed — even without symbols loaded.
VirtualController is part of the indigo-nx hardware tools ecosystem. It's not released yet — this is the build diary. Follow along at indigo-nx.com.