Analyzing Plug and Play Device Tree Information in Windows Kernel Mode
Understanding the Problem
You’re debugging why your driver takes 3 seconds to enumerate USB devices on boot, or why SetupAPI calls timeout after installing new hardware. The problem isn’t your code—it’s understanding that Windows maintains two separate representations of the device tree: the in-memory runtime tree the PnP Manager maintains, and the persistent registry hierarchy under
HKLM\SYSTEM\CurrentControlSet\Enum. Every property query you make traverses both, and most drivers do this wrong.In production, I’ve seen device enumeration storms where a single USB hub creates 50+ child PDOs, each triggering property queries that hit the registry. At scale, this compounds: 200ms per device × 50 devices = 10 seconds of boot delay. The device tree isn’t static—it rebuilds during every PnP rebalance, dock/undock event, and surprise removal. If you’re treating it as a read-once data structure, you’re already wrong.
How It Actually Works
The Device Tree Structure
Each device in Windows exists as a DEVICE_OBJECT structure pointed to by a DeviceNode in the PnP Manager’s tree. When you call IoGetDevicePropertyData(), the kernel walks from your DEVICE_OBJECT up through the device stack, checks each layer’s property store, then falls back to registry keys under Enum\<Enumerator>\<DeviceID>. This isn’t a simple lookup—it’s a multi-step traversal with locking at each level.
The modern property model uses DEVPROPKEY structures (GUID + property ID) instead of the old SPDRP_* integers. This matters because IoGetDeviceProperty() only supports 30 legacy properties, while IoGetDevicePropertyData() exposes 200+ properties including custom vendor extensions. Using the old API means you can’t access container IDs, device metadata, or modern power capabilities.
Memory and IRQL Constraints
Property queries require PASSIVE_LEVEL because they hit the registry through CM_Get_DevNode_Property_Keys(). If you’re at DISPATCH_LEVEL and call this, instant bugcheck (IRQL_NOT_LESS_OR_EQUAL). The registry access itself uses ZwQueryValueKey(), which can block for milliseconds if the registry hive isn’t cached. I’ve measured 5-15ms for cold registry reads vs. 50µs for cached property data.
Device Stack Walking
When an IRP_MN_QUERY_DEVICE_RELATIONS comes down, each driver in the stack must either pass it down or complete it with a DEVICE_RELATIONS structure. The PDO (bottom of stack) creates child PDOs and returns them. The PnP Manager then recursively enumerates each child. Miss one reference count increment on these DEVICE_OBJECTS and you get use-after-free crashes hours later when the device is removed.
Real-World Evidence
Using WinDbg, !devnode 0 1 shows the entire device tree with timing info. On a laptop with 150 devices, full enumeration takes 2.3 seconds—mostly in registry reads. Breaking in the kernel during CM_Get_DevNode_Property_Keys() shows stack traces hitting CmpQueryKeyValueData(), proving the registry path.
Run devcon findall =* and watch Process Monitor filter on RegQueryValue operations. You’ll see thousands of hits to HKLM\SYSTEM\CurrentControlSet\Enum\*\Device Parameters. Each SetupAPI call from user mode translates to 5-10 registry operations in kernel mode.
Performance comparison: enumerating 100 devices using cached properties (IoGetDevicePropertyData with cache hints) completes in 120ms. Same enumeration forcing registry reads takes 1.8 seconds. The 15x difference is why phantom devices—registry entries for removed hardware—kill boot performance.
What You Can Do Right Now
Use Modern APIs
Replace IoGetDeviceProperty() with IoGetDevicePropertyData(). For KMDF drivers, use WdfDeviceQueryProperty() or WdfDeviceQueryPropertyEx(). These APIs support the full property store and include cache optimization hints.
Handle Async Correctly
Always check for STATUS_PENDING from property queries. Use KeWaitForSingleObject() if synchronous behavior is required, or set a completion routine if calling from a dispatch routine. Never busy-wait.
Enumerate Efficiently
Cache device properties you query repeatedly. The property store doesn’t change unless a PnP event occurs—listen for IRP_MN_DEVICE_USAGE_NOTIFICATION to invalidate your cache.
Debug With WinDbg
!devnode 0 1shows the full tree with properties!devstack <PDO>displays driver layeringdt nt!_DEVICE_OBJECTexamines the structure!object \Devicelists all device objects
Cleanup Phantom Devices
Use devcon removeall @* cautiously, or programmatically call SetupDiRemoveDevice() on non-present devices. This prevents registry bloat that slows enumeration.
The device tree is live data that changes constantly. Treat it like a database under concurrent modification—query what you need, cache appropriately, and handle removal races. Understanding the dual in-memory/registry model is what separates drivers that work from drivers that work reliably at scale.
Hands-On Implementation
Github Link:
https://github.com/sysdr/howtech/tree/main/windows_device_tree/windows_device_tree_architectureNow let’s build working code that demonstrates these concepts. We’ll create a kernel driver and user-mode tool to measure actual performance.
Running the Demo
The included demo script builds everything automatically:
bash
./demo.shThis creates all source files, compiles the user-mode tool, and runs a monitor showing real-time device enumeration with performance timing.
What Gets Built
The demo generates three main components:
1. Kernel Driver (src/driver/device_enum.c)
This KMDF driver shows the correct way to query device properties. Key features:
Uses
IoGetDevicePropertyData()instead of deprecated APIsChecks IRQL before registry access
Measures query timing with
KeQuerySystemTime()Proper memory management with pool tags
Queries multiple properties: DeviceDesc, HardwareIDs, InstanceID, ClassGUID
2. User-Mode Tool (src/usermode/enum_devices.cpp)
A SetupAPI-based enumerator that walks the device tree and measures performance:
Calls
SetupDiGetDevicePropertyW()for modern property accessUses
QueryPerformanceCounter()for microsecond timingShows cache hit vs. registry read differences
Displays statistics: min/max/average query times
3. WinDbg Scripts (src/windbg/)
Ready-to-use debugging commands for analyzing device trees in the kernel debugger.
Building on Windows
If you’re on Windows with the Windows Driver Kit installed:
User-Mode Tool:
cmd
cd src\usermode
cl /W4 /EHsc enum_devices.cpp /link setupapi.lib cfgmgr32.lib
enum_devices.exeKernel Driver:
cmd
cd src\driver
msbuild /p:Configuration=Release /p:Platform=x64
devcon install device_enum.inf ROOT\DEVICEENUMUnderstanding the Code
Here’s the critical part of the kernel driver that shows proper property querying:
c
// Must be at PASSIVE_LEVEL for registry access
if (KeGetCurrentIrql() != PASSIVE_LEVEL) {
return STATUS_INVALID_DEVICE_REQUEST;
}
// Start timing
KeQuerySystemTime(&startTime);
// First call to get required size
status = IoGetDevicePropertyData(
pdo,
PropertyKey,
LOCALE_NEUTRAL,
0, 0, NULL,
&requiredSize,
&propertyType
);
// Allocate buffer
buffer = ExAllocatePoolWithTag(PagedPool, requiredSize, TAG);
// Second call to get actual data
status = IoGetDevicePropertyData(
pdo,
PropertyKey,
LOCALE_NEUTRAL,
0,
requiredSize,
buffer,
&requiredSize,
&propertyType
);
// End timing
KeQuerySystemTime(&endTime);
```
This pattern ensures you never bugcheck from IRQL violations and always measure real performance.
### Performance Results You'll See
When you run the demo, the monitor displays timing for each device:
- Green results (50-100µs): Cached properties from the property store
- Yellow results (100-300µs): Partial cache with some registry access
- Red results (300-500µs): Full registry reads via `ZwQueryValueKey()`
The summary shows:
- Total devices enumerated
- Total property queries
- Average time per query
- Min/max timing
- Cache effectiveness percentage
### Testing Different Scenarios
Try these experiments with the code:
**Test 1: Clean Device Tree**
Run immediately after boot when caches are warm. You'll see mostly green (fast) results.
**Test 2: After Adding Devices**
Install new hardware, then run the tool. Watch registry reads spike as new device properties get queried for the first time.
**Test 3: With Phantom Devices**
Don't clean phantom devices for a while. Run the tool and observe how query times increase as the registry grows bloated.
### Debugging with WinDbg
Load the WinDbg scripts and try these commands while your driver is running:
```
!devnode 0 1
```
Shows your device in the tree with all properties.
```
bp nt!IoGetDevicePropertyData
```
Breaks when any driver queries properties. Use `kb` to see the call stack.
```
dt nt!_DEVICE_OBJECT poi(your_device_address)Examines the device structure to see the device stack pointers.
Key Takeaways
Windows stores device info in two places: memory and registry
Registry reads are 300 times slower than cached properties
Always check IRQL before calling property APIs
Cache properties you use repeatedly
Clean phantom devices to maintain boot performance
Use modern APIs that support 200+ properties
The device tree is constantly changing. Your code needs to handle this dynamic behavior correctly while maintaining good performance.
Additional Resources
The complete source code includes:
KMDF driver with full error handling
Cross-platform build scripts
Docker container for reproducible builds
Comprehensive WinDbg command reference
Performance monitoring tools


