Lab 2: Your First Module (Milestone 2)
Background
A kernel module is a .ko file the kernel links into itself at runtime. There is no sandbox, no
verification, and no privilege boundary: insmod relocates your code into kernel address space and
calls your init function at ring 0.
This lab writes one. It is deliberately small, and the lesson is not the "hello world" — it is the error path, the teardown, and the parameter that becomes a bug if you are not careful.
Why This Lab Matters
- Every driver you will ever write is this module plus a subsystem registration.
- The
gotounwind from Kernel C is the single most-reviewed pattern in the kernel, and this is where you write it for real. - The load/unload cycle is your test harness for the rest of Foundations.
checkpatch,sparse, andkmemleakon your own code, before anyone else runs them, is the habit that makes Lab 7 uneventful.
Prerequisites
- Lab 1 complete: you can build, boot, and load a module from
/mnt/host. - Kernel C read, especially the error-handling section.
- Memory read as far as GFP flags.
modules/01-hellofrom the companion workspace read line by line.
Predict First
Write these down before you write any code.
insmoda module whoseinitreturns-EINVAL. What doesinsmodprint? Does the module appear inlsmod? Doesexitrun?- A module parameter declared with mode
0644can be written through/syswhile the module is loaded. What breaks if that parameter is the size of an array you allocated at load time? - You allocate three things in
initand the third fails. If you forget to free the first two, what doeslsmodshow? What doesdmesgshow? What doeskmemleakshow? rmmoda module while apr_infofrom it is still in the log ring buffer. Does anything bad happen? Now: while a work item it queued is still pending?- What does
MODULE_LICENSE("Proprietary")change about which functions you can call?
The Target
insmod lab.ko depth=8 label=lab
│
▼
module_init(lab_init)
├── validate parameters → -EINVAL if bad
├── kcalloc(depth, ...) → -ENOMEM
├── kasprintf per slot → -ENOMEM, unwind the earlier ones
├── kzalloc(PAGE_SIZE) → -ENOMEM, unwind everything
└── return 0 → module is now loaded
│
├── /sys/module/lab/parameters/{depth,label,verbose}
├── lsmod / /proc/modules
└── modinfo lab.ko
│
rmmod lab
▼
module_exit(lab_exit) → free everything, in reverse order
Step-by-Step Tasks
Step 1: The Makefile
# SPDX-License-Identifier: GPL-2.0
obj-m += lab.o
KDIR ?= $(HOME)/kernel/build
all:
$(MAKE) -C $(KDIR) M=$(CURDIR) modules
check:
$(MAKE) -C $(KDIR) M=$(CURDIR) C=2 W=1 modules
clean:
$(MAKE) -C $(KDIR) M=$(CURDIR) clean
.PHONY: all check clean
M=$(CURDIR) tells kbuild "build the modules in this external directory against the kernel in
KDIR". The check target runs sparse (C=2) and the extra warnings (W=1) — the same two
things the 0-day bot will run on your patch.
Step 2: The module
lab.c:
// SPDX-License-Identifier: GPL-2.0
/*
* A first out-of-tree module: parameters, a real error path, and a clean
* teardown. Everything here is boring on purpose; the interesting parts
* are the unwind and the parameter trap.
*/
/* Prefix every pr_* with the module name, automatically. Must come BEFORE
* the includes, because printk.h uses it. */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/slab.h>
#define LAB_MAX_DEPTH 64
static int depth = 4;
module_param(depth, int, 0444); /* READ-ONLY. See step 5 for why. */
MODULE_PARM_DESC(depth, "number of slots to allocate (1-" __stringify(LAB_MAX_DEPTH) ")");
static char *label = "lab";
module_param(label, charp, 0444);
MODULE_PARM_DESC(label, "prefix for slot names");
static bool verbose;
module_param(verbose, bool, 0644); /* writable: it changes nothing structural */
MODULE_PARM_DESC(verbose, "print one line per slot");
struct lab_slot {
int id;
char *name;
};
static struct lab_slot *lab_slots;
static unsigned int lab_nslots; /* the count we ACTUALLY allocated */
static char *lab_scratch;
static void lab_free_slots(unsigned int n)
{
unsigned int i;
for (i = 0; i < n; i++)
kfree(lab_slots[i].name);
kfree(lab_slots);
lab_slots = NULL;
lab_nslots = 0;
}
static int __init lab_init(void)
{
unsigned int i;
int ret;
if (depth < 1 || depth > LAB_MAX_DEPTH) {
pr_err("depth must be 1..%d (got %d)\n", LAB_MAX_DEPTH, depth);
return -EINVAL;
}
/* kcalloc, not kmalloc(n * size): the multiplication is checked. */
lab_slots = kcalloc(depth, sizeof(*lab_slots), GFP_KERNEL);
if (!lab_slots)
return -ENOMEM;
/* Snapshot the count we allocated with. Never re-read `depth` later. */
lab_nslots = depth;
for (i = 0; i < lab_nslots; i++) {
lab_slots[i].name = kasprintf(GFP_KERNEL, "%s-%u", label, i);
if (!lab_slots[i].name) {
ret = -ENOMEM;
goto err_free_slots; /* frees names 0..i-1 and the array */
}
lab_slots[i].id = i;
if (verbose)
pr_info("slot %u = %s\n", i, lab_slots[i].name);
}
lab_scratch = kzalloc(PAGE_SIZE, GFP_KERNEL);
if (!lab_scratch) {
ret = -ENOMEM;
goto err_free_all_slots;
}
pr_info("loaded: depth=%u label=%s\n", lab_nslots, label);
return 0;
/* Labels named for what they undo, falling through in reverse order
* of acquisition. Jump to the one that undoes the step BEFORE the
* one that failed. */
err_free_all_slots:
i = lab_nslots;
err_free_slots:
lab_free_slots(i);
return ret;
}
static void __exit lab_exit(void)
{
kfree(lab_scratch);
lab_scratch = NULL;
lab_free_slots(lab_nslots); /* the SNAPSHOT, not `depth` */
pr_info("unloaded\n");
}
module_init(lab_init);
module_exit(lab_exit);
MODULE_DESCRIPTION("Foundations Lab 2: parameters, error paths, teardown");
MODULE_AUTHOR("You <you@example.com>");
MODULE_LICENSE("GPL");
MODULE_VERSION("0.1");
Step 3: Build and inspect it before loading
make
ls -lh lab.ko
modinfo lab.ko # your metadata, parameters, and vermagic
nm lab.ko | grep -E ' [tT] ' # your symbols
Predict before running modinfo: what does the vermagic line contain, and what happens if you
try to load this .ko on a kernel built with a different config?
Step 4: Load, poke, unload
# In the guest:
insmod /mnt/host/lab/lab.ko depth=8 label=slot verbose=1
dmesg | tail -12
lsmod | head -3
cat /proc/modules | head -3
ls /sys/module/lab/
cat /sys/module/lab/parameters/depth
cat /sys/module/lab/parameters/label
cat /sys/module/lab/parameters/verbose
echo 1 > /sys/module/lab/parameters/verbose # writable
echo 9 > /sys/module/lab/parameters/depth # NOT writable — what happens?
rmmod lab
dmesg | tail -3
Step 5: The parameter trap
Change depth's mode from 0444 to 0644, rebuild, and reload. Then:
insmod /mnt/host/lab/lab.ko depth=8
echo 64 > /sys/module/lab/parameters/depth # now allowed
rmmod lab # ← what does exit free?
Predict first: you allocated 8 slots. depth now reads 64. If lab_exit had used depth
instead of the snapshot lab_nslots, what would it do?
This is why the module above snapshots the count, and it is a real bug class: a writable module
parameter that is also a structural constant. If a parameter controls sizes, allocation counts, or
anything you act on at load time, make it read-only (0444) or snapshot it and never re-read it.
Verify the correct version behaves:
insmod /mnt/host/lab/lab.ko depth=8
echo 64 > /sys/module/lab/parameters/depth 2>/dev/null || echo "read-only, good"
rmmod lab
Step 6: Exercise the error paths
Fault injection makes the paths nothing tests actually run:
# On a lab-paranoid kernel:
F=/sys/kernel/debug/failslab
echo 30 > $F/probability
echo -1 > $F/times
echo N > $F/ignore-gfp-wait
for i in $(seq 30); do insmod /mnt/host/lab/lab.ko depth=16 2>/dev/null && rmmod lab; done
echo 0 > $F/probability
echo scan > /sys/kernel/debug/kmemleak
sleep 5
echo scan > /sys/kernel/debug/kmemleak # scan twice: the first may report transients
cat /sys/kernel/debug/kmemleak
Predict first: with a 30% failure rate and 17 allocations per load, roughly what fraction of loads should fail? And of those failures, how many leak?
An empty kmemleak output is the pass condition. Anything in it is a real bug in your unwind.
Step 7: The ten-cycle check
for i in $(seq 10); do
insmod /mnt/host/lab/lab.ko depth=16 || { echo "load $i failed"; break; }
rmmod lab || { echo "unload $i failed"; break; }
done
dmesg | tail -5
grep -E '^(Slab|SReclaimable)' /proc/meminfo
echo scan > /sys/kernel/debug/kmemleak; cat /sys/kernel/debug/kmemleak
Slab usage must return to roughly where it started, and kmemleak must be empty.
Step 8: Run the checkers
# On the host:
make check # sparse (C=2) and W=1
~/kernel/linux/scripts/checkpatch.pl --no-tree --strict -f lab.c
Fix everything, and be able to explain each fix. Do not silence a warning you do not understand —
checkpatch is occasionally wrong, and knowing which times those are is a skill you build by
understanding the times it is right.
Implementation Requirements / Deliverables
-
The module builds with no warnings under
make check(C=2 W=1). -
checkpatch.pl --strict -fis clean, and you can explain every fix you made. -
modinfoshows description, author, license, version, and all three parameters with descriptions. -
insmodwith an out-of-rangedepthfails with-EINVAL, prints a useful message, and leaves nothing loaded. -
Ten load/unload cycles leave
kmemleakempty and slab usage flat. -
Thirty fault-injected loads leave
kmemleakempty. - The parameter trap is demonstrated and written up: what the writable version would have done.
-
pr_fmtis used; noprintkin the file has a hand-typed module prefix. -
modules/01-hellofrom the companion workspace read and annotated.
Expected Output
# insmod /mnt/host/lab/lab.ko depth=8 label=slot verbose=1
[ 42.113] lab: slot 0 = slot-0
[ 42.113] lab: slot 1 = slot-1
...
[ 42.114] lab: loaded: depth=8 label=slot
# lsmod
Module Size Used by
lab 16384 0
# ls /sys/module/lab/parameters/
depth label verbose
# cat /sys/module/lab/parameters/depth
8
# echo 9 > /sys/module/lab/parameters/depth
sh: write error: Permission denied ← 0444, as intended
# insmod /mnt/host/lab/lab.ko depth=999
insmod: ERROR: could not insert module lab.ko: Invalid parameters
[ 58.220] lab: depth must be 1..64 (got 999)
# rmmod lab
[ 61.004] lab: unloaded
Debugging Steps
insmod: ERROR: could not insert module: Invalid module format
The .ko was built against a different kernel. Compare:
modinfo lab.ko | grep vermagic
uname -r # in the guest
Rebuild with KDIR pointing at the build directory for the kernel you actually booted.
insmod: ERROR: could not insert module: Unknown symbol in module
You called something that is not exported, or that is exported _GPL while your
MODULE_LICENSE is not GPL-compatible. dmesg names the symbol:
dmesg | tail -5
sudo grep -w '<the symbol>' /proc/kallsyms
rg -n "EXPORT_SYMBOL(_GPL)?\(<the symbol>\)" ~/kernel/linux/
insmod succeeds but nothing appears in dmesg
The console log level is filtering it. pr_info is level 6:
cat /proc/sys/kernel/printk # current, default, minimum, boot
echo 8 > /proc/sys/kernel/printk # show everything on the console
dmesg | tail # it was in the ring buffer all along
rmmod: ERROR: Module lab is in use
Something holds a reference. lsmod's third column names the dependents; for a char device it is an
open file descriptor (which is the correct behavior — see
the device model).
The module loads but rmmod hangs
An unfinished worker, timer, or thread. This lab has none, so if you hit it you added one — see Deferred Work.
modpost: WARNING: modpost: missing MODULE_DESCRIPTION()
Exactly what it says. Recent kernels warn (and with W=1, loudly) because a module with no
description is unhelpful in modinfo. Add one.
Section mismatch in reference from the function X() to the function .init.text:Y()
You called an __init function from non-init code. __init functions are discarded after boot (or
after module load), so the call would jump into freed memory. Remove the annotation, or do not call
it.
kmemleak reports something after a clean-looking cycle
Scan twice, several seconds apart — the first scan reports transients that are still referenced. If it persists, the backtrace in the report names the allocation site:
echo scan > /sys/kernel/debug/kmemleak; sleep 10
echo scan > /sys/kernel/debug/kmemleak; cat /sys/kernel/debug/kmemleak
echo clear > /sys/kernel/debug/kmemleak # reset between experiments
Experiment
CLAIM. MODULE_LICENSE is not metadata — it changes which kernel functions your module is
allowed to link against, and it taints the kernel.
METHOD.
- Add a call to a
_GPL-only symbol. Pick one from your tree:
rg -n "EXPORT_SYMBOL_GPL" kernel/sched/core.c kernel/workqueue.c | head -5
- Build and load with
MODULE_LICENSE("GPL"). Confirm it works. - Change to
MODULE_LICENSE("Proprietary"), rebuild, and load. - Check the taint state either way:
cat /proc/sys/kernel/tainted
$EDITOR ~/kernel/linux/Documentation/admin-guide/tainted-kernels.rst
dmesg | grep -i taint
PREDICTION. Before running: (a) does step 3 fail at build time, at load time, or neither?
(b) what does /proc/sys/kernel/tainted read in each case? (c) if you now cause an oops, what
appears on the Tainted: line of the report, and how would a maintainer react to receiving it?
RESULT. Record all three. Then connect it back to
why the license matters:
EXPORT_SYMBOL_GPL is a licence boundary enforced by the linker, and the taint flag is how a
maintainer knows not to spend their evening on your bug report.
Test
Kernel modules are tested from outside. A shell test that drives load/unload and asserts on dmesg
is the right shape here; Lab 6 turns this into a real kselftest.
cat > test-lab.sh <<'EOF'
#!/bin/sh
# Run inside the guest. Exits 0 on success.
set -e
KO=${1:-/mnt/host/lab/lab.ko}
fail() { echo "FAIL: $*"; exit 1; }
# 1. A valid load works and reports the depth we asked for.
insmod "$KO" depth=8 label=t
dmesg | tail -1 | grep -q "depth=8" || fail "wrong depth reported"
[ "$(cat /sys/module/lab/parameters/depth)" = 8 ] || fail "sysfs depth wrong"
rmmod lab
# 2. depth is read-only.
insmod "$KO" depth=8
if echo 64 > /sys/module/lab/parameters/depth 2>/dev/null; then
rmmod lab; fail "depth must not be writable"
fi
rmmod lab
# 3. An invalid depth is rejected, and nothing is left loaded.
if insmod "$KO" depth=999 2>/dev/null; then
rmmod lab; fail "depth=999 should have been rejected"
fi
lsmod | grep -q '^lab ' && fail "module loaded despite failing init"
# 4. Ten cycles leave nothing behind.
i=0
while [ $i -lt 10 ]; do insmod "$KO" depth=16; rmmod lab; i=$((i+1)); done
echo scan > /sys/kernel/debug/kmemleak 2>/dev/null || true
sleep 2
echo scan > /sys/kernel/debug/kmemleak 2>/dev/null || true
if [ -s /sys/kernel/debug/kmemleak ]; then
cat /sys/kernel/debug/kmemleak; fail "kmemleak reported a leak"
fi
echo PASS
EOF
chmod +x test-lab.sh
Verify the test can fail. Comment out one kfree in lab_free_slots, rebuild, re-run. If the
test still passes, the test is wrong, not the code.
Challenge Extensions
-
Convert the unwind to a single-exit function. Rewrite
lab_initso there is exactly onereturnat the end, using onlygoto. Then decide which version you find more reviewable, and write one sentence defending your answer. (Both styles exist in the tree; the maintainers of the file you are patching have an opinion, and matching it is the actual rule.) -
Add a sysfs attribute. Not a module parameter — a real one under
/sys/kernel/lab/, usingkobject_create_and_addandsysfs_create_group. Then explain why the device model chapter prefersdev_groupsfor a driver, and what race you have just reintroduced. -
Make
depthgenuinely reconfigurable. Allow writingdepthat runtime, and make the module correctly reallocate. You will need a lock, and you will discover which one. This is much harder than it looks — which is the point, and why "make it read-only" is usually the right answer. -
Add
MODULE_ALIASand autoloading. Give the module an alias, rundepmod, and load it by alias rather than by path. Then read/lib/modules/$(uname -r)/modules.aliasand connect it toMODULE_DEVICE_TABLEfrom the device-model chapter. -
Measure
__initsavings. Print the size of.init.textin your module (objdump -h lab.ko | grep init), then remove all__init/__exitannotations and compare. Then find the total the kernel frees at boot:dmesg | grep -i "freeing unused kernel". -
Break
vermagicon purpose. Build against a kernel with a differentCONFIG_SMPorCONFIG_PREEMPTsetting and try to load it. Read thedmesgmessage. Then look upMODULE_FORCE_LOADand write down why forcing it is a bad idea.
Validation / Self-check
- What does
insmodactually do — name the syscall and the three things the kernel does with the.ko. - Why is there no isolation between a module and the rest of the kernel?
- Give the three rules of
goto-based unwinding, and point at each one in yourlab_init. - Your init allocates three things and the third fails. Walk through exactly which labels run.
- Why does
lab_exituselab_nslotsrather thandepth? Describe the bug that would result. - What does mode
0444versus0644on amodule_paramchange, and how do you decide? - Why is
kcallocused rather thankmalloc(n * sizeof(*p))? - What is
pr_fmtfor, and why must it be defined before the includes? - What is
vermagic, and name three config differences that would make a.korefuse to load. - What does
EXPORT_SYMBOL_GPLenforce, and what does the kernel taint flag communicate to a maintainer? - What is
__init, what happens to it after load, and what is a section mismatch? - Why does
rmmodfail with-EBUSYsometimes, and why is that the desirable behavior? - Your module loads and unloads cleanly ten times, but
kmemleakreports a leak after fault injection. What class of bug is that, and where is it?
Next: Lab 3 — A Character Device, where user space can finally reach your code — and where the first real race appears.