Blog Post

Windows Driver Developer Blog
8 MIN READ

Towards Rust in Windows Drivers

nate_deisinger's avatar
Sep 02, 2025

Over the last several years, Microsoft and the industry as a whole have embraced Rust as a vital tool to reduce bugs, improve security, and encourage safe, maintainable programming.  With its compile-time guarantees around memory safety and strong ergonomics, Rust is a powerful tool for systems-level programming.

We’ve received lots of feedback from driver developers who want to begin writing Windows drivers in Rust so they can take advantage of these benefits when working with the Windows kernel.  In response, the Windows team has been working to create a framework for writing and deploying drivers in Rust.  By building off the terrific efforts of the Surface team, we’re starting our journey to make Rust a first-class language for driver developers around the world.

This blog post will cover the current state of Rust for Windows Drivers and discuss our vision for the future.

windows-drivers-rs and the WDK environment

Driver developers are used to writing drivers in C by linking to the libraries included in the Windows Driver Kit (WDK).  Our goal is to ensure that Rust developers can take advantage of the same libraries and headers in the WDK as C developers, granting them access to all the DDIs needed to write a Windows driver.

windows-drivers-rs is the primary Git repo for enabling developers to begin using Rust with the WDK.  Pioneered by Surface, this repo contributes the following crates to the ecosystem:

  • wdk-build: A library to configure a Cargo build script that performs downstream linking of the WDK and generates Rust bindings.
  • wdk-sys: Direct FFI bindings to APIs available in the WDK. This includes both autogenerated ffi bindings from bindgen, and also manual re-implementations of macros that bindgen fails to generate.
  • wdk: Safe idiomatic bindings to APIs available in the WDK.
  • wdk-panic: Default panic handler implementations for programs built with WDK
  • wdk-alloc: A global allocator for use by drivers.
  • wdk-macros: A collection of macros that help make it easier to interact with wdk-sys's direct bindings. This crate is re-exported via wdk-sys and crates should typically never need to directly depend on wdk-macros.
  • cargo-wdk: A cargo tool to make setting up a Rust driver environment simpler.

Using these crates, driver developers can create valid WDM, KMDF, and UMDF driver binaries that load and run on a Windows 11 machine.  For example, this is what a driver entry routine looks like in our KMDF sample:

/// `DriverEntry` initializes the driver and is the first routine called by the
/// system after the driver is loaded. `DriverEntry` specifies the other entry
/// points in the function driver, such as `EvtDevice` and `DriverUnload`.
///
/// # Arguments
///
/// * `driver` - represents the instance of the function driver that is loaded
///   into memory. `DriverEntry` must initialize members of `DriverObject`
///   before it returns to the caller. `DriverObject` is allocated by the system
///   before the driver is loaded, and it is released by the system after the
///   system unloads the function driver from memory.
/// * `registry_path` - represents the driver specific path in the Registry. The
///   function driver can use the path to store driver related data between
///   reboots. The path does not store hardware instance specific data.
///
/// # Return value:
///
/// * `STATUS_SUCCESS` - if successful,
/// * `STATUS_UNSUCCESSFUL` - otherwise.
#[link_section = "INIT"]
#[export_name = "DriverEntry"] // WDF expects a symbol with the name DriverEntry
extern "system" fn driver_entry(
    driver: &mut DRIVER_OBJECT,
    registry_path: PCUNICODE_STRING,
) -> NTSTATUS {
    let mut driver_config = WDF_DRIVER_CONFIG {
        Size: WDF_DRIVER_CONFIG_SIZE,
        EvtDriverDeviceAdd: Some(echo_evt_device_add),
        ..WDF_DRIVER_CONFIG::default()
    };
    let driver_handle_output = WDF_NO_HANDLE.cast::<WDFDRIVER>();

    let nt_status = unsafe {
        call_unsafe_wdf_function_binding!(
            WdfDriverCreate,
            driver as PDRIVER_OBJECT,
            registry_path,
            WDF_NO_OBJECT_ATTRIBUTES,
            &raw mut driver_config,
            driver_handle_output,
        )
    };

    if !nt_success(nt_status) {
        println!("Error: WdfDriverCreate failed {nt_status:#010X}");
        return nt_status;
    }

    echo_print_driver_version();

    nt_status
}

Drivers written in this manner still need to make use of unsafe blocks for interacting with the Windows operating system, but can take advantage of Rust’s rich type system as well as its safety guarantees for business logic implemented in safe Rust.  Though there is still significant work to be done on abstracting away these unsafe blocks (more on this below), these Rust drivers can load and run on Windows systems just like their C counterparts.

Introducing cargo-wdk

Traditionally, drivers developed in C have access to various templates in Visual Studio to get up and running quickly.  To help make Rust driver development similarly convenient, the Rust for Drivers team has developed cargo-wdk.  This cargo extension enables developers to create empty driver projects with all the necessary linkage, build steps, and dependencies pre-populated.  It also is capable of calling WDK tools like InfVerif, much as would be done when building a driver in C in Visual Studio.  The animation below demonstrates the process:

The above GIF demonstrates a basic cargo-wdk workload. The developer can create a KMDF driver template by calling "cargo wdk new --kmdf". By then calling "cargo wdk build", cargo-wdk invokes INFVerif on the driver and surfaces any errors, as it would in Visual Studio. The developer then edits the INF appropriately, runs cargo-wdk again, and sees that the driver is built alongside the appropriate certificates.

Our long-term goal for cargo-wdk is to enable a developer working in a cargo environment to have access to all the same build tools and configuration options as a developer working in Visual Studio. Some of our medium-term feature plans include:

  • Automatic installation of dependencies such as the WDK itself
  • NT_TARGET_VERSION support
  • Additional driver templates
  • Full ARM64 support
  • Increased configurability of build and package steps
  • Driver deployment to test machines

The next steps: going from unsafe Rust to safe Rust

Today, windows-drivers-rs provides all the building blocks you need to write some kinds of drivers in Rust, but it still requires writing significant amounts of unsafe Rust.  Although unsafe Rust still provides benefits such as strong type-checking, zero-cost abstractions, and good FFI compatibility, we recognize that many developers are seeking an experience where the majority of driver code can be written in safe Rust.  We also believe that Rust’s type system and borrow checker can be used to prevent certain classes of bugs that could previously only be caught via additional static analysis.  As such, we have ongoing efforts to expand the scope of windows-drivers-rs  and create safe abstractions for kernel-mode structs and DDIs:

  • For many device classes in Windows, drivers are written using the Windows Driver Framework (WDF). This covers both kernel-mode drivers (KMDF) and user-mode drivers (UMDF). The WDF team is working on designing safe Rust abstractions in consultation with Rust experts. The goal is to design interfaces that let developers write more idiomatic Rust code. By providing safe Rust bindings and abstractions, developers can take advantage of the language’s safety and reliability features to write more secure and reliable code. This should greatly reduce the amount of unsafe code blocks required and provide additional compile time safety.
  • Other teams within Microsoft are working to develop safe structs and APIs beyond WDF to be used in both core Windows kernel development and in third-party driver development. Some of this work ships today in the Windows kernel. 

Due to the complexity and generic nature of the Windows kernel APIs, we expect this design process to take time as we work across multiple teams to find safe, sound ways to expose the Windows API surface.  This means that the first wrapper we develop is not always going to be the final wrapper we ship.  As an example, here is one team’s experimental wrapper for LookasideList structs in use today internally:

