Skip to main content

Command Palette

Search for a command to run...

Implementing a Linux Device Driver in Rust

Part I - Development Environment

Updated
•8 min read•View as Markdown

I've been looking for a new challenge lately and decided to write a device driver in Rust, and since Rust is definitely in the kernel to stay, I thought it might be helpful to other engineers or developers if I wrote about the process. The end-state is a driver, targeted to a Raspberry Pi 4 running Raspbian, to control the ubiquitous 16x2 LCD display that comes in so many micro-controller kits. The second part of this series will have a full specification, but for now we're just going to worry about the development environment.

Before I get into it, the environment setup is based on Ubuntu 26.04 LTS and known to work as of 27 September 2026. I may test this out on more distros in the future, but for now, I had a devil of a time just getting everything to work in the distro with whih I'm most familiar, and a quick evaluation using a Fedora LiveUSB wasn't promising.

AI Use disclaimer: I used Google Gemini in the process of figuring all of this out, primarily to help with some of the build errors and environment issues I ran into. The "Hello Rust" example module I'll introduce later is mostly code generated by Gemini with a small modification I made to the Makefile. All other code examples will be either entirely mine, or have inline attribution to the original source.

Hello Rust

I figured a smart place to start would be a simple "Hello World" kernel module, so I Googled how to write a Kernel module in Rust, and Gemini provided a complete and simple example with three components:

# Makefile for 'Hello Rust'
KDIR ?= /lib/modules/$(uname -r)/build

default:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
# Kbuild file for 'Hello Rust'
obj-m := hello_rust.o
//! A simple "Hello World" in Rust

module! {
	type: HelloRust,
	name: "hello_rust",
	authors: ["James Osterhage"],
	description: "A simple Rust kernel module",
	license: "GPL",
}

struct HelloRust;

impl kernel::Module for HelloRust {
	fn init(_module: &'static ThisModule) -> Result<Self> {
		pr_info("Hello from Rust!\n");
		Ok(HelloRust)
	}
}

impl Drop for HelloRust {
	fn drop(&mut self) {
		pr_info("Goodbye from Rust!\n");
	}
}

Ideally, assuming my environment was correct and $KDIR in the Makefile pointed to the right module build directory, I should have been able to build that by invoking make LLVM=1, but I couldn't get it to work. I've been messing with this for a week, and I still can't get it to build a native, out-of-tree kernel module written in Rust (more on that later). At first I thought maybe I'd jacked up some cache Rust uses for its libraries, so I decided to try building HelloRust for the Raspberry Pi, and I was able to get that running.

EDIT: It took me a week to get this to work, and I kind of blame it on Ubuntu. Basically, I thought I had everything I needed: rustc, the normal modules headers, the rust-src package, &c. It turns out, Ubuntu's kernel build is setup such that you need the Ubuntu-managed linux-lib-rust-{version} package for your current kernel version.

Hello Rust, Again

Building a kernel module for the Raspberry Pi wasn't exactly trivial, but neither was it especially complex. The first hurdle was that the Raspbian kernel isn't build with Rust enabled. So I followed the very well-written instructions for building a custom Pi kernel (including cross-compilation instructions), modified the configuration, then built and installed the new kernel. A quick note here: the Pi kernel config has CONFIG_MODVERSIONS=Y and CONFIG_GENKSYMS=Y set, which is incompatible with enabling Rust. The easiest workaround is to set CONFIG_MODVERSIONS=N, but if you want/need module version data, you can enable ksyms generated from the DWARF debugger information by first setting CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT (or specifying a DWARF version) in Kernel Hacking---> Compile-time checks and compiler options---> Debug information then setting CONFIG_GENDWARFKSYMS=Y in Enable loadable module support---> Module versioning support---> Module versioning implementation.

At first I tried compiling the module directly on the Raspberry Pi. Using C, that would have worked, but it only partly worked for Rust. The first time I built it, I got a pair of "undefined symbol" errors, one on call_printk and the other on format_strings::INFO, which I initially resolved by making unsafe calls directly to kernel::bindings::_printk just to actually get the module to finally build. I'm still getting familiar with the Rust build system, but as I understand it, rustc needed some pre-built objects from the kernel build, which I didn't transfer, whereas C just needs some header files that can be generated directly by make modules_prepare. In any event, I decided the best way forward would be to build the module on the same system where I built the kernel.

