Last reviewed and updated: 10 August 2020
Most drivers don’t care when an application or a driver opens or closes their device. In fact, not caring is so common that you have to go out of your way in WDF to be notified of these operations. However, you might want to be notified in order to track the number of open instances on your device, or to keep track of per open instance state. For example, let’s say that your driver provides some sort of encryption service. Each time a user opens your encryption device, you generate a unique key that’ll be used by all I/O requests sent on that open instance.
Of course, any time you implement features that aren’t commonly used, there’s a chance for the documentation and samples to be a bit lacking. In this article, we’ll cover all the options that are available to you if you find yourself in the not so common case.
WDFFILEOBJECTs and KMDF
The native File Object represents a single, specific, open instance of a device (or a file on a device). Applications create new File Objects by calling CreateFile and destroy File Objects by calling CloseHandle.
The KMDF abstraction of the native File Object is the WDFFILEOBJECT. The WDF File Object closely mirrors the native File Object and is used to represent a unique open instance of a WDF Device Object. As with all WDF objects, the WDF File Object supports a series of Event Processing Callbacks, properties, and the standard Common Object Attributes.
WDFFILEOBJECT Event Processing Callbacks
The WDF File Object supports the following Event Processing Callbacks provided as part of the WDF_FILEOBJECT_CONFIG structure: EvtDeviceFileCreate, EvtFileCleanup, and EvtFileClose. Let’s examine the purpose of each of these Event Processing Callbacks in turn.
EvtDeviceFileCreate
While slightly odd in its naming, EvtDeviceFileCreate is the Event Processing Callback raised when a WDF File Object is created. This would be, for example, as a result of a user application’s call to CreateFile. It is during the processing of this routine that the driver instantiates any unique state for this open instance.
The EvtDeviceFileCreate function prototype is as follows:
VOID EvtDeviceFileCreate( _In_ WDFDEVICE Device, _In_ WDFREQUEST Request, _In_ WDFFILEOBJECT FileObject );
Note that we are provided a WDF Device Object handle, which represents the WDF File Object’s target device. We are also provided a WDF Request Object handle, which is the WDF abstraction of the native I/O operation representing the creation of the File Object. As part of handling this Event Processing Callback, the driver must call WdfRequestComplete on this request to indicate the result of the operation to the requestor. As an aside that we will revisit later, this WDF Request Object is unique in KMDF in that it is not queue presented, meaning that it has no parent WDF Queue Object.
Lastly, we are provided the handle of the WDF File Object that is currently being instantiated.
EvtFileCleanup
EvtFileCleanup is the Event Processing Callback invoked when the native File Object enters the cleaned up state. That is to say, this event is raised when the native handle count drops to zero. This state is triggered directly as a result of the application’s call to CloseHandle.
The EvtFileCleanup function prototype is as follows:
VOID EvtFileCleanup( _In_ WDFFILEOBJECT FileObject );
Unlike in the case of EvtDeviceFileCreate, we are not provided a WDF Request Object handle as EvtFileCleanup operations. This is because Cleanup operations are always assumed to be successful. Note however that cleanup operations do arrive at the Framework as native I/O requests, so KMDF is completing these requests on our behalf “underneath the covers.”
EvtFileClose
EvtFileClose is the Event Processing Callback raised when the native File Object enters the closed state. That is to say, this event is raised when the native reference count drops to zero. This state is triggered some time after the application’s call to CloseHandle.
You might notice that this Event Processing Callback seems similar to EvtFileCleanup. Both are ultimately called as a result of the application calling CloseHandle, so why is there a distinction? In EvtFileCleanup, we are simply notified that the applicationhas closed its handle and can no longer use the native File Object to perform I/O. However, there still may be I/O operations submitted via the File Object that have yet to complete. Once the last I/O completes, the File Object is no longer in use and the EvtFileClose Event Processing Callback is raised.
In general, this is why it makes sense to defer tearing down any unique state for this open instance until the EvtFileClose Event Processing Callback. This eliminates any races between tearing down the open instance state and using the state in the other I/O paths.
The EvtFileClose function prototype is as follows:
VOID EvtFileClose( _In_ WDFFILEOBJECT FileObject );
Once again, we are not provided a WDF Request Object handle in this callback, even though close operations also arrive at the Framework as native I/O requests.
WDFFILEOBJECT Properties
In addition to the Event Processing Callbacks, the WDF_FILEOBJECT_CONFIG structure supports the setting of the following properties: AutoForwardCleanupClose and FileObject Class.
Property: AutoForwardCleanupClose
Type: WDF_TRI_STATE
In general, create requests are ultimately completed by the Functional Device Object (FDO) owner in the stack. Given that the FDO owner completes the create request, it then makes sense that the FDO owner would be responsible for completing the accompanying native I/O operations for cleanup and close notification.
A filter driver, on the other hand, typically does not play an active role in create, cleanup, or close processing. The filter driver may want to be notified of these events, but would not complete the native I/O operations. Instead, the filter would perform the necessary work and then pass the requests on to the next lower driver.
The AutoForwardCleanupClose property tells the Framework which default processing path to take for cleanup, close, and, despite the name, create operations. If the property is set to WdfFalse, the FDO based processing is chosen and the I/O requests are completed by the Framework. If WdfTrue, the filter driver processing is taken and the requests are passed on. WdfDefault chooses the correct behavior based on target device type.
Property: FileObjectClass
Type: WDF_FILEOBJECT_CLASS
The FileObjectClass property is overloaded and used to specify two unrelated options. The first option is whether or not the driver requires WDF File Object at all. By specifying a value of WdfFileObjectNotRequired, the Framework will short circuit its processing and raise the EvtDeviceFileCreate Event Processing Callback with a NULL FileObject parameter.
This may seem like a very odd option and, in fact, it is! A typical driver receiving standard create, cleanup, and close requests would almost never set this option. However, there is a very specific use case that is being addressed here. Namely, the Framework must have a model for handling non-standard, driver-generated create requests that do not contain valid native File Objects. The O/S itself will never send these, but a pair of cooperating drivers may leverage them to establish a connection.
Assuming that a File Object is required, the next series of values indicate optimizations that the Framework may take to facilitate the quick retrieval of the WDF File Object from a native File Object pointer:
- WdfFileObjectWdfCanUseFsContext
- WdfFileObjectWdfCanUseFsContext2
- WdfFileObjectWdfCannotUseFsContexts
The native File Object has two context areas that are free for use by the driver that completes the create request: FsContext and FsContext2. While this would seem a natural place for the Framework to store the WDF File Object, there are cases in which these fields are already in use by another driver in the stack or have special meaning. This then requires the Framework to maintain its own lookup table to convert the native File Object into a WDF File Object. Due to the fact that the Framework cannot know the meaning of FsContext and FsContext2 for an arbitrary device stack, WdfFileObjectWdf CannotUseFsContexts is chosen by default.
Lastly, KMDF v1.9 adds a new value, WdfFileObjectCanBe Optional. Unlike the previous values for this property, this is a flag value that may optionally be combined with WdfFileObjectWdfCanUseFsContext,WdfFileObjectWdfCanUse FsContext2, or WdfFileObjectWdfCannotUseFsContexts. This again has a very specific purpose resulting from potential driver–to-driver communication cases.
All I/O in Windows is sent by way of a native File Object that represents an open instance of a particular device object. This means that all I/O requests have an associated File Object that references the target device for the I/O operation. However, we again have to deal with the case of driver–to-driver communication. For example, Driver A may choose to send a request to Driver B without a File Object. Or with a File Object that points to Driver C. Without this optional flag set, if Driver B calls the WdfRequestGetFileObject in either of these cases there will be a KMDF Verifier exception thrown. If it is unclear whether or not a driver falls into either of these categories, the driver should not specify this option as it may mask a serious issue.
Common Object Attributes
As with all KMDF objects, the WDF File Object also supports the Common Object Attributes as defined by the WDF_OBJECT_ATTRIBUTES structure. These attributes are optionally supplied when calling WdfDeviceInitSetFileObject Config to apply the WDF_FILEOBJECT_CONFIG structure to a PWDFDEVICE_INIT structure. There are potentially a few surprises lurking in the options available here, so we’ll go through each of the Common Object Attributes and describe each one as they apply for this particular object.
EvtCleanupCallback
The EvtCleanupCallback callback is common to all Framework objects and indicates that either the Framework or the driver has triggered the teardown of a Framework object. In the case of WDF File Object, the naming is potentially confusing due to the native File Object’s use of the term, “cleanup.”
The lifetime of a WDF File Object is tied to the underlying native File Object. Thus, teardown of the WDF File Object does not start until the native File Object has been destroyed. Therefore the WDF File Object’s EvtCleanupCallback will not trigger until afterEvtFileClose.
EvtDestroyCallback
The EvtDestroyCallback callback is closely related to the EvtCleanupCallback. While EvtCleanupCallback indicates that teardown of a WDF object has begun, the EvtDestroyCallback indicates that teardown is complete and the WDF reference count of the object has gone to zero. Again, do not confuse this with the native File Object reference count; in order for the WDF File Object reference count to drop to zero the native File Object reference count must already be zero. Assuming that the driver has not done anything exotic, such as calling WdfObjectReference on the WDF File Object, the EvtDestroyCallback executes immediately following the EvtCleanupCallback.
ExecutionLevel
The ExecutionLevel constraint is interesting with the WDF File Object. The Framework disallows any create requests arriving at IRQL >= DISPATCH_LEVEL, regardless of the ExecutionLevel provided. The underlying issue is that some portions of the native File Object are pageable, thus processing create requests at IRQL >= DISPATCH_LEVEL is not architecturally defined within Windows itself. If the driver is aware of the risks in processing create requests at elevated IRQL and has incorporated them into its design, there are other methods to handle this condition. We will introduce one of the methods at the conclusion of this article.
However, if the driver does not specify an ExecutionLevel constraint of WdfExecutionLevelPassive, it should expect its EvtCleanupCallback and EvtDestroyCallback to be callable at IRQLs greater than PASSIVE_LEVEL.
SynchronizationScope
The rules for SynchronizationScope on WDF File Object are not made entirely clear by the documentation, though are in fact quite simple. First, it is always correct to specify a synchronization scope of WdfSynchronizationScopeNone, which causes the Framework to provide no specific serialization of the WDFFILEOBJECT Event Processing Callbacks.
Next, it is never correct to specify a synchronization scope of WdfSynchronizationScopeQueue on a WDF File Object. Earlier, we mentioned that the WDF Request Object passed to the EvtDeviceFileCreate Event Processing Callback is unique in that it is not queue presented. By this we mean that the WDF Request Object has no associated WDF Queue Object, therefore it makes no sense to have a synchronization scope of queue. Attempts to create a WDF Device Object with WDF File Object Event Processing Callbacks set to WdfSynchronizationScopeQueue fail with STATUS_INVALID_DEVICE_REQUEST.
Lastly, WdfSynchronizationScopeDevice is a valid choice only if the parent WDF Device Object has a PASSIVE_LEVEL execution constraint, otherwise the Framework fails the attempt to create the device. While seemingly onerous, as mentioned previously, processing create requests at elevated IRQL does not necessarily make sense. If the Framework allowed a driver to specify a synchronization scope of device with no execution level constraint, the WDF File Object Event Processing Callbacks would always execute at raised IRQL.
ParentObject
It is invalid to override this property for WDF File Object objects; The parent must always be the WDF Device Object being created.
ContextSizeOverride/ContextTypeInfo
Standard options for providing per-object context are available for WDF File Objects.
What About Queue Presented Creates?
As an alternative to providing an EvtDeviceFileCreate Event Processing Callback, it is possible to route create requests to a driver created queue by calling WdfDeviceConfigureRequest Dispatching. In this case, the create request arrives at the queue’s EvtIoDefault Event Processing Callback and is subject to the synchronization scope and execution level constraints applied to the queue. An EvtDeviceFileCreate Event Processing Callback may also be registered without error, though it will in fact never execute.
What is interesting about this option is that it does not impose the execution level constraint requirements of the EvtDeviceFileCreate Event Processing Callback. Therefore it is possible to route create requests to a queue with a synchronization scope of device and no execution level constraint, resulting in the driver’s create request processing occurring at IRQL DISPATCH_LEVEL. In this case, the driver must be careful to not access any members of the native File Object that are pageable, such as the file name.
Another interesting point to note is that as a result of this method, the create request becomes queue presented. Thus, unlike handling create operations in your EvtDeviceFileCreate Event Processing Callback, create requests that are handled in EvtIoDefault as a result of request dispatching will have an associated WDFQUEUE.
Note that even if create requests are routed to a queue, cleanup and close operations are still only accessible via the EvtFileCleanup and EvtFileClose Event Processing Callbacks.
Closing Remarks
We’ve found WDFFILEOBJECTs to be quite handy over the years in the drivers we’ve written. Hopefully you too now have a handle (get it?) on the mechanics of working with WDFFILEOBJECTs and can start using them effectively in your drivers.