Published on January 25, 2023
Some background
After watching Alex Gaynor and Geoffrey Thomas their presentation and video about writing Linux kernel modules, I have been trying to keep a close eye on their progress. Especially since I have been playing around with the now archived linux-kernel-module-rust repository, to port over one of my toy Linux kernel modules to fiddle around with physical memory and page tables from C to Rust.
Three years after the presentation we are now looking at initial Rust support being part of the Linux 6.1 kernel. Shortly after that Raphael Nestler wrote some interesting blog posts about building out-of-tree Rust kernel modules on ArchLinux (you can read part one and part two here). These articles got me wondering about whether this is possible on Gentoo. Of course, while we focus on Gentoo here, most of this knowledge is also applicable to other Linux distributions.
Installing the Linux kernel
As not everyone is familiar with Gentoo, we will first take a look at the various ways of installing a Linux kernel. In fact, many people, including many Gentoo users, do not seem to be aware that Gentoo ships sys-kernel/gentoo-kernel-bin, which is a precompiled binary of the Linux kernel with Gentoo-specific patches which has been available since January 10, 2020.
However, while a precompiled binary of the Linux kernel exists, many users instead compile the Linux kernel from source. While Gentoo offers many flavors of the kernel source, the typical ones are sys-kernel/vanilla-sources, which provides the Linux kernel source with no modifications, sys-kernel/gentoo-sources, which provides the Linux kernel source with Gentoo-specific patches and sys-kernel/git-sources, which provides the Linux kernel source for current release candidates.
Once installed, you would use eselect kernel list
to list the installed kernel sources and eselect kernel set 1
to set the first entry in the list to be the default kernel source to use.
More specifically, eselect kernel set 1
creates a symbolic link from /usr/src/linux
to the directory containing that specific kernel source.
After setting up the kernel source, the next step is to get a working kernel configuration.
A relatively straightforward to obtain a working kernel configuration is from an existing Linux kernel that can properly boot your system, such as the kernel from another distribution like Ubuntu or ArchLinux, or from sys-kernel/gentoo-kernel-bin.
Once you having a working kernel configuration, you would typically copy it over using zcat /proc/config.gz > .config
or from your /boot
directory, and run make olddefconfig
before building the Linux kernel.
Finally, you can perform further tweaking of the kernel configuration using make menuconfig
.
If you want to manually build and install the Linux kernel and the kernel modules, you can do so using the following commands:
make -j16
make INSTALL_MOD_STRIP=1 modules_install
make install
To build your own initramfs, you can look into tools like dracut. Alternatively, you can use genkernel to build a Linux kernel and initramfs by simply running something like the following command:
genkernel --clean --menuconfig --install all
After building and installing the Linux kernel using either way, we need to run the following command to update GRUB's configuration file:
grub-mkconfig -o /boot/grub/grub.cfg
What are the prerequisites to enable Rust in the Linux kernel?
The first part of Raphael's blog post provides some good pointers of where to start.
More specifically, it points us to a basic template for an out-of-tree Linux kernel module in Rust that we can use.
One of the requirements listed there is that our Linux kernel has to be built with CONFIG_RUST=y
.
To check this we can simply use grep
on our kernel configuration:
grep CONFIG_RUST .config
If we run the above command, there is simply no output, which indicates that the option is not set.
So let's run make menuconfig
and press /, type in Rust and press Enter to look for CONFIG_RUST
.
It should look something like the image below:
More specifically, it shows the following text:
Symbol: RUST [=n]
Type : bool
Defined at init/Kconfig:1913
Prompt: Rust support
Depends on: HAVE_RUST [=y] && RUST_IS_AVAILABLE [=n] && !MODVERSIONS [=y] && !GCC_PLUGINS [=y] && !RANDSTRUCT [=n] && !DEBUG_INFO_BTF [=n]
Location:
(1) -> General setup
-> Rust support (RUST [=n])
Selects: CONSTRUCTORS [=n]
Looking more closely at Depends on:
, we can see the other configuration options that need to be set or not for CONFIG_RUST=y
.
In our case, RUST_IS_AVAILABLE
is set to n
, while MODVERSIONS
and GCC_PLUGINS
are set to y
.
So let's tackle each of these issues, one at a time.
Ensuring a proper Rust toolchain is available
The Linux documentation provides some helpful instructions to figure out how to check if a proper Rust toolchain is available or not, among other things.
In fact, we can run make rustavailable
to check if our system is set up accordingly:
make rustavailable
***
*** Rust bindings generator 'bindgen' could not be found.
***
make: *** [Makefile:1800: rustavailable] Error 1
It turns out that we don't have the dev-util/bindgen package installed. So let's unmask it, since there is no stable version available yet:
echo "dev-util/bindgen ~amd64" >> /etc/portage/package.accept_keywords/rust
Then we can install it by running emerge bindgen
.
From looking at the output, it turns out we also need to enable the rustfmt
USE-flag:
Calculating dependencies... done!
[ebuild R ] dev-lang/rust-bin-1.66.1 USE="rustfmt*"
[ebuild R ] virtual/rust-1.66.1 USE="rustfmt*"
[ebuild N ~] dev-util/bindgen-0.62.0 USE="-debug" ABI_X86="32 (64) (-x32)"
The following USE changes are necessary to proceed:
(see "package.use" in the portage(5) man page for more details)
# required by dev-util/bindgen-0.62.0::gentoo
# required by bindgen (argument)
>=virtual/rust-1.66.1 rustfmt
# required by virtual/rust-1.66.1::gentoo
# required by dev-util/bindgen-0.62.0::gentoo
# required by bindgen (argument)
>=dev-lang/rust-bin-1.66.1 rustfmt
So let's enable that USE-flag:
echo "virtual/rust rustfmt" >> /etc/portage/package.use/rust
echo "dev-lang/rust-bin rustfmt" >> /etc/portage/package.use/rust
Now we run emerge bindgen
again, and it succeeds, and from running make rustavailable
again, we can see that we now have a working Rust compiler as well as the bindgen crate installed:
***
*** Rust compiler 'rustc' is too new. This may or may not work.
*** Your version: 1.66.1
*** Expected version: 1.62.0
***
***
*** Rust bindings generator 'bindgen' is too new. This may or may not work.
*** Your version: 0.62.0
*** Expected version: 0.56.0
***
***
*** Source code for the 'core' standard library could not be found
*** at '/opt/rust-bin-1.66.1/lib/rustlib/src/rust/library/core/src/lib.rs'.
***
Rust is available!
Apparently, we should enable the rust-src
USE-flag for dev-lang/rust:
echo "dev-lang/rust-bin rust-src" >> /etc/portage/package.use/rust
In addition, the versions of the Rust compiler and the bindgen are a bit new compared to what make rustavailable
expects.
This makes sense, since the Linux kernel has to support distributions that are not rolling release distributions like Debian, which currently ships Rust 1.63 and 1.64, and Ubuntu, which currently ships Rust 1.61.
Unfortunately, if we try to build the Linux kernel with Rust 1.65 or 1.66.1
However, if we run eix dev-lang/rust-bin
and eix virtual/rust
, we see that Gentoo only provides 1.65.0 and 1.66.1 at the time of writing.
Fortunately, Rust 1.62.0 used to be available at some point, thus we can simply run a local ebuild repository and grab the old ebuilds.
Setting up a local repository
The Gentoo wiki already provides instructions on how to set up a local repository, but it boils down to running emerge pkgdev
to make sure that we have pkgdev
installed, and running eselect repository create local
to set up the skeleton directory structure for our ebuild repository.
Our ebuild repository can then be found at /var/db/repos/local
:
cd /var/db/repos/local
We then need to create the appropriate directories for dev-lang/rust-bin
and virtual/rust
:
mkdir -p dev-lang/rust-bin
mkdir -p virtual/rust
Then we can download the old ebuilds from their respective commits for dev-lang/rust-bin and virtual/rust as follows:
wget https://gitweb.gentoo.org/repo/gentoo.git/plain/dev-lang/rust-bin/rust-bin-1.62.0.ebuild?id=0d7d7b32c9b472a25fbe998ed6ad196501d383a1 -O dev-lang/rust-bin/rust-bin-1.62.0.ebuild
wget https://gitweb.gentoo.org/repo/gentoo.git/plain/virtual/rust/rust-1.62.0.ebuild?id=6302c360cd3bbf54c716d1642e329d14046fd722 -O virtual/rust/rust-1.62.0.ebuild
We then need to create a package manifest for both packages:
pushd dev-lang/rust-bin
pkgdev manifest
popd
pushd virtual/rust
pkgdev manifest
popd
Make sure to run cp /usr/share/portage/config/repos.conf /etc/portage/repos.conf
if the following error appears:
pkgdev: error: repos.conf: default repo 'gentoo' is undefined or invalid
Finally, we can run eix-update
to update our eix database, and the packages should be available to install.
Since bindgen-0.56.0 is still available at the time of writing, we don't have to create our own ebuild for bindgen.
Now to ensure that we have the appropriate versions installed, we can pin the versions we want by writing the following to /etc/portage/package.mask/kernel
:
>dev-lang/rust-bin-1.62.0
>dev-util/bindgen-0.56.0
>virtual/rust-1.62.0
We should also make sure to unmask our ebuilds:
echo "dev-lang/rust-bin ~amd64" >> /etc/portage/package.accept_keywords/rust
echo "virtual/rust ~amd64" >> /etc/portage/package.accept_keywords/rust
Then we can simply install Rust and bindgen as follows:
emerge rust-bin bindgen virtual/rust
Now if we run make rustavailable
again, it should output the following:
Rust is available!
Building the kernel with Clang
We can try to set CONFIG_GCC_PLUGINS=n
by deselecting the following option in make menuconfig
to set CONFIG_RUST=y
:
General architecture-dependent options --->
[ ] GCC plugins
However, building the Linux kernel with gcc seems to result in the following error:
arch/x86/tools/insn_decoder_test: error: malformed line 1318434:
tBb_+0x102>
Let's try building the kernel (and our initramfs) with Clang instead, as that is the recommended way of building the Linux kernel to enable Rust support.
Especially since the bindgen crate relies on libclang, thus LLVM and Clang to produce Rust bindings from C source code, anyway.
We can simply achieve this by changing the /etc/genkernel.conf
file.
First, we will add the LLVM=1 LLVM_IAS=1
options to MAKEOPTS
:
MAKEOPTS="$(portageq envvar MAKEOPTS) LLVM=1 LLVM_IAS=1"
Then we make sure to point the toolchain used to build the kernel to LLVM/Clang:
KERNEL_AS="llvm-as"
KERNEL_AR="llvm-ar"
KERNEL_CC="clang"
KERNEL_LD="ld.lld"
KERNEL_NM="llvm-nm"
KERNEL_OBJCOPY="llvm-objcopy"
KERNEL_OBJDUMP="llvm-objdump"
KERNEL_READELF="llvm-readelf"
KERNEL_STRIP="llvm-strip"
KERNEL_RANLIB="llvm-ranlib"
We also do the same for building the initramfs:
UTILS_AS="llvm-as"
UTILS_AR="llvm-ar"
UTILS_CC="clang"
UTILS_CXX="clang++"
UTILS_LD="ld.lld"
UTILS_NM="llvm-nm"
UTILS_OBJCOPY="llvm-objcopy"
UTILS_OBJDUMP="llvm-objdump"
UTILS_READELF="llvm-readelf"
UTILS_RANLIB="llvm-ranlib"
Note that we do not set the UTILS_STRIP="llvm-strip"
as this currently results in the following error:
llvm-strip: error: Link field value 34 in section .rela.plt is not a symbol table
* ERROR: Failed to strip '/var/tmp/genkernel/gk_dZRzC8UH/util-linux/image/sbin/blkid'!
* ERROR: create_initramfs(): append_data(): append_util-linux(): populate_binpkg(): gkbuild(): Failed to create binpkg of util-linux-2.37.2!
* Please consult '/var/log/genkernel.log' for more information and any
* errors that were reported above.
As genkernel
typically caches some of the build artifacts, we should also make sure to clear the cache to enforce genkernel
to rebuild the kernel and initramfs.
We can do this by setting the following option to yes
:
CLEAR_CACHEDIR="yes"
Now we should be able to run genkernel
as follows:
genkernel --clean --menuconfig --install all
Building the kernel with Rust support
Finally, we need to make sure that MODVERSIONS=n
, thus we need to deselect the following option in make menuconfig
:
Enable loading module support --->
[ ] Module versioning support
Now we should be able to set CONFIG_RUST=y
:
General setup --->
[*] Rust support
Then we can simply save the kernel configuration and let genkernel
build and install the kernel.
Kernel panic?
After building and installing both the kernel and our initramfs using genkernel
, we should now be able to reboot into our kernel.
Unfortunately, it seems to result in a kernel panic because busybox sh
seems to run into segmentation fault.
What is going on?
One quick way of testing what is going on with busybox sh
, we can build and install sys-apps/busybox using Clang and then run it with dev-util/valgrind.
Fortunately, the Gentoo wiki explains how to set up Portage to compile and build busybox with Clang.
To do so, we first need to create /etc/portage/env/compiler-clang
with the following contents:
COMMON_FLAGS="-O2 -march=native"
CFLAGS="${COMMON_FLAGS}"
CXXFLAGS="${COMMON_FLAGS}"
CC="clang"
CXX="clang++"
AR="llvm-ar"
NM="llvm-nm"
RANLIB="llvm-ranlib"
LDFLAGS="${LDFLAGS} -fuse-ld=lld -rtlib=compiler-rt -unwindlib=libunwind -Wl,--as-needed"
Then we can tell Portage to build busybox with Clang:
echo "sys-apps/busybox compiler-clang" >> /etc/portage/package.env/compiler
Then we install both busybox and valgrind using emerge
.
Running busybox sh
seems to result in the same Segmentation fault.
If we run valgrind busybox sh
instead, we get a better idea of what is going on:
valgrind: mmap(0x143000, 1093632) failed in UME with error 22 (Invalid argument).
valgrind: this can be caused by executables with very large text, data or bss segments.
Fortunately, the following bug report seems to describe a similar issue. Let's try installing busybox without PIE set (as Clang 15 now defaults to using PIE):
LDFLAGS="-no-pie" emerge busybox
Now if we run valgrind busybox sh
, we seem to run into another issue resulting in a segmentation fault:
==1795== Invalid write of size 4
==1795== at 0x47F0E0: ash_main (in /bin/busybox)
==1795== Address 0x4 is not stack'd, malloc'd or (recently) free'd
After trying to experiment a bit by copying over the ebuild and modifying it a bit to enable the USE_PORTABLE_CODE
and DEBUG
configuration options when building busybox:
busybox_config_option y USE_PORTABLE_CODE
busybox_config_option y DEBUG
We do get a working busybox sh
that no longer results in a segmentation fault.
After inspecting eix busybox
, it seems that version 1.35.0-r1 is available too.
Let's unmask it and install it to see what happens:
echo "sys-apps/busybox **" >> /etc/portage/package.accept_keywords/busybox
emerge busybox
All right, busybox sh
seems to work fine for version 1.35.0-r1 without it resulting in any segmentation faults when built with Clang.
So how do we tell genkernel
to use this version of busybox instead?
Well, genkernel
relies on a shell script to install the software /usr/share/genkernel/defaults/software.sh
.
More specifically, we can change the version of sys-apps/busybox that genkernel
should build and install (1.34.1-r1 at the time of writing):
GKPKG_BUSYBOX_PV="${GKPKG_BUSYBOX_PV:-1.34.1}"
We can simply change the above line to:
GKPKG_BUSYBOX_PV="${GKPKG_BUSYBOX_PV:-1.35.0}"
Then we can copy over the busybox-1.35.0.tar.bz
archive from our distfiles to the distfiles directory used by genkernel:
cp /var/cache/distfiles/busybox-1.35.0.tar.bz2 /usr/share/genkernel/distfiles
Now we run genkernel
again to rebuild and install the kernel:
genkernel --clean --no-menuconfig --install all
Testing our kernel
After booting into our new Linux kernel with Rust support, we can run zgrep RUST /proc/config.gz
to confirm that Rust support is enabled as shown in Figure 2.
Now that we have Rust support enabled in our Linux kernel, we can clone the basic template from before and try to compile and load it:
git clone https://github.com/Rust-for-Linux/rust-out-of-tree-module
Since we need to use the same Rust compiler that we used to build the Linux kernel, we might have to specify PATH="/usr/bin:$PATH"
in case the user has their own Rust compiler installed locally through rustup.
We should then be able to build the Linux kernel module as follows:
PATH="/usr/bin:$PATH" make
After building the kernel module, we should then be able to load and unload it as follows:
sudo insmod rust_out_of_tree.ko
sudo rmmod rust_out_of_tree.ko
If everything worked correctly, running dmesg
should show like the following:
[54488.118458] rust_out_of_tree: loading out-of-tree module taints kernel.
[54488.118500] rust_out_of_tree: module verification failed: signature and/or required key missing - tainting kernel
[54488.119702] rust_out_of_tree: Rust out-of-tree sample (init)
[54506.127820] rust_out_of_tree: My numbers are [72, 108, 200]
[54506.127825] rust_out_of_tree: Rust out-of-tree sample (exit)
Conclusion
We had to jump through quite some hoops, but in the end we managed to build and install a Linux kernel with Rust support enabled and build and load a basic kernel module written in Rust. It turns out that we needed an older version of the Rust compiler and bindgen installed to build the Linux kernel with Rust support. Hopefully, Gentoo will make these ebuilds available again for a more streamlined experience.
Furthermore, we had to use the Clang toolchain to build the Linux kernel, and optionally our initramfs. While genkernel seems to have proper support for this, we ran into an issue where version 1.34.1 of busybox results in a segmentation fault when compiled with Clang. Fortunately, this issue seems to be solved with version 1.35.0 and it was pretty straightforward to have genkernel use this version of busybox instead.
Maybe the sys-kernel/gentoo-kernel-bin package will come with Rust support too some day.
If you like my work or if my work has been useful to you in any way, then feel free to donate me a cup of coffee. Any donation is much appreciated!