Windows Drivers in Rust: Safe Framework

Published on September 25, 2021

Introduction

In the previous article we looked at using bindgen to generate more complete bindings for the Windows kernel API. These bindings can also be found at Github. However, the problem is that we still have to deal with writing unsafe code. In this article we will be looking at creating a Microsoft Windows kernel driver framework with safe Rust abstractions in a new crate called windows-kernel-rs.

Memory Allocations

As mentioned before we can currently not rely on the standard Rust library which provides types and data structures such as String, Vec and HashMap, and it would be very convenient if we could actually use those. It turns out that these will help us implement the print! and println! macros that we are used to in Rust. Fortunately, these types are available in the alloc crate, which we can use once we implemented our very own allocator. In fact, the Windows kernel API provides us with functions to allocate and free our own memory called ExAllocatePool and ExFreePool which we can use to implement our own allocator in Rust. We just have to define our own type and then implement the core::alloc::GlobalAlloc trait.

So we set up our new crate as before:

cargo new --lib windows-kernel-rs

Similar to our example driver we will be setting the panic handling to abort and to use our windows-kernel-sys crate, which provides the unsafe bindings.

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

[dependencies]
windows-kernel-sys = { path = "../windows-kernel-sys" }

We then change src/lib.rs to state that we are not using the standard library and import our allocator module that we will be writing:

#![no_std]

pub mod allocator;

We then implement our own allocator called KernelAllocator in src/allocator.rs:

use core::alloc::{GlobalAlloc, Layout};
use windows_kernel_sys::base::_POOL_TYPE as POOL_TYPE;
use windows_kernel_sys::ntoskrnl::{ExAllocatePool, ExFreePool};

pub struct KernelAllocator;

unsafe impl GlobalAllocator for KernelAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let ptr = ExAllocatePool(POOL_TYPE::NonPagedPool, layout.size() as u64);

        if ptr.is_null() {
            panic!("[kernel-alloc] failed to allocate pool.");
        }

        ptr as _
    }

    unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
        ExFreePool(ptr as _)
    }
}

Then we have to set up the allocator by using the global_allocator attribute as follows in src/lib.rs:

#[global_allocator]
static ALLOCATOR: allocator::KernelAllocator = allocator::KernelAllocator;

However, if we try building this we will run into an error that there is no alloc_error_handler. So let's define one. We will tell Rust that we want to use the experimental alloc_error_handler feature as follows in src/lib.rs:

#![feature(alloc_error_handler)]

Then we define the actual allocation error handler in src/allocator.rs:

#[alloc_error_handler]
fn alloc_error(_: Layout) -> ! {
    loop {}
}

While we are at it, we can also move our panic handler into this crate:

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

Then there are currently two more issues with _fltused and __CxxFrameHandler3 caused by the fact that we actually invoke the panic! macro from our allocator. These are actually bugs that we can temporarily resolve as follows:

#[used]
#[no_mangle]
pub static _fltused: i32 = 0;

#[no_mangle]
pub extern "system" fn __CxxFrameHandler3() -> i32 {
    0
}

As of writing I cannot find much about the current state of __CxxFrameHandler3. The issue was opened before, and this pull request should address the issue somewhat, but that does not seem to be entirely true.

With all this our kernel allocator is in place and we can actually start using the alloc crate.

It turns out that ExAllocatePool and ExFreePool have been deprecated since Microsoft Windows 10, version 2004 and later. Since we used bingen the replacements are actually available to us, but I still have to look into actually using them. I will update this part of the article once I get to that point.

Printing Macros

Now that our allocator is set up, we will actually have access to the format! macro which will turn out to be very useful when implementing the print! and println! macros. The details have actually been covered by both Matthias and Philipp who both provide excellent articles on implementing print! and println! macros.

Remember that until now we have been using the DbgPrint function which expects a null-terminated or C-style string. In our case we will implement a function called _print that accepts core::fmt::Arguments and then uses alloc::format! to actually format the string, but at the same time ensure that the string is null-terminated. Thus we first import the DbgPrint function in src/io.rs:

use windows_kernel_sys::ntoskrnl::DbgPrint;

Then we will implement our _print function as follows:

#[doc(hidden)]
pub fn _print(args: core::fmt::Arguments) {
    let s = alloc::format!("{}\0", args);

    // Print the string.
    unsafe { DbgPrint(s.as_ptr() as _) };
}

Note that we are using #[doc(hidden)] as this is more of an internal function that we are not going to use directly, but to implement the print! and println! macros. For the print! macro, we will use format_args! and then simply call this function:

#[macro_export]
macro_rules! print {
    ($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}

As you may have noticed, our macro refers to the _print function using $crate::io::_print, this way when we actually import and use the macro from our driver, we don't have to also import the _print function itself. Now that we have the print! macro, we can use that to implement the println! macro which simply prints a newline if no arguments are given, or formats the arguments and prints a newline:

#[macro_export]
macro_rules! println {
    () => ($crate::print!("\n"));
    ($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}

This completes our implementation of the print! and println! macros.

Handling Errors

The functions in the Windows kernel API uses the NTSTATUS type to indicate success or return an error. Therefore, we will simply wrap NTSTATUS using our own Error type in src/error.rs:

pub struct Error(NTSTATUS)

We can then add existing status codes as error by defining constants and add methods to wrap NTSTATUS into an Error and to extract the NTSTATUS from the Error:

impl Error {
    pub const UNSUCCESSFUL: Error = Error(STATUS_UNSUCCESSFUL);
    pub const NOT_IMPLEMENTED: Error = Error(STATUS_NOT_IMPLEMENTED);

    pub fn from_kernel_errno(status: NTSTATUS) -> Error {
        Error(status)
    }

    pub fn to_kernel_errno(&self) -> NTSTATUS {
        self.0
    }
}

With the Error type defined, we can now simply use Result<T, Error> where Ok replaces STATUS_SUCCESS.

Boilerplate Code

The last part of our framework involves providing a macro that essentially creates the driver_entry and driver_exit functions for us. We first start by providing a trait that the user of our crate can implement that simply has an init and cleanup function.

pub trait KernelModule: Sized + Sync {
    fn init(driver: Driver, registry_path: &str) -> Result<Self, Error>;
    fn cleanup(&mut self, driver: Driver);
}

Since we can't couple the KernelModule to the DRIVER_OBJECT we have to globally declare it. We will be using a trick in our macro where we declare it as follows:

static mut __MOD: Option<$module> = None;

However, to be able to declare it as such, we have to make sure the KernelModule is both Sized and Sync.

We also want to provide safe abstractions for the parameters passed to the driver_entry and the driver_exit functions. For the first parameter, we will start with a basic abstraction for the DRIVER_OBJECT in src/driver.rs:

pub struct Driver {
    pub(crate) raw: *mut DRIVER_OBJECT,
}

impl Driver {
    pub unsafe fn from_raw(raw: *mut DRIVER_OBJECT) -> Self {
        Self {
            raw,
        }
    }

    pub unsafe fn as_raw(&self) -> *const DRIVER_OBJECT {
        self.raw as _
    }

    pub unsafe fn as_raw_mut(&mut self) -> *mut DRIVER_OBJECT {
        self.raw as _
    }
}

For the second parameter, we will be relying on the widestring crate to convert the UTF-16 string into &str. We can then use the following code to safely wrap these parameters:

let driver = unsafe {
    Driver::from_raw(driver)
};

let registry_path = unsafe {
    U16CString::from_ptr_str(registry_path.Buffer)
};
let registry_path = registry_path.to_string_lossy();

Finally, we can implement the full macro kernel_module as follows:

pub use crate::driver::Driver;
pub use crate::error::Error;
pub use widestring::U16CString;
pub use windows_kernel_sys::base::{DRIVER_OBJECT, NTSTATUS, STATUS_SUCCESS, UNICODE_STRING};

#[macro_export]
macro_rules! kernel_module {
    ($module:ty) => {
        static mut __MOD: Option<$module> = None;

        #[no_mangle]
        pub extern "system" fn driver_entry(
            driver: &mut $crate::DRIVER_OBJECT,
            registry_path: &$crate::UNICODE_STRING,
        ) -> $crate::NTSTATUS {
            unsafe {
                driver.DriverUnload = Some(driver_exit);
            }

            let driver = unsafe {
                Driver::from_raw(driver)
            };

            let registry_path = unsafe {
                $crate::U16CString::from_ptr_str(registry_path.Buffer)
            };
            let registry_path = registry_path.to_string_lossy();

            match <$module as $crate::KernelModule>::init(driver, registry_path.as_str()) {
                Ok(m) => {
                    unsafe {
                        __MOD = Some(m);
                    }

                    $crate::STATUS_SUCCESS
                }
                Err(e) => {
                    e.to_kernel_errno()
                }
            }
        }

        pub unsafe extern "C" fn driver_exit(
            driver: *mut $crate::DRIVER_OBJECT,
        ) {
            let driver = unsafe {
                Driver::from_raw(driver)
            };

            match __MOD.take() {
                Some(mut m) => m.cleanup(driver),
                _ => (),
            }
        }
    };
}

The macro above generates the boilerplate for our kernel driver implementing both the driver_entry and driver_exit functions. The driver_entry function converts the parameters into safe types, calls the init function of the KernelModule and stores the constructed module into __MOD. The driver_exit function takes the module from __MOD and then calls the cleanup function and implicitly drops the module.

Using the Framework

With our new framework in place, we can rewrite our driver into a much cleaner version. The code can also be found at Github:

#![no_std]

use windows_kernel_rs::{Driver, Error, kernel_module, KernelModule, println};

struct Module;

impl KernelModule for Module {
    fn init(_: Driver, _: &str) -> Result<Self, Error> {
        println!("Hello, world!");

        Ok(Module)
    }

    fn cleanup(&mut self, _: Driver) {
        println!("Bye bye!");
    }
}

kernel_module!(Module);

What's Next?

We now have a nice safe framework in Rust that we can use to write Windows kernel drivers, but we haven't been doing much interesting as our driver only prints "Hello, world" and "Bye bye" whenever we load and unload it. In the next article we will look at how to create device files, such that userspace can actually interact with our driver.

Acknowledgements

Thanks to Matthias for taking the time to explore how to write kernel drivers for Microsoft Windows and especially taking the time to write up the details of the process. Especially the blog post about kernel printing with Rust has been valuable. Similarly, I would like to thank Philipp for explaining how to implement the print! and println! macros in an operating system or no_std environment in general. Finally, I would like to thank Alex and Geoffrey (among others) for the Linux Kernel Modules in Rust project which explores safe abstractions in Rust for Linux kernel modules.

References

  1. https://github.com/Trantect/win_driver_example/issues/4
  2. 'Updating Deprecated ExAllocatePool Calls" - MSDN
  3. "Kernel Printing with Rust" - Not Matthias
  4. "Writing an OS in Rust - VGA Text Mode" - Philipp Oppermann


If you like my work or if my work has been useful to you in any way, then feel free to donate me a cup of coffee. Any donation is much appreciated!