Chapter 26: Rust in Firmware

Introduction

Firmware is among the most security-sensitive code on a computing platform. It runs before the operating system, with full hardware access and no memory protection. Yet the vast majority of firmware is written in C – a language where a single buffer overflow, use-after-free, or null pointer dereference can compromise the entire system.

Rust offers a compelling alternative. With its ownership model, borrow checker, and type system, Rust eliminates entire classes of memory safety bugs at compile time – without the overhead of garbage collection. Project Mu has been at the forefront of integrating Rust into the UEFI firmware ecosystem, enabling developers to write firmware modules in Rust alongside existing C/EDK2 code.

This chapter covers the motivation for Rust in firmware, how to set up the Rust toolchain for UEFI development, writing UEFI modules in Rust, interoperating with existing C code through FFI, and building Rust modules within the Project Mu build system.


Why Rust for Firmware?

Memory Safety Without Garbage Collection

Firmware operates in a constrained environment where garbage collection is not feasible. Rust provides memory safety through compile-time ownership analysis rather than runtime garbage collection:

graph LR
    subgraph C["C Language"]
        CM[Manual Memory<br/>Management] --> CB[Buffer Overflows]
        CM --> CU[Use-After-Free]
        CM --> CD[Double Free]
        CM --> CN[Null Dereference]
    end

    subgraph Rust["Rust Language"]
        RO[Ownership +<br/>Borrow Checker] --> RS[Compile-Time<br/>Safety Guarantees]
        RS --> RN[No Buffer Overflows]
        RS --> RU[No Use-After-Free]
        RS --> RD[No Double Free]
        RS --> RNP[No Null Dereference<br/>via Option type]
    end

The Cost of C in Firmware

Microsoft has reported that approximately 70% of CVEs in their products are memory safety issues. Firmware is no exception. Common firmware vulnerabilities include:

  • Buffer overflows in SMM handlers: Can allow privilege escalation to System Management Mode
  • Use-after-free in DXE drivers: Can allow boot-time code injection
  • Integer overflows in protocol implementations: Can lead to incorrect memory allocations
  • Format string vulnerabilities in debug output: Can leak or corrupt memory

What Rust Brings to Firmware

Property Benefit for Firmware
Ownership model Prevents use-after-free and double-free at compile time
Borrow checker Ensures references are always valid; no dangling pointers
No null pointers Option<T> forces explicit handling of absent values
Array bounds checking Prevents buffer overflows (with zero-cost abstractions for known-size arrays)
Type safety Prevents type confusion bugs common in C void pointer usage
Pattern matching Exhaustive matching ensures all cases are handled
No undefined behavior Safe Rust has no undefined behavior by definition
Zero-cost abstractions High-level safety without runtime overhead

Project Mu’s Rust Integration

Current State

Project Mu has been building Rust support incrementally. The integration includes:

  • Build system support: Stuart can build Rust modules alongside C modules
  • UEFI target support: Rust’s x86_64-unknown-uefi target produces PE32+ binaries compatible with the UEFI loader
  • Core library availability: core and alloc crates work in the UEFI no_std environment
  • FFI interoperability: Rust modules can call into and be called from C/EDK2 code
  • Base crate ecosystem: Foundational crates for UEFI types, protocols, and services

Architecture

graph TB
    subgraph Build["Build System"]
        Stuart["Stuart Build"]
        Cargo["Cargo (Rust)"]
        Stuart --> Cargo
    end

    subgraph RustModules["Rust UEFI Modules"]
        DxeRust["DXE Driver<br/>(Rust)"]
        LibRust["Library<br/>(Rust)"]
    end

    subgraph CModules["C / EDK2 Modules"]
        DxeC["DXE Driver<br/>(C)"]
        LibC["Library<br/>(C)"]
    end

    subgraph Core["Core Infrastructure"]
        UefiCrate["r-efi / uefi-rs<br/>Crate"]
        FFIBindings["FFI Bindings"]
        AllocImpl["UEFI Allocator<br/>Implementation"]
    end

    Build --> RustModules
    Build --> CModules
    RustModules --> Core
    CModules --> FFIBindings
    FFIBindings --> RustModules
    DxeRust --> UefiCrate
    DxeRust --> AllocImpl

Setting Up the Rust Toolchain for UEFI

Prerequisites

Rust UEFI development historically required the nightly toolchain because the UEFI targets and certain features (abi_efiapi, allocator APIs) were not yet stabilized.

