1. WMI Request Handler
WMI provides a way for drivers to export information to other components. Drivers typically use WMI to enable the following:
User mode applications to query and set device-related information, such as time-out values.
An administrator with the necessary privileges to control a device by running an application on a remote system.
A driver that supports WMI registers as a provider of
information and registers one or more instances of that information.
Each WMI provider is associated with a particular Globally Unique
Identifier (GUID). Another component can register with the same GUID to
consume the data from the instances. User mode components request WMI
instance data by calling COM functions, which the system translates into
IRP_MJ_SYSTEM_CONTROL requests and sends to the target providers.
KMDF supports WMI requests through its WMI request handler, which provides the following features for drivers:
A default WMI implementation. Drivers that do
not provide WMI data are not required to register as WMI providers;
KMDF handles all IRP_MJ_SYSTEM_CONTROL requests.
Callbacks
on individual instances, rather than just at the device object level,
so that different instances can behave differently.
Validation
of buffer sizes to ensure that buffers that are used in WMI queries
meet the size requirements of the associated provider and instance.
The default WMI implementation includes support for
the check boxes on the Power Management tab of Device Manager. These
check boxes enable a user to control whether the device can wake the
system and whether the system can power down the device when it is idle.
WDM drivers must include code
to support the WMI controls that map to these check boxes, but KMDF
drivers do not require such code. If the driver enables this feature in
its power policy options, KMDF handles these requests automatically.
The driver enables buffer size validation when it configures a WMI provider object (WDFWMIPROVIDER). In the WDF_WMI_PROVIDER_CONFIG structure, the driver can specify the minimum size of the buffer that is required for the provider’s EvtWmiInstanceQueryInstance and EvtWmiInstanceSetInstance callbacks. If the driver specifies such a value, KMDF validates the buffer size when the IRP_MJ_SYSTEM_CONTROL
request arrives and calls the callbacks only if the supplied buffer is
large enough. If the driver does not configure a buffer size—because the
instance size is either dynamic or is not available when the provider
is created—the driver should specify zero for this field and the
callbacks themselves should validate the buffer size.
When KMDF receives an IRP_MJ_SYSTEM_CONTROL request that is targeted at a KMDF driver, it proceeds as follows:
If the driver has registered as a WMI
provider and registered one or more instances, the WMI handler invokes
the callbacks for those instances as appropriate.
If
the driver has not registered any WMI instances, the WMI handler
responds to the request by providing the requested data (if it can),
passing the request to the next lower driver, or failing the request.
Like all KMDF objects, WMI instance objects (WDFWMIINSTANCE) have a context area. A driver can use the context area of a WDFWMIINSTANCE object as a source of read-only data, thus enabling easy data collection with minimal effort. A driver can delete WDFWMIINSTANCE objects any time after their creation.
WMI callbacks are not synchronized with the Plug and
Play and power management state of the device. Therefore, when WMI
events occur, KMDF calls a driver’s WMI callbacks even if the device is
not in the working state.
2. Synchronization Issues
Because Windows is a pre-emptive, multitasking
operating system, multiple threads can try to access shared data
structures or resources concurrently and multiple driver routines can
run concurrently. To ensure data integrity, all drivers must synchronize
access to shared data structures. Correctly implementing such
synchronization can be difficult in WDM drivers.
For KMDF drivers, ensuring proper synchronization requires attention to several areas:
The number of concurrently active requests that are dispatched from a particular queue.
The number of concurrently active callbacks for a particular object.
The driver utility functions that access object-specific data.
The IRQL at which an object’s callbacks run.
The dispatch method for an I/O queue controls the
number of requests from the queue that can be concurrently active in the
driver.
Limiting concurrent requests does not, however, resolve all potential
synchronization issues. Concurrently active callbacks on the same object
might require access to shared object-specific data, such as the
information that is stored in the object context area. Similarly, driver
utility functions might share object-specific data with callbacks.
Furthermore, a driver must be aware of the IRQL at which its callbacks can be invoked. At DISPATCH_LEVEL and above, drivers must not access pageable data and thread pre-emption does not occur.
KMDF simplifies synchronization for driver by
providing automatic synchronization of many callbacks. Calls to most
PDO, FDO, Plug and Play, and power event callback functions are
synchronized so that only one such callback function is invoked at a
time for each device. These callback functions are called at IRQL PASSIVE_LEVEL. Note, however, that calls to the EvtDeviceSurpriseRemoval, EvtDeviceQueryRemove, and EvtDeviceQueryStop
callbacks are not synchronized with the other callbacks and so occur
while the device is changing power state or is not in the working state.
For other types of callbacks—primarily I/O related
callbacks—the driver can specify the synchronization scope (degree of
concurrency) and the maximum execution level (IRQL).
KMDF provides the following configurable synchronization features:
Synchronization scope
Execution level
Locks
Although
implementing synchronization is much less complicated in KMDF drivers
than in WDM drivers, you should nevertheless be familiar with the basics
of Windows IRQL, synchronization, and locking.
2.1. Synchronization Scope
KMDF provides configurable concurrency control, called synchronization scope,
for the callbacks of several types of objects. An object’s
synchronization scope determines whether KMDF invokes certain event
callbacks on the object concurrently.
KMDF define the following synchronization scopes:
Device scope
means that KMDF does not call certain I/O event callbacks concurrently
for an individual device object or any file objects or queues that are
children of the device object. Specifically, device scope applies to the
following event callbacks: EvtDeviceFileCreate, EvtFileCleanup, EvtFileClose, EvtIoDefault, EvtIoRead, EvtIoWrite, EvtIoDeviceControl, EvtIoInternalDeviceControl, EvtIoStop, EvtIoResume, EvtIoQueueState, EvtIoCanceledOnQueue, and EvtRequestCancel.
However,
callbacks for different device objects that were created by the same
driver object can be called concurrently. Internally, KMDF creates a synchronization lock
for each device object. To implement device synchronization scope, KMDF
acquires this lock before invoking any of the device callbacks.
Queue scope
means that KMDF does not call certain I/O callbacks concurrently on a
per-queue basis. If a Kernel Mode Driver specifies queue scope for a
device object, some callbacks for the device object and its queues can
run concurrently. However, the following callbacks for an individual
queue object are not called concurrently: EvtIoDefault, EvtIoRead, EvtIoWrite, EvtIoDeviceControl, EvtIoInternalDeviceControl, EvtIoStop, EvtIoResume, EvtIoQueueState, EvtIoCanceledOnQueue, and EvtRequestCancel.
If the driver specifies queue scope, KMDF creates a synchronization
lock for each queue object and acquires this lock before invoking any of
the listed callbacks.
No scope
means that KMDF does not acquire any locks and can call any event
callback concurrently with any other event callback. The driver must
create and acquire all its own locks. By default, KMDF uses no scope. A
driver must “opt in” to synchronization for its objects by setting
device scope explicitly.
Each KMDF object inherits its scope from its parent object (WdfSynchronizationScopeInheritFromParent). The parent of each WDFDEVICE object is the WDFDRIVER object, and the default value of the synchronization scope for the WDFDRIVER object is WdfSynchronizationScopeNone. Thus, a driver must explicitly set the synchronization scope on its objects to use frameworks synchronization.
A driver can change the scope by setting a value in the WDR_OBJECT_ATTRIBUTES
structure when it creates the object. Because scope is inherited, a
driver can easily set synchronization for most of its objects by setting
the scope for the device object, which is the parent to most KMDF
objects.
For example, to set the concurrency for its I/O callback functions, a driver sets the SynchronizationScope in the WDF_OBJECT_ATTRIBUTES for the device object that is the parent to the I/O queues. If the driver sets device scope (WdfSynchronizationScopeDevice), KMDF calls only one I/O callback function at a time across all the queues. To use queue scope, the driver sets WdfSynchronizationScopeQueue for the device object and WdfSynchronizationScopeInheritFromParent
for the queue object. Queue scope means that only one of the listed
callback functions can be active for the queue at any time. A driver
cannot set concurrency separately for each queue. Restricting the
concurrency of I/O callbacks can help to manage access to shared data in
the WDFQUEUE context memory.
By default, a file object inherits its scope from its
parent device object. Attempting to set queue scope for a file object
causes an error. Therefore, drivers that set queue scope for a device
object must manually set the synchronization scope for any file objects
that are its children. The best practice for file objects is to use no
scope and to acquire locks in the event callback functions when they are
required to synchronize access.
If a driver sets device scope for a file object, it
must also set the passive execution level for the object, as described
in the upcoming section “Execution Level.” The reason is that the framework uses spin locks (which raise IRQL to DISPATCH_LEVEL) to synchronize access to objects with device scope. However, the EvtDeviceFileCreate, EvtFileClose, and EvtFileCleanup callbacks run in the caller’s thread context and use pageable data, so they must be called at PASSIVE_LEVEL. At PASSIVE_LEVEL, the framework uses a FAST_MUTEX instead of a spin lock for synchronization.
Interrupt objects are the children of device objects.
KMDF acquires the interrupt object’s spin lock at device interrupt
request level (DIRQL) to synchronize calls to the EvtInterruptEnable, EvtInterruptDisable, and EvtInterruptlsr callbacks. A driver can also ensure that calls to its interrupt object’s EvtInterruptDpc callback are serialized with other callbacks on the parent device object.
Deferred Procedure Call (DPC), timer, and work item
objects can be children of device objects or of queue objects. To
simplify a driver’s implementation of callbacks for DPCs, timers, and
work items, KMDF enables the driver to synchronize their callbacks with
those of either the associated queue object or the device object (which
might be the parent or the grandparent of the DPC, timer, or work item).
A driver sets callback synchronization on interrupt, DPC, timer, and work item objects by setting AutomaticSerialization in the object’s configuration structure during object creation.
2.2. Execution Level
KMDF drivers can specify the maximum IRQL
at which the callbacks for driver, device, file, and general objects
are invoked. Like synchronization scope, execution level is an attribute
that the driver can configure when it creates the object. KMDF supports
the following execution levels:
Default execution level indicates that the driver has placed no particular constraints on the IRQL at which the callbacks for the object can be invoked. For most objects, this is the default.
Passive execution level (WdfExecutionLevelPassive) means that all event callbacks for the object occur at PASSIVE_LEVEL.
If necessary, KMDF invokes the callback from a system worker thread.
Drivers can set this level only for device and file object. Typically, a
driver should set passive execution level only if the callbacks access
pageable code or data or call other functions that must be called at PASSIVE_LEVEL.
Callbacks for events on file objects (WDFFILEOBJECT type) are always called at PASSIVE_LEVEL because these functions must be able to access pageable code and data.
Dispatch execution level (WdfExecutionLevelDispatch) means that KMDF can invoke the callbacks from any IRQL up to and including DISPATCH_LEVEL. This setting does not force all callbacks to occur at DISPATCH_LEVEL. However, if a callback requires synchronization, KMDF uses a spin lock, which raises IRQL to DISPATCH_LEVEL. Drivers can set dispatch execution level but nevertheless ensure that some tasks are performed at PASSIVE_LEVEL by using work items (WDFWORKITEM objects). Work item callbacks are always invoked at PASSIVE_LEVEL in the context of a system thread.
By default, an object inherits its execution level from its parent object. The default execution level for the WDFDRIVER object is WdfExecutionLevelDispatch.
2.3. Locks
In addition to internal synchronization,
synchronization scope, and execution level, KMDF provides the following
additional ways for a driver to synchronize operations:
Acquire the lock that is associated with a device or queue object.
Create and use additional, KMDF-defined, driver-created lock objects.
Driver code that runs outside an event callback
sometimes must synchronize with code that runs inside an event callback.
To accomplish this synchronization, KMDF provides methods (WdfObjectAcquireLock and WdfObjectReleaseLock)
through which the driver can acquire and release the internal framework
lock that is associated with a particular device or queue object.
Given the handle to a device or queue object, WdfObjectAcquireLock
acquires the lock that protects that object. After acquiring the lock,
the driver can safely access the object context data or properties and
can perform other actions that affect the object. If the driver has set WdfExecutionLevelPassive for the object (or if the object has inherited this value from its parent), KMDF uses a PASSIVE_LEVEL synchronization primitive (a fast mutex) for the lock. If the object does not have this constraint, use of the lock raises IRQL to DISPATCH_LEVEL and, while the driver holds the lock, it must not touch pageable code or data or call functions that must run at PASSIVE_LEVEL.
KMDF also defines two types of lock objects:
Wait locks (WDFWAITLOCK) synchronize access from code that runs at IRQL PASSIVE_LEVEL or APC_LEVEL.
Such locks prevent thread suspension. Internally, KMDF implements wait
locks by using kernel dispatcher events, so each wait lock is associated
with an optional time-out value (as are the kernel dispatcher events).
If the time-out value is zero, the driver can acquire the lock at DISPATCH_LEVEL.
Spin locks (WDFSPINLOCK) synchronize access from code that runs at any IRQL up to DISPATCH_LEVEL. Because code that holds a spin lock runs at DISPATCH_LEVEL, it cannot take a page fault and therefore must not access any pageable data. The WDFSPINLOCK
object keeps track of its acquisition history and ensures that
deadlocks cannot occur. Internally, KMDF uses the system’s spin lock
mechanisms to implement spin lock objects.
As with all other KMDF objects, each instance of a
lock object can have its own context area that holds lock-specific
information.
Drivers that do not use the built-in frameworks locking (synchronization scope, execution level, and AutomaticSerialization) can implement their own locking schemes by using KMDF wait
locks and spin locks. Drivers that use frameworks locking can use KMDF
wait locks and spin locks to synchronize access to data that is not
associated with a particular device or queue object. In general, drivers
can rely on frameworks locking while communicating with their own
hardware and calling within their own code. Drivers that communicate
with other drivers generally must implement their own locking schemes.
2.4. Interaction of Synchronization Mechanisms
Synchronization scope and execution level interact
because of the way in which KMDF implements synchronization. By default,
KMDF uses spin locks, which raise IRQL to DISPATCH_LEVEL,
to synchronize callbacks. Therefore, if the driver specifies device or
queue synchronization scope, its device and queue callbacks must be able
to run at DISPATCH_LEVEL.
If the driver sets the WdfExecutionLevelPassive
constraint for a parent device or queue object, KMDF uses a fast mutex
instead of a spin lock. In this case, however, KMDF cannot automatically
synchronize callbacks for timer and DPC child objects (including the
DPC object that is associated with the interrupt object) because DPC and
timer callbacks, by definition, always run at DISPATCH_LEVEL. Trying to create any of these objects with AutomaticSerialization fails if the WdfExecutionLevelPassive constraint is set for the parent object.
Instead, the driver can synchronize the event callbacks for these objects by using a WDFSPINLOCK object. The driver acquires and releases the lock manually by the KMDF locking methods WdfSpinLockAcquire and WdfSpinLockRelease.
Alternatively, the driver can perform whatever
processing is required within the DPC or timer callback and then queue a
work item that is synchronized with the callbacks at PASSIVE_LEVEL to perform further detailed processing.
3. Security
KMDF is designed to enhance the creation of secure driver by providing:
3.1. Safe Defaults
Unless the driver specifies otherwise, KMDF provides
access control lists (ACLs) that require Administrator privileges for
access to any exposed driver constructs, such as names, device IDs, WMI
management interfaces, and so forth. In addition, KMDF automatically
handles I/O requests for which a driver has not registered by completing
them with STATUS_INVALID_DEVICE_REQUEST.
3.2. Parameter Validation
One of the most common driver security problems involves improper handling of buffers in IOCTL requests, particularly requests that specify neither buffered nor direct I/O (METHOD_NEITHER).
By default, KMDF does not grant drivers direct access to user mode
buffer pointers, which is inherently unsafe. Instead, it provides
methods for accessing the user mode buffer pointer that require probing
and locking, and it provides methods to probe and lock the buffer for
reading and writing.
All KMDF DDIs that require a buffer take a length parameter that specifies a required minimum buffer size. I/O buffers use the WDFMEMORY
object, which provides data access methods that automatically validate
length and determine whether the buffer permissions allow write access
to the underlying memory.
3.3. Counted UNICODE Strings
To help prevent string handling errors, KMDF DDIs use only counted PUNICCODE_STRING values. To aid drivers in using and formatting UNICODE_STRING values, the safe string routines in ntstrsafe.h have been updated to take PUNICODE_STRING parameters.
3.4. Safe Device Naming Techniques
KMDF device objects do not have fixed names. KMDF sets FILE_AUTOGENERATED_DEVICE_NAME in the device’s characteristics for PDOs, according to the WDM requirements.
KMDF also supports the creation and registration of
device interfaces on all Plug and Play devices and manages device
interfaces for its drivers. Whenever possible, you should use device
interfaces instead of the older fixed name/symbolic link techniques.
However, if legacy applications require that a device
has a name, KMDF enables you to name a device and to specify its
security description definition language (SDDL). The SDDL controls which
users can open the device.
By convention, a fixed device name is associated with a fixed symbolic link name (such as \DosDevices\MyDeviceName).
KMDF supports the creation and management of a symbolic link and
automatically deletes the link when the device is destroyed. KMDF also
enables the creation of a symbolic link name for an unnamed Plug and
Play device.