Resolving Race Conditions in Device Drivers (Windows CE 5.0)
The XXX_PreClose (Device Manager) and XXX_PreDeinit (Device Manager) stream interface driver entry points handle race conditions in which one thread opens or closes a device handle while another performs I/O, or in which one thread opens, closes, or performs I/O on a handle while another thread deactivates the device driver.
Device Manager maintains a reference count of threads executing in driver entry points. When the thread reference count reaches zero, Device Manager can safely unload the driver. Device Manager uses InterlockedIncrement and related synchronization operations to manage reference counts.
Device Manager maintains a global critical section for validating device handles returned from ActivateDeviceEx and RegisterDevice. This global critical section is also used for validating device file handles returned from CreateFile.
In the XXX_Init (Device Manager), XXX_Deinit (Device Manager), XXX_Open (Device Manager), and XXX_Close (Device Manager) entry points, Device Manager validates handles, releases the critical section, and then calls into the appropriate device driver entry point.
For performance reasons, Device Manager does not take the global critical section during I/O calls such as XXX_Read (Device Manager), XXX_Write (Device Manager), SetFilePointer, and DeviceIoControl. Instead, it relies on use counts to keep the driver from unloading while threads are executing code in the driver. Threads performing device file-handle-based I/O, increment a use count in the Device Manager handle descriptor structure and in the device driver descriptor structure.
The driver's XXX_Close entry point wakes any threads that are blocked on the handle specified by the hOpenContext parameter, and releases any resources associated with hOpenContext.
The XXX_Deinit entry point wakes any threads blocked on I/O on all handles associated with the device instance specified by hDeviceContext. It then releases resources for all device file handles, which are all hOpenContext values returned from XXX_Open calls, for the device instance. Finally, it releases resources for the device instance. Once XXX_Deinit is called on a device, XXX_Close is no longer called on the open device handles.
A race condition might occur during periods of heavy I/O for devices being loaded and unloaded. Because Device Manager releases its global critical section before calling device driver entry points, it is possible that one thread might call XXX_Read, XXX_Write, XXX_Open, or XXX_Close, and also call into the device driver's entry point, while another thread calls XXX_Deinit on the device instance. In this situation, it appears to the driver that XXX_Read, XXX_Write, XXX_Open, or xxx_Close has been called after the call to xxx_Deinit. If the driver assumes that the context parameter to XXX_Read, XXX_Write, XXX_Open, or xxx_Close is valid, the driver might dereference freed resources and cause a crash. A similar race condition exists between XXX_Read, XXX_Write operations and calls to CloseHandle.
The XXX_PreClose and XXX_PreDeinit entry points allow drivers to eliminate these race conditions. Although both entry points are optional, if you implement XXX_PreClose, you must implement XXX_PreDeinit or the driver will not load. To prevent drivers from releasing resources associated with the driver, these entry points separate waking threads and invalidating handles.
The XXX_PreClose entry point allows the driver to wake sleeping threads and mark the handle as closed so that further I/O attempts fail gracefully. When Device Manager confirms that no threads are scheduled to execute in the driver, Device Manager calls the XXX_Close entry point so that the driver can free handle resources.
Similarly, the XXX_PreDeinit entry point allows the driver to wake sleeping threads, mark all handles as closing, and mark the device itself as deinitializing so that further attempts to open handles fail gracefully. When Device Manager confirms that no threads are scheduled to execute in the driver, Device Manager calls the XXX_Deinit entry point so that the driver can release resources for all handles associated with the device and with the device itself.
Drivers that implement these two entry points can expect that the handle contexts passed to their entry points will always be valid, because the handles are not released until Device Manager has confirmed that no threads are accessing them.
It is possible that the driver will be deactivated after the call to XXX_PreClose, but before the call to XXX_Close. From the perspective of the driver in this case, XXX_PreClose wakes sleeping threads, but XXX_Deinit releases handle resources.
Note Without using the XXX_PreClose and XXX_PreDeinit entry points, a driver can work around this problem by creating a global critical section in its DLL entry point in conjunction with use counts on its device and handle instance structures. This method is fairly complex to implement and difficult to test.
See Also
Device File Names | Device Manager | Device Manager Reference | Device Manager Registry Keys | I/O Resource Manager | Stream Interface Drivers | Synchronization Reference
Send Feedback on this topic to the authors