Update (Rust 1.86+): The extern "efiapi" calling convention was stabilized in Rust 1.86 (early 2025), so nightly is no longer required solely for efiapi. However, nightly may still be needed for other features such as the UEFI targets themselves, build-std, or the global allocator API, depending on your toolchain version.

# Install Rust via rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install the nightly toolchain
rustup toolchain install nightly

# Add the UEFI target
rustup target add x86_64-unknown-uefi --toolchain nightly

# For AArch64 UEFI targets
rustup target add aarch64-unknown-uefi --toolchain nightly

# Install useful components
rustup component add rust-src --toolchain nightly
rustup component add clippy --toolchain nightly
rustup component add rustfmt --toolchain nightly

Verifying the Setup

# Confirm the UEFI target is available
rustup target list --installed --toolchain nightly
# Should show: x86_64-unknown-uefi

# Verify cargo can create a project
cargo +nightly new --lib my_uefi_module

Cargo Configuration for UEFI

Create a .cargo/config.toml in your project:

[build]
target = "x86_64-unknown-uefi"

[unstable]
build-std = ["core", "compiler_builtins", "alloc"]
build-std-features = ["compiler-builtins-mem"]

[target.x86_64-unknown-uefi]
runner = "qemu-system-x86_64 -drive if=pflash,format=raw,file=OVMF.fd -drive format=raw,file=fat:rw:esp"

The build-std option is important: it tells Cargo to recompile the standard library crates (core, alloc) for the UEFI target, since pre-compiled versions are not shipped.


Writing a UEFI Module in Rust

Minimal UEFI Application

Here is a complete, minimal UEFI application written in Rust:

// src/main.rs
#![no_main]
#![no_std]

use core::fmt::Write;
use core::panic::PanicInfo;
use r_efi::efi;

// Panic handler -- required in no_std environments
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

// UEFI application entry point
#[export_name = "efi_main"]
pub extern "efiapi" fn efi_main(
    image_handle: efi::Handle,
    system_table: *mut efi::SystemTable,
) -> efi::Status {
    // Safety: system_table is provided by the UEFI firmware and is valid
    let st = unsafe { &mut *system_table };
    let con_out = unsafe { &mut *st.con_out };

    // Print "Hello from Rust!" to the console
    let hello = "Hello from Rust UEFI!\r\n";
    for c in hello.encode_utf16() {
        let chars = [c, 0u16];
        unsafe {
            (con_out.output_string)(con_out, chars.as_ptr() as *mut efi::Char16);
        }
    }

    efi::Status::SUCCESS
}

Cargo.toml

[package]
name = "hello-uefi"
version = "0.1.0"
edition = "2021"

[dependencies]
r-efi = "4"              # Use major version to get latest 4.x; check crates.io for updates

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"
opt-level = "z"    # Optimize for size
lto = true          # Link-time optimization

Building and Running

# Build the UEFI application
cargo +nightly build --target x86_64-unknown-uefi

# The output is at target/x86_64-unknown-uefi/debug/hello-uefi.efi

# Set up an ESP directory structure for QEMU
mkdir -p esp/EFI/BOOT
cp target/x86_64-unknown-uefi/debug/hello-uefi.efi esp/EFI/BOOT/BOOTX64.EFI

# Run in QEMU
qemu-system-x86_64 \
    -drive if=pflash,format=raw,readonly=on,file=OVMF_CODE.fd \
    -drive if=pflash,format=raw,file=OVMF_VARS.fd \
    -drive format=raw,file=fat:rw:esp \
    -nographic

A More Complete UEFI Driver

DXE Driver in Rust

A DXE driver that installs a custom protocol:

#![no_main]
#![no_std]

extern crate alloc;

use alloc::boxed::Box;
use core::panic::PanicInfo;
use r_efi::efi;

// Custom protocol GUID
const MY_PROTOCOL_GUID: efi::Guid = efi::Guid::from_fields(
    0x12345678, 0xABCD, 0xEF01,
    0x23, 0x45, &[0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01],
);

// Protocol interface structure
#[repr(C)]
struct MyProtocol {
    revision: u64,
    get_value: unsafe extern "efiapi" fn(
        this: *mut MyProtocol,
        value: *mut u32,
    ) -> efi::Status,
}

// Protocol method implementation
unsafe extern "efiapi" fn my_get_value(
    _this: *mut MyProtocol,
    value: *mut u32,
) -> efi::Status {
    if value.is_null() {
        return efi::Status::INVALID_PARAMETER;
    }
    unsafe {
        *value = 42;
    }
    efi::Status::SUCCESS
}

