Linux Fu: Kernel Modules Have Privileges

Most Linux programs live in user space, where the kernel watches them like a patemory, open files, communicate with devices, and occasionally make questionable decisions, but they must use system calls to ask the kernel for permission.

A loadable kernel module plays by very different rules. Once Linux accepts and loads the module, its code becomes part of the running kernel. It can interact directly with kernel data structures, hardware interfaces, memory-management facilities, networking subsystems, and device drivers. That power makes kernel modules incredibly usefuland about as forgiving as juggling chainsaws while standing on a rolling office chair.

This guide explains what Linux kernel module privileges really mean, how modules are built and managed, why development mistakes can crash an entire machine, and which security controls can reduce the risk.

What Is a Linux Kernel Module?

A Linux kernel module is a compiled object that can extend the kernel without requiring administrators to rebuild the entire kernel or reboot the computer. Module files commonly use the .ko extension, although compression may produce filenames such as .ko.xz or .ko.zst.

Modules are commonly used to provide:

  • Device drivers for graphics cards, storage controllers, network adapters, sensors, and other hardware
  • File-system support such as NFS, Btrfs features, or specialized storage formats
  • Networking protocols, packet filters, and virtual network devices
  • Cryptographic algorithms and security-related functions
  • Virtualization, tracing, debugging, and performance-monitoring features

Instead of placing every possible driver permanently inside the kernel image, Linux can load the required components when hardware appears or a feature is requested. This modular design keeps the base kernel manageable while supporting an enormous variety of systems.

Why Kernel Modules Have So Much Authority

Kernel space is not simply “more powerful root”

The root account is still a user-space identity. A root process receives broad permissions, but the kernel remains the authority that enforces those permissions. Kernel module code sits on the other side of that boundary.

After loading, a module executes in kernel context. Depending on its purpose and the architecture, it may register interrupt handlers, communicate with hardware, allocate kernel memory, expose device interfaces, inspect process information, or modify kernel-managed state. There is no ordinary process sandbox separating a loaded module from the kernel it has joined.

This is why a defective module can cause a kernel oops, corrupt data, freeze the computer, or trigger a panic. A user-space application that dereferences a bad pointer usually kills itself. A module that dereferences a bad pointer may invite the entire operating system to join it.

CAP_SYS_MODULE controls loading, not module behavior

Linux capabilities divide traditional root authority into smaller privileges. The capability associated with loading and unloading modules is CAP_SYS_MODULE.

This capability determines whether a process may request module insertion or removal. It does not place restrictions around the code after the kernel accepts it. In other words, the gatekeeper can decide who enters the building, but it does not put an ankle monitor on every module after admission.

For this reason, administrators should treat CAP_SYS_MODULE as a highly sensitive capability. Granting it to an application, container, or service effectively gives that workload a path to introducing code into the kernel.

How Linux Loads and Manages Modules

Inspecting the running system

The lsmod command displays modules currently loaded by the kernel:

Its output includes the module name, approximate size, and usage information. Underneath the friendly formatting, the information comes from /proc/modules.

To inspect a particular module before loading it, use modinfo:

Useful metadata can include the author, description, license, dependencies, parameters, aliases, signing details, and kernel-version information.

modprobe versus insmod

The low-level insmod utility inserts a module file directly:

For normal administration, modprobe is usually the better choice because it understands module aliases, configuration files, and dependencies:

Modules associated with the running kernel normally reside below:

If loading fails, the command-line error may be delightfully vague. The kernel log usually offers better clues:

Building a Minimal External Kernel Module

The Linux kernel uses its own build system, known as kbuild. An external module should be compiled against the configuration and headers for the target kernel. Building against random headers found in a dusty download directory is an excellent way to manufacture confusing errors.

A small demonstration module might contain the following:

The initialization function runs when the module is inserted. If it returns a negative error value, the kernel rejects the load. The exit function performs cleanup when the module is removed.

A corresponding Makefile can use kbuild:

Build and test it with:

This example does little more than write messages to the kernel log. That is intentional. The best first module is boring, predictable, and unlikely to turn the screen black while the cooling fans begin expressing concern.

What a Legitimate Module Can Do

Modules become useful when they register with an existing kernel subsystem. A driver might register with the PCI, USB, input, networking, block-device, or character-device framework. The subsystem then calls the module when matching hardware or events appear.

Communicate with hardware

A device driver can map hardware resources, respond to interrupts, transfer data, manage device power states, and expose an interface to user-space software. The driver must use the appropriate kernel APIs and carefully coordinate concurrent activity.

Create user-space interfaces

Modules can expose controlled functionality through device nodes, sysfs attributes, procfs entries, debugfs, netlink sockets, or subsystem-specific interfaces. A well-designed module does not allow arbitrary user-space access merely because it can. It validates inputs, checks permissions, limits resource use, and keeps its interface as small as practical.

