Windows PCI Device Driver Development Guide: How to Trigger User-Space Events from Interrupt Handlers

In the article on interrupt handling in drivers, we introduced how to add logic for handling periodic Timer interrupts in the driver for our simulated PCIe device. To test the Timer counting functionality, we also created a user-space test program that reads the Timer’s count register every second. While this approach can test whether the Timer’s interrupts and counting functions are working correctly, it does not notify the user-space test program of Timer interrupt events. Instead, the user-space program uses a polling method to read the Timer’s count value. In this article, we will introduce how to notify the user-space test program of Timer interrupt events, allowing the test program to stop polling and read the Timer count value only when an interrupt event is notified.To notify interrupt events to the user-space program, we have two approaches.The first approach: Let the PCIe device driver call the kernel function ZwCreateEvent to create a Named Global Event. Each time the interrupt handler is called, it calls ZwSetEvent to notify this event. The user-space test program calls the user-space API OpenEvent to open this event using the name of the Named Global Event to obtain an event handle. Before reading the Timer count value, it calls the WaitForSingleObject function to wait for this event. Once the Timer interrupt is triggered, causing the driver’s interrupt handler to be called, this event will be notified, and the user-space test program will be awakened to read the Timer’s count register. This approach utilizes the characteristics of Named Global Events, allowing us to avoid adding extra interfaces (IOCTL) between the user-space program and the kernel driver. Both sides agree to use the same name to identify the event, and each obtains a handle pointing to this event, allowing the driver to Set (notify) and the test program to Wait (wait for notification).The second approach: The user-space test program calls the user-space API CreateEvent to create an anonymous event and obtain its handle. It then sends this event handle to the driver through an IOCTL request, allowing the driver to notify this event when the interrupt handler is called. However, there is a problem: when a process creates an anonymous event and obtains its handle, this handle only belongs to that process. Other processes and the kernel driver do not recognize this handle, and it is only valid in the context of that process. Since the kernel driver’s interrupt handler is unlikely to execute in the context of this process, how can we notify this event from the kernel driver? The Windows kernel provides a powerful function called ObReferenceObjectByHandle, which is a crucial object management function in the Windows kernel that implements a secure conversion from user-space handles to kernel objects. Using this function allows us to obtain the pointer to this event in the Windows kernel (KEVENT) based on the event handle of the current process, enabling us to call the kernel function KeSetEvent to notify this event in the interrupt handler, allowing the user-space to receive the event notification. In summary, this approach allows the user-space program to use the event handle while the kernel driver uses the kernel object of that event, enabling notification and waiting.Clearly, the first approach is simpler, as a Named Global Event suffices, but it lacks challenge, so I decided to implement the second approach. 😁First, we need to define an IOCTL OpCode to allow the user-space program to pass the handle of the event it created to the kernel driver. We name this IOCTL: Give Event Handle.Windows PCI Device Driver Development Guide: How to Trigger User-Space Events from Interrupt HandlersNext, we need to implement the handling of this IOCTL request, which is similar to the handling of the IOCTL requests we added previously. First, we check the parameters, then retrieve the event handle passed from the user-space program from the inputBuffer, and pass this handle to our implemented function GetUserModeEventObject to obtain the kernel object of this event. This operation must be performed in the current process context, and asynchronous processing is not allowed, as this handle becomes meaningless once it leaves the current process context.Windows PCI Device Driver Development Guide: How to Trigger User-Space Events from Interrupt HandlersNow let’s look at the implementation of the function GetUserModeEventObject, which calls the aforementioned powerful Windows kernel function ObReferenceObjectByHandle. This function has three parameters to note:DesiredAccess: The access rights for this object, which must be compatible with the object type. The value range for this parameter varies for different types of objects. For event objects, we choose EVENT_MODIFY_STATE, indicating that our driver can use the KeSetEvent function to notify this event.

ObjectType: The type of the kernel object pointed to by this handle. The kernel will verify whether the handle indeed points to this type of object. Here, we need to specify this parameter as ExEventObjectType, which is a global pointer variable defined by the Windows kernel that points to the event object type defined by the kernel. In fact, for other types of kernel objects, the Windows kernel has defined corresponding pointer variables, which can be referenced in the relevant definitions in the WDK header file wdm.h.

