Linux Kernel Modules: Auto-Loading, Security & Coding Style

Linux Kernel Module Auto-loading, Security and Coding Style | Free Linux Kernel Programming Course

EmbeddedPathashalaFree Linux Kernel Programming Course › Auto-loading, Security & Kernel Coding Style

Linux Kernel Module Auto-loading, Security, and Kernel Coding Style

Free Linux Kernel Programming Course — LKMs Part 2, Section 3

Level
Intermediate
Read Time
~24 min
Kernel
Linux 6.x
Cost
Free

Part of the Free Linux Kernel Programming Course at EmbeddedPathashala — updated for Linux 6.x

Topics Covered:
Auto-loading at Boot /etc/modules-load.d Kernel Module Security Module Signing Kernel Lockdown Linux Coding Style Contributing to Mainline Free Kernel Course

What You Will Learn

  • How to make a Linux kernel module auto-load at system boot using modern systemd-based and traditional methods
  • How to pass boot-time parameters to auto-loaded modules
  • The key security concerns around kernel modules — module signing, Secure Boot, and kernel lockdown mode
  • How module signing works and how to sign your own modules for production systems
  • The complete Linux kernel coding style guide — indentation, naming, braces, comments, and function length
  • How to use checkpatch.pl effectively and what to do when it complains
  • An overview of how to contribute a kernel module or driver to the mainline Linux kernel

Auto-loading Linux Kernel Modules at System Boot

Manually loading a module with insmod every time you reboot is not practical for production systems. Linux provides clean mechanisms to automatically load kernel modules at boot time. On modern Linux distributions using systemd (which is the standard on Ubuntu, Fedora, Debian, and Raspberry Pi OS), the preferred method is the /etc/modules-load.d/ directory.