// Global boot services pointer (set during entry)
static mut BOOT_SERVICES: *mut efi::BootServices = core::ptr::null_mut();

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

#[export_name = "efi_main"]
pub extern "efiapi" fn efi_main(
    image_handle: efi::Handle,
    system_table: *mut efi::SystemTable,
) -> efi::Status {
    let st = unsafe { &*system_table };
    let bs = unsafe { &*st.boot_services };

    // Store boot services for later use
    unsafe {
        BOOT_SERVICES = st.boot_services;
    }

    // Create the protocol instance
    let protocol = Box::new(MyProtocol {
        revision: 1,
        get_value: my_get_value,
    });

    // Leak the box -- firmware protocols must live for the lifetime of the driver
    let protocol_ptr = Box::into_raw(protocol);

    // Install the protocol on a new handle
    let mut handle: efi::Handle = core::ptr::null_mut();
    let status = unsafe {
        (bs.install_protocol_interface)(
            &mut handle,
            &MY_PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid,
            efi::NATIVE_INTERFACE,
            protocol_ptr as *mut core::ffi::c_void,
        )
    };

    status
}

Rust-C FFI for EDK2 Interop

Calling C from Rust

Existing EDK2 libraries and protocols can be called from Rust through FFI declarations:

// Declare external C functions from EDK2
extern "C" {
    /// DebugPrint from DebugLib
    fn DebugPrint(error_level: usize, format: *const u8, ...);

    /// AllocatePool from MemoryAllocationLib
    fn AllocatePool(size: usize) -> *mut u8;

    /// FreePool from MemoryAllocationLib
    fn FreePool(buffer: *mut u8);

    /// CopyMem from BaseMemoryLib
    fn CopyMem(dest: *mut u8, src: *const u8, length: usize);
}

// Safe wrapper around AllocatePool/FreePool
pub struct UefiBuffer {
    ptr: *mut u8,
    size: usize,
}

impl UefiBuffer {
    pub fn new(size: usize) -> Option<Self> {
        let ptr = unsafe { AllocatePool(size) };
        if ptr.is_null() {
            None
        } else {
            Some(UefiBuffer { ptr, size })
        }
    }

    pub fn as_slice(&self) -> &[u8] {
        unsafe { core::slice::from_raw_parts(self.ptr, self.size) }
    }

    pub fn as_mut_slice(&mut self) -> &mut [u8] {
        unsafe { core::slice::from_raw_parts_mut(self.ptr, self.size) }
    }
}

impl Drop for UefiBuffer {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { FreePool(self.ptr) };
        }
    }
}

Calling Rust from C

To expose Rust functions to C code:

/// A function callable from C
///
/// # Safety
/// `buffer` must point to at least `buffer_size` bytes of valid memory.
#[no_mangle]
pub unsafe extern "C" fn RustValidateBuffer(
    buffer: *const u8,
    buffer_size: usize,
) -> u32 {
    if buffer.is_null() || buffer_size == 0 {
        return 1; // Error: invalid parameter
    }

    let data = unsafe { core::slice::from_raw_parts(buffer, buffer_size) };

    // Perform validation using safe Rust
    if validate_contents(data) {
        0 // Success
    } else {
        2 // Error: validation failed
    }
}

fn validate_contents(data: &[u8]) -> bool {
    // Safe Rust code with bounds checking, no buffer overflows possible
    if data.len() < 4 {
        return false;
    }

    let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
    magic == 0x5A4D_4F52 // Expected magic number
}

The corresponding C header:

// RustValidation.h
#ifndef RUST_VALIDATION_H_
#define RUST_VALIDATION_H_

#include <Uefi.h>

/**
    Validate a buffer using Rust implementation.

    @param[in] Buffer       Pointer to the buffer to validate.
    @param[in] BufferSize   Size of the buffer in bytes.

    @retval 0   Validation successful.
    @retval 1   Invalid parameter.
    @retval 2   Validation failed.
**/
UINT32
EFIAPI
RustValidateBuffer (
    IN CONST UINT8  *Buffer,
    IN UINTN        BufferSize
);

#endif

Using core and alloc in UEFI

The no_std Environment

UEFI applications run in a no_std environment, meaning the Rust standard library (std) is not available. However, two foundational crates are available:

  • core: Provides fundamental types (Option, Result, slice, and others), traits, and utilities that require no OS support
  • alloc: Provides heap allocation types (Box, Vec, String, BTreeMap) given a global allocator

Implementing a UEFI Allocator

To use alloc, you must provide a global allocator that uses UEFI’s memory allocation services:

use core::alloc::{GlobalAlloc, Layout};
use r_efi::efi;

struct UefiAllocator;

unsafe impl GlobalAlloc for UefiAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let bs = unsafe { &*BOOT_SERVICES };
        let mut ptr: *mut core::ffi::c_void = core::ptr::null_mut();

        // UEFI AllocatePool guarantees 8-byte alignment.
        // For larger alignments, we over-allocate to make room for alignment
        // padding and to store the original pointer within the allocated block
        // (writing before the allocated region would corrupt UEFI pool headers).
        let ptr_size = core::mem::size_of::<*mut u8>();
        let (alloc_size, needs_alignment) = if layout.align() <= 8 {
            (layout.size(), false)
        } else {
            // Extra space: alignment padding + room to store original pointer
            (layout.size() + layout.align() + ptr_size, true)
        };

        let status = unsafe {
            (bs.allocate_pool)(
                efi::LOADER_DATA,
                alloc_size,
                &mut ptr,
            )
        };

        if status != efi::Status::SUCCESS || ptr.is_null() {
            return core::ptr::null_mut();
        }

        if !needs_alignment {
            ptr as *mut u8
        } else {
            let raw = ptr as usize;
            // Reserve space at the start of the block to store the original pointer,
            // then align the address after that storage area
            let after_storage = raw + ptr_size;
            let aligned = (after_storage + layout.align() - 1) & !(layout.align() - 1);
            // Store the original pointer immediately before the aligned address
            // (this location is guaranteed to be within our allocated block)
            unsafe {
                *((aligned - ptr_size) as *mut usize) = raw;
            }
            aligned as *mut u8
        }
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        let bs = unsafe { &*BOOT_SERVICES };
        let actual_ptr = if layout.align() <= 8 {
            ptr as *mut core::ffi::c_void
        } else {
            let raw = unsafe {
                *((ptr as usize - core::mem::size_of::<usize>()) as *const usize)
            };
            raw as *mut core::ffi::c_void
        };
        unsafe {
            (bs.free_pool)(actual_ptr);
        }
    }
}

#[global_allocator]
static ALLOCATOR: UefiAllocator = UefiAllocator;

With this allocator in place, you can use Vec, String, Box, and other heap-allocated types:

extern crate alloc;

use alloc::vec::Vec;
use alloc::string::String;
use alloc::format;

fn process_data() -> Vec<u8> {
    let mut buffer = Vec::with_capacity(256);
    buffer.extend_from_slice(b"UEFI data processed by Rust");
    buffer
}

fn format_version(major: u16, minor: u16) -> String {
    format!("v{}.{}", major, minor)
}

Building Rust Modules with Stuart

Integration with the EDK2 Build System

Project Mu’s build system (Stuart) integrates Rust compilation through custom build plugins. The general approach:

  1. Rust code lives in a subdirectory of the EDK2 module
  2. A Cargo.toml defines the Rust crate
  3. The INF file references the Rust build output as a binary or library
  4. Stuart invokes Cargo during the build process

Module Directory Structure

MyPlatformPkg/
  RustModule/
    RustModule.inf          # EDK2 module description
    RustModule.c            # Thin C wrapper (if needed)
    rust/
      Cargo.toml            # Rust crate definition
      src/
        lib.rs              # Rust library code
      .cargo/
        config.toml         # Cargo configuration for UEFI target

INF File with Rust Integration

[Defines]
    INF_VERSION    = 0x00010017
    BASE_NAME      = RustModule
    FILE_GUID      = AABBCCDD-1122-3344-5566-778899AABBCC
    MODULE_TYPE    = DXE_DRIVER
    VERSION_STRING = 1.0
    ENTRY_POINT    = RustModuleEntryPoint

[Sources]
    RustModule.c

[Packages]
    MdePkg/MdePkg.dec
    MdeModulePkg/MdeModulePkg.dec

[LibraryClasses]
    UefiDriverEntryPoint
    UefiBootServicesTableLib
    DebugLib

[Depex]
    TRUE

Build Plugin Configuration

In your platform’s stuart configuration, enable Rust support:

# PlatformBuild.py
class PlatformBuilder(UefiBuilder):
    def GetActiveScopes(self):
        scopes = super().GetActiveScopes()
        scopes += ("rust",)
        return scopes

    def GetPackagesPath(self):
        paths = super().GetPackagesPath()
        return paths

Practical Patterns for Rust Firmware

Error Handling

Rust’s Result type maps naturally to UEFI’s EFI_STATUS pattern:

use r_efi::efi;

// Define a firmware-specific error type
#[derive(Debug)]
enum FirmwareError {
    InvalidParameter,
    OutOfResources,
    DeviceError,
    NotFound,
}

impl From<FirmwareError> for efi::Status {
    fn from(e: FirmwareError) -> efi::Status {
        match e {
            FirmwareError::InvalidParameter => efi::Status::INVALID_PARAMETER,
            FirmwareError::OutOfResources => efi::Status::OUT_OF_RESOURCES,
            FirmwareError::DeviceError => efi::Status::DEVICE_ERROR,
            FirmwareError::NotFound => efi::Status::NOT_FOUND,
        }
    }
}

// Use Result for internal logic
fn read_config_value(key: &str) -> Result<u32, FirmwareError> {
    match key {
        "timeout" => Ok(30),
        "retries" => Ok(3),
        _ => Err(FirmwareError::NotFound),
    }
}

// Convert at the FFI boundary
#[no_mangle]
pub extern "efiapi" fn GetConfigValue(
    key: *const u8,
    key_len: usize,
    value: *mut u32,
) -> efi::Status {
    if key.is_null() || value.is_null() {
        return efi::Status::INVALID_PARAMETER;
    }

    let key_slice = unsafe { core::slice::from_raw_parts(key, key_len) };
    let key_str = match core::str::from_utf8(key_slice) {
        Ok(s) => s,
        Err(_) => return efi::Status::INVALID_PARAMETER,
    };

    match read_config_value(key_str) {
        Ok(v) => {
            unsafe { *value = v };
            efi::Status::SUCCESS
        }
        Err(e) => e.into(),
    }
}

Safe Protocol Wrappers

/// Type-safe wrapper around a UEFI protocol handle
pub struct ProtocolHandle<'a, T> {
    interface: &'a T,
    handle: efi::Handle,
}

impl<'a, T> ProtocolHandle<'a, T> {
    /// Locate and open a protocol by GUID
    pub unsafe fn locate(
        bs: &efi::BootServices,
        guid: &efi::Guid,
    ) -> Result<Self, efi::Status> {
        let mut handle: efi::Handle = core::ptr::null_mut();
        // ... locate protocol logic
        todo!()
    }

    pub fn interface(&self) -> &T {
        self.interface
    }
}

Current Limitations and Roadmap

Limitations

Limitation Details
Nightly toolchain required The x86_64-unknown-uefi target is not yet stabilized
Limited crate ecosystem Most crates depend on std and cannot be used in UEFI
Debugging Rust debug info in UEFI is less mature than C/DWARF
Build integration Cargo and EDK2 build systems require glue code to work together
SMM support Writing SMM handlers in Rust requires additional safety analysis
Code size Rust generics can lead to code bloat in size-constrained firmware

Roadmap

The Rust-in-firmware ecosystem is evolving rapidly:

  • Target stabilization: The UEFI targets are on track for stabilization in Rust
  • Standard UEFI crates: Crates like r-efi and uefi-rs continue to mature. r-efi is a thin FFI binding layer that provides raw UEFI type definitions and constants with minimal abstraction, while uefi-rs is a higher-level safe Rust wrapper that offers ergonomic APIs, automatic resource management, and safe abstractions over UEFI services. The code examples in this chapter use r-efi
  • Better build integration: Tighter integration between Cargo and EDK2/Stuart
  • More Rust modules in Project Mu: Gradual migration of security-critical code to Rust
  • Formal verification: Rust’s type system enables easier formal verification of firmware properties
  • Community growth: Increasing interest from the firmware development community

Summary

Rust brings transformative safety improvements to firmware development. By eliminating memory safety bugs at compile time, Rust can prevent the majority of firmware vulnerabilities without sacrificing performance or adding runtime overhead.

Key takeaways:

  • Memory safety without GC makes Rust uniquely suited for firmware environments
  • The x86_64-unknown-uefi target allows Rust to produce UEFI-compatible binaries directly
  • no_std with core and alloc provides a rich programming environment without OS dependencies
  • FFI interoperability allows incremental adoption alongside existing C/EDK2 code
  • Project Mu’s build integration enables Rust modules to coexist in the firmware build
  • Safe wrappers around unsafe FFI boundaries contain the risk to narrow, auditable code sections

Rust is not a silver bullet – it requires learning new patterns and working within the constraints of the no_std environment. But for security-critical firmware code, the compile-time guarantees are well worth the investment.


Next: Chapter 27 - Platform Testing Previous: Chapter 25 - DFCI