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 goto unwind 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, and kmemleak on 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-hello from the companion workspace read line by line.

Predict First

Write these down before you write any code.

  1. insmod a module whose init returns -EINVAL. What does insmod print? Does the module appear in lsmod? Does exit run?
  2. A module parameter declared with mode 0644 can be written through /sys while the module is loaded. What breaks if that parameter is the size of an array you allocated at load time?
  3. You allocate three things in init and the third fails. If you forget to free the first two, what does lsmod show? What does dmesg show? What does kmemleak show?
  4. rmmod a module while a pr_info from it is still in the log ring buffer. Does anything bad happen? Now: while a work item it queued is still pending?
  5. 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 -f is clean, and you can explain every fix you made.
  • modinfo shows description, author, license, version, and all three parameters with descriptions.
  • insmod with an out-of-range depth fails with -EINVAL, prints a useful message, and leaves nothing loaded.
  • Ten load/unload cycles leave kmemleak empty and slab usage flat.
  • Thirty fault-injected loads leave kmemleak empty.
  • The parameter trap is demonstrated and written up: what the writable version would have done.
  • pr_fmt is used; no printk in the file has a hand-typed module prefix.
  • modules/01-hello from 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.

  1. 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
  1. Build and load with MODULE_LICENSE("GPL"). Confirm it works.
  2. Change to MODULE_LICENSE("Proprietary"), rebuild, and load.
  3. 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

  1. Convert the unwind to a single-exit function. Rewrite lab_init so there is exactly one return at the end, using only goto. 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.)

  2. Add a sysfs attribute. Not a module parameter — a real one under /sys/kernel/lab/, using kobject_create_and_add and sysfs_create_group. Then explain why the device model chapter prefers dev_groups for a driver, and what race you have just reintroduced.

  3. Make depth genuinely reconfigurable. Allow writing depth at 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.

  4. Add MODULE_ALIAS and autoloading. Give the module an alias, run depmod, and load it by alias rather than by path. Then read /lib/modules/$(uname -r)/modules.alias and connect it to MODULE_DEVICE_TABLE from the device-model chapter.

  5. Measure __init savings. Print the size of .init.text in your module (objdump -h lab.ko | grep init), then remove all __init/__exit annotations and compare. Then find the total the kernel frees at boot: dmesg | grep -i "freeing unused kernel".

  6. Break vermagic on purpose. Build against a kernel with a different CONFIG_SMP or CONFIG_PREEMPT setting and try to load it. Read the dmesg message. Then look up MODULE_FORCE_LOAD and write down why forcing it is a bad idea.


Validation / Self-check

  1. What does insmod actually do — name the syscall and the three things the kernel does with the .ko.
  2. Why is there no isolation between a module and the rest of the kernel?
  3. Give the three rules of goto-based unwinding, and point at each one in your lab_init.
  4. Your init allocates three things and the third fails. Walk through exactly which labels run.
  5. Why does lab_exit use lab_nslots rather than depth? Describe the bug that would result.
  6. What does mode 0444 versus 0644 on a module_param change, and how do you decide?
  7. Why is kcalloc used rather than kmalloc(n * sizeof(*p))?
  8. What is pr_fmt for, and why must it be defined before the includes?
  9. What is vermagic, and name three config differences that would make a .ko refuse to load.
  10. What does EXPORT_SYMBOL_GPL enforce, and what does the kernel taint flag communicate to a maintainer?
  11. What is __init, what happens to it after load, and what is a section mismatch?
  12. Why does rmmod fail with -EBUSY sometimes, and why is that the desirable behavior?
  13. Your module loads and unloads cleanly ten times, but kmemleak reports 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.