Windows Driver IRQL and acquiring a Driver Mutex

Avoiding blue screens by understanding IRQL, and how to acquire a Windows Driver Mutex in Rust


Intro

Before we start, if you like my content please be sure to give me a follow on GitHub and give my Windows driver a star! it means a lot to me!

In this post, we will look at how to acquire a Mutex in your driver in Rust, and what an IRQL is.

I have developed an open source Rust crate for using an idiomatic mutex in the Windows Kernel for drivers called wdk-rust. Check that post out here, or keep reading to learn about IRQL and mutex's!

IRQL

If you have never done driver development before, and have lived only in userland, IRQL will be a new concept. It stands for Interrupt Request Level, and simply, it means how important your interrupt is when compared to all the other code and threads going on in each processor. Code running at a lower IRQL can be interrupted by code coming in at a higher IRQL.

Code in usermode (and most standard driver code) exists at the lowest IRQL, 0 (also known as Passive Level). In usermode, it is not possible to raise the IRQL above 0; hence why this may be a new topic.

The levels are:

Checking IRQL

To check the IRQ level you are operating at we can use the function from the Rust WDK crate, KeGetCurrentIrql -> MSDN, Rust Docs.

This function will return the IRQL the code is operating at; and in Rust you can call it like so:

let irql = unsafe { KeGetCurrentIrql() };
if irql > APC_LEVEL as u8 {
    // handle IRQL being too high, such as returning from a function - i.e. you are checking that you
    // aren't about to cause a BSOD
}

Acquiring a kernel driver mutex

An example of why we may need to be mindful of IRQL is in the case of trying to acquire a FAST_MUTEX. When looking at using kernel mode functions you must look at the IRQL specified in the Requirements section at the bottom of the docs, for instance, for ExAcquireFastMutex it specifies:

Windows Driver Rust IRQL

We can see, it specifies we must be at an IRQL <= to APC_LEVEL (aka level 1).

Even more importantly, the documentation says:

Callers of ExAcquireFastMutex must be running at IRQL <= APC_LEVEL. ExAcquireFastMutex sets the IRQL to APC_LEVEL, and the caller continues to run at APC_LEVEL after ExAcquireFastMutex returns. ExAcquireFastMutex saves the caller’s previous IRQL in the mutex, however, and that IRQL is restored when the caller invokes ExReleaseFastMutex.

The key point, is this function will set the IRQL to APC_LEVEL for the code executing within the Mutex, and will return the IRQL to its previous level when you release the Mutex.

As we are at no higher than APC_LEVEL, this also means we can access both paged and non-paged memory.

To see how I have implemented this; inclusive of checking the IRQL for safety, see below. I have had to write my own implementation of the function ExInitializeFastMutex as it is not present in the Rust WDK crate.

As this is not a std Rust Mutex; it does not protect an inner value; though the below could be refactored to make it more idiomatic in line with Rust mutexes. Instead, the mutex should be used to access my custom struct. This is a very unsafe implementation, as if anybody other than me were to use this, say if it was a public crate, they may not know that the mutex needs to be locked before accessing the struct’s fields, and thus, could result in a race condition.

// our own implementation of ExInitializeFastMutex 
#[allow(non_snake_case)]
pub unsafe fn ExInitializeFastMutex(kmutex: *mut FAST_MUTEX) {
    // check IRQL
    let irql = unsafe { KeGetCurrentIrql() };
    assert!(irql as u32 <= DISPATCH_LEVEL);

    core::ptr::write_volatile(&mut (*kmutex).Count, FM_LOCK_BIT as i32);

    (*kmutex).Owner = core::ptr::null_mut();
    (*kmutex).Contention = 0;
    KeInitializeEvent(&mut (*kmutex).Event, SynchronizationEvent, FALSE as _)
}

// implement the default trait for my type, which contains the mutex
pub struct DriverMessagesWithMutex {
    lock: FAST_MUTEX,
    is_empty: bool,
    data: DriverMessages,
}

impl Default for DriverMessagesWithMutex {
    fn default() -> Self {
        let mut mutex = FAST_MUTEX::default();
        unsafe { ExInitializeFastMutex(&mut mutex) };
        let data = DriverMessages::default();

        DriverMessagesWithMutex { lock: mutex, is_empty: true, data }
    }
}

// example of accessing the struct, and locking the mutex
fn extract_all(&mut self) -> Option<DriverMessages> {

    // check the IRQL is safe as per the docs
    let irql = unsafe { KeGetCurrentIrql() };
    if irql > APC_LEVEL as u8 {
        println!("[sanctum] [-] IRQL is above APC_LEVEL: {}", irql);
        return None;
    }

    unsafe { ExAcquireFastMutex(&mut self.lock) };


    if self.is_empty {
        // release the mutex
        unsafe { ExReleaseFastMutex(&mut self.lock) }; 
        return None;
    }

    //
    // Do stuff here now the mutex is locked
    //
    
    let extracted_data = mem::take(&mut self.data);
    self.is_empty = true;

    // release the mutex
    unsafe { ExReleaseFastMutex(&mut self.lock) }; 

    Some(extracted_data)
}

One final note

Make sure you lower the IRQL if you manually raised it in the same function. Not doing this can result in undefined behaviour and ultimately system crashes.