16 September 2026
For most of computing history, operating system kernels have been written in C. That is not an accident or a tradition. It is a consequence of C being close to the metal, predictable, portable across architectures, and supported by every compiler toolchain that matters. When you are writing code that manages page tables, interrupt descriptors, and scheduler run queues, you want a language that does not get in the way.
Rust did not arrive to replace C out of ideology. It arrived because the class of bugs that plague kernels, especially memory safety bugs, keep showing up in shipped software decade after decade. Microsoft and Google have both publicly discussed how a large share of serious security vulnerabilities in their products trace back to memory safety issues in C and C++ code. That is not a knock on the engineers writing that code. It is a statement about what the language makes easy and what it makes hard.
This article is about what actually changes when you bring Rust into kernel work, where it fits well, where it does not, and what teams should think about before betting on it.

Kernels also cannot rely on the safety nets that user space programs take for granted. There is no operating system underneath the kernel to catch a segmentation fault. There is no allocator that can politely fail. There is no process isolation to contain the blast radius. The kernel is the thing that provides those guarantees to everyone else, so it has to be correct on its own terms.
This is why C has been the default for so long. It is small, it maps closely to hardware, and it does not impose hidden runtime behavior. The trade-off is that the compiler trusts you. If you index past the end of an array, it will not stop you. If you use a pointer after freeing it, the compiler will happily generate code that does whatever the memory happens to contain at that moment.
Rust's central claim is that you can keep the low-level control and give up the class of bugs that come from unmanaged memory access. That claim is testable, and the kernel community has been testing it.
Ownership means every value has exactly one owner. When the owner goes out of scope, the value is dropped. There is no ambiguity about who is responsible for cleanup.
Borrowing means you can lend out references to a value, but with rules. You can have many immutable references, or one mutable reference, but not both at once. This prevents data races at compile time.
Lifetimes are how the compiler tracks how long a reference is valid. If you try to return a reference to a local variable, the compiler rejects it. If you try to store a reference in a struct without telling the compiler how long it lives, it rejects that too.
None of this requires a garbage collector. The checks happen at compile time. At runtime, the generated code looks a lot like what a careful C programmer would write.
That last point matters for kernels. A garbage-collected language would be a nonstarter in most kernel contexts because you cannot pause the world to collect memory while you are servicing an interrupt. Rust avoids that problem entirely.