AccessMode: The access mode determines how the kernel checks security permissions. Here, we must fill in UserMode, as this handle was created in user-space and passed to the kernel driver, requiring a check based on user-space permissions..

Windows PCI Device Driver Development Guide: How to Trigger User-Space Events from Interrupt HandlersAdditionally, it is important to note that when calling the ObReferenceObjectByHandle function to obtain the kernel object, the reference count of that kernel object will be incremented by 1. Therefore, when we no longer need to use this kernel object, we must remember to call the function ObDereferenceObject to decrement the reference count of that kernel object; otherwise, it will not be released. Since I did not want to add another IOCTL for releasing the kernel object, I placed the release logic in the callback function for closing the device file. When the user-space program closes the device file, we certainly no longer need this event object.Windows PCI Device Driver Development Guide: How to Trigger User-Space Events from Interrupt HandlersAfter obtaining the pointer to the kernel object of this event from the handle, we record this pointer in the device context structure DEVICE_CONTEXT. In the interrupt handler, we can then retrieve this event’s kernel object pointer from the device context and call KeSetEvent to notify this event.Windows PCI Device Driver Development Guide: How to Trigger User-Space Events from Interrupt HandlersWait, we called KeSetEvent in the interrupt handler. Shouldn’t we check the IRQL required by this kernel API? Whether checking the MSDN help documentation for this function or looking at the declaration of this function in the wdm.h header file, we can find that when the third parameter Wait is set to FALSE, the maximum required IRQL is DISPATCH_LEVEL = 2.Windows PCI Device Driver Development Guide: How to Trigger User-Space Events from Interrupt HandlersCurrently, the IRQL of the CPU when the interrupt handler runs is DIRQL > DISPATCH_LEVEL (refer to Windows PCI Device Driver Development Guide: IRQL), which is clearly not acceptable. What should we do? Actually, we can change one thing. WDF interrupt configuration allows us to set the IRQL at which the interrupt handler executes to PASSIVE_LEVEL = 0. When the WDF_INTERRUPT_CONFIG‘s PassiveHandling is set to FALSE, the interrupt handler will execute at DIRQL. When set to TRUE, the interrupt handler will execute at PASSIVE_LEVEL, allowing us to safely call the KeSetEvent function.Windows PCI Device Driver Development Guide: How to Trigger User-Space Events from Interrupt HandlersThese are the modifications to the driver. Now let’s look at the modifications to the test program. The test program needs to call the Windows API function CreateEvent to create an event and obtain a handle, then send this event handle to the driver through the Give Event Handle IOCTL request. Finally, before reading the Timer count value register, it calls the Windows API function WaitForSingleObject to wait on this event (with a timeout set to 10 seconds). Once the driver receives the Timer interrupt, it will notify this event in the interrupt handler, waking the test program process to read the Timer count value register.Windows PCI Device Driver Development Guide: How to Trigger User-Space Events from Interrupt HandlersNow that both the driver and the test program have been modified, let’s test the effect. The following animation shows the situation when the test program is running. On the surface, it looks no different from before, but in fact, each read of the Timer count value is triggered by the Timer interrupt.Windows PCI Device Driver Development Guide: How to Trigger User-Space Events from Interrupt HandlersNext, let’s look at the logs output by the driver. We can see that GetUserModeEventObject successfully obtained the pointer to the kernel object of the event, and the IRQL printed by the interrupt handler has also changed to PASSIVE_LEVEL = 0.Windows PCI Device Driver Development Guide: How to Trigger User-Space Events from Interrupt HandlersIn summary, the most significant function in this implementation is the Windows kernel API ObReferenceObjectByHandle, which is very useful in scenarios where kernel objects need to be used across process contexts. Additionally, it is important to pay attention to the IRQL at which the kernel function is called in the driver’s interrupt handler and whether it matches the IRQL required by the kernel function being called. If they do not match, consider using passive-level interrupt handling or other kernel functions with appropriate IRQL.

Leave a Comment