Skip to main content

Loading Out of Tree Rust in Linux

· 10 min read

Rust is taking off in the Linux Kernel and improved support and features make it possible to develop drivers with Rust.

It's no secret that C is not a memory-safe language. In the router world, there are tons of bugs that surface, many related to memory safety. These bugs are often rooted in low-level device firmware and kernel drivers, making them very dangerous and difficult to work with. Supernetworks is exploring ways of moving wireless protocol parsing out of firmware and drivers and pushing them into userland, with a minimal custom kernel module. Writing this module in Rust would avoid many of the safety pitfalls of C.

The 6.9 Linux release landed Rust support for arm64 which opens the door for us to use Rust for our arm64 based hardware.

One challenge is how to distribute kernel code to users. It’s difficult to maintain a kernel branch so we’d like to explore distributing an out of tree kernel module instead. So when Ubuntu finally lands kernels with Rust for ARM64 (expected to be this year), we’ll be able to distribute a Rust module to load in directly.

TL;DR

Rust abstractions can be put out of tree with your module, they don't need to be compiled in with the kernel. Bindgen still works, since it doesn't change the kernel ABI. Helper functions that wrap inlined functions and macros can just be put inside of another external module, and the compiler will figure out the dependency chain.

Out of Tree Modules

With Out of Tree Modules, developers can build Linux Kernel Modules and ship them separately from the loaded Linux kernel build. These modules aren't baked into the Linux kernel. Instead, they rely on symbols that the kernel exports to call into kernel code, allowing them to draw on the vast capabilities of the Linux kernel. With Rust, however, calling into the kernel is a little more complicated.

One of the main problems with writing external modules in Rust is that individual subsystems have little built-in support for Rust, as of the time of writing this article. Each subsystem has many functions that drivers can expect to exist in the kernel ABI, but these functions don't have Rust abstractions written for them, at least not ones rolled into the kernel. The Linux kernel developers are hopeful that coverage for Rust abstractions will increase in time, but for now, it is not feasible to write a safe external module for the mainline Linux kernel without these abstractions. To understand how we can use Rust in external kernel modules, let's first explore what Rust abstractions are and why they're important.

What are abstractions?

Rust, as a systems programming language, has essentially seamless integration with C through a Foreign Function Interface (FFI). Using the extern keyword, Rust can make a call to any C function in the kernel as if it were a normal Rust function, with no changes to the kernel. There is only one notable exception: Memory safety guarantees are dropped.

When C code is compiled, all functions except for inline functions and macros are given symbols. These symbols are exposed through the Application Binary Interface (ABI), allowing other code to call those functions. The ABI contains information such as parameter types, return types, and more, but no information about memory safety. If we want to use Rust's memory safety model, we need to specify these guarantees ourselves, with Rust code. By writing abstraction layers around FFI calls, we can manually define what Rust should expect regarding memory safety. Take this example, based on a similar one from The Rust Book:

int c_abs(int num) {
return abs(num);
}
// EXPORT_SYMBOL_GPL(c_abs);

void c_abs_in_place(int* num) {
*num = abs(*num);
}
// EXPORT_SYMBOL_GPL(c_abs_in_place);
extern "C" {
// Define our extern functions. This doesn't compile to anything, it just
// tells Rust to expect that it will be able to use these functions with
// these symbols
fn c_abs(num: i32) -> i32;
// Note that 'mut' isn't strictly required. Omitting it would still compile,
// but it would break Rust's memory safety guarantees, allowing the programmer
// to make a mistake.
fn c_abs_in_place(num: *mut i32);
}

// We wrap the FFI call in an abstraction, so that we can isolate the unsafe call
// here. If we didn't have this, we would have to use unsafe in our main function,
// which would make it hard to figure out where memory safety bugs happen.
fn rust_abs(num: i32) -> i32 {
// SAFETY: This is just an FFI call.
unsafe { c_abs(num) }
}

fn rust_abs_in_place(num: &mut i32) {
// SAFETY: Since num is a reference, we know its pointer is still valid.
// Otherwise, this is just an FFI call.
unsafe { c_abs_in_place(num as *mut i32) }
}

fn main() {
// Since we used safe abstractions, we don't need to use unsafe code
// in our main function.
let foo = rust_abs(-5);
let mut bar = -5;
rust_abs_in_place(&mut bar); // Note that rust forces us to use mut because
// of how we defined our safe wrapper.
println!("Foo is {} and bar is {}!", foo, bar);
// Foo is 5 and bar is 5!
}

Since abstractions are wrappers around essential subsystem functions, they would normally be rolled into the kernel so any driver could access them. That way, each C subsystem function only needs to be wrapped once. Any module or driver, out-of-tree or not, could then use them without having to rewrite the abstraction for every single driver.

As we saw before, abstractions are just normal Rust code that wraps an unsafe function. So, what's the problem with just moving it out-of-tree and putting it with your other code? There's none, it really is that easy! While abstractions solve most of our issues, there are some issues that it doesn't solve. To understand why, let's look at bindgen and how it works.

Why Bindgen isn't a problem

As the name implies, bindgen automatically generates bindings for Rust code. These bindings are the same as the extern block above, only a lot more complicated. The only confusion is that bindgen must be compiled with the kernel, but it doesn't change the ABI. The only things that go in the extern block are definitions: function prototypes, constants, and structure definitions that don't compile to anything. Instead, in the case of function prototypes, they're used by the Rust compiler to place symbols. When the linker links all the object files together, it will find the C function symbols that were exposed by bindgen, and call it from Rust. Bindgen doesn't create anything new; it just exposes existing symbols to Rust.

