IRQL suspiciously changing inside WdfRequestComplete

I have a driver which emulates USB devices, based on the UDE framework. I've recently started encountering bugcheck IRQL_GT_ZERO_AT_SYSTEM_SERVICE, indicating that my code is leaving its DeviceIoControl callback with a raised IRQL.

I've managed to track this issue down to a snippet like this:

KIRQL old_irql = KeGetCurrentIrql();
WdfRequestComplete(...);
NT_ASSERT(KeGetCurrentIrql() == old_irql);

In some (mostly reproducible) cases, the IRQL changes from PASSIVE to DISPATCH across the WdfRequestComplete call. The "downstream" driver is dc1_controller.sys (responsible for XBox One gamepads).

I've noticed a cryptic note in the UDE docs, stating:

To ensure compatibility with existing USB drivers, the UDE client must call WdfRequestComplete at DISPATCH_LEVEL.

Sure enough, wrapping the WdfRequestComplete call in KeRaiseIrql/KeLowerIrql "solves" the problem. But this still doesn't sound right. Why would the dc1 driver raise the IRQL and keep it raised? I've tried stepping through that code, but this seems to disrupt the test conditions and the issue usually fails to appear.

What is even more suspicious, I've made an experiment where I would raise the IRQL above DISPATCH_LEVEL. After WdfRequestComplete, the IRQL is back to DISPATCH. How could this happen?

Why would the dc1 driver raise the IRQL and keep it raised?

Poor design decision? It is a fact that USB completions are called in the interrupt DPC handler for the USB host controller, so they are at DISPATCH. Perhaps this driver knew of this, assumed it, and forced the IRQL to DISPATCH to support that assumption without lowering it later.

I was thinking along the same lines, but KeRaiseIrql(DISPATCH_LEVEL) would bugcheck if the initial irql was higher. And I don't know if there is an API to "blindly" set the IRQL to a specific value. Besides, that specific driver doesn't reference the Ke*Irql functions directly.

I think I've managed to figure this out - I've finally hit a bugcheck reporting a completion routine that changed the IRQL. The dc1-controller driver runs on top of xboxgip. It turns out that xboxgip uses all sorts of spinlock APIs, including the *AtDpcLevel ones, running under the assumption that the completion routines will be called at DISPATCH_LEVEL. When its requests are completed at different IRQL, a specific sequence of these spinlock calls changes the IRQL to dispatch.

Bottom line: It's not legal to complete USB requests with a non-dispatch IRQL. I'm not sure if this is a formal rule, or just Hyrum's Law at work, but it doesn't really matter at this point. Any violations will eventually result in various difficult-to-debug issues.

1 Like