
Intro
If you develop Windows drivers you most likely heard of the WDF. That is the framework that Microsoft encourages everyone to use to develop modern kernel drivers. WDF is quite well documented and is widely used by driver developers. What is not very well documented, that may easily throw off new driver writers, is a certain sequence of calls to WDF functions that one needs to abide by to avoid some very puzzling crashes and error messages.
It surely threw me off when I was learning WDF, and thus I want to share it with my readers in hopes of saving you time for debugging some strange status codes and WDF Verifier bugchecks.
WdfDriverCreate Function
WdfDriverCreate is a special function within WDF in that it has to be called first before any other WDF function.
Note that this rule does not apply to older WDM functions.
And even though it's a very simple rule, unfortunately it trips up too many novice developers. Let me explain how this can happen with an example.
Let's assume that you have the following WDF driver initialization routine:
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
WDF_DRIVER_CONFIG config;
NTSTATUS status;
WDF_OBJECT_ATTRIBUTES attributes;
WPP_INIT_TRACING(DriverObject, RegistryPath);
WDF_OBJECT_ATTRIBUTES_INIT(&attributes);
attributes.EvtCleanupCallback = EvtDriverContextCleanup;
WDF_DRIVER_CONFIG_INIT(&config, EvtDeviceAdd);
status = WdfDriverCreate(DriverObject,
RegistryPath,
&attributes,
&config,
WDF_NO_HANDLE
);
if(NT_SUCCESS(status))
{
//Our driver object was created
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "Our driver object was created!");
}
else
{
//Driver object was not created
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "ERROR: Failed to create driver object with status: %!STATUS!", status);
//Undo anything that was created in this function
//IMPORTANT: This is the only place where WDF will not do automatic cleanup for us!
//...
//Finally, unregister WPP tracing
WPP_CLEANUP(DriverObject);
}
return status;
}That is pretty much the backbone code to create a WDF driver object that the Visual Studio will give us when we create a new "Kernel Mode Driver (KMDF)" project.
We just need to add a few callbacks to it. Obviously the cleanup:
VOID
EvtDriverContextCleanup(
_In_ WDFOBJECT DriverObject
)
{
PAGED_CODE();
//Unregister WPP logger (tracing)
WPP_CLEANUP(WdfDriverWdmGetDriverObject((WDFDRIVER)DriverObject));
}We may also need a callback to add a new device for our driver. This is where most of the devices begin their lifecycle:
NTSTATUS
EvtDeviceAdd(
_In_ WDFDRIVER Driver,
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
NTSTATUS status;
PAGED_CODE();
while(1)
{
//We'll create a device first
WDF_OBJECT_ATTRIBUTES deviceAttributes;
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
if(!NT_SUCCESS(status))
{
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "ERROR: Failed to create device with status: %!STATUS!", status);
break;
}
//Initialize its context (to store device's custom data)
PDEVICE_CONTEXT deviceContext;
deviceContext = DeviceGetContext(device);
ASSERT(deviceContext);
//Create a device interface so that user-mode can send it IOCTLs
status = WdfDeviceCreateDeviceInterface(device, &GUID_DEVINTERFACE_MyDriver, NULL);
if(!NT_SUCCESS(status))
{
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "ERROR: Failed to create device interface with status: %!STATUS!", status);
break;
}
//Create a device queue so that it can receive requests
WDFQUEUE queue;
WDF_IO_QUEUE_CONFIG queueConfig;
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queueConfig, WdfIoQueueDispatchParallel);
queueConfig.EvtIoDeviceControl = EvtIoDeviceControl;
status = WdfIoQueueCreate(Device, &queueConfig, WDF_NO_OBJECT_ATTRIBUTES, &queue);
if(!NT_SUCCESS(status))
{
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "ERROR: Failed to create device queue with status: %!STATUS!", status);
break;
}
break;
}
return status;
}And finally we probably need a callback to process IOCTLs that will be sent to this driver:
VOID
EvtIoDeviceControl(
_In_ WDFQUEUE Queue,
_In_ WDFREQUEST Request,
_In_ size_t OutputBufferLength,
_In_ size_t InputBufferLength,
_In_ ULONG IoControlCode
)
{
NTSTATUS status = STATUS_NOT_IMPLEMENTED;
//Need to complete it
WdfRequestComplete(Request, status);
}Since we're not doing anything in our IOCTL routine, we'll return the error status code of STATUS_NOT_IMPLEMENTED in case someone tries to send us a request.
And that's pretty much the skeleton KMDF driver that Visual Studio gives us.
Next, let's see what happens if we decide to add some code to it.
Premature Initialization Before WdfDriverCreate
Let's say that we want to add some shared object to this driver and need to synchronize access to it. Let's also assume that we'll use a wait-lock object for that. Let's add a global variable to store it:
Then let's assume that we will need to use it inside our EvtDeviceAdd callback. Let's add it somewhere at the end there:
NTSTATUS
EvtDeviceAdd(
_In_ WDFDRIVER Driver,
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
NTSTATUS status;
PAGED_CODE();
while(1)
{
//...
//After we've created the device queue...
//Acquire the lock
WdfWaitLockAcquire(wdfGlobalLock, NULL);
//Safely access our shared resource
//...
//Release the lock
WdfWaitLockRelease(wdfGlobalLock);
}
return status;
}But before we can use our wdfGlobalLock object we need to initialize, or create it. We can do it using the WdfWaitLockCreate function.
But the question becomes, "Where do we create it?"
The logic may tell us that since we're using the wdfGlobalLock object inside the EvtDeviceAdd callback, we need to create it before that callback may start running. OK. In that case, we can trace it back to where EvtDeviceAdd callback was initialized, and thus find a safe place to create our wdfGlobalLock.
That brings us to WdfDriverCreate that we saw earlier in our DriverEntry routine, with WdfDriverCreate being the function that initializes the EvtDeviceAdd callback.
Thus, a novice WDF developer may think about adding the creation code for the wdfGlobalLock object before WdfDriverCreate is called:
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
//...
//WRONG: DO NOT DO IT HERE!!!
status = WdfWaitLockCreate(WDF_NO_OBJECT_ATTRIBUTES, &wdfGlobalLock);
ASSERT(status == STATUS_SUCCESS);
//...
//And create our device
status = WdfDriverCreate(DriverObject,
RegistryPath,
&attributes,
&config,
WDF_NO_HANDLE
);
//...
}That is wrong! But, why? You may ask.
I'm not totally sure about the specifics of the decision to implement it like that, but MSFT gives it away with a single sentence in the WdfDriverCreate function documentation:
A driver that uses Kernel-Mode Driver Framework must call WdfDriverCreate from within its DriverEntry routine, before calling any other framework routines.
Thus, the correct way to do it would be to create our wdfGlobalLock object after WdfDriverCreate returns success:
NTSTATUS
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
//...
//Create our device
status = WdfDriverCreate(DriverObject,
RegistryPath,
&attributes,
&config,
WDF_NO_HANDLE
);
if(NT_SUCCESS(status))
{
//Our driver object was created
//CORRECT: Now we can create our wait-lock
status = WdfWaitLockCreate(WDF_NO_OBJECT_ATTRIBUTES, &wdfGlobalLock);
ASSERT(status == STATUS_SUCCESS);
}
//...
}But that looks kinda wrong, doesn't it?
Wouldn't that create a race condition? It's kinda like initializing a global that is used by some thread after that thread was created. That is a good recipe for a nasty race condition, isn't it?
Well, the answer is, "No, not with the WDF in this case."
The thing with the DriverEntry routine is that WDF treats it in a pure synchronous fashion, meaning that it will not do any parallel processing until that routine returns. And that is what makes it possible to safely initialize our globals after the WdfDriverCreate function.
Moreoever, the WdfDriverCreate internally initializes the global WDF state for your driver, stuff like its internal lookup tables, globals, properties, etc. And if you try to invoke most of the WDF functions before that point, it will attempt to use the WDF globals in an uninitialized state, that in turn may cause all sorts of issues.
That is one of those gotchas that should probably be documented in bold for all WDF functions. But, unless I missed it, I don't see it documented anywhere, aside from just a single sentence in the WdfDriverCreate function's MSDN page.
Let me illustrate just some of the insanity that may ensue if you break that rule.
Bad Consequences
To make matters worse, if you try to call some WDF function before WdfDriverCreate, like I showed here, it may even work on some systems.
For instance, that same example with a premature call to WdfWaitLockCreate could work in an older OS, while on Windows 11 it may return STATUS_INSUFFICIENT_RESOURCES (or 0xC000009A.) Then, if you check the documentation for the function, that status code will be frustratingly absent from it.
Such confusion can seriously slow down any progress for a novice developer.
I wish owners of WDF added a debugging ASSERT-ion at the beginning of all WDF functions to check if WdfDriverCreate was called. That way, the first test-run of an improperly codes WDF driver would generate that debugging assertion. And coupled with a useful comment, or a well documented MSDN page, should explain the failure for a new WDF developer and preclude some guy on the internet from writing a blog post about it.
Additionally, if you enable WDF Verifier on your driver that has premature calls to WDF function(s) before WdfDriverCreate, it may throw the BAD_POOL_CALLER bugcheck right at the start of your driver when you attempt to load it:
BAD_POOL_CALLER (c2)
Arg1: 000000000000009b, Attempt to allocate pool with a tag of zero. This would make the pool untrackable and worse, corrupt the existing tag tables.
Arg2: 0000000000000200, Pool type
Arg3: 00000000000000d0, Size of allocation in bytes
Arg4: fffff802a28893ee, Caller's address.As you can see, the name and the description for that bugcheck tells you very little about the true nature of the problem.
The reason why the WDF Verifier raises that bugcheck is because
WdfWaitLockCreateinternally attempts to allocate memory and callsExAllocatePool2type function (insideWdf01000!MxMemory::MxAllocatePoolWithTag) with aTagparameter being set to NULL. Verifier treats it as a bad practice, and thus raises that bugcheck.The reason why the
Tagparameter was 0 is because the global struct that holds all WDF variables in your driver (DriverGlobalsof typeWdf01000!WDF_DRIVER_GLOBALS) was not initialized yet, which would've been done by the framework when you call theWdfDriverCreatefunction.But note, that this bugcheck has only an indirect connection to the actual failure, which makes debugging it quite challenging, especially for a notice coder.
So if you try to search for that bugcheck (BAD_POOL_CALLER), you will find a multitude of misleading answers to why this had happened.
The answer and the fix to both of these issues is to move any WDF function calls to some spot inside DriverEntry after WdfDriverCreate returns success, like I showed here.
It is a bit counterintuitive (at least for me) but that is the correct approach here.
Conclusion
There's no way around it except to just memorize this simple rule if you decide to write your own KMDF drivers. Better yet, put all your initialization code into a function and call it after WdfDriverCreate returns success. That way the placement of the initialization code of your objects will not confuse you in the future.
Unfortunately, the WDF has quite a few of these quirks that I will try to share in my future blog posts as time permits. So please stay tuned.

