Last reviewed and updated: 10 August 2020
Every time I teach a class — every single time — I learn something. Sometimes I learn that I’m not nearly as patient as I wish I was. But two weeks ago, Jon, who was in a group of interesting and capable driver developers I had the pleasure of working with, brought to my attention a long-standing WDF default that really surprised me. I’ll start from the beginning and tell you what I learned.
WDF has this dandy feature called Synchronization Scope. Most people call it “Sync Scope” for short (not to be confused with syncope, which I usually experience when I look at a driver that uses Sync Scope… but I digress). Sync Scope allows you to tell the Franework that you want many of your Event Processing Callback functions (such as your EvtIoXxx Event Processing Callbacks) serialized on a per-Device or per-Queue basis. See more about Sync Scope in this article from The NT Insider. When you use Sync Scope, you may also elect to extend the chosen Sync Scope to particular WDFTIMER and WDFWORKITEM callbacks as well. As the WDK says:
Note that setting a SynchronizationScope value synchronizes only the callback functions that the previous table contains. If you want the framework to also synchronize the driver’s … work-item, and timer object callback functions, the driver must set the AutomaticSerialization member of these objects’ configuration structures to TRUE.
So, put simply, if you want to include a given Timer or Work Item callback in your defined Sync Scope, you need to set the AutomaticSerialization field (which is in the associated WDF_object-name_CONFIG structure) to TRUE when instantiating the Object. That’s all good, right? Well, yes… but guess what? What I learned from Jon is that AutomaticSerialization is set to TRUE by default for WDFTIMER and WDFWORKITEM Objects. This is done when you call WDF_object-name_CONFIG_INIT. So, for example, let’s say you have a WDF_TIMER object that you initialize in the normal way (example taken from the WDK 8.1 CDROM Class Driver):
//create main timer object. WDF_TIMER_CONFIG timerConfig; WDF_TIMER_CONFIG_INIT(&timerConfig, DeviceMainTimerTickHandler);
The AutomaticSerialization field of the WDF_TIMER_CONFIG structure will be set to TRUE. Don’t believe me (I didn’t believe Jon initially either)? Here’s the definition for the function from KMDF V1.13:
VOID FORCEINLINE WDF_TIMER_CONFIG_INIT( _Out_ PWDF_TIMER_CONFIG Config, _In_ PFN_WDF_TIMER EvtTimerFunc ) { RtlZeroMemory(Config, sizeof(WDF_TIMER_CONFIG)); Config->Size = sizeof(WDF_TIMER_CONFIG); Config->EvtTimerFunc = EvtTimerFunc; Config->Period = 0; Config->AutomaticSerialization = TRUE; Config->TolerableDelay = 0; }
Likewise, the default is the same for WDF_WORKITEMs:
VOID FORCEINLINE WDF_WORKITEM_CONFIG_INIT( _Out_ PWDF_WORKITEM_CONFIG Config, _In_ PFN_WDF_WORKITEM EvtWorkItemFunc ) { RtlZeroMemory(Config, sizeof(WDF_WORKITEM_CONFIG)); Config->Size = sizeof(WDF_WORKITEM_CONFIG); Config->EvtWorkItemFunc = EvtWorkItemFunc; Config->AutomaticSerialization = TRUE; }
So there it is. And it’s the same in UMDF V2. And it’s been this way for both of these objects since KMDF V1.0!
Maybe you’re wondering why you should care about this. Well, the answer is you shouldn’t care, except if you declare an explicit Sync Scope in your driver. Now, you’re thinking this is pretty obvious, right? You’re thinking that the next thing I’m going to say “You care about this because your Timer or Work Item callback will be serialized, perhaps unnecessarily, with your EvtIoXxx Event Processing Callbacks.” While this is true, and something you should be aware of, it’s also not the primary issue. The primary issue has to do with IRQL. As I’m sure you know, by default your EvtIoXxx Event Processing Callbacks can be called at IRQL DISPATCH_LEVEL or below. However, the callback for a WDF_WORKITEM is required to always run at IRQL PASSIVE_LEVEL. This ordinarily isn’t a problem, unless you specify both AutomaticSerialization for the Work Item and a Sync Scope for your EvtIoXxx functions. When Sync Scope is specified, you now have three conflicting constraints:
- Your EvtIoXxx Event Processing Callbacks can execute at any IRQL <= IRQL DISPATCH_LEVEL.
- Your Work Item callback must run at IRQL PASSIVE_LEVEL.
- You want to serialize the execution of these functions.
These things things are in conflict because there is no single lock that can be used to satisfy all of these conditions. Yet you’ve requested (by default, because AutomaticSerialization defaults to TRUE) that both your EvtIoXxx and your Work Item functions must be serialized. The Framework can’t use a Spin lock to implement your Sync Scope requirement because this will cause the Work Item to run at IRQL DISPATCH_LEVEL. And it can’t use a Mutex because your EvtoXxx Event Processing Callback can execute at IRQL DISPATCH_LEVEL.
You can fix this by specifying an ExecutionLevel constraint requiring that your EvtIoXxx Event Processing Callbacks run at IRQL PASSIVE_LEVEL. If this is really what you want to do. But this introduces issues of its own: Do you really want KMDF to defer any I/O processing that happens to arrive at an IRQL DISPATCH_LEVEL to an internal Work Item? Because that’s what specifying an IRQL PASSIVE_LEVEL ExecutionLevel constraint will do. Yuck.
Admittedly, you already have to deal with potential conflicting IRQLs and Sync Scope requirements. This is the case when you specify an IRQL PASSIVE_LEVEL execution level constraint, but you want to serialize against your DpcForIsr. DPCs always runs at IRQL DISPATCH_LEVEL. Thus, if you set AutomaticSerialization for your DPC, once again there’s no lock that can be used to meet your IRQL constraints (PASSIVE_LEVEL EvtIoXxx callbacks), Windows OS constraints (DPCs must run at DISPATCH_LEVEL), and the serialization requirement that you’ve specified (serialize all these callbacks, which requires using a single lock). The difference between having this issue with your DPC and that with a Timer or Work Item is that you must manually set AutomaticSerialization to TRUE for your DPC. It defaults to FALSE. Therefore, if you manually set this field, you knowingly assume the burden of dealing with any potential conflicts. But with Timers and Work Items, as we’ve just discovered, AutomaticSerialization is set to TRUE by default. And unless you’re aware of this issue, you will certainly spend time trying to find your problem.
What should you do about this? Well, the first thing is just be aware of it. If you’re not aware that AutomaticSerialization is set to TRUE by default, and you use Sync Scope, and you also happen to use either a Timer or Work Item in your driver, you will have to deal with this eventually. You’ll get Object creation failures (failure to create your Timer Object, for example) and you’ll get SDV errors. And when you get these errors, you’ll be as mystified and annoyed as Jon and his colleagues until you realize that AutomaticSerialization is set to TRUE by default.
Also, for the purpose of clarity, I’d suggest that anytime you call WDF_WORKITEM_CONFIG_INIT or WDF_TIMER_CONFIG_INIT you follow that with setting AutomaticSerialization to either TRUE or FALSE, depending on what you specifically intend. Do not allow the field to default. This will make both your intent, and the expected behavior, clear to whoever reads your code.
You don’t use Sync Scope so you don’t care, you say? I still recommend you manually set the AutomaticSerialization field after you initialize your Timer or Work Item. This’ll make sure the behavior is clear in the future, when somebody enables Sync Scope in their desperate attempt to find and fix what they think is a serialization problem. WDF truly is awesome. It makes so many things easy. But that doesn’t mean you don’t have to pay attention to the many little details.