[ blog » 2022 » 09_rust-kernel-module ] [PART 1/3] Corroding the Kernel: CONFIG_RUST=y (by Philipp Gesang)

The Development Setup

At the time of writing this series none of the Rust integration has made it into mainline yet, so to obtain a Rust-enabled kernel one needs either the most recent patchset from the kernel mailing list or the tree from Rust for Linux repo:

$ git remote add rust https://github.com/Rust-for-Linux/linux.git

The code is still experimental so it would probably be unwise to unleash it on a live system. I picked an old VM I had lying around for debugging kernel issues; I found Arch particularly suitable for this task as it has hooks for adding unpackaged kernels and initrds, and stays out of your way when modifying the system.

What’s interesting about the Rust for Linux repository is that the maintainers use Github for tracking issues and will actually consider pull requests submitted there. The project of course has a Mailing list, as is tradition, and new iterations of the patchset will be posted on the LKML.

After launching menuconfig on the rust branch, chances are none of the Rust related options are shown. This is because they depend on CONFIG_RUST_IS_AVAILABLE=y which is determined at config time. The rustavailable target can be used to check whether Kconfig considers Rust “available”:

$ make LLVM=1 rustavailable
*** Rust bindings generator 'bindgen' is too new. This may or may not work.
***   Your version:     0.60.1
***   Expected version: 0.56.0
Rust is available!

A slight toolchain mismatch but it works out in practice. Besides bindgen a number of other tools need to be present that aren’t usually required for building the kernel: llvm, clang, the Rust compiler and source code. Not just any version will do, it needs to be a fairly recent Rust 1.62 toolchain. A stable toolchain, that is, which doesn’t require any out-of-tree patches anymore as all the necessary changes seem to have been merged into Rust at this point. It does enable a number of unstable features though. The quick-start guide has detailed instructions on how to obtain the right versions of these tools; in most cases it will boil down to feeding the output of a script into rustup. Passing LLVM=1 is mandatory; fingers crossed the LLVM dependency will be replaced by GCC-rs at some point in the future.

With all the prerequisites in place, the Rust related menu entries appear. Be sure to enable the examples under “Kernel hacking → Sample kernel code → Rust samples” as well. With CONFIG_RUST=y set, the kernel can now be built with the usual make && make modules && make bzImage sequence. No additional commands are required – which confirms the impression that Rust support is not just bolted on but first-class.

The resulting additions to the config look something like this:

$ zgrep _RUST /proc/config.gz
CONFIG_RUSTC_VERSION_TEXT="rustc 1.62.0 (a8314ef7d 2022-06-27)"
# […] skip some samples

During the build various Rust related tools will then show up as “quiet commands”:

DESCEND objtool
CALL    scripts/atomic/check-atomics.sh
CALL    scripts/checksyscalls.sh
RUSTC L rust/bindings.o
EXPORTS rust/exports_bindings_generated.h
RUSTC L rust/kernel.o
EXPORTS rust/exports_kernel_generated.h
CHK     include/generated/compile.h

The Kernel-Side Rust Environment

Enabling Rust results in a large number of symbols getting added, some unmangled and readable but many more mangled ones:

0x00000000      _RNvXs_NtCs3yuwAp0waWO_4core5asciiNtB4_13EscapeDefaultNtNtNtNtB6_4iter6traits12double_ended19DoubleEndedIterator9next_back      vmlinux EXPORT_SYMBOL_GPL

Now wait, this doesn’t resemble traditional Rust mangling at all! Where are my dollar signs‽ Rather, this looks a lot like the still somewhat experimental v0 mangling style. And sure enough, a comment in rust/exports.c confirms this. Mangled symbols are quite unwieldy compared to the flat C style symbols; the longest name in my kernel’s vmlinux.symvers is 163 characters long which exceeds the cap on symbol length in current mainline kernels (128). Part of the Rust support is a set of patches that lift these size requirements and raise the limit to 512 characters.

Ultra long symbol names notwithstanding, the impact of Rust on kernel image size is moderate. My stripped down version of the Archlinux .config results in a zlib compressed image of 7'953'536 bytes with a regular v5.19 kernel, and 8'131'232 bytes with CONFIG_RUST=y; CC=clang was used in both cases.

The makefile sets some rather exotic flags on the core and alloc crates:

core-cfgs = \
    --cfg no_fp_fmt_parse

alloc-cfgs = \
    --cfg no_global_oom_handling \
    --cfg no_rc \
    --cfg no_sync

no_fp_fmt_parse is self-explatory in kernel context where we don’t have floating point arithmetic at our disposal, so it doesn’t come as a surprise that the flag was added to Rust explicitly for that purpose. Likewise no_global_oom_handling which seems to have originated in a comment of Linus’ on an earlier stage of the Rust patchset. It disables a large number of “pseudo-infallible” APIs like for e. g. Box::new() which simply panic!() on OOM. Part two of this series will discuss in more detail what effects this has on how to write Rust code to run in kernel space. Finally, the alloc::sync and alloc::rc modules are filtered out because the kernel provides its own implementations for refcounting and synchronization primitives.

As for the tooling, both rustfmt and clippy are made available through build rules, but they don’t seem to be integrated in checkpatch.pl yet. (It does perform some basic checks on Rust files though, for example the validation of SPDX headers.)


gallery image thumbnail