Apr 28, 2025by Benjamin Gallois

📚 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:

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:

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:

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:

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.