Now, can call into any exported symbol that exists in the C kernel. But there is one more challenge: Handling macros and inline functions in our Rust modules.

Macros and Inlines

Macros and inline functions are different from regular functions in that they don't get assigned a symbol. Instead, they are directly inserted during pre-processing or compilation. These functions aren't in the kernel ABI, so we don't have any way to access them from Rust!

The way this is normally handled in the kernel is by generating helper functions, like this:

void rust_helper_mutex_lock(struct mutex *lock)
{
mutex_lock(lock);
}
EXPORT_SYMBOL_GPL(rust_helper_mutex_lock);

Each of these helper functions is compiled into the kernel and given a symbol so that they can be used from Rust. This is convenient for making use of commonly used macros and inline functions from multiple places. However, our focus is on distributing the external module without having to distribute a kernel as well. Since these helper functions generate symbols that would not be present in the mainline kernel, running our driver would require a custom kernel.

To generate these symbols externally, we must write our own helpers with our external module. Unlike abstractions, these helpers aren't just more Rust code. They're C code that needs to be compiled first. We can compile these helper functions using another external module.

Depending on an external module

By simply writing these functions in a C file and specifying it as a module, we can generate symbols for C functions. Then, by compiling both the Rust and C modules together, the Linux kernel's build system will automatically figure out that the Rust module depends on the C helper module!

// SPDX-License-Identifier: GPL-2.0


#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/version.h>

int linux_version_code(void);
int linux_version_code(void) {
// We can't access LINUX_VERSION_CODE in Rust because it's defined
// using #define. This helper function exports it to rust. This
// works with macros and inline functions as well.
return LINUX_VERSION_CODE;
}
EXPORT_SYMBOL_GPL(linux_version_code);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Jeremy Goldberger");
MODULE_DESCRIPTION("A simple helper module.");
// SPDX-License-Identifier: GPL-2.0

//! Inspired by Rust hello world example by Adrea Righi

use kernel::prelude::*;

module! {
type: ModuleExample,
name: "oot_example",
author: "Jeremy Goldberger",
description: "Rust external module example",
license: "GPL",
}

struct ModuleExample {
}

impl kernel::Module for ModuleExample {
fn init(_module: &'static ThisModule) -> Result<Self> {
pr_info!("Hello from Rust\n");
pr_info!("The current time according to C is {}.\n", rust_ktime_get());
pr_info!("The current linux version according to C is {}.\n", rust_linux_version_code());
Ok(ModuleExample { })
}
}

fn rust_ktime_get() -> i64 {
// Note that we don't need to define ktime_get() in an extern block anywhere;
// it was already done in bindgen! Even if it wasn't, we could add whatever
// header we wanted to bindings_helper, and we could use it without messing
// up the kernel ABI.
unsafe { kernel::bindings::ktime_get() }
}

extern "C" {
// We define this extern block ourselves. Because it's a helper function
// that doesn't exist in the Linux kernel, bindgen wouldn't be able
// to generate it without doing some weird stuff.
fn linux_version_code() -> core::ffi::c_int;
}

fn rust_linux_version_code() -> i32 {
unsafe { linux_version_code() }
}
info

There is already a ktime abstraction written for Rust. I was running 6.9 here, so it wasn't available here. That abstraction leverages Rust types to make it more robust, a common theme in abstractions more complex than the ones above.

Running the module in a qemu instance:

ubuntu@ubuntu-24-cloud-image:~$ ls
helpers.ko rust_module.ko
ubuntu@ubuntu-24-cloud-image:~$ uname -r
6.9.0
ubuntu@ubuntu-24-cloud-image:~$ lsmod
Module Size Used by
ubuntu@ubuntu-24-cloud-image:~$ sudo insmod rust_module.ko
insmod: ERROR: could not insert module rust_module.ko: Unknown symbol in module
ubuntu@ubuntu-24-cloud-image:~$ sudo insmod helpers.ko
ubuntu@ubuntu-24-cloud-image:~$ sudo insmod rust_module.ko
ubuntu@ubuntu-24-cloud-image:~$ lsmod
Module Size Used by
rust_module 12288 0
helpers 12288 1 rust_module
ubuntu@ubuntu-24-cloud-image:~$ sudo dmesg

...

[ 37.728226] rust_module: loading out-of-tree module taints kernel.
[ 37.729919] rust_module: Unknown symbol linux_version_code (err -2)
[ 62.071684] oot_example: Hello from Rust
[ 62.073836] oot_example: The current time according to C is 62047385799.
[ 62.074243] oot_example: The current linux version according to C is 395520.

Note that the first time we tried to load the module, it threw an error because our helpers module wasn't loaded. The second time, it loaded correctly, since it found the linux_version_code symbol in our helper module. Additionally, when we ran lsmod, we saw that helpers was listed as a dependency of rust_module.

Now, we have an external module written in Rust, but we can make full use of existing C infrastructure even if it's not baked into the kernel yet! By leveraging external modules, we can start benefiting from Rust's safety features in kernel development without waiting for full mainline support. The rapid development of Rust support in the Linux kernel means that many of these workarounds will become unnecessary over time. As more subsystems gain native Rust abstractions, writing Rust kernel modules will become increasingly straightforward and powerful.

The potential for Rust in the Linux kernel is vast, and we're just at the beginning. Whether you're a seasoned kernel developer or new to systems programming like I am, now is an exciting time to get involved and contribute to open-source development.