Linux Module Auto-loading Flow at System Boot
System Boot Sequence
1. Bootloader
GRUB/U-Boot loads kernel image
2. Kernel Init
Built-in modules initialized
3. systemd
Reads /etc/modules-load.d/*.conf
4. systemd-modules-load.service
Calls modprobe for each module listed → modules loaded with parameters from /etc/modprobe.d/*.conf
Result: your module is available before any user services start

Method 1: /etc/modules-load.d/ (Recommended for systemd systems)

# Create a .conf file for your module in /etc/modules-load.d/
# The filename can be anything ending in .conf
sudo nano /etc/modules-load.d/my_lkm.conf

# Add the module name (one per line, no .ko extension)
my_lkm

# Reboot and verify
sudo reboot

# After reboot, check if it loaded
lsmod | grep my_lkm
# my_lkm    16384  0

# Or check systemd's module loading service
sudo systemctl status systemd-modules-load.service

Method 2: /etc/modules (Traditional, still works)

# The traditional /etc/modules file works on all distros
# It is processed by systemd on modern systems, but is the legacy approach
sudo nano /etc/modules

# Add your module name at the end
# /etc/modules: kernel modules to load at boot time
my_lkm
helper_module

Passing Parameters to Auto-loaded Modules

When a module auto-loads, you cannot pass parameters the same way as insmod. Instead, you use a separate configuration file in /etc/modprobe.d/:

# Create a modprobe configuration file for your module
sudo nano /etc/modprobe.d/my_lkm.conf

# Syntax: options module_name param1=value1 param2=value2
options my_lkm debug_level=2 timeout_ms=500

# This applies every time the module is loaded —
# whether manually via modprobe or automatically at boot
# Verify the options are being read
modprobe --show-options my_lkm
# options my_lkm debug_level=2 timeout_ms=500

# You can also blacklist a module to prevent auto-loading
# (useful for disabling kernel-built drivers you want to replace)
sudo nano /etc/modprobe.d/blacklist-nouveau.conf
# blacklist nouveau
# options nouveau modeset=0

Auto-loading via udev (Hardware Detection)

For hardware drivers, there is a third mechanism: udev automatic loading. When a device is plugged in or detected at boot, udev reads the module’s alias table to figure out which kernel module handles that device, then calls modprobe automatically. This is how USB drives, WiFi cards, and other hot-pluggable devices load their drivers without any manual configuration.

# Check what device aliases a module declares
modinfo usb_storage | grep alias
# alias:  usb:v*p*d*dc*dsc*dp*ic08isc06ip50in*
# (this means: any USB device with device class 08, subclass 06, protocol 50)

# Check all module aliases registered in the system
cat /lib/modules/$(uname -r)/modules.alias | head -20

# When a USB device is plugged in:
# 1. udev gets a hotplug event with the device's vendor/product/class IDs
# 2. udev matches the IDs against modules.alias
# 3. udev calls: modprobe usb_storage (or whatever matches)
# 4. The driver module loads automatically

# In your own driver, declare alias using MODULE_DEVICE_TABLE
# This is covered in detail in the device driver chapters

Kernel Module Security — What Every Developer Must Understand

Kernel modules are a significant security surface. Because they run at the highest privilege level, a malicious or buggy kernel module can compromise the entire system. Modern Linux kernels include several security mechanisms to address this. Understanding them is essential for professional free Linux device driver and kernel module development.

The Kernel Module Security Threat Model

Kernel Module Security Layers — Defense in Depth
Layer Mechanism What It Prevents
1 (Outermost) Root privilege required for insmod/modprobe Unprivileged users loading arbitrary modules
2 Module signature verification Loading unsigned or tampered modules
3 Secure Boot (UEFI) + kernel lockdown Bypassing signature checks by rebooting into unsigned kernel
4 SELinux / AppArmor policies Restricting which processes can call insmod/modprobe
5 (Innermost) KASAN, LOCKDEP (debug kernels) Runtime memory and locking bugs in module code

Module Signing

Module signing is a mechanism where each .ko file carries a cryptographic signature. When the kernel loads a module, it verifies the signature against a trusted certificate embedded in the kernel. This prevents attackers from loading a malicious kernel module even if they have root access.

On Ubuntu, Debian, and Fedora with Secure Boot enabled, the kernel is configured to require module signatures (CONFIG_MODULE_SIG_FORCE=y). Out-of-tree modules (like yours) must be signed with a key that the kernel trusts.

# Step 1: Generate your own signing key and certificate
# The kernel build system provides a helper script for this
cd /usr/src/linux-headers-$(uname -r)

# Generate a signing key pair (valid for 3 years, RSA 4096-bit)
openssl req -new -x509 -newkey rsa:4096 \
    -keyout signing_key.pem \
    -out signing_cert.pem \
    -days 1095 -nodes \
    -subj "/CN=My LKM Signing Key/"

# Step 2: Sign the compiled module
/usr/src/linux-headers-$(uname -r)/scripts/sign-file \
    sha256 \
    signing_key.pem \
    signing_cert.pem \
    my_lkm.ko

# Step 3: Verify the signature is embedded
modinfo my_lkm.ko | grep sig
# sig_id:         PKCS#7
# signer:         My LKM Signing Key
# sig_key:        ...
# sig_hashalgo:   sha256

# Step 4: Add your certificate to the kernel's trusted keyring
# (required for the kernel to accept your signed modules)
sudo mokutil --import signing_cert.pem
# You will be prompted to set a MOK enrollment password
# Reboot and enter the password in the MOK Manager to enroll the cert
Note on MOK: MOK stands for Machine Owner Key. It is a mechanism in UEFI Secure Boot environments that lets you add your own signing certificates to the system’s trusted keyring without disabling Secure Boot entirely. After enrolling your cert via mokutil --import, you confirm it in the pre-boot MOK Manager screen on the next reboot.

Kernel Lockdown Mode

Linux kernel lockdown (introduced in Linux 5.4) restricts access to kernel features that could be used to tamper with the running kernel. When a system boots with UEFI Secure Boot enabled, the kernel typically enters lockdown mode automatically.

In lockdown mode, several operations are restricted:

  • Loading unsigned kernel modules is blocked (even as root)
  • Direct access to /dev/mem and /dev/kmem is blocked
  • Loading custom firmware to kernel code regions is restricted
  • Hibernation is disabled (the hibernation image could contain malicious code)
  • kprobes and BPF JIT are restricted
# Check if lockdown is active
cat /sys/kernel/security/lockdown
# none [integrity] confidentiality
# (the [] shows the current mode — "integrity" means unsigned modules are blocked)

# Check lockdown mode
cat /proc/sys/kernel/perf_event_paranoid  # side indicator

# Check kernel config
grep MODULE_SIG /boot/config-$(uname -r)
# CONFIG_MODULE_SIG=y
# CONFIG_MODULE_SIG_FORCE=y
# CONFIG_MODULE_SIG_ALL=y
# CONFIG_MODULE_SIG_SHA256=y

The kernel.modules_disabled sysctl

On systems where all required modules are loaded at boot, administrators can disable further module loading at runtime. This is a simple but effective way to prevent attackers from loading a malicious module on a running system.

# Check the current state
cat /proc/sys/kernel/modules_disabled
# 0 (modules can be loaded)

# Disable module loading for the current session (cannot be reversed without reboot)
# Only root can do this
sudo sysctl kernel.modules_disabled=1

# Now any insmod or modprobe attempt fails
sudo insmod my_lkm.ko
# insmod: ERROR: could not insert module my_lkm.ko: Operation not permitted

# Make it permanent at boot
echo "kernel.modules_disabled=1" | sudo tee /etc/sysctl.d/99-disable-modules.conf
Caution: Setting modules_disabled=1 is a one-way operation per boot. Once set, you cannot load any more modules until reboot. Only do this on embedded or production systems where the full set of required modules is already loaded. Never do this on a development machine.

Security Best Practices in Your Kernel Module Code

  • Validate all data from userspace: Use copy_from_user() for all reads from user buffers. Check the return value — a non-zero return means the copy was incomplete.
  • Check all return values: kmalloc() can return NULL. copy_from_user() can fail. request_irq() can fail. Always handle error paths.
  • Use __user annotations: Marking userspace pointers with __user helps sparse catch bugs where kernel code accidentally dereferences them.
  • Avoid hardcoded buffer sizes: Always use sizeof() and pass size limits explicitly to copy functions.
  • Prefer kzalloc() over kmalloc(): Zero-initialized memory prevents information leaks from uninitialized kernel memory being returned to userspace.
  • Release resources in all error paths: Use goto labels for structured error cleanup to ensure locks, memory, and IRQs are always properly released.
/* Example: proper error handling with goto cleanup */
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/interrupt.h>

