Search code examples
windowsmultithreadingkerneldriverwdk

KSPIN_LOCK blocks when acquiring from Driver's main thread


I have a KSPIN_LOCK which is shared among a Windows driver's main thread and some threads I created with PsCreateSystemThread. The problem is that the main thread blocks if I try to acquire the spinlock and doesn't unblock. I'm very confused as to why this happens.. it's probably somehow connected to the fact that the main thread runs at driver IRQL, while the other threads run at PASSIVE_LEVEL as far as I know.

NOTE: If I only run the main thread, acquiring/releasing the lock works just fine.

NOTE: I'm using the functions KeAcquireSpinLock and KeReleaseSpinLock to acquire/release the lock.


Solution

  • Here's my checklist for a "stuck" spinlock:

    1. Make sure the spinlock was initialized with KeInitializeSpinLock. If the KSPIN_LOCK holds uninitialized garbage, then the first attempt to acquire it will likely spin forever.
    2. Check that you're not acquiring it recursively/nested. KSPIN_LOCK does not support recursion, and if you try it, it will spin forever.
    3. Normal spinlocks must be acquired at IRQL <= DISPATCH_LEVEL. If you need something that works at DIRQL, check out [1] and [2].
    4. Check for leaks. If one processor acquires the spinlock, but forgets to release it, then the next processor will spin forever when trying to acquire the lock.
    5. Ensure there's no memory-safety issues. If code randomly writes a non-zero value on top of the spinlock, that'll cause it to appear to be acquired, and the next acquisition will spin forever.

    Some of these issues can be caught easily and automatically with Driver Verifier; use it if you're not using it already. Other issues can be caught if you encapsulate the spinlock in a little helper that adds your own asserts. For example:

    typedef struct _MY_LOCK {
        KSPIN_LOCK Lock;
        ULONG OwningProcessor;
        KIRQL OldIrql;
    } MY_LOCK;
    
    void MyInitialize(MY_LOCK *lock) {
        KeInitializeSpinLock(&lock->Lock);
        lock->OwningProcessor = (ULONG)-1;
    }
    
    void MyAcquire(MY_LOCK *lock) {
        ULONG current = KeGetCurrentProcessorIndex();
        NT_ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);
        NT_ASSERT(current != lock->OwningProcessor); // check for recursion
        KeAcquireSpinLock(&lock->Lock, &lock->OldIrql);
        NT_ASSERT(lock->OwningProcessor == (ULONG)-1); // check lock was inited
        lock->OwningProcessor = current;
    }
    
    void MyRelease(MY_LOCK *lock) {
        NT_ASSERT(KeGetCurrentProcessorIndex() == lock->OwningProcessor);
        lock->OwningProcessor = (ULONG)-1;
        KeReleaseSpinLock(&lock->Lock, lock->OldIrql);
    }
    

    Wrappers around KSPIN_LOCK are common. The KSPIN_LOCK is like a race car that has all the optional features stripped off to maximize raw speed. If you aren't counting microseconds, you might reasonably decide to add back the heated seats and FM radio by wrapping the low-level KSPIN_LOCK in something like the above. (And with the magic of #ifdefs, you can always take the airbags out of your retail builds, if you need to.)