The first is the Linux kernel. Rust support was merged into the mainline tree in 2022, initially for drivers and infrastructure rather than core subsystems. The approach was deliberate: prove the model in lower-risk areas before touching the scheduler or memory management. Since then, work has continued on abstractions for things like character devices, network drivers, and file system pieces, though the pace and scope have been a matter of ongoing discussion in the community.
The second is Android. Google has been using Rust for userspace components and, more recently, for kernel modules on some devices. The company has publicly noted a reduction in memory safety vulnerabilities in code that moved to Rust, though absolute numbers depend heavily on what you are counting and over what period.
Beyond those, there is a cluster of newer projects that are Rust-first from the start. Redox OS is a microkernel-style operating system written in Rust. Theseus is a research operating system from Rice University that leans heavily on Rust's type system for its design. Hubris, from Oxide Computer, is a small embedded operating system written in Rust for their server management controllers. Each of these makes different trade-offs, and none of them is a drop-in replacement for Linux.
The pattern is consistent: Rust enters through the edges, proves itself, and either expands or does not depending on how the abstractions hold up.
Consider a network driver parsing packets. The input comes from the wire, which means an attacker controls it. Every length field, every offset, every protocol header is potentially hostile. In C, you write careful bounds checks and hope you did not miss one. In Rust, the type system can encode invariants like "this slice is at least this long" and refuse to compile code that violates them.
Consider a filesystem driver reading on-disk structures. The disk could be malicious, or just corrupt. In C, a malformed superblock can turn into a wild pointer. In Rust, parsing that structure into a typed representation forces you to handle the failure cases or the compiler stops you.
Consider a USB driver. USB descriptors are famously fiddly, and the history of USB vulnerabilities is long. Rust does not make the protocol simpler, but it does make it harder to shoot yourself in the foot while implementing it.
In all these cases, the value is not that Rust is faster or more elegant. The value is that a whole category of bug becomes a compile error instead of a CVE.
First, Rust does not eliminate all bugs. It eliminates memory safety bugs in safe code. If you use unsafe blocks, and kernels use a lot of them, you are back to C-level discipline. The difference is that unsafe is opt-in and visible. You can audit it. You can count it. You can write tests around it. But you cannot pretend it does not exist.
Second, Rust does not eliminate logic bugs. A scheduler that picks the wrong task, a page replacement policy that thrashes, a locking protocol that deadlocks: none of these care what language you write them in.
Third, Rust adds compile time. Large Rust codebases can take a long time to build, and kernel builds are already slow. This is a real cost, especially for developers who rebuild frequently.
Fourth, Rust adds cognitive load. Ownership and lifetimes are learnable, but they are not free. A team of experienced C kernel developers will not be as productive in Rust on day one as they are in C. The question is whether they are more productive on day thirty, and the answer depends on the kind of code they are writing.
Fifth, Rust's kernel abstractions are still maturing. Writing a driver in Rust today often means working with APIs that are less documented and less stable than their C counterparts. You may find yourself reading the abstraction source code more than you would like.
Unsafe does not mean "this code is dangerous." It means "the compiler cannot verify the invariants here, so the programmer is asserting them." Inside an unsafe block, you can dereference raw pointers, call unsafe functions, and access mutable statics. What you cannot do is pretend the rest of the language stops working. Ownership still applies. Borrowing still applies. The unsafe keyword is a localized escape hatch, not a mode switch.
Good kernel Rust keeps unsafe blocks small and wraps them in safe abstractions. The pattern looks like this: a small unsafe function that does the raw pointer manipulation, and a safe function on top that enforces the preconditions. Callers use the safe function. Auditors look at the unsafe one.
This is how the Linux kernel's Rust abstractions are structured, and it is how most mature Rust projects handle FFI and hardware access. The goal is not zero unsafe. The goal is concentrated unsafe that is easy to review.
A common mistake for newcomers is to sprinkle unsafe everywhere to make the compiler stop complaining. That defeats the purpose. If you find yourself doing that, the right move is usually to rethink the data structure, not to reach for unsafe.
Rust can call C functions through the foreign function interface, and C can call Rust functions that are marked for external linkage. The bindings are generated, often with a tool like bindgen, and the result is a Rust module that looks like any other C header from the outside.
The friction points are real. Rust and C have different ideas about what a struct looks like in memory unless you tell Rust to match C's layout. Rust has different calling conventions if you do not specify them. Error handling conventions differ: C returns error codes, Rust returns Result types. You end up writing glue code that translates between the two worlds.
The best practice, based on what has worked in Linux and Android, is to keep the boundary thin. Do not have Rust call deep into C internals and vice versa. Define a small, stable interface and let each side manage its own internals.
Rust's zero-cost abstractions are real. Iterators, generics, and trait dispatch typically compile down to the same machine code you would write by hand in C. There are cases where Rust does better because it can inline more aggressively, and cases where C does better because the programmer made a choice the compiler could not.
The place where Rust sometimes loses is in code that fights the borrow checker. If you contort your data structures to satisfy the compiler, you can end up with extra indirection or copies. Experienced Rust developers learn to design around this, but it is a real effect, especially in the early days of a project.
The place where Rust sometimes wins is in concurrency. Rust's type system makes data races a compile error in safe code, which means you can write lock-free or fine-grained-locking code with more confidence. In a kernel, where concurrency bugs are the hardest to reproduce and the most damaging, this is not a small thing.
The first few weeks are rough. You write code that looks fine, the compiler rejects it, and you do not immediately understand why. Then it clicks, and you start to see the borrow checker as a collaborator rather than an adversary.
For kernel developers specifically, there is a second learning curve: the kernel's Rust abstractions. These are not the same as userspace Rust. You cannot use the standard library. Allocation is restricted. Some patterns that are idiomatic in userspace are wrong in kernel context.
The teams that have had the smoothest transitions tend to pair an experienced Rust developer with an experienced kernel developer. Neither alone is enough. The Rust developer knows the language but not the domain. The kernel developer knows the domain but not the language. Together they can build the abstractions that make everyone else productive.
The first is that Rust is only for new projects. This is false. Linux and Android both integrate Rust into existing C codebases. The pattern is incremental, not revolutionary.
The second is that Rust will replace C entirely. This is unlikely in any near-term horizon. C is too embedded, too well understood, and too widely deployed. The realistic future is coexistence, with Rust taking on new code and C maintaining what exists.
The third is that Rust is slow. This is false for runtime performance and true for compile time. The distinction matters.
The fourth is that unsafe Rust is just C. This is misleading. Unsafe Rust is C-like in its freedom, but it still benefits from Rust's type system, module system, and tooling. It is a different thing from C, even when it looks similar.
The fifth is that Rust solves security. It reduces one class of vulnerability. It does not address logic bugs, misconfiguration, side channels, or the long tail of issues that have nothing to do with memory safety.
Start with a bounded, self-contained component. A driver for a specific piece of hardware is a good candidate. A core subsystem is not. You want something where you can measure success and failure without risking the whole system.
Invest in the abstractions before you invest in features. The Linux kernel's approach of building safe wrappers around core kernel facilities took time, but it is what makes writing drivers in Rust tractable. If you skip this step, every developer will write their own unsafe code, and you will lose the benefits.
Train your team, but do not expect overnight conversion. Budget for a few months of reduced velocity while people learn. The productivity curve is real, but it is not instant.
Keep your unsafe code auditable. Track it. Review it. Consider it a separate category of code with its own standards.
Do not rewrite working C code just to have Rust. The value of Rust is in new code and in code that is being substantially rewritten anyway. A working driver that has been in production for years is not improved by being ported.
Measure what matters. Memory safety bugs prevented is a real metric. Compile time added is a real cost. Both belong in the decision.
A generation of kernel developers is learning that they can have low-level control without accepting the full burden of manual memory management. A generation of Rust developers is learning that the language's safety guarantees are most valuable in exactly the places where the cost of failure is highest. And the tooling, the abstractions, and the community practices are maturing in public, with all the messiness that implies.
The impact is not that Rust has won. The impact is that the conversation has changed. When a new kernel project starts today, the question is no longer "why would you use Rust" but "why would you not." That is a meaningful shift, and it happened in less than a decade.
For anyone working in this space, the practical takeaway is to stay fluent in both languages. C is not going anywhere. Rust is not going anywhere. The developers who can move between them, and who understand the trade-offs at the boundary, will be the ones who shape what comes next.
all images in this post were generated using AI tools
Category:
Operating SystemsAuthor:
Ugo Coleman