#define MY_IRQ_NUM  17
#define BUF_SIZE    256

static char *my_buffer;

static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
	/* IRQ handler code here */
	return IRQ_HANDLED;
}

static int __init secure_init(void)
{
	int ret;

	/* Step 1: Allocate memory */
	my_buffer = kzalloc(BUF_SIZE, GFP_KERNEL);
	if (!my_buffer) {
		pr_err("secure: failed to allocate buffer\n");
		ret = -ENOMEM;
		goto err_alloc;
	}

	/* Step 2: Request IRQ */
	ret = request_irq(MY_IRQ_NUM, my_irq_handler,
	                  IRQF_SHARED, "my_device", my_buffer);
	if (ret) {
		pr_err("secure: failed to request IRQ %d (err=%d)\n",
		       MY_IRQ_NUM, ret);
		goto err_irq;
	}

	pr_info("secure: init complete\n");
	return 0;

/* Cleanup labels — executed in reverse init order */
err_irq:
	kfree(my_buffer);
	my_buffer = NULL;
err_alloc:
	return ret;
}

static void __exit secure_exit(void)
{
	free_irq(MY_IRQ_NUM, my_buffer);
	kfree(my_buffer);
	my_buffer = NULL;
	pr_info("secure: unloaded\n");
}

module_init(secure_init);
module_exit(secure_exit);
MODULE_LICENSE("GPL");

Linux Kernel Coding Style — The Complete Guide

The Linux kernel has a well-defined coding style that all contributors are expected to follow. This is not about personal preference — consistent style across millions of lines of kernel code written by thousands of developers worldwide is what keeps the code readable and maintainable. If you plan to contribute to the mainline kernel or work on production driver code, following this style is non-negotiable.

The official style guide lives at Documentation/process/coding-style.rst in the kernel source tree. Here are the most important rules, with examples.

Rule 1: Indentation — Tabs Only, 8 Characters Wide

The kernel uses hard tab characters for indentation, with each tab visually displayed as 8 characters. This is different from most modern style guides (which use 2 or 4 spaces). The 8-character tab rule exists for a specific reason: with 8-character indentation, deeply nested code (4+ levels) quickly becomes unreadably wide, which forces developers to break complex functions into smaller, simpler ones.

