Building a Linux kernel with Rust support on Gentoo

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:

menuconfig for RUST_CONFIG

Figure 1: make menuconfig showing the CONFIG_RUST option and its dependencies.

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.

Booting the Linux kernel with Rust support

Figure 2: the result of running zgrep RUST /proc/config.gz and uname -a after booting our Linux kernel with Rust support enabled.

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!