So, I modified the Makefile to point to the tree I used to cross-compile the kernel and, after messing around with rustup toolchain settings for a little bit, I managed to make it work beautifully.

Concise process for setting up the environment (Raspberry Pi)

  1. Install all of the necessary build tools:
$ sudo apt install git crossbuild-essential-arm64 clang llvm lld git rustup
$ sudo apt install libelf-dev libdw-dev zlib1g-dev

(You can omit the libraries if you don't wish to use DWARF ksyms)

  1. Download and build the Raspberry Pi kernel (included because the steps I use differ slightly from the Raspberry Pi instructions):
$ git clone --depth=1 https://github.com/raspberrypi/linux
$ cd linux
linux$ KERNEL=kernel8

At this point I recommend ensuring that Kbuild can use rustc:

linux$ make LLVM=1 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- rustavailable

If everything's good, you'll get a simple message: Rust is available! If, like me the first time I tried it, everything is not good and you get an error message, try changing the active toolchain:

linux$ rustup override set 1.93.1

I picked version 1.93.1 because it's the version with which my host kernel was built, but I don't believe I could get it to run with the stable toolchain, either as stable or using the explicit version number (1.98.1 as of writing this).

Next pull in the default config for the Pi's SoC then make menuconfig to enable rust (my Pi 4 uses the bcm2711, if you're not using a Pi 4, pick the correct config).

linux$ make LLVM=1 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- bcm2711_defconfig
linux$ make LLVM=1 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig

Finally, build your kernel

linux$ make LLVM=1 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image modules dtbs

The steps to install the kernel match the guide from Raspberry Pi.

  1. Prepare the module source directory and Makefile
    Just as with the kernel's build tree, I had to override the Rust toolchain in my module's build directory:
module$ rustup override set 1.93.1

And I modified the Makefile to point $KDIR at the path with my Pi kernel source:

# Obviously use your local kernel source path here
KDIR := /home/user/linux
  1. Make (Edited 29 September 2026, I forgot to add the cross-compilation flags)
module$ make LLVM=1 ARCH=arm64 CROSS=aarch64-linux-gnu-

Congratulations, you should have a kernel module ready to move over to your Pi. I transferred it using SCP myself:

module$ scp ./hello_rust.ko [user]@[host]:/[path]/hello_rust.ko

And tested it by logging into the Pi and loading with insmod:

pi$ sudo insmod hello_rust.ko
pi$ sudo dmesg
pi$ sudo rmmod hello_rust
pi$ sudo dmesg

If everything worked you should see:

[timestamp] hello_rust: Hello from Rust!
[timestamp] hello_rust: Goodbye from Rust!

Concise process for setting up the environment (Ubuntu 26.04+ native)

Don't use rustup or the installation instructions on the Rust website that tell you to pull with curl. Instead, install rust, its kernel-specific dependencies, and Ubuntu's rust-lib via apt.

$ sudo apt install rust rust-src bindgen rustfmt rust-clippy
$ sudo apt install linux-lib-rust-$(uname -r)
$ export RUST_LIB_SRC=/usr/src/rustc-$(rustc --version | cut -d' ' -f2)/library

Make sure the module is pointing to the correct $KDIR:

KDIR := /lib/modules/$(uname -r)/build

And build:

module$ make

Note I didn't use LLVM=1. Ubuntu's managed Rust-lib is setup to use gcc, just like their prepackaged kernels. If you use LLVM=1 you'll get a bunch of build errors. Also, you'll probably have to go through the process of signing the module to get it to load past secure boot. If you don't know how to do that, the first answer on this stackexchange post explains all the steps.

Final note

I just got the native build to work. The cross-compile build for the Pi relied on rustup's toolchain management to get the correct rust-src objects, but I had to remove rustup to get the native build to work. I tried building for the Pi again, but I think I ran make clean and make mrproper on that source tree sometime yesterday, so it didn't have all of the info it needed to build. I imagine for most use cases just being able to do one or the other works just fine.

Edit (29 September 2023):

I just rebuilt my cross-compiled kernel and building the Pi module worked straightaway.

Next article

In the next article I'll go over the full specification for the driver and provide a minimally functioning example that if nothing else prints text to the LCD.

Kernel Rust

Part 1 of 1

Articles on writing Linux kernel code in Rust