impl<T> LookasideList<T> {
    /// Creates a new lookaside list for type `T`.
    ///
    /// # Arguments
    /// * `pool_type` - The type of memory pool (e.g., NonPagedPoolNx).
    /// * `tag` - A four-byte tag used for memory tracking.
    ///
    /// # Returns
    /// * `Ok(Arc<Self>)` if initialization succeeds.
    /// * `Err(status)` if initialization fails.
    pub fn new(pool_type: POOL_TYPE, tag: u32) -> Result<Arc<Self>> {
        if size_of::<T>() == 0 {
            return Err(STATUS_NOT_SUPPORTED);
        }

        // Allocate uninitialized memory for LookasideList<T>, but use MaybeUninit<T> correctly.
        let arc = Arc::new(MaybeUninit::<LookasideList<T>>::uninit())?;

        // SAFETY: Initialize the LookasideList<T> in place.
        let status = unsafe {
            ExInitializeLookasideListEx(
                (*arc.as_ptr()).assume_init_ref().inner.get(),
                None,
                None,
                pool_type,
                0,
                size_of::<T>() as SIZE_T,
                tag,
                0,
            )
        };

        if !NT_SUCCESS(status) {
            return Err(status);
        }

        // SAFETY: Now the MaybeUninit<T> is fully initialized, so we can assume initialization safely.
        Ok(unsafe { arc.assume_init() })
    }

    /// Allocates a new instance of `T` from the lookaside list.
    ///
    /// # Returns
    /// * `Ok(ptr)` if allocation succeeds.
    /// * `Err(STATUS_NO_MEMORY)` if allocation fails.
    fn allocate(&self) -> Result<*mut T> {
        // SAFETY: The inner pointer is valid and pinned, so it won't move. Lookaside list is internally synchronized.
        let ptr = unsafe { ExAllocateFromLookasideListEx(self.inner.get()) } as *mut T;

        if ptr.is_null() {
            Err(STATUS_NO_MEMORY)
        } else {
            Ok(ptr)
        }
    }

    /// Frees a previously allocated instance of `T` back to the lookaside list.
    ///
    /// The pointer must have been allocated from this lookaside list.
    ///
    /// # Safety
    ///
    /// `ptr` must come from a previous call to `LookasideList::allocate`, it hasn't been freed yet and won't be used anymore.
    unsafe fn free(&self, ptr: *mut T) {
        // The inner pointer is valid and pinned, so it won't move. Lookaside list is internally synchronized.
        // Cast to *mut _ is because ExFreeToLookasideListEx expects a void *.
        ExFreeToLookasideListEx(self.inner.get(), ptr as *mut _);
    }
}

impl<T> Drop for LookasideList<T> {
    /// Cleans up the lookaside list when it goes out of scope.
    fn drop(&mut self) {
        // SAFETY: The lookaside list was initialized on new.
        unsafe { ExDeleteLookasideListEx(self.inner.get()) };
    }
}

Though we believe this wrapper to be sound for the purposes of the team that developed it, it requires further review and testing before we can publish it as the “official” wrapper for these APIs.  Thus the above should be considered a possible look at what Rust abstractions for our kernel mode might look like, and not final code.

In the long term, as we make design decisions and finalize our wrappers, our intent is to publish these wrapper crates on crates.io as first-class members of the Rust ecosystem.

Our long-term vision

We believe that memory-safe languages such as Rust represent the future of secure software engineering.  Today’s security landscape demands reliability and safety guarantees at every surface from the edge to the cloud, and device drivers play a role at every layer in the stack.  At the same time, we recognize that there are many driver models and scenarios that Windows needs to support, and it will take time to fully develop Rust bindings and tooling for all of them.

We encourage you to keep an eye on this blog and our windows-drivers-rs GitHub repo for the latest information and tools involved in Rust development for the Windows kernel.  Over the coming months, we will publish more information about best practices for using Rust in your driver, submitting Rust drivers to WHCP, using CodeQL for static analysis, and more.

We look forward to advancing the state of the art in memory safety with you all!

 

Updated Sep 02, 2025
Version 1.0
No CommentsBe the first to comment