Considerations when inside a Resource Lock(Mutex)

Hey everyone,

I wanted to understand the limitations (if any) I would have inside the context of a call to ExAcquireResourceExclusiveLite(). Namely the documentation suggests the code inside of this should either enter a Critical Region to disable all Kernel APCs.

What I’m failing to understand is what limitations does this impose on the function running within this ‘Mutex’. So lets say I am executing callback functions like:

if ExAcquireResourceExclusiveLite
 then CallFunc()
ExReleaseResourceLite()

Assuming KeEnterCriticalRegion is called before the acqusition is made (or not?). Is there anything specific that I need to know about the limitations imposed on CallFunc() , besides the fact it can’t Queue any APCs, can it hold a Lock? Is IRQL important or relevant here?

The docs are pretty clear that disabling APC delivery is a requirement: “Normal kernel APC delivery must be disabled before calling this routine.”

Some implications beyond that of APC delivery that I am aware of:

  • You should avoid calling code that that could reenter, which may cause deadlocks.
  • Your CallFunc() can acquire additional locks, but you should be careful about the potential deadlocks if different threads acquire the two locks in a different order. (KeEnterCriticalRegion can be called recursively so that every resource acquire is paired with the critical region. There’s even a function ExEnterCriticalRegionAndAcquireResourceExclusive() that does this for you.)

ok I think what I might actually need here is something closer to a RundownProtection. Thanks for the clarification Jeremy

Just a note about the Critical Region thing because it’s always confusing: practically speaking all KeEnterCriticalRegion does is disable thread suspension. The majority of locks in Windows already do this for you but the ERESOURCE doesn’t as a potential performance optimization. The idea being that because they’re recursively acquirable you only need to enter the Critical Region once even if you recursively acquire the lock multiple times. I suspect that this optimization was much more important back when this code was first written and now it is just yet another way to confuse people ?

For a bit more background on WHY you need to disable thread suspension:

Drivers are generally called in the context of the requesting thread*. Imagine you’re writing a file system and using a global lock acquired around all I/O operations. These I/O operations sometimes come from privileged applications, sometimes from unprivileged, sometimes the OS, etc.

Now say you get an I/O request from some unprivileged thread/application. While running in the context of this unprivileged thread, you grab the global lock and - WHAM! - someone suspends the unprivileged thread holding the lock. You’ve now caused a DoS for every other thread that tries to access your file system as everyone goes to sleep waiting for the lock held by the suspended thread. If you enter the Critical Region before acquiring the lock then you’re all good…The worst that happens is the unprivileged app gets DoS’d and “that’s life”.

And, lastly, RundownProtection is awesome. We’ve used it a bunch of times and it usually turns out to be an elegant solution to the “accessing a resource that comes and goes” problem.

-scott

*There are a LOT of exceptions to this, but that’s another discussion…The key point is that the I/O Manager always initially dispatches the IRP synchronously and in the context of the requesting thread. Once the drivers are involved lots of other stuff can happen…

Also, just to be clear: This issues applies to any driver. The file system case I used is meant “by way of example and not limitation”.

rundown protection, also called reference tracking and other names, has been independently invented by many people over the years - including once by me. It is an indispensable technique in a high performance multi-threaded environment. tracking the number of, and location of, pointers to ‘objects’ that are ‘visible’ to multiple threads and also must be deallocated at some point, can dramatically reduce the lock scope and allows the safe use of such pointers from stack local variables

1 Like