Building and Loading Your Own Linux Kernel Module
Build, inspect, sign, load and remove a minimal out-of-tree Linux module inside a disposable VM with matching kbuild artifacts and safe rollback.
A loadable kernel module executes with kernel privilege and can corrupt memory, storage, or the host instantly. The correct first laboratory is a disposable virtual machine with a snapshot and console access—not a workstation or production server. The build, signing, loading, testing, and rollback path are all part of the module.
Linux supports in-tree and out-of-tree modules, but it does not promise a stable internal module ABI across arbitrary kernel builds. Match the running kernel’s build artifacts and configuration exactly.
Create an isolated laboratory
Use a VM dedicated to kernel development, take a powered-off snapshot, and ensure the hypervisor console works without networking. Keep valuable files and host directories unshared. Record:
uname -a
uname -r
cat /proc/version
cat /proc/sys/kernel/tainted
Install compiler, make, ELF/binutils tooling, and the distribution package that provides the prepared build tree for that exact kernel. Package names vary (linux-headers, kernel-devel, and others). /lib/modules/$(uname -r)/build must resolve to a configured/prepared tree; generic userspace headers are not enough.
Write the smallest observable module
Use pr_info() and a unique prefix. Return errors from initialization so a partial setup does not remain live:
// hello.c
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
pr_info("audit_hello: loaded\n");
return 0;
}
static void __exit hello_exit(void)
{
pr_info("audit_hello: unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Daniel Cosenza");
MODULE_DESCRIPTION("Minimal disposable-lab example");
MODULE_LICENSE() is metadata used by the kernel, including access to GPL-only exports and taint decisions. It does not authenticate the author, prove source correspondence, or make the object trustworthy.
Build through kbuild
An external module Makefile delegates compilation to the kernel build system:
obj-m := hello.o
.PHONY: all clean
all:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(CURDIR) modules
clean:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(CURDIR) clean
Run as the ordinary user in a clean source directory:
make V=1
file hello.ko
modinfo ./hello.ko
Never build as root. Review compiler warnings, generated .mod.c, symbol/version metadata, dependencies, and object architecture before loading.
Understand version matching
The vermagic field reflects kernel release and selected build properties. With CONFIG_MODVERSIONS, imported symbol CRCs add compatibility checks. Distribution kernels may maintain a kABI policy for supported modules, but upstream internal APIs can change without preserving external compatibility.
modinfo -F vermagic ./hello.ko
uname -r
Do not use insmod --force, remove version metadata, or rebuild against “close enough” headers. Boot the intended kernel or obtain its exact prepared build tree.
Inspect before granting kernel execution
Hash the source, Makefile, object, compiler, and build inputs. Examine metadata and undefined symbols:
sha256sum hello.c Makefile hello.ko
modinfo ./hello.ko
readelf -h -S -s ./hello.ko | less
nm -u ./hello.ko
modinfo output is supplied by the module and is not a security review. For third-party code, require source provenance, reproducible build evidence, license review, and security maintenance.
Account for module signing and lockdown
When signature enforcement or kernel lockdown applies, an unsigned/untrusted module is rejected. The kernel build system can sign modules, and distributions manage trusted keys through their Secure Boot process. A locally generated key must be enrolled through the platform’s supported governance; disabling Secure Boot is not a development shortcut.
Inspect relevant fields and kernel logs:
modinfo ./hello.ko | grep -E '^(signer|sig_key|sig_hashalgo):'
journalctl -k -b | tail -100
Signing proves possession of a trusted signing key, not code quality. Protect that private key outside the VM and never commit it.
Load only after opening a second console
Keep a second VM console ready. Capture the kernel log cursor, then load the local path:
sudo insmod ./hello.ko
lsmod | grep '^hello '
journalctl -k -b --since '1 minute ago'
insmod inserts one file and does not resolve dependencies. Installed modules are normally loaded with modprobe, which uses dependency and policy databases. Do not copy an experiment into /lib/modules or run depmod until packaging/install behavior is intentionally tested.
Check taint and runtime state
Loading out-of-tree, unsigned, proprietary, forced, or warning-producing modules can set taint flags. Taint is evidence retained for diagnostics; unloading usually does not erase it.
cat /proc/sys/kernel/tainted
cat /sys/module/hello/taint 2>/dev/null || true
ls -l /sys/module/hello
Decode the value with the current kernel taint documentation. Report it with bug evidence instead of hiding it by rebooting and omitting the prior event.
Make parameters least-privileged
module_param() exposes values under /sys/module/<name>/parameters. Permission bits determine sysfs access; a writable parameter becomes a live kernel control surface. Validate ranges and synchronization in the setter rather than exposing a raw variable when changes have side effects.
static unsigned int count = 1;
module_param(count, uint, 0444);
MODULE_PARM_DESC(count, "Number of messages at load (1..10)");
Validate count in init and reject out-of-range values. Prefer read-only 0444 for load-time configuration; world-writable modes are inappropriate.
Do not start with a hand-made device node
A robust character driver allocates a device number, initializes cdev, creates a class/device, implements file operations with correct lifetime/concurrency, and lets devtmpfs/udev create the node with policy-controlled ownership. A bare register_chrdev() plus mknod example omits teardown and access-control details.
Use the kernel driver model documentation before exposing userspace I/O. Every error path must unwind only resources already acquired, in reverse order. .owner = THIS_MODULE helps module lifetime but does not solve use-after-free in driver data.
Test failure and concurrency paths
Run static analysis and kernel-supported sanitizers in disposable kernels where practical: compiler warnings, sparse, Coccinelle, KASAN, KCSAN, lockdep, kmemleak, and fault injection each cover different classes. A successful load/unload says little about races.
Exercise repeated open/close, simultaneous readers, invalid lengths/pointers, interrupted operations, allocation failure, and unload while users hold references. Never dereference userspace pointers directly; use the documented copy_to_user()/copy_from_user() interfaces and handle partial copies.
Remove and prove cleanup
Unload only if the module supports it and is not in use:
sudo rmmod hello
test ! -e /sys/module/hello
journalctl -k -b --since '2 minutes ago'
Do not force removal. Confirm no device nodes, sysfs entries, workqueues, timers, IRQs, references, memory allocations, or pinned resources remain. Reboot/restore the VM snapshot after a panic, warning, sanitizer finding, or unexplained taint.
Package instead of copying by hand
For a maintained external module, use the distribution’s supported mechanism such as DKMS or akmods, with source version, patches, signing integration, build logs, and uninstall scripts. Rebuild/test for every kernel update before rebooting critical systems.
The experiment is complete when exact inputs reproduce the object, signature policy is respected, load/unload leaves no resources, negative tests do not warn or panic, and snapshot restoration is proven. Treat every one of those five conditions as independently required — a module that loads and unloads cleanly but hasn’t been checked against fault injection or concurrent access has only demonstrated the easy path works, not that it’s actually safe.
Related:
- Building and Loading a Custom Kernel Module with DKMS
- eBPF Explained: Safe, Programmable Observability in the Linux Kernel
Sources: