📚 Part_1: Writing Your First Linux Kernel Module in Rust
Introduction
This section will focus on setting up our project to be compiled as a kernel module, which is a critical step in enabling our hypervisor to run. A kernel module acts as a piece of code that can be loaded and unloaded into the Linux kernel at runtime, allowing us to interact with the kernel's internal functions and access privileged hardware resources, such as virtualization features. This step is necessary for implementing the low-level functionality of our hypervisor.
As a reminder, there are two primary types of hypervisors: Type I and Type II. Type I hypervisors run directly on bare metal (the hardware), controlling the system's resources and directly managing virtual machines. In contrast, Type II hypervisors operate on top of an existing operating system, relying on the host OS for resource management.
A Type II hypervisor on Linux typically leverages the underlying operating system to access hardware virtualization features, such as VMX (Virtual Machine Extensions), which the processor provides. Existing solutions like QEMU use kernel modules like KVM (Kernel-based Virtual Machine) to interact with these hardware features. KVM acts as a bridge between the hypervisor and the kernel, enabling the management of virtual machine execution. However, we are building our own minimal hypervisor from scratch in this project, so we will not use existing hypervisor solutions. Instead, we will write a custom kernel module that directly manages VMX instructions, allowing our hypervisor to interact with the hardware without relying on KVM or similar modules.
Our minimal hypervisor will be written in Rust, a modern systems programming language known for its memory safety and performance. In recent years, the Rust-for-Linux project has enabled developers to write kernel modules in Rust, making it an ideal choice for our project. This approach combines the low-level control of kernel programming with the safety and reliability of Rust, ensuring that our custom module is both efficient and less prone to memory errors.
Kernel
To compile a kernel module, we must build the kernel itself. Several options are available. I will detail the commands and path for the Arch-based Linux distribution, but the same commands and path can be easily found for other distributions.
What we need is:
- The kernel build with the same version as the test machine running kernel and with rust support activated.
- The Rustc with the same version as the one used to compile the kernel.
There is a very straightforward way to use
pacman -S linux-headers rust
to install the kernel build in
/usr/lib/modules/$(shell uname—r)/build
and the
system-managed Rust. With this solution, there is minimal download and
build to be made. The only inconvenience is that installing the
system-managed Rust can conflict with Rustup, which is generally used to
manage toolchains easily.
The second solution is to build the kernel from the source:
- Determine the kernel using "uname -r"
-
Download the kernel source (repository can depend on distribution)
git clone git@github.com:archlinux/linux.git
- Go inside the repository and select the correct branch tags to match with the system kernel. The version should match exactly.
-
Either take the same kernel configuration
zcat /proc/config.gz > .config
or generate a minimal new configurationmake defconfig && make menuconfig
.
Simple Kernel Module
We now have all the build tools needed to compile our kernel module. Unlike a typical Rust crate, the module will be compiled using a Makefile. The primary challenge is that we cannot directly load a Rust module with external dependencies into the Linux kernel. To address this, we will take the following approach:
- Rust Crate with Hypervisor Logic: We'll create a regular Rust crate to house the logic for the hypervisor, which will be compiled using the usual Rust procedure via Cargo. This crate will expose a function using the C calling convention (extern "C"), allowing it to be used from within the kernel.
- Object File (.o): The Rust crate will be compiled into an object file (.o) containing the hypervisor's logic. This object file will then be linked with our kernel module, allowing the kernel module to access the hypervisor logic.
- Kernel Module: We will link the object file generated by the Rust crate in our kernel module. This intermediate step will allow us to use the functions from the Rust crate within the kernel module, utilizing the logic of the hypervisor as needed.
By separating the Rust crate logic and the kernel module and using an object file as a bridge, we can effectively integrate Rust with kernel-level functionality. This approach enables us to take advantage of Rust's power for kernel modules while adhering to the C conventions necessary for kernel development.
Our Rust code for our module will be
src/hypervisor_module.rs
#![allow(missing_docs)] use kernel::prelude::*; extern "C" { pub fn load_hypervisor(); } module! { type: Hypervisor, name: "testhypervisor", author: "Benjamin Gallois", description: "A simple VMX hypervisor module in Rust", license: "GPL", } struct Hypervisor; impl kernel::Module for Hypervisor { fn init(_module: &'static ThisModule) -> Result<Self> { unsafe { load_hypervisor(); } pr_info!("Our hypervisor is starting...\n"); Ok(Hypervisor) } }
Our Rust code for our library will be src/lib.rs
#![warn(missing_docs)] #[unsafe(no_mangle)] pub extern "C" fn load_hypervisor() { // The hypervisor will be loaded there }
And a Makefile to place in the same directory ./Makefile
obj-m += hypervisor_module.o hypervisor_module-y := src/hypervisor_module.o src/libhypervisor_test.o KERNELDIR :=./linux/ # Should be replaced to the kernel build PWD := $(shell pwd) RUST_RELEASE := release RUST_LIB_NAME := hypervisor_test RUST_LIB_PATH := target/$(RUST_RELEASE)/lib$(RUST_LIB_NAME).a RUST_FILES := src/*.rs all: hypervisor_module.ko hypervisor_module.ko: libhypervisor_test.o $(MAKE) -C $(KERNELDIR) M=$(PWD) modules $(RUST_LIB_PATH): cargo rustc --release -- --emit=obj libhypervisor_test.o: $(RUST_LIB_PATH) @cp target/$(RUST_RELEASE)/deps/hypervisor_test-*.o src/$@ @echo "cmd_target/$(RUST_RELEASE)/deps/hypervisor_test-*.o := cp $< src/$@" > src/.libhypervisor_test.o.cmd clean: cargo clean rm -rf *.o *~ core .depend *.mod.o .*.cmd *.ko *.mod.c *.mod rm -rf *.tmp_versions *.markers .*.symvers modules.order rm -rf Module.symvers rm -rf *.rmeta
Finally, we check our module by first loading the module and then displaying the kernel ring buffer and checking if our module is loading as expected.
sudo insmod src/hypervisor_module.ko sudo dmesg
Testing a Kernel Module and Hypervisor via Nested Virtualization
Introduction to KVM and QEMU
KVM (Kernel-based Virtual Machine) is a Linux kernel module that enables hardware virtualization by leveraging CPU virtualization extensions (Intel VT or AMD-V). QEMU is a powerful open-source emulator that, when combined with KVM, provides fast and efficient virtualization by running guest code directly on the host CPU. KVM and QEMU form a robust platform for testing operating systems, hypervisors, and low-level kernel modules in isolated environments.
One effective way to test a kernel module and hypervisor is to use a virtual machine via nested virtualization. The main advantage is safety: if something goes wrong, such as a system freeze or the need for a reboot, you can reset the virtual machine instead of physically rebooting your main development machine.
Nested virtualization can be a bit confusing, so let's clarify the terminology:
- The host is our physical computer.
- The QEMU guest is a VM you run on the host using QEMU/KVM that will also be our hypervisor host.
- Inside this guest, you run our hypervisor and test it by launching a minimal guest from it.
Step 1: Create a QEMU Disk Image
qemu-img create -f qcow2 kernel-host.qcow2 10G
sudo qemu-nbd -c /dev/nbd0 kernel-host.qcow2
sudo fdisk /dev/nbd0 //Create a partition here n -> p -> 1 -> enter -> w
sudo mkfs.ext4 /dev/nbd0p1
sudo mount /dev/nbd0p1 /mnt
sudo pacstrap /mnt base base-devel
sudo cp -r /lib/modules/6.14.4-arch1dev /mnt/lib/modules/
sudo arch-chroot /mnt
passwd
exit
sudo umount /mnt
sudo qemu-nbd -d /dev/nbd0
Step 2: Boot the QEMU Guest with the Host's Kernel
To ensure module compatibility, boot the QEMU guest using the same kernel as your host. Also, set up a shared folder pointing to your module build directory so you can access the compiled module in the guest.
sudo qemu-system-x86_64 \
-enable-kvm \
-m 2G \
-cpu host,+vmx \
-smp 2 \
-drive file=kernel-host.qcow2,if=virtio,format=qcow2,readonly=off \
-kernel /boot/vmlinuz-linuxdev \
-initrd /boot/initramfs-linuxdev.img \
-append "root=/dev/vda1 rw console=ttyS0" \
-serial mon:stdio \
-fsdev local,id=fsdev0,path=.,security_model=none \
-device virtio-9p-pci,fsdev=fsdev0,mount_tag=hostshare \
-net nic -net user
Step 3: Mount the Shared Folder in the QEMU Guest
Once the guest boots, mount the shared folder (if kernel support 9p, if not files sharing can be done using SCP):
sudo modprobe 9p
sudo mkdir -p /mnt/hostshare
sudo mount -t 9p -o trans=virtio hostshare /mnt/hostshare
Resetting the Environment
If the guest freezes or you need to restart it quickly, simply kill the QEMU process:
pkill qemu-system-x86_64
To reset the VM state completely, delete and recreate the disk image.
Conclusion
In this post, we have covered how to build a Linux kernel module using pure Rust and load it into the kernel. A key insight from this process is that the kernel build used to compile the module must match the running kernel version exactly. Any discrepancies in kernel headers or versions can cause errors or prevent the module from loading successfully.
The next step involves setting up a standard Rust crate for the hypervisor logic. However, there is a notable challenge: the kernel module cannot directly use external dependencies, such as libraries. To work around this limitation, we compiled our Rust-based hypervisor into a static object file (.o). This object file contains the hypervisor logic and is linked directly to the kernel module during the build process . By doing so, we can integrate the features and functionality of our hypervisor into the kernel module while maintaining the integrity and simplicity of the kernel module's build process.