Écrire un module noyau en Rust

Depuis sa version 6.1, Linux supporte, en plus du C, le langage Rust pour écrire certains composants du noyau. Même si la prise en charge était minimaliste dans les débuts, il est maintenant possible d'écrire des modules noyau et de les charger sans avoir à recompiler Linux. C'est le principe des LKM (Loadable Kernel Module).
Dans cette article, je vous montre comment écrire un module noyau simple : un pilote pour un device
de type char
, générant des hurlements (i.e. "aAaaaAAhHHaaHH"), délire partant d'un message sur Mastodon.
Prérequis
Puisque le support de Rust est assez récent, il est nécessaire de mettre en place quelques outils afin d'activer Rust dans le noyau. Ces prérequis viennent de la documentation officielle du projet Rust for Linux. Nous allons voir ensemble comment satisfaire ces prérequis, qui sont :
- avoir un noyau Linux en version 6.3+
- disposer de la toolchain Rust
La version 6.3 étant assez récente (24 avril 2023) et le support de Rust étant désactivé par défaut sur la plupart des noyaux dans les distributions, nous allons devoir le compiler nous-mêmes. Pour savoir si Rust est activé dans le noyau que vous utilisez, on peut utiliser la commande grep CONFIG_RUST=y /boot/config-$(uname -r)
: si la ligne s'y trouve, alors Rust est disponible (et dans ce cas il n'est peut-être pas nécessaire de compiler Linux).
Pour des raisons de simplicité et aussi pour éviter de mettre le bazar sur votre machine personnelle, il est conseillé de réaliser les opérations dans une machine virtuelle.
Machine virtuelle
J'utilise pour cela Vagrant
, permettant de démarrer une machine rapidement. Le Vagrantfile
utilisé est celui-ci :
Vagrant.configure("2") do |config|
config.vm.box = "debian/bookworm64"
config.vm.provider "libvirt" do |v|
v.memory = 2048
v.cpus = 4
end
end
On l'enregistre dans un fichier Vagrantfile
puis on lance la commande vagrant up
:
‣ vagrant up
Bringing machine 'default' up with 'libvirt' provider...
...
==> default: Machine booted and ready!
Une fois démarrée, on lance la commande vagrant ssh
puis sudo su
pour être connecté en root
sur la machine :
‣ vagrant ssh
...
vagrant@bookworm:~$ sudo su
root@bookworm:/home/vagrant#
On va maintenant pouvoir préparer la machine pour faire tourner un noyau 6.3 avec le support de Rust.
Installer les dépendances de paquets
Installer les paquets nécessaires avec la commande suivante :
# apt update
# apt install -y flex bison clang lld build-essential llvm git libelf-dev libclang-13-dev libssl-dev tmux curl
Télécharger les sources du noyau Linux
On le télécharge directement depuis le dépôt git :
# git clone --depth=1 https://github.com/Rust-for-Linux/linux.git
# cd linux
Installer la toolchain Rust
Pour cela, lancer les commandes :
# curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# source $HOME/.cargo/env
# rustup component add rust-src
# rustup override set $(scripts/min-tool-version.sh rustc)
# cargo install --locked --version $(scripts/min-tool-version.sh bindgen) bindgen
Pour vérifier si le compilateur détecte correctement la disponibilité de Rust, on utilise :
# make LLVM=1 rustavailable
Rust is available!
À partir de là, on peut compiler Linux avec le support de Rust !
Compiler Linux
Nous allons compiler Linux en activant le support de Rust. Pour cela, nous allons éditer légèrement la configuration du noyau.
D'abord, on reprend la configuration de la distribution :
# cp /boot/config-$(uname -r) .config
# make oldconfig
Puis on l'édite :
# make menuconfig
et on active le support de Rust :
General setup ---> [*] Rust support
On quitte et on sauvegarde.
On peut lancer la compilation (ça va durer un certain temps) :
# make LLVM=1 -j4
On peut ensuite installer les modules et le noyau sur le système de la machine virtuelle
# make modules_install
# make install
On rédémarre la VM et on doit maintenant tourner sur le nouveau noyau :
# uname -a
Linux bookworm 6.3.0+ #2 SMP PREEMPT_DYNAMIC Sat Aug 12 09:01:08 UTC 2023 x86_64 GNU/Linu
On va pouvoir passer aux choses sérieuses !
Écrire le module en Rust
Le dépôt git du projet Rust for Linux fournit quelques exemples de modules que l'on peut écrire. Ce que l'on souhaite faire ici, c'est un device
de type char
qui renvoit une suite de caractères choisis aléatoirement dans l'alphabet aAhH
. On créé un fichier dev_scream.rs
, dans un nouveau dossier :
# mkdir rust_dev_scream
# cd rust_dev_scream
# touch dev_scream.rs
puis on y copie l'exemple rust_chrdev.rs
, ressemblant à ceci :
// SPDX-License-Identifier: GPL-2.0
//! Rust character device sample.
use kernel::prelude::*;
use kernel::{chrdev, file};
module! {
type: RustChrdev,
name: "rust_chrdev",
author: "Rust for Linux Contributors",
description: "Rust character device sample",
license: "GPL",
}
struct RustFile;
#[vtable]
impl file::Operations for RustFile {
fn open(_shared: &(), _file: &file::File) -> Result {
Ok(())
}
}
struct RustChrdev {
_dev: Pin<Box<chrdev::Registration<2>>>,
}
impl kernel::Module for RustChrdev {
fn init(name: &'static CStr, module: &'static ThisModule) -> Result<Self> {
pr_info!("Rust character device sample (init)\n");
let mut chrdev_reg = chrdev::Registration::new_pinned(name, 0, module)?;
// Register the same kind of device twice, we're just demonstrating
// that you can use multiple minors. There are two minors in this case
// because its type is `chrdev::Registration<2>`
chrdev_reg.as_mut().register::<RustFile>()?;
chrdev_reg.as_mut().register::<RustFile>()?;
Ok(RustChrdev { _dev: chrdev_reg })
}
}
impl Drop for RustChrdev {
fn drop(&mut self) {
pr_info!("Rust character device sample (exit)\n");
}
}
Nous allons commenter les parties qui nous intéressent.
On déclare les métadonnées de notre module :
module! {
type: RustChrdev, // Le type du module (ici char device)
name: "rust_chrdev", // le nom du module
author: "Rust for Linux Contributors",
description: "Rust character device sample",
license: "GPL",
}
Ensuite vient la fonction d'initialisation, permettant d'exécuter du code quand le module est chargé :
impl kernel::Module for RustChrdev {
fn init(name: &'static CStr, module: &'static ThisModule) -> Result<Self> {
pr_info!("Rust character device sample (init)\n");
let mut chrdev_reg = chrdev::Registration::new_pinned(name, 0, module)?;
// Register the same kind of device twice, we're just demonstrating
// that you can use multiple minors. There are two minors in this case
// because its type is `chrdev::Registration<2>`
chrdev_reg.as_mut().register::<RustFile>()?;
chrdev_reg.as_mut().register::<RustFile>()?;
Ok(RustChrdev { _dev: chrdev_reg })
}
}
dans cette fonction sont initialisées toutes les variables utiles à l'exécution du module. Il existe la même chose quand le module est déchargé :
impl Drop for RustChrdev {
fn drop(&mut self) {
pr_info!("Rust character device sample (exit)\n");
}
}
Ce code est quasiment satisfaisant, on change le nom du module en name: "rust_scream"
et il ne nous manque plus qu'une seule chose, la fonction qui gère la lecture du device
!
Pour cela, on ajoute la fonction read
à RustFile
, comme ceci :
#[vtable]
impl file::Operations for RustFile {
fn read(
_this: (),
_file: &file::File,
buf: &mut impl io_buffer::IoBufferWriter,
_: u64,
) -> Result<usize> {
let total_len = buf.len();
let mut chunkbuf = [0; 256];
while !buf.is_empty() {
let chunk = &mut chunkbuf[0..1];
kernel::random::getrandom_nonblock(chunk)?;
let r: usize = <u8 as Into<usize>>::into(chunk[0]) % ALPHABET.len();
let c = ALPHABET.chars().nth(r).unwrap();
buf.write_slice(&[c as u8])?;
}
Ok(total_len)
}
}
Et oui ! Nous sommes dans le noyau, ce qui signifie que nous n'avons pas accès à toute la bibliothèque standard de Rust, comme habituellement ! Ainsi, pour générer de l'aléatoire, on doit demander au noyau avec la fonction kernel::random::getrandom_nonblock(chunk)
qui remplit le chunk
d'aléa. Ici, nonblock
est satisfaisant car la qualité de l'aléa n'est pas critique.
On n'a plus qu'à compiler et charger le module ! Toujours dans le dossier rust_dev_scream
, faire :
# echo 'obj-m := dev_scream.o' > Kbuild
# make LLVM=1 -C /lib/modules/$(uname -r)/build M=$PWD modules
make: Entering directory '/vagrant/linux'
MODPOST /vagrant/rust_dev_scream/Module.symvers
make: Leaving directory '/vagrant/linux'
# make LLVM=1 -C /lib/modules/$(uname -r)/build M=$PWD modules_install
make: Entering directory '/vagrant/linux'
INSTALL /lib/modules/6.3.0+/updates/dev_scream.ko
SIGN /lib/modules/6.3.0+/updates/dev_scream.ko
DEPMOD /lib/modules/6.3.0+
make: Leaving directory '/vagrant/linux'
On voit que le fichier /lib/modules/6.3.0+/updates/dev_scream.ko
a été créé ! Chargeons maintenant le module :
# modprobe dev_scream
si tout se passe bien, vous devriez voir un device
rust_scream
dans /proc/devices
!
# grep rust_scream /proc/devices
248 rust_scream
Nous n'avons plus qu'à lui joindre un device
pour s'en servir :
# mknod /dev/scream c 248 0
et voilà, on peut lire dedans avec n'importe quelle commande :
# head -c 100 /dev/scream
AAaAHAaHhHAHHAHhHHAHHhhHHHAHhAHHAAAhAAAHAaHHAaAHhHhHAaaAHhHHaHaaAHHhHaahHAHaaaHHaaAHhahAhhAhAhAHAAAA
L'ensemble du code source peut être trouvé sur ce dépôt git.