/* CORRECT: tabs for indentation */
static int my_function(int value)
{
	if (value > 0) {
		if (value < 100) {
			pr_info("value in range: %d\n", value);
			return 0;
		}
	}
	return -EINVAL;
}

/* WRONG: spaces for indentation */
static int my_function(int value)
{
    if (value > 0) {           /* 4 spaces — WRONG */
        if (value < 100) {     /* 8 spaces — WRONG */
            return 0;
        }
    }
    return -EINVAL;
}
Editor Tip: In vim, add set noexpandtab tabstop=8 shiftwidth=8 to your ~/.vimrc. In VS Code, use the kernel’s provided .editorconfig file or set tab size to 8 and disable “Insert Spaces”.

Rule 2: Line Length — Prefer Under 80 Characters

The traditional kernel limit is 80 characters per line. This was recently relaxed — lines up to 100 characters are acceptable for readability, but you should still aim for 80 when possible. This rule exists so code is readable on any terminal, in side-by-side diffs, and in code review tools.

/* CORRECT: long line broken for readability */
ret = platform_get_irq_byname(pdev, "my_irq");
if (ret < 0) {
	dev_err(&pdev->dev,
	        "failed to get IRQ 'my_irq': %d\n", ret);
	return ret;
}

/* WRONG: excessively long single line */
ret = platform_get_irq_byname(pdev, "my_irq"); if (ret dev, "failed to get IRQ: %d\n", ret); return ret; }

Rule 3: Braces and Spaces

The kernel follows K&R (Kernighan and Ritchie) brace style. The opening brace goes at the end of the line for control flow statements, but on its own line for function definitions. No space before opening parenthesis in function calls, but spaces after keywords.

/* CORRECT brace style */

/* Function definition: opening brace on its own line */
static int my_driver_probe(struct platform_device *pdev)
{
	int ret;

	/* Control flow: opening brace on same line */
	if (condition) {
		do_something();
	} else {
		do_other();
	}

	for (i = 0; i < count; i++) {
		process(i);
	}

	while (!done) {
		wait();
	}

	return 0;
}

/* Exception: single-statement if does NOT need braces */
if (error)
	return -EINVAL;

/* But: ALWAYS use braces when else is present */
if (value > 0) {
	do_positive();
} else {
	do_negative();
}

Rule 4: Naming Conventions

What Convention Example
Local variables lowercase_with_underscores int byte_count;
Functions lowercase_with_underscores static int my_dev_open(...);
Global variables lowercase, descriptive, with module prefix static int ep_dev_major;
Macros / Constants UPPERCASE_WITH_UNDERSCORES #define MAX_BUF_SIZE 256
Struct types lowercase_with_underscores (no typedef) struct my_device_data { ... };
Enum values UPPERCASE or Mixed enum { STATE_IDLE, STATE_BUSY };
No typedef for structs: The kernel style guide strongly discourages using typedef for struct types. Instead of typedef struct my_dev { ... } my_dev_t;, just use struct my_dev { ... }; and declare variables as struct my_dev *dev;. Typedefs hide the type information and make the code harder to understand.

Rule 5: Function Length and Complexity

Functions in the kernel should do one thing and do it well. A function that exceeds one or two screens in length is generally a signal that it needs to be split. The kernel coding style guide puts it clearly: a function that cannot fit on two screens is too long.

  • Aim for functions under 50 lines
  • If a function has more than 2–3 levels of nesting, refactor into helper functions
  • Use the goto pattern for error cleanup (as shown earlier) — it is the accepted kernel idiom, not a code smell

Rule 6: Comments

/*
 * Multi-line comments use this style in the kernel.
 * Each line starts with a space and asterisk.
 * This is the block comment style used throughout the kernel source.
 */

/* Short single-line comments use this style */

/* DON'T use C++ style comments in kernel C code: */
// This is wrong for kernel code

/**
 * function_name - brief description (for kernel-doc format)
 * @param1: description of first parameter
 * @param2: description of second parameter
 *
 * Longer description of what the function does.
 * This format is parsed by the kernel-doc tool to generate
 * HTML documentation.
 *
 * Return: 0 on success, negative errno on failure.
 */
