Real Hardware, Finally: Rust PHY Drivers in the Kernel
I swear, if I had to read one more mailing list thread back in ’23 about how Rust was going to “complicate the build system” without delivering actual drivers, I was going to switch to BSD. I’m not even joking.
For the longest time, the “Rust in Linux” experiment felt like a massive academic exercise. Lots of infrastructure, lots of debating alloc failures, and a whole lot of “Hello World” modules that didn’t actually talk to hardware. But looking at the tree today, specifically the networking subsystem, I have to eat my words. A little bit.
We finally have real, boring, functional PHY drivers written in Rust. And they are… fine. Actually, they’re better than fine.
The PHY Abstraction is actually readable
I’ve spent way too many nights debugging C PHY drivers where someone copy-pasted a struct definition and forgot to update the mask for the status register. It happens. You stare at hex dumps until your eyes bleed.
The recent batch of Rust modules—specifically the abstractions landing in drivers/net/phy—changes the workflow. It’s not just about memory safety (which everyone won’t shut up about). It’s about the API forcing you to implement the driver correctly.
Take a look at how we used to do this in C. You’d set up a phy_driver struct, populate function pointers, and hope you didn’t mix up the suspend/resume signatures or return the wrong error code convention.
Here is what it looks like now with the Rust abstractions. This isn’t pseudocode; this is roughly what I was playing with last Tuesday on a test board.
use kernel::net::phy::{self, DeviceId, Driver};
use kernel::prelude::*;
kernel::module_phy_driver! {
drivers: [MyRustPhy],
device_table: [
DeviceId::new_with_driver::<MyRustPhy>(),
],
name: "my_rust_phy",
author: "Kernel Hacker <hack@kernel.org>",
description: "Rust PHY driver example",
license: "GPL",
}
struct MyRustPhy;
#[vtable]
impl Driver for MyRustPhy {
const NAME: &'static CStr = c_str!("Rust PHY");
const PHY_ID: u32 = 0x00112233;
const PHY_ID_MASK: u32 = 0xffffffff;
fn probe(_dev: &mut phy::Device) -> Result<()> {
pr_info!("Rust PHY probed successfully\n");
Ok(())
}
fn config_init(dev: &mut phy::Device) -> Result<()> {
// No raw pointer arithmetic here.
// The API wraps the C functions safely.
dev.genphy_soft_reset()?;
Ok(())
}
fn read_status(dev: &mut phy::Device) -> Result<u16> {
// Direct register access, but checked
let val = dev.read(0x10)?;
Ok(val)
}
}
See that #[vtable] attribute? That’s doing so much heavy lifting. It ensures at compile time that I’m matching the kernel’s expected interface for a PHY driver. If I mess up the signature of config_init, the compiler yells at me immediately. It doesn’t wait for me to boot the kernel and watch it panic because I dereferenced a void pointer broadly.
Why PHYs?
It makes sense that PHYs (Physical Layer drivers) are the beachhead here. They are relatively self-contained. They talk over MDIO (mostly), they have a standardized state machine, and they don’t usually require complex DMA mapping or high-performance interrupt handling that the Rust abstractions are still ironing out.
I was skeptical when the Asix PHY driver was first discussed as a candidate for rewrite. Why fix what isn’t broken? But the reality is, a lot of these old C drivers are brittle. They rely on implicit state management that is documented nowhere except in the head of a maintainer who retired four years ago.
By moving them to Rust, we aren’t just changing languages. We are documenting the hardware behavior through the type system. If the hardware requires a lock to access a specific register page, the Rust struct can own that Mutex. You literally cannot access the register without taking the lock. In C, that’s a comment: /* MUST HOLD LOCK HERE */. We all know how well people read comments.
The “Tax” is Still There
Look, I’m not going to sit here and pretend it’s all sunshine. Compiling the kernel with Rust support enabled still takes longer. The toolchain versioning dance is better than it was in 2024, but it’s still annoying. I had to update my rustc version twice last month just to keep up with linux-next.
And then there’s the mental shift. I tried to port a slightly more complex driver last weekend—one involving some weird interrupt handling—and I hit a wall. The abstractions just aren’t there yet for everything. You end up writing so much unsafe glue code to talk to C functions that you wonder if it’s worth the effort.
But for these PHY modules? It’s a clean win. The code is smaller (lines of code dropped by like 15% in the one I looked at), and the logic is linear.
What this signals for 2026
We are seeing a pattern. The kernel team is picking specific subsystems—GPIO, PHYs, maybe simple input devices next—and building rock-solid abstractions for them. They aren’t trying to rewrite the scheduler or the memory management subsystem (thank god).
This “pair of modules” news we saw recently isn’t a floodgate opening. It’s a proof of stability. It means the infrastructure is boring enough to be used for real work. And honestly? I like boring.
If you are a driver developer, you should probably stop rolling your eyes at Rust. You don’t have to rewrite your 10-year-old PCI driver yet. But if you’re adding support for a new Ethernet PHY? You might actually want to check out the Rust bindings. It might save you a weekend of debugging race conditions.
Just don’t tell the mailing list I said that. I have a reputation to maintain.
