Linux Kernel Module Auto-loading, Security, and Kernel Coding Style
Free Linux Kernel Programming Course — LKMs Part 2, Section 3
Intermediate
~24 min
Linux 6.x
Free
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.
| 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
| 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
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/memand/dev/kmemis 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
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
__userannotations: Marking userspace pointers with__userhelps 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()overkmalloc(): 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;
}
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 }; |
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
gotopattern 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.
| 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
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,
insmodwill 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 usepr_info(),pr_err(),pr_warn(),pr_debug(), or the device-specificdev_info(),dev_err()wrappers. - Using C++ style comments in kernel code:
// commentstyle 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 withcopy_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.plon 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 withgit format-patch, and sending them viagit send-email.
Frequently Asked Questions
/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.CONFIG_MODULE_SIG_FORCE=y — if yes, all modules must be signed.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.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.Authoritative References
- Linux Kernel Coding Style Guide (kernel.org)
- Submitting Patches to the Linux Kernel (kernel.org)
- Linux Kernel Module Signing (kernel.org)
- Linux Kernel Security Features Overview (kernel.org)
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