int function_name(int param1, char *param2)
{
	/* implementation */
	return 0;
}

Using checkpatch.pl Effectively

# Run checkpatch on your source file
/usr/src/linux-headers-$(uname -r)/scripts/checkpatch.pl \
    --no-tree -f my_driver.c

# Or if you have the full kernel source
scripts/checkpatch.pl --no-tree -f my_driver.c

# Run on a git commit (if your code is in a git repo)
scripts/checkpatch.pl HEAD

# Common warnings and what they mean:
# WARNING: line over 80 characters
#   → break the line into multiple lines

# ERROR: do not use assignment in if condition
#   → if ((ret = some_func()) < 0) should use separate assignment

# WARNING: printk() should include KERN_ facility level
#   → use pr_info(), pr_err(), pr_warn() instead of raw printk()

# ERROR: space required before the open parenthesis
#   → if(x) should be if (x)

# WARNING: braces {} are not necessary for single statement blocks
#   → or conversely, that you are missing braces

# Ignore specific checks if you are sure they are false positives
scripts/checkpatch.pl --ignore LONG_LINE -f my_driver.c

Contributing a Linux Kernel Module to the Mainline Kernel

Getting a driver or module accepted into the mainline Linux kernel is a significant milestone. It means your code is reviewed, maintained, and shipped with every Linux distribution in the world. Here is the process at a high level — the same process used by every kernel developer, from students to employees at major companies.

Linux Kernel Patch Submission Workflow
Step Action Tool/Resource
1 Write the driver following coding style checkpatch.pl, sparse
2 Find the right subsystem maintainer scripts/get_maintainer.pl
3 Format patches from git commits git format-patch
4 Send patches to the mailing list git send-email
5 Respond to review feedback, send v2/v3 Email, lore.kernel.org
6 Maintainer adds Reviewed-by, picks patch Subsystem maintainer tree
7 Linus pulls subsystem tree during merge window linux-next, mainline
# Step 1: Find who maintains the relevant subsystem
# (run from within the kernel source tree)
scripts/get_maintainer.pl drivers/i2c/my_driver.c
# Jane Doe <jane@example.com> (maintainer:I2C SUBSYSTEM)
# linux-i2c@vger.kernel.org (open list:I2C SUBSYSTEM)
# linux-kernel@vger.kernel.org (open list)

# Step 2: Format your patch from git
git format-patch -1 HEAD --subject-prefix="PATCH v1"

# The patch file will be named something like:
# 0001-i2c-add-driver-for-my-sensor-chip.patch

# Step 3: Check the patch with checkpatch
scripts/checkpatch.pl 0001-i2c-add-driver-for-my-sensor-chip.patch

# Step 4: Send via git send-email
git send-email \
    --to="jane@example.com" \
    --cc="linux-i2c@vger.kernel.org" \
    --cc="linux-kernel@vger.kernel.org" \
    0001-i2c-add-driver-for-my-sensor-chip.patch
First-timer tip: Before submitting a real patch, read Documentation/process/submitting-patches.rst in the kernel source. Also subscribe to the relevant mailing list and read it for a few weeks to understand the tone and style of feedback. Your first patch will almost certainly get review comments — that is normal and expected. Be responsive, courteous, and patient.

Common Mistakes with Kernel Module Security and Style

  • Not signing modules on Secure Boot systems: If Secure Boot is enabled and you did not sign your module, insmod will fail with a cryptic error. Always sign modules for production deployment.
  • Using tabs and spaces mixed: Some editors (especially with auto-indent) insert spaces instead of tabs, or mix the two. This causes checkpatch to complain and the kernel build system to misparse the Makefile. Configure your editor correctly for kernel work.
  • Using printk without log level: Raw printk("message\n") without a log level inherits the default level, which may be filtered out. Always use pr_info(), pr_err(), pr_warn(), pr_debug(), or the device-specific dev_info(), dev_err() wrappers.
  • Using C++ style comments in kernel code: // comment style is not accepted by kernel maintainers. Use only /* comment */ style.
  • Forgetting the Signed-off-by line in patches: Every kernel patch must include a Signed-off-by: Your Name <email> line in the commit message. This is a legal certification (the Developer’s Certificate of Origin) that you have the right to submit the code. Patches without it are rejected immediately.

Key Takeaways

  • Use /etc/modules-load.d/ for auto-loading modules at boot on modern systemd systems. Use /etc/modprobe.d/ to pass parameters to auto-loaded modules.
  • Module signing, Secure Boot, and kernel lockdown work together to prevent unauthorized modules from loading on production systems.
  • Always use kzalloc(), validate userspace inputs with copy_from_user(), check all return values, and use goto labels for error cleanup.
  • The Linux kernel coding style requires: tabs (not spaces) for indentation, lines under 80 characters, K&R brace style, lowercase_underscore naming, and C-style block comments only.
  • Run checkpatch.pl on every source file before committing. Zero warnings is the target.
  • Contributing to mainline involves: writing compliant code, finding the right maintainer with get_maintainer.pl, formatting patches with git format-patch, and sending them via git send-email.

Frequently Asked Questions

Q1. How do I auto-load a kernel module with parameters at boot on Ubuntu?
Two steps: First, create /etc/modules-load.d/mymodule.conf with the module name on a line by itself. Second, create /etc/modprobe.d/mymodule.conf with options mymodule param1=value1 param2=value2. On next boot, systemd’s modules-load service will call modprobe mymodule, which reads the options from modprobe.d and passes them to the module.
Q2. Do I need to sign my kernel module on a Raspberry Pi?
On standard Raspberry Pi OS, module signing is not enforced by default (Secure Boot is not enabled on Raspberry Pi hardware in the traditional sense). You can load unsigned modules freely. However, on enterprise embedded ARM platforms (like those using UEFI with Secure Boot), signing is required. Check whether your kernel was built with CONFIG_MODULE_SIG_FORCE=y — if yes, all modules must be signed.
Q3. Why does the kernel use 8-character indentation instead of 4?
The 8-character tab rule is intentional design, not historical accident. When code is nested 4 or more levels deep, 8-character indentation makes it immediately obvious that the function is too complex. With 4-character indentation, deep nesting “feels comfortable” and developers do not feel pressure to simplify. The 8-space rule forces good design by making complexity visually painful.
Q4. Can I use pr_debug() in production kernel modules?
Yes, and it is actually the preferred way to add diagnostic messages to production drivers. pr_debug() compiles to a no-op unless either CONFIG_DYNAMIC_DEBUG or DEBUG is defined during compilation. This means zero performance overhead in production builds. In the field, you can enable specific debug messages at runtime via /sys/kernel/debug/dynamic_debug/control without recompiling the module.
Q5. How long does it typically take to get a kernel patch accepted?
For a simple, clean patch to a well-maintained subsystem, expect 2–6 weeks from submission to merge into the subsystem tree, then another 1–3 months to be included in a mainline kernel release. The Linux kernel follows a roughly 8–10 week release cycle. A new driver that needs multiple review rounds may take several months. Complex subsystem changes can take a year or more. Patience and responsiveness to reviewer feedback are the most important factors.
Q6. What is the Developer’s Certificate of Origin (DCO) and why is Signed-off-by required?
The DCO is a legal declaration that you certify: you wrote the code yourself, or you have the right to contribute it under its open-source license, and you grant permission for it to be distributed under the kernel’s license (GPLv2). By adding Signed-off-by: Name <email> to your commit, you are making this certification. It creates a clear legal trail for all contributions to the kernel. It is strictly required — patches without it are immediately rejected.
Q7. Is the free Linux kernel programming course on EmbeddedPathashala suitable for complete beginners?
The free Linux kernel programming course on EmbeddedPathashala starts from the basics of setting up a build environment and writing a first kernel module, making it accessible to developers with C programming experience and basic Linux command-line knowledge. You do not need prior kernel programming experience. The course progressively covers LKM fundamentals, kernel internals, memory management, interrupts, and device driver development.

Authoritative References

Free Linux Kernel Programming — Complete & Updated

You have completed the LKMs Part 2 series. EmbeddedPathashala’s free Linux kernel programming course continues with kernel memory management, synchronization, character device drivers, and more. All content is original, practical, and updated for Linux 6.x.

View Full Course Index Subscribe on YouTube

Leave a Reply

Your email address will not be published. Required fields are marked *