The Rust language ecosystem is growing each day, its popularity increasing, and with good reason. It’s the only mainstream language that provides memory and concurrency safety at compile time, with a powerful and rich build system (cargo), and a growing number of packages (crates).
My daily driver is still C++, as most of my work is about low-level system and kernel programming, where the Windows C and COM APIs are easy to consume. Rust is a system programming language, however, which means it plays, or at least can play, in the same playground as C/C++. The main snag is the verbosity required when converting C types to Rust. This “verbosity” can be alleviated with appropriate wrappers and macros. I decided to try writing a simple WDM driver that is not useless – it’s a Rust version of the “Booster” driver I demonstrate in my book (Windows Kernel Programming), that allows changing the priority of any thread to any value.
Getting Started
To prepare for building drivers, consult Windows Drivers-rs, but basically you should have a WDK installation (either normal or the EWDK). Also, the docs require installing LLVM, to gain access to the Clang compiler. I am going to assume you have these installed if you’d like to try the following yourself.
We can start by creating a new Rust library project (as a driver is a technically a DLL loaded into kernel space):
We can open the booster folder in VS Code, and begin are coding. First, there are some preparations to do in order for actual code to compile and link successfully. We need a build.rs file to tell cargo to link statically to the CRT. Add a build.rs file to the root booster folder, with the following code:
fn main() -> Result<(), wdk_build::ConfigError> { std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static"); wdk_build::configure_wdk_binary_build() }
(Syntax highlighting is imperfect because the WordPress editor I use does not support syntax highlighting for Rust)
Next, we need to edit cargo.toml and add all kinds of dependencies. The following is the minimum I could get away with:
[package] name = "booster" version = "0.1.0" edition = "2021" [package.metadata.wdk.driver-model] driver-type = "WDM" [lib] crate-type = ["cdylib"] test = false [build-dependencies] wdk-build = "0.3.0" [dependencies] wdk = "0.3.0" wdk-macros = "0.3.0" wdk-alloc = "0.3.0" wdk-panic = "0.3.0" wdk-sys = "0.3.0" [features] default = [] nightly = ["wdk/nightly", "wdk-sys/nightly"] [profile.dev] panic = "abort" lto = true [profile.release] panic = "abort" lto = true
The important parts are the WDK crates dependencies. It’s time to get to the actual code in lib.rs.
The Code
We start by removing the standard library, as it does not exist in the kernel:
Next, we’ll add a few use
statements to make the code less verbose:
use core::ffi::c_void; use core::ptr::null_mut; use alloc::vec::Vec; use alloc::{slice, string::String}; use wdk::*; use wdk_alloc::WdkAllocator; use wdk_sys::ntddk::*; use wdk_sys::*;
The wdk_sys
crate provides the low level interop kernel functions. the wdk
crate provides higher-level wrappers. alloc::vec::Vec
is an interesting one. Since we can’t use the standard library, you would think the types like std::vec::Vec<>
are not available, and technically that’s correct. However, Vec
is actually defined in a lower level module named alloc::vec
, that can be used outside the standard library. This works because the only requirement for Vec
is to have a way to allocate and deallocate memory. Rust exposes this aspect through a global allocator object, that anyone can provide. Since we have no standard library, there is no global allocator, so one must be provided. Then, Vec
(and String
) can work normally:
#[global_allocator] static GLOBAL_ALLOCATOR: WdkAllocator = WdkAllocator;
This is the global allocator provided by the WDK crates, that use ExAllocatePool2
and ExFreePool
to manage allocations, just like would do manually.
Next, we add two extern
crates to get the support for the allocator and a panic handler – another thing that must be provided since the standard library is not included. Cargo.toml has a setting to abort the driver (crash the system) if any code panics:
extern crate wdk_panic; extern crate alloc;
Now it’s time to write the actual code. We start with DriverEntry
, the entry point to any Windows kernel driver:
#[export_name = "DriverEntry"] pub unsafe extern "system" fn driver_entry( driver: &mut DRIVER_OBJECT, registry_path: PUNICODE_STRING, ) -> NTSTATUS {
Those familiar
6 Comments
mastax
I had an idea to write a filesystem filter driver which would let you configure path remapping rules of sorts, depending on the application. Things like:
– %userprofile%.vscode -> %appdata%vscode
– %CSIDL_MYDOCUMENTS%Call of Duty -> %userprofile%Saved GamesCall of Duty
Because my documents and home directories filling up with a bunch of garbage which has a designated place on the filesystem filled me with impotent rage. I scaffolded out a project to write a filter driver in rust, read through the minifilter documentation, realized how much work it was going to be, and gave up.
I have made my peace with the fact that a windows system is just going to be filled with garbage.
the__alchemist
Interesting! This looks very different from embedded drivers, which I've done a lot of in rust. Those are mostly reg reads, writes, bit shifting, DMA, and data sheet references.
ilrwbwrkhv
Great article and even more impressive design of the blog. Just clean straight forward easy on the eyes and loads instantly.
gpm
Semi-related, anyone have any up to date information on rust usage in the windows kernel?
Almost 2 years ago they said "36,000 lines of code including a systemcall" [1], I'm curious how that project has progressed.
[1] https://www.thurrott.com/windows/282471/microsoft-is-rewriti…
justmarc
Some 25 years ago I was tasked with writing a certain driver for Windows.
Being totally migrated to Linux by then I refused to use Windows for writing as well as building it, so I worked hard to build it with MSYS.
Long story short, I made it, and the driver worked great.
I think I had to write a patcher for the resulting PE (.sys) to get it to actually load.
Fun times.
AndrewGaspar
The code here looks to be essentially C with different syntax – every function marked unsafe, all resources manually managed. Sorry to be blunt, but what's the point of this?