Why Writing Device Drivers in Rust Changes Everything
There’s a quiet revolution happening in the kernel space — and it’s written in Rust. For decades, device drivers have been the most crash-prone, security-sensitive, and soul-draining part of system software. A small mistake in a pointer dereference or a missing free() call could bring down an entire system. C and C++ gave us speed and control — but at a brutal cost: undefined behavior. Then came Rust — and suddenly, the idea of writing safe, fast, memory-efficient drivers didn’t sound like a fantasy anymore. Let’s dive deep into how Rust is changing the game at the hardware boundary, what’s really happening under the hood, and why even Linux — the temple of C — has started letting Rust in. The Core Idea: Safety Without Sacrifice Writing device drivers is like walking a tightrope above a volcano. You deal with:
- Raw memory (MMIO registers, DMA buffers)
- Interrupts and race conditions
- Concurrency across threads and hardware
- Strict timing and real-time constraints
Traditionally, the only language trusted for this job was C. But here’s the thing: C trusts you back. It assumes you’ll never misuse a pointer, never race two threads, never access freed memory. Rust doesn’t make that assumption. Its ownership model and borrow checker make sure that at compile time, the same bugs that plagued decades of C drivers simply cannot compile. A Minimal Rust Driver Example Let’s look at how a toy Rust driver might interact with memory-mapped I/O (MMIO) registers. use core::ptr::{read_volatile, write_volatile};
const DEVICE_BASE: usize = 0x1000_0000; const REG_STATUS: usize = DEVICE_BASE + 0x00; const REG_CONTROL: usize = DEVICE_BASE + 0x04; pub struct MyDevice; impl MyDevice {
pub fn read_status(&self) -> u32 {
unsafe { read_volatile(REG_STATUS as *const u32) }
}
pub fn write_control(&self, value: u32) {
unsafe { write_volatile(REG_CONTROL as *mut u32, value) }
}
} Even here, the unsafe blocks are explicit — not accidental. Rust forces you to acknowledge when you’re doing something risky. That’s the big difference: Rust doesn’t remove low-level power — it just makes unsafe code visible and containable. Architecture of a Rust Driver A modern Rust-based driver typically follows this layered architecture: ┌──────────────────────────────┐ │ User Space Interface │ ← sysfs, ioctls, etc. ├──────────────────────────────┤ │ Safe Rust Abstractions │ ← typed wrappers for registers, DMA, buffers ├──────────────────────────────┤ │ Unsafe HAL Layer │ ← raw MMIO, interrupts, hardware setup ├──────────────────────────────┤ │ Kernel Integration Layer │ ← Linux driver APIs or RTOS hooks └──────────────────────────────┘ Code Flow Example
- User process sends a command via /dev/my_device.
- Kernel calls into Rust driver entry point.
- Safe layer decodes command → calls into HAL.
- HAL performs memory-mapped write using controlled unsafe.
- Response bubbles back through safe abstractions to user.
Rust ensures that only one layer is truly unsafe — the HAL. Everything above it benefits from strong typing and ownership rules. Real Reason Behind Rust in Kernel Space Why did Linux — the most conservative kernel project — agree to include Rust drivers? Because C’s safety debt has become unsustainable. Here’s what the Linux Foundation’s security team found: ~70% of kernel vulnerabilities trace back to memory safety issues. That’s not about logic errors or bad design — that’s about pointer mistakes. Rust eliminates those entirely for most driver logic. Miguel Ojeda, the engineer leading the Rust-for-Linux project, put it best: “Rust can give kernel developers memory safety by default, while still letting them go low-level when they need to.” Example: Writing a Rust Driver for a UART Controller Here’s a simplified Rust UART driver snippet: pub struct Uart {
base_addr: *mut u8,
}
impl Uart {
pub fn new(base: usize) -> Self {
Self { base_addr: base as *mut u8 }
}
pub fn write_byte(&self, byte: u8) {
unsafe {
core::ptr::write_volatile(self.base_addr, byte);
}
}
pub fn read_byte(&self) -> u8 {
unsafe { core::ptr::read_volatile(self.base_addr) }
}
} Now compare that with its C equivalent — 20 lines shorter but 10x riskier. One null pointer or concurrent access, and the system could crash. In Rust, these risks are isolated and explicit — and can be further wrapped in safe abstractions. Diagram: Rust vs. C Driver Safety Boundaries C Driver: +-----------------------------------+ | Application Logic ⚠️ Unsafe Zone | +-----------------------------------+ | Hardware Access ⚠️ Unsafe Zone | +-----------------------------------+ Rust Driver: +-----------------------------------+ | Application Logic ✅ Safe | +-----------------------------------+ | HAL Layer ⚠️ Explicitly Unsafe | +-----------------------------------+ Rust confines unsafe to small, audited boundaries — rather than letting it leak everywhere. How It Actually Works Under the Hood When you write a Rust driver:
- No standard library: You’re compiling with #![no_std].
- Custom allocators: Drivers can’t rely on heap allocation, so you use alloc or fixed buffers.
- No OS dependencies: Drivers often compile to target_os = "none".
- Linked with kernel symbols: For Linux, Rust’s bindgen generates bindings to existing kernel C APIs
The Rust compiler then produces object files just like C, which the kernel build system links as .o modules. Real-World Use Cases
- Google’s Android team is already writing Rust HAL components.
- Microsoft built Rust drivers for Windows to replace C++ ones.
- Linux mainline kernel (v6.1+) now supports Rust driver modules.
- ESP32 and Raspberry Pi Pico have embedded Rust HAL crates (embedded-hal, rp2040-hal) in production.
These aren’t experiments — they’re production hardware drivers. The Emotional Side: The First Time I Trusted a Driver When I wrote my first driver in Rust for an embedded SPI peripheral, I did something I’d never done before: I slept peacefully. No memory leaks. No dangling pointers. No random kernel panics at 2 a.m. after a long compile. Just deterministic behavior — in a system that talks directly to metal. That’s the psychological gift Rust gives to systems developers: fearless low-level programming. The Tradeoffs Rust isn’t a silver bullet.
- Compile times are longer.
- Tooling integration with Linux is still maturing.
- You still need unsafe for hardware I/O.
But the payoff — memory safety and long-term reliability — is worth every second. The Architecture of the Future The future of drivers looks like this: ┌────────────────────────────┐ │ Safe Rust Device Logic │ ← ownership, lifetimes, no UB ├────────────────────────────┤ │ Unsafe HAL (audited) │ ← tightly scoped ├────────────────────────────┤ │ OS Integration Layer │ ← Linux, RTOS, etc. └────────────────────────────┘ The entire ecosystem — from embedded to desktop — is slowly converging toward this model. Final Thoughts Rust didn’t invent safe systems programming — it just proved it’s possible. Writing drivers in Rust doesn’t just reduce crashes; it redefines what “safe low-level programming” means. It shows that performance and reliability don’t have to fight each other. If C was the language that built the hardware world, Rust might just be the one that saves it. Rust isn’t just another language for writing drivers — it’s a cultural shift. It makes “safe device driver” stop sounding like an oxymoron.
Read the full article here: https://medium.com/@theopinionatedev/why-writing-device-drivers-in-rust-changes-everything-a59853dbd236
