
Intro
You probably know that you can specify a clean-up callback routine for most of the WDF objects that will be called before that object is deleted. But have you ever wondered why are there two such callbacks: EvtCleanupCallback and EvtDestroyCallback?
This blog post will shed some light on the important differences between the two.
WDF Object Creation & Parent-Child Hierarchy
To visualize it better, let's make a concrete example first that we can work with. Say, we have a custom struct:
struct MYSTRUCT
{
size_t szcb;
LIST_ENTRY listChain;
int* pArray;
KEVENT eventStopNow;
WDFMEMORY wdfChild;
};
//This will declare the context for our struct
WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(MYSTRUCT, WDFDEVICE_to_MYSTRUCT);Then we can allocate and initialize a context for it as such:
WDF_OBJECT_ATTRIBUTES attr;
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attr, MYSTRUCT);
//We will set two test callbacks for it here
attr.EvtCleanupCallback = cleanupCallback;
attr.EvtDestroyCallback = destroyCallback;
WDFDEVICE wdfDevice;
status = WdfDeviceCreate(&DeviceInit, &attr, &wdfDevice);
if(status == STATUS_SUCCESS)
{
//Get pointer to our allocated context & clear it
MYSTRUCT* pStruct = WDFDEVICE_to_MYSTRUCT(wdfDevice);
memset(pStruct, 0, sizeof(*pStruct));
//We'll track a chain of something in this linked list
InitializeListHead(&pStruct->listChain);
//Allocate memory for something else (using an older WDM function)
pStruct->pArray = (int*)ExAllocatePool2(POOL_FLAG_PAGED, sizeof(int) * 0x100, '1gat');
ASSERT(pStruct->pArray);
//Create a signaling event (that will be used later)
KeInitializeEvent(&pStruct->eventStopNow, NotificationEvent, FALSE);
//Create a child WDF object just to illustrate the parent-child hierarchy in WDF
WDF_OBJECT_ATTRIBUTES attChild;
WDF_OBJECT_ATTRIBUTES_INIT(&attChild);
attChild.ParentObject = wdfDevice; //We'll make its parent to be our test WDF device
status = WdfMemoryCreate(&attChild, PagedPool, '2gat', PAGE_SIZE, &pStruct->wdfChild, NULL);
if(status == STATUS_SUCCESS)
{
//There's no need to increment the reference count on the child object here!
//INFO: We're doing it just to illustrate this concept in the callbacks later...
WdfObjectReference(pStruct->wdfChild);
}
//...
}Let's use the kernel debugger to see the state of the WDF objects right after they were created. We can do so from a breakpoint after the code sequence above.
First the values inside the MYSTRUCT struct. We can get them by using its local variable pStruct that holds a pointer to it:
3: kd> ?? pStruct
struct MYSTRUCT * 0xffffbb86`963c6f80
+0x000 szcb : 0
+0x008 listChain : _LIST_ENTRY [ 0xffffbb86`963c6f88 - 0xffffbb86`963c6f88 ]
+0x018 pArray : 0xffffce8a`9f672c00 -> 0n0
+0x020 eventStopNow : _KEVENT
+0x038 wdfChild : 0x00004479`7a3e90a8 WDFMEMORY__Then let's pull up the value of the wdfDevice handle and its properties:
3: kd> ?? wdfDevice
struct WDFDEVICE__ * 0x00004479`69c39368
+0x000 unused : ??
3: kd> !wdfhandle 0x00004479`69c39368
Treating handle as a KMDF handle!
Dumping WDFHANDLE 0x0000447969c39368
=============================
Handle type is WDFDEVICE
Refcount: 1
Contexts:
context: dt 0xffffbb86963c6f80 MyDriver!MYSTRUCT (size is 0x40 bytes)
EvtCleanupCallback fffff802ac9b4960 MyDriver!cleanupCallback
EvtDestroyCallback fffff802ac9a11c0 MyDriver!destroyCallback
Parent: !wdfhandle 0x0000447978fb9198, type is WDFDRIVER
Owning device: !wdfdevice 0x0000447969c39368
!wdfobject 0xffffbb86963c6c90Thus, we can see that the initial reference count for our wdfDevice was 1. Which is normal for an object that was just created.
Then we can do the same for our wdfChild using its handle value that we got from reading the pStruct struct above:
3: kd> !wdfhandle 0x00004479`7a3e90a8
Treating handle as a KMDF handle!
Dumping WDFHANDLE 0x000044797a3e90a8
=============================
Refcount: 2
Contexts:
<no associated contexts or attribute callbacks>
Parent: !wdfhandle 0x0000447969c39368, type is WDFDEVICE
Owning device: !wdfdevice 0x0000447969c39368
!wdfobject 0xffffbb8685c16f50The value of its reference count was 2 since we also incremented it with the WdfObjectReference macro.
EvtCleanupCallback
I would say that this is the most popular callback among the two. You can get more details about it from the official documentation here.
The main distinction of the "cleanup" callback is that it is invoked as soon as its associated WDF object is slated for deletion, but before any removal had been done. This usually happens when the WdfObjectDelete is called on that object. Such could be done explicitly by your code, or implicitly by the framework itself.
When the EvtCleanupCallback is invoked, the reference count on the object is unchanged, its memory is still available and you can generally do the same "usual stuff" with that object as you would otherwise.
The "cleanup" callback is the framework's notification that it is starting the deletion of the underlying WDF object.
Note that there's no way to cancel, or stop the deletion of WDF objects from this callback.
When the associated wdfDevice is deleted, the framework will invoke the EvtCleanupCallback callback, or our cleanupCallback first.
Thus, we can still use all of our MYSTRUCT members, the WDF object that is being deleted itself, and its child WDF objects, as if those are "alive and well." For instance:
static void cleanupCallback(__in WDFOBJECT wdfDevice)
{
MYSTRUCT* pStruct = WDFDEVICE_to_MYSTRUCT(wdfDevice);
ASSERT(pStruct);
//Notify the work item to begin its own tear-down
//NOTE that the work item thread will be responsible for the tear-down process itself ...
KeSetEvent(&pStruct->eventStopNow, 0, FALSE);
//Remove (IRP) request cancelation (and wait for it if it's pending)
//
//INFO: I didn't include 'wdfRequest' in this example for brevity.
// In real-world situation, it may be some IRP request that your driver may be still processing...
//
NTSTATUS status = WdfRequestUnmarkCancelable(wdfRequest);
if(status == STATUS_CANCELLED)
{
//Need to wait for the request cancelation routine to finish processing
//
//INFO: The 'eventFinishedCancelation' will be signaled by the cancelation routine
// after it finishes the cancelation, and especially after it is done using any of the
// underlying WDF objects that are being deleted here (including their children.)
// (I didn't include that logic in this example for brevity.)
//
KeWaitForSingleObject(&eventFinishedCancelation, Executive, KernelMode, FALSE, NULL);
}
else if(status != STATUS_SUCCESS)
{
//Some break-down of our logic - this needs further debugging!
ASSERT(FALSE);
KeBugCheckEx(MANUALLY_INITIATED_CRASH, status, wdfRequest, NULL, NULL);
}
//Finally, since we incremented the reference count on our 'wdfChild' object we need to decrement it as well
//INFO: Without this call we will have a memory leak because our 'wdfChild' will never be freed!
WdfObjectDereference(pStruct->wdfChild);
//...
}The most reasonable actions within the EvtCleanupCallback callback is to prepare your custom objects for deletion, by doing either of:
- Dereference all objects that the underlying object that is being deleted owns. You would normally do so by calling
WdfObjectDereference, or a lower-levelObDereferenceObjectmacros.It is important to keep in mind the parent-child hierarchy that WDF maintains, and the fact that child objects are deleted automatically by the framework before the parent object is deleted. That is why we don't really need to worry about deleting the
wdfChildin our example.Another gotcha here are the default parent objects that many WDF objects maintain. This means that if you didn't specify a parent when you created, or became an owner of a WDF object, it may implicitly be initialized to a certain parent anyway. For instance, if you created an
WDFFILEOBJECTwithout specifying its parent, the framework will assign your "WDF device object" as its parent anyway. And as this table shows, there's no way for you to override this behavior for theWDFFILEOBJECT. - Signal all work item (threads) to stop, and tear themselves down, that the underlying object that is being deleted has references to. In our example we used the
eventStopNowfor that.Note that due to the asynchronous nature of the kernel, the actual work to tear down the objects associated with the work item threads will be done in those work items themselves. So make sure to use proper reference counting on those objects to prevent their premature deletion.
- Close or stop I/O targets, queues, timers, etc., that the underlying object that is being deleted owns.
- Unregister any callbacks that may fire later and touch the underlying object that is being deleted.
The important part here is to ensure that those callbacks were indeed stopped. Otherwise, if any of the callbacks are still pending, or active, proceeding with the teardown of the underlying object may create a use-after-free bug, which will be very difficult to pinpoint and fix.
So unless the framework function can wait for any pending callbacks to finish, that the underlying object that is being deleted owns, make sure to wait for them to finish in your own code. This is especially true with the cancelable
WDFREQUESTs that you might have set up using theWdfRequestMarkCancelableExandWdfRequestUnmarkCancelablefunctions. For instance, ifWdfRequestUnmarkCancelablereturnsSTATUS_CANCELLEDthis would indicate that the request (or IRP) cancelation callback is either pending or had already run. In that case you need to use your own method of determining the outcome, and if the cancelation callback is still pending, wait for it to finish before returning from yourEvtCleanupCallbackroutine.I showed a brief excerpt of how do it in the code sample above, using the
eventFinishedCancelationevent. A more complete example deserves its own blog post, that I should write later. - Account for any
DPC's that might be still pending and properly cancel those.The same warning that I gave for the cancelable
WDFREQUEST's above goes forDPCs as well. If you queued a work item to process yourDPCmake sure to cancel that work item, and if it is already pending, wait for it to finish before returning from yourEvtCleanupCallbackroutine.Otherwise, leaving your
DPCprocessing work item routine running after you return from theEvtCleanupCallbackcallback may create a use-after-free bug that will be very difficult to diagnose and fix.
Even though this was not intended by the authors of the WDF, many drivers also free other resources and non-WDF objects in theEvtCleanupCallback. Doing so in this callback is much safer than in theEvtDestroyCallback.
Just to illustrate the fact that the object that is being deleted and its children are in the same state as they were right before deletion, let's use the kernel debugger to look up information on the wdfDevice and its wdfChild from a breakpoint in the beginning of the cleanupCallback routine.
First, we can check the state of our wdfDevice:
3: kd> ?? wdfDevice
struct WDFDEVICE__ * 0x00004479`69c39368
+0x000 unused : ??
3: kd> !wdfhandle 0x00004479`69c39368
Treating handle as a KMDF handle!
Dumping WDFHANDLE 0x0000447969c39368
=============================
Handle type is WDFDEVICE
Refcount: 2
Contexts:
context: dt 0xffffbb86963c6f80 MyDriver!MYSTRUCT (size is 0x40 bytes)
EvtCleanupCallback fffff802ac9b4960 MyDriver!cleanupCallback
EvtDestroyCallback fffff802ac9a11c0 MyDriver!destroyCallback
Parent: !wdfhandle 0x0000447978fb9198, type is WDFDRIVER
Owning device: !wdfdevice 0x0000447969c39368
!wdfobject 0xffffbb86963c6c90Its reference count is 2. It was incremented by the framework after creation to keep our device alive.
Let's retrieve the state of MYSTRUCT by its pointer in the local variable pStruct next:
3: kd> ?? pStruct
struct MYSTRUCT * 0xffffbb86`963c6f80
+0x000 szcb : 0
+0x008 listChain : _LIST_ENTRY [ 0xffffbb86`963c6f88 - 0xffffbb86`963c6f88 ]
+0x018 pArray : 0xffffce8a`9f672c00 -> 0n0
+0x020 eventStopNow : _KEVENT
+0x038 wdfChild : 0x00004479`7a3e90a8 WDFMEMORY__Then we can read the state of the wdfChild object using the handle from the pStruct that we obtained earlier:
0: kd> !wdfhandle 0x00004479`7a3e90a8
Treating handle as a KMDF handle!
Dumping WDFHANDLE 0x000044797a3e90a8
=============================
Refcount: 2
Contexts:
<no associated contexts or attribute callbacks>
Parent: !wdfhandle 0x0000447969c39368, type is WDFDEVICE
Owning device: !wdfdevice 0x0000447969c39368
!wdfobject 0xffffbb8685c16f50Its reference count is the same as it was right after creation: 2. The framework did not touch it because our code owns it.
Finally, let's see that our wdfChild refers to a valid chunk of memory:
3: kd> !wdfmemory 0x00004479`7a3e90a8
Treating handle as a KMDF handle!
WDFMEMORY 0x00004479`7a3e90a8: Buffer 0xffffab07fa72b000, Length 0x1000 (4096) bytes
3: kd> db 0xffffab07fa72b000
ffffab07`fa72b000 15 15 15 15 15 15 15 15-15 15 15 15 15 15 15 15 ................
ffffab07`fa72b010 15 15 15 15 15 15 15 15-15 15 15 15 15 15 15 15 ................
ffffab07`fa72b020 15 15 15 15 15 15 15 15-15 15 15 15 15 15 15 15 ................
ffffab07`fa72b030 15 15 15 15 15 15 15 15-15 15 15 15 15 15 15 15 ................
ffffab07`fa72b040 15 15 15 15 15 15 15 15-15 15 15 15 15 15 15 15 ................
ffffab07`fa72b050 15 15 15 15 15 15 15 15-15 15 15 15 15 15 15 15 ................
ffffab07`fa72b060 15 15 15 15 15 15 15 15-15 15 15 15 15 15 15 15 ................
ffffab07`fa72b070 15 15 15 15 15 15 15 15-15 15 15 15 15 15 15 15 ................The framework initialized it to some random byte for us.
WDFMEMORYis WDF's way of allocating and managing a chunk of memory on the heap, what WDM would do withExAllocatePool2.
Next let's look at another type of the deletion callback.
EvtDestroyCallback
Unlike the first callback, the EvtDestroyCallback is invoked after the reference count for the underlying WDF object is decremented but before its memory is freed.
This creates a very unique state of the underlying WDF objects when they remain in a sort of "half-alive, half-dead" state if their reference count reaches 0. Because of that, I'm not a big fan of the EvtDestroyCallback.
You can technically do the following in the EvtDestroyCallback, but you have to be very careful with it and not deviate from these rules:
- The context of the underlying object that is being deleted is still alive. So you can use it. Although many WDF objects in it may not be.
Because of this weird state of associated WDF objects, I would try to avoid calling any methods on those WDF objects, other than the ones that delete them, such as
WdfObjectDelete.Additionally, do not touch any child WDF objects for the object that is being deleted.
- We can delete any WDF objects that the underlying object that is being deleted owns using
WdfObjectDelete.Make sure to account for the implicit (or default) parent association between WDF objects. Many objects are automatically deleted when their parent object is deleted as well. Not realizing that may create its own slew of the use-after-free bugs.
This automatic parenting structure of the WDF is my main praise and also the pet peeve. On one hand, it is very convenient to let it delete all child objects for you. But then, there seems to be no easy way to tell the WDF to back off and let me handle the life-cycle of certain WDF objects on my own.
So be very careful with this concept. It created quite a few bugs that I've seen in the wild.
- All WDF child objects of the underlying WDF object that is being deleted will initiate their deletion. Thus, make sure not to use those child objects any longer. Do not call
WdfObjectDeleteon the child objects either!I will show an example of what happens with WDF child objects later.
- We can free any non-WDF objects that are not parented by WDF, such as: WDM file handles, registry handles, OB kernel objects, etc.
- I have to give one very important caveat here: if you incremented the reference count on the underlying WDF object that is being deleted (say, by using
WdfObjectReference) and forgot to dereference it later, this will create a memory leak that can jeopardize the stability of the target OS.So make sure to account for all manual reference counting that you do on the objects!
You probably realized by now that the main mantra of the EvtDestroyCallback is to only free the underlying objects, and not to do anything else with them!
This is especially true about any logging functions, such as WPP traces. A driver writer may inadvertently include a WPP trace that prints out the object(s) being deleted, not realizing that some of those object properties and child objects may no longer be available.So if you do include any logging in
EvtDestroyCallback, make sure to limit its output to a minimum. For instance, you may safely print the values of WDF handles. Just make sure not to call any of the WDF functions on those objects.
Because of what I said above, I would advise you to avoid using theEvtDestroyCallbackaltogether and to go with theEvtCleanupCallbackinstead. The latter callback is much safer to work with, provided that all your cleanup goes intoEvtCleanupCallbackand you completely avoid usingEvtDestroyCallback. Using both will introduce a slew of bugs of their own.
Having said that, let me show how you can handle the EvtDestroyCallback for our previous example:
static void destroyCallback(__in WDFOBJECT wdfDevice)
{
MYSTRUCT* pStruct = WDFDEVICE_to_MYSTRUCT(wdfDevice);
ASSERT(pStruct);
//You can still do WPP tracing, but be very careful about what you do there
TraceEvents(TRACE_LEVEL_VERBOSE, TRACE_QUEUE,
"wdfDevice=%p, listChain(%p):{F=%p,B=%p}",
wdfDevice, //I'm only using the value of the WDF handle itself
&pStruct->listChain, //I can only safely use primitive data types in the context as well
pStruct->listChain.Flink,
pStruct->listChain.Blink);
//Free the non-WDF allocated memory
if(pStruct->pArray)
{
ExFreePool(pStruct->pArray);
pStruct->pArray = NULL; //I usually also null-out the pointer for safely of a possible reuse later
}
}As you can see, my destroyCallback handler is very light and only tends to free objects and does some very basic logging.
Then let's use the kernel debugger again to pull up the state of our WDF objects inside my destroyCallback.
First, the wdfDevice:
3: kd> ?? wdfDevice
struct WDFDEVICE__ * 0x00004479`69c39368
+0x000 unused : ??
3: kd> !wdfhandle 0x00004479`69c39368
Treating handle as a KMDF handle!
Dumping WDFHANDLE 0x0000447969c39368
=============================
Handle type is WDFDEVICE
Refcount: 0
Contexts:
context: dt 0xffffbb86963c6f80 MyDriver!MYSTRUCT (size is 0x40 bytes)
EvtCleanupCallback fffff802ac9b4960 MyDriver!cleanupCallback
EvtDestroyCallback fffff802ac9a11c0 MyDriver!destroyCallback
Parent: !wdfhandle 0x0000447978fb9198, type is WDFDRIVER
Owning device: !wdfdevice 0x0000447969c39368
!wdfobject 0xffffbb86963c6c90As you can see its reference count is 0.
Then let's pull up the state of wdfChild using the pStruct local variable as a pointer to MYSTRUCT:
3: kd> ?? pStruct
struct MYSTRUCT * 0xffffbb86`963c6f80
+0x000 szcb : 0
+0x008 listChain : _LIST_ENTRY [ 0xffffbb86`963c6f88 - 0xffffbb86`963c6f88 ]
+0x018 pArray : 0xffffce8a`9f672c00 -> 0n0
+0x020 eventStopNow : _KEVENT
+0x038 wdfChild : 0x00004479`7a3e90a8 WDFMEMORY__
0: kd> !wdfhandle 0x00004479`7a3e90a8
Treating handle as a KMDF handle!
Dumping WDFHANDLE 0x000044797a3e90a8
=============================
Refcount: 0
Contexts:
<no associated contexts or attribute callbacks>
Parent: !wdfhandle 0x0000447969c39368, type is WDFDEVICE
Owning device: !wdfdevice 0x0000447969c39368
!wdfobject 0xffffbb8685c16f50Its reference count is also 0, which illustrates the fact that the WDF decremented the reference count on the child object before it attempted to delete the parent object.
As an interesting aside let's see if our wdfChild still refers to the valid memory:
3: kd> !wdfmemory 0x00004479`7a3e90a8
Treating handle as a KMDF handle!
could not retrieve vtable from ffffc78f00588fb8Oops, no it is not. As you can see the framework tore it down before our destroyCallback was invoked. That is one of the reasons why the EvtDestroyCallback is dangerous, and also why you should not rely on the WDF objects related to the deleted object to be in a usable state in that callback.
Mismatched WDF Reference Counts & Unreleased Resources
As an experiment, let's see what happens if you forget to decrement the reference count on a WDF object.
Say, let's comment out the following line in our cleanupCallback example:
Because the reference count on the wdfChild will never reach 0, this will create a memory leak and even the WDF with its automatic parent-child hierarchy and automatic deletion of child objects will not be able to help. All because we artificially incremented the reference count on the object that we owned and never decremented it.
That is by the way a "hack" to prevent the framework from automatically deleting a child WDF object. In case you prefer to manage its life-cycle on your own.
But, there are built-in mechanisms to detect other memory leaks. Say, if we forgot to free the memory in our MYSTRUCT::pArray member. Let's comment out the following line in our destroyCallback routine to show what happens:
If we enabled Driver Verifier for our driver, along with the NT module (i.e. ntoskrnl.exe) for the "Special pool" and "Pool tracking" options, the verifier will crash the target OS when our driver attempts to unload with the following bugcheck:
DRIVER_VERIFIER_DETECTED_VIOLATION (c4)
A device driver attempting to corrupt the system has been caught. This is
because the driver was specified in the registry as being suspect (by the
administrator) and the kernel has enabled substantial checking of this driver.
If the driver attempts to corrupt the system, BugChecks 0xC4, 0xC1 and 0xA will
be among the most commonly seen crashes.
Arguments:
Arg1: 0000000000000062, A driver has forgotten to free its pool allocations prior to unloading.
Arg2: ffffbb86a7690fb8, name of the driver having the issue.
Arg3: ffffbb86869d6ed0, verifier internal structure with driver information.
Arg4: 0000000000000001, total # of (paged+nonpaged) allocations that weren't freed.
Type !verifier 3 drivername.sys for info on the allocations
that were leaked that caused the bugcheck.We can expand on it, by retrieving the name of the offending driver:
Yep, it's us. Then let's use suggested verifier command to get more details about the memory leak:
3: kd> !verifier 3 MyDriver.sys
Verify Flags Level 0x0002099b
STANDARD FLAGS:
[X] (0x00000000) Automatic Checks
[X] (0x00000001) Special pool
[X] (0x00000002) Force IRQL checking
[X] (0x00000008) Pool tracking
[X] (0x00000010) I/O verification
[ ] (0x00000020) Deadlock detection
[X] (0x00000080) DMA checking
[X] (0x00000100) Security checks
[X] (0x00000800) Miscellaneous checks
[X] (0x00020000) DDI compliance checking
ADDITIONAL FLAGS:
[ ] (0x00000004) Randomized low resources simulation
[ ] (0x00000200) Force pending I/O requests
[ ] (0x00000400) IRP logging
[ ] (0x00002000) Invariant MDL checking for stack
[ ] (0x00004000) Invariant MDL checking for driver
[ ] (0x00008000) Power framework delay fuzzing
[ ] (0x00010000) Port/miniport interface checking
[ ] (0x00040000) Systematic low resources simulation
[ ] (0x00080000) DDI compliance checking (additional)
[ ] (0x00200000) NDIS/WIFI verification
[ ] (0x00800000) Kernel synchronization delay fuzzing
[ ] (0x01000000) VM switch verification
[ ] (0x02000000) Code integrity checks
[X] Indicates flag is enabled
Summary of All Verifier Statistics
RaiseIrqls 0x0
AcquireSpinLocks 0x0
Synch Executions 0x0
Trims 0x0
Pool Allocations Attempted 0x48f7078
Pool Allocations Succeeded 0x48f7075
Pool Allocations Succeeded SpecialPool 0x48f7074
Pool Allocations With NO TAG 0x4
Pool Allocations Failed 0x0
Current paged pool allocations 0x20eaf for 038E3C2A bytes
Peak paged pool allocations 0x22835 for 03D8CE8D bytes
Current nonpaged pool allocations 0x210f4 for 0386E9B4 bytes
Peak nonpaged pool allocations 0x24520 for 04452E86 bytes
Driver Verification List
------------------------
nt!_VF_TARGET_DRIVER 0xffffbb86806d4fc0: MyDriver.sys (Loaded and Unloaded)
Pool Allocation Statistics: ( NonPagedPool / PagedPool )
Current Pool Allocations: ( 0x00000000 / 0x00000001 )
Current Pool Bytes: ( 0x00000000 / 0x00000400 )
Peak Pool Allocations: ( 0x00000003 / 0x00000002 )
Peak Pool Bytes: ( 0x000020a8 / 0x00000600 )
Contiguous Memory Bytes: 0x00000000
Peak Contiguous Memory Bytes: 0x00000000
Pool Allocations:
Address Length Tag Caller Address
------------------ ---------- ---- ------------------
0xffffce8a9f672c00 0x00000400 tag1 0xfffff802ac9b3537 MyDriver!addVirtualDevice+0x1d7
Contiguous allocations are not displayed with public symbols.Such bugcheck is merely a helpful tip to show that there was an issue in our driver.
We can see immediately at the bottom of the Driver Verifier's output which memory allocation tag was not released, i.e. tag1. After that it takes just a few searches in our source code to find that memory allocation.
One thing to keep in mind is that the tag name is often inverted by the allocation DDI. For instance, thetag1tag may appear in source code as'1gat'. So search for both variations to be sure.
Enabling Driver Verifier during the development phase of your driver will greatly facilitate your debugging process further down the road. I will try to cover the topic of the Driver Verifier in some future blog posts.
As you can see above the Driver Verifier can help in detecting memory issues, but as the example above showed, it is not perfect. So do your own due diligence to check the code in your drivers.
Additionally, consider using WDF Verifier to check for WDF-specific issues. I will have to leave that topic for another blog post though.
Conclusion
From my previous experience debugging issues in Windows kernel drivers, the most dangerous and error-prone part of the code is the tear-down of the asynchronous objects. Anything that deals with cancelation routines and pending callbacks creates a nightmare of bugs to root out. So, please try to follow the basic rules that I outlined above which should help you write bug-free kernel drivers.