Participate in networking and storage

Kernel modules can implement protocols, packet-processing features, virtual interfaces, file systems, encryption layers, and storage drivers. These components may process untrusted data at high speed, making bounds checking and lifecycle management essential.

Why Kernel Module Debugging Is Unforgiving

Memory errors affect the entire system

Kernel code does not receive the same protective isolation as an ordinary application. An out-of-bounds write, invalid pointer, use-after-free condition, or double release can corrupt unrelated kernel state. The visible failure may occur much later, making the original bug difficult to identify.

Concurrency appears everywhere

A module may interact with multiple processes, CPUs, interrupts, timers, work queues, and asynchronous device events. Data that appears safe during a single-threaded test can fail spectacularly under load.

Locks, atomic operations, reference counts, memory barriers, and read-copy-update mechanisms exist for specific reasons. Substituting optimism for synchronization is rarely a winning benchmark strategy.

Not every context may sleep

Some kernel operations run in contexts where blocking is forbidden. Calling a function that may sleep from an interrupt handler or while holding certain locks can produce warnings, deadlocks, or system instability.

Module developers must understand the execution context of every callback. A function that works perfectly during initialization may be completely inappropriate inside an interrupt path.

Cleanup is part of correctness

A removable module must release everything it acquired: memory, device numbers, registered handlers, sysfs files, timers, work items, interrupts, and subsystem registrations. Cleanup must also prevent new operations while waiting for existing users to finish.

Removing a module while its callback, worker, or timer can still execute creates a classic route to use-after-free bugs. The exit path is not housekeeping added after the “real” code. It is part of the real code.

Kernel Versions and Compatibility Problems

Linux does not promise a permanently stable internal kernel API for out-of-tree modules. Internal interfaces may evolve as developers improve security, performance, maintainability, or hardware support.

An external module generally needs to be rebuilt when the kernel changes. Distribution mechanisms such as DKMS can automate rebuilding, but they cannot magically repair source code that depends on an interface removed by a newer kernel.

Common loading failures include:

  • Building against headers for a different kernel release
  • Missing exported symbols or dependencies
  • Module-versioning mismatches
  • Unsupported compiler or configuration differences
  • Rejected signatures under Secure Boot
  • Incorrect parameters or device aliases

Commands such as uname -r, modinfo, modprobe --show-depends, and dmesg usually provide the fastest route toward an explanation.

Security Controls for Privileged Kernel Modules

Module signing and Secure Boot

Linux supports cryptographic module signatures. When signature enforcement is enabled, the kernel can reject unsigned modules or modules whose signatures cannot be validated against a trusted key.

Distributions commonly integrate module verification with UEFI Secure Boot. Vendor-provided modules are signed during the distribution’s build process. Locally compiled or third-party modules may require signing with a Machine Owner Key enrolled on the system.

Signing answers an important question: was this module approved by a trusted key holder? It does not prove that the code is bug-free. A perfectly signed null-pointer dereference remains perfectly capable of ruining the afternoon.

Kernel taint tracking

Linux tracks conditions that may affect debugging and support. Loading proprietary, externally built, forced, or unsigned modules can set kernel taint flags, depending on the circumstances and configuration.

The current taint value can be checked with:

A value of zero means no taint flags are set. A nonzero value is a bitmask describing one or more conditions. Importantly, unloading the module does not necessarily clear the taint. The kernel remembers that unsupported or potentially influential code was present.

Permanently disabling module changes until reboot

Systems that finish loading all required modules during startup can disable further module loading and unloading:

This setting cannot be changed back to zero without rebooting. That one-way behavior is deliberate: an attacker should not be able to re-enable module loading after the administrator locks it down.

Before using this control, test the complete workload carefully. Hot-plugged hardware, specialized file systems, networking features, and diagnostic tools may request modules later.

Hardening individual services

Systemd services that do not need kernel-module control can use:

This option removes module-related capability access from the service, blocks relevant system calls, and restricts access to module directories. It is a practical defense-in-depth setting for web servers, database services, application workers, and most network daemons.

Denylisting unnecessary modules

Administrators can configure modprobe to prevent unwanted modules from loading automatically. This reduces exposed kernel functionality, particularly on servers that never use uncommon protocols, file systems, or device classes.

A denylist is useful operational hardening, but it is not an absolute security boundary against an attacker who already possesses unrestricted module-loading authority. Strong protection combines capability restrictions, signature enforcement, Secure Boot, service sandboxing, and module-loading lockdown.

A Safer Kernel Module Development Workflow

  1. Use a virtual machine. Keep snapshots so a panic becomes a restart instead of a recovery project.
  2. Match the target kernel. Build against the correct configuration and header package.
  3. Start with a minimal module. Confirm loading, logging, and unloading before adding hardware access or concurrency.
  4. Check every return value. Kernel functions frequently report partial failure through error codes or encoded error pointers.
  5. Design cleanup alongside initialization. Each acquired resource should have an obvious release path.
  6. Use kernel debugging tools. Dynamic debug, tracing, lock debugging, sanitizers, fault injection, and crash dumps can reveal problems earlier.
  7. Avoid forced unloading. Forcing removal can bypass safety checks and leave active code paths pointing into freed module memory.
  8. Review exposed interfaces. Treat data received from user space, devices, networks, and files as untrusted.

Practical Experience Notes: Lessons That Arrive Quickly

The first successful load can create false confidence

The earliest kernel-module experiment often feels surprisingly easy. The source file is short, kbuild produces a .ko file, insmod returns without complaint, and a cheerful message appears in dmesg. It is tempting to conclude that kernel development is simply ordinary C wearing a more serious hat.

The illusion usually disappears as soon as the module manages a shared resource or asynchronous event. A timer fires after cleanup, a device disappears during an operation, or two CPUs enter a callback simultaneously. Code that survived a calm five-minute test may fail within seconds under repeated loading, unloading, and concurrent access.

A useful testing habit is to automate stress cycles. Repeatedly load the module, exercise its interface, remove it, and inspect the kernel log. Resource leaks and lifecycle mistakes become easier to spot when the test runs hundreds of times instead of once.

The kernel log becomes the closest thing to a conversation

When a normal application crashes, developers may attach a debugger, examine a core file, or print information to a terminal. Kernel failures often provide a different experience: the display freezes, the network connection vanishes, and the computer stops answering questions.

That is why disciplined logging matters. Messages should identify the module, operation, and relevant state without flooding the log. A consistent prefix makes filtering easier:

Logging alone is not enough, but it can show how far initialization progressed, which cleanup branch executed, and whether a callback occurred after removal began. These clues frequently turn an apparently random failure into a specific lifecycle error.

Cleanup bugs often teach the most memorable lessons

A classic beginner experience is a module that loads successfully but refuses to unloador unloads and causes trouble several seconds later. The delayed failure often points to forgotten asynchronous work, a timer, an interrupt handler, or an outstanding reference.

The practical lesson is to write initialization as a sequence of acquisitions and cleanup as the exact reverse sequence. If step five fails, the error path must release resources from steps four through one. Structured labels in C may look unfashionable in application code, but they are frequently the clearest way to implement kernel cleanup without duplicating logic.

Kernel upgrades expose hidden assumptions

An out-of-tree module may work flawlessly for months and then fail to compile after a kernel update. This does not automatically mean the kernel developers broke an official application interface. Internal kernel APIs are allowed to change, and external modules must keep up.

The experience encourages developers to minimize dependencies on internal details, use established subsystem APIs, and upstream generally useful drivers whenever practical. Code maintained inside the kernel tree is updated alongside interface changes. A private module maintained by nobody except a future, sleep-deprived version of its author receives no such luxury.

Virtual machines turn fear into experimentation

Developing on a disposable virtual machine changes the entire learning process. Instead of cautiously editing one line and hoping the workstation survives, a developer can enable debugging options, create snapshots, inject failures, and test dangerous edge cases.

A good lab setup includes a serial console or virtual console log, remote access from the host, matching debug symbols, and an easily restored snapshot. Keeping the module source on the host also prevents an unbootable guest from trapping the only copy of the latest changes inside itselfa surprisingly effective form of involuntary version control.

The broader experience is humbling but rewarding. Kernel modules reveal how much work Linux performs beneath familiar commands and APIs. They also demonstrate why kernel development values careful review, explicit ownership rules, predictable cleanup, and conservative interfaces. The kernel is powerful because it controls the whole machine. Module developers inherit that power, along with every consequence attached to it.

Conclusion

Linux kernel modules allow the operating system to gain new drivers, protocols, file systems, and other features while it is running. Their flexibility is one of Linux’s greatest strengths, but it comes from executing code inside the kernel’s trusted environment.

The most important lesson is that module-loading permission and module execution privilege are different concepts. CAP_SYS_MODULE governs who may request loading or unloading. Once a module has been validated and accepted, it operates as kernel code, where a small programming error can affect the entire system.

Safe module work therefore requires more than a compiler and administrative access. It requires matching headers, disciplined resource management, concurrency awareness, careful testing, cryptographic signing, restricted module-loading authority, and a development machine that can be cheerfully restored after an experiment becomes unexpectedly educational.

Note: Build and test experimental modules only on systems you are authorized to modify. A virtual machine with snapshots is strongly recommended, and production systems should enforce signed modules and tightly restrict CAP_SYS_MODULE.