How is a kernel module loaded into the kernel during runtime

How is a kernel module loaded into the kernel during runtime

One of the most powerful features of a modern operating system is the ability to extend the kernel while it’s running, without rebooting, without recompiling, without any downtime at all. This is runtime kernel module loading, and it’s something people use constantly without necessarily thinking about the machinery behind it — every time you plug in a new USB device and it “just works,” a module load happened somewhere in the background. I want to walk through exactly how this works, from the moment you type a command to the moment new code is executing with full kernel privileges.

The Core Idea: Dynamic Linking, Kernel Style

At a conceptual level, loading a kernel module at runtime is similar to dynamic linking in userspace — think loading a shared library (.so on Linux, .dll on Windows) into a running process. The key difference is the stakes: a userspace shared library that misbehaves can crash a single process. A kernel module that misbehaves can crash, corrupt, or compromise the entire machine, because it runs with unrestricted access to hardware and memory. This is why the runtime loading process is layered with so many checks and safeguards.

Step 1: Triggering the Load

There are three broad ways a module load gets triggered on a running Linux system:

Manual loading: An administrator explicitly runs insmod modulename.ko (loads exactly the file specified, no dependency resolution) or, far more commonly, modprobe modulename (which looks up the module by name in the system’s module directory, typically /lib/modules/$(uname -r)/, and resolves dependencies automatically using metadata generated by depmod).

Automatic hotplug loading: This is the mechanism behind plug-and-play. When new hardware is detected — say, a USB device is plugged in — the kernel’s USB core enumerates the device, reads its vendor and product ID from its device descriptor, and generates a “uevent,” a kernel event broadcast to userspace. The udev daemon (or systemd-udevd on modern systemd-based distributions) picks up this uevent, consults a module alias database, and if a matching driver module exists, invokes modprobe on your behalf — all of this typically happening within milliseconds of the physical plug-in event.

Boot-time and demand loading: Modules listed in files under /etc/modules-load.d/ are loaded automatically during early boot by systemd-modules-load.service. Additionally, the kernel itself can request module loading on demand via the internal request_module() kernel function — for example, when you try to mount a filesystem type that isn’t currently supported by any loaded module, the kernel automatically attempts to load a matching filesystem module before failing the mount.

Step 2: Reading the Module into Memory (Userspace Side)

Whichever path triggered the load, eventually a userspace tool (insmod or modprobe, ultimately built on the same underlying libkmod library on modern systems) needs to get the module’s binary content ready to hand off to the kernel. There are two system call approaches:

init_module(): The traditional approach. The userspace tool reads the entire .ko file into a memory buffer and passes that buffer directly to the kernel via this system call.

finit_module(): A more modern approach, introduced specifically to improve security and efficiency. Instead of passing a memory buffer, the tool passes an open file descriptor pointing to the .ko file, letting the kernel read and validate the file directly. This is particularly valuable in combination with IMA (Integrity Measurement Architecture) and other kernel-level file integrity verification systems, since the kernel can verify the file’s integrity based on the file descriptor before trusting its contents.

Step 3: Crossing into Kernel Space

Once the system call is invoked, execution transitions from userspace into kernel space — a privilege level transition managed by the CPU itself (via a syscall instruction on x86-64, trapping into kernel mode). From this point forward, the kernel’s own module-loading subsystem (found in kernel/module/ in the Linux kernel source tree) takes over completely.

Step 4: Validating the Module

The kernel performs several validation checks before doing anything else:

  • ELF format validation: Confirming the file is a properly formed ELF object with the expected sections.
  • Version magic string check: Comparing the module’s embedded version string against the running kernel’s version and configuration, immediately rejecting mismatches to avoid loading binary-incompatible code.
  • Signature verification: On systems configured to require it (common in security-hardened distributions and mandatory when Secure Boot is active on many distros), the kernel verifies a cryptographic signature embedded in the module against a set of keys it trusts, rejecting unsigned or improperly signed modules outright.
  • License compatibility check: Reading the module’s MODULE_LICENSE() declaration, which affects which kernel symbols the module is permitted to use later during symbol resolution.

Step 5: Parsing Sections and Extracting Metadata

The kernel parses the ELF sections to extract the pieces it needs: the actual executable code, initialized and uninitialized data sections, the list of symbols the module exports for others to use, the list of symbols it requires from elsewhere, and any module parameters declared via module_param() (which allow administrators to pass configuration values at load time, like insmod mymodule.ko debug=1).

Step 6: Allocating Kernel Memory for the Module

The kernel allocates dedicated memory regions to hold the module once loaded — separate regions for code (marked executable, ideally read-only once finalized), read-only data, and writable data, following the principle of least privilege at the memory-protection level. This separation matters for security: keeping code non-writable and data non-executable (a form of W^X, write-xor-execute protection) makes certain classes of memory-corruption exploits significantly harder to pull off even if a bug exists somewhere in the module.

Step 7: Symbol Resolution

Here the kernel walks through the module’s list of required (undefined) symbols and resolves each one against:

  1. The core kernel’s own exported symbol table (every EXPORT_SYMBOL()/EXPORT_SYMBOL_GPL() in the running kernel).
  2. The exported symbols of any already-loaded modules this module depends on.

Each successful resolution patches the module’s code with the actual runtime address of the target symbol. If a symbol can’t be resolved — a dependency module isn’t loaded, or genuinely doesn’t exist in this kernel build — the entire load aborts at this point with an “unknown symbol” error, before any module code has run at all.

Step 8: Relocation

Because the module wasn’t compiled with a fixed, predetermined load address (it could end up anywhere in the kernel’s available module memory space, especially with kernel address space layout randomization, KASLR, enabled), the loader performs relocation — patching internal references within the module’s code and data to reflect wherever it actually ended up in memory. This is conceptually identical to what a userspace dynamic linker does when loading a position-independent shared library.

Step 9: Running the Module’s Init Function

With code loaded, symbols resolved, and relocations applied, the kernel finally invokes the module’s registered initialization function (the one wrapped in module_init()). This is where the module actually does its setup work — registering devices, allocating runtime data structures, setting up interrupt handlers, and so on, as covered in detail in the dedicated initialization-process discussion.

Step 10: Finalizing and Publishing the Module

Assuming the init function returns success (0), the kernel marks the module as “live,” adds it to the kernel’s internal linked list of loaded modules (visible via /proc/modules and the lsmod command), and makes its exported symbols available for future module loads that might depend on it. The syscall returns success to the calling userspace tool, and from the user’s perspective, the module is now “loaded” — though as you can see, that single word is doing a lot of work to summarize this whole pipeline.

A Concrete Walkthrough: modprobe in Action

$ sudo modprobe nvidia

Behind this single command:

  1. modprobe consults /lib/modules/$(uname -r)/modules.dep (generated ahead of time by depmod) to determine nvidia‘s dependencies — perhaps drm, i2c-core, and others.
  2. Each dependency is checked against currently loaded modules (/proc/modules); anything not already loaded gets loaded first, recursively, in correct dependency order.
  3. For each module in the chain, modprobe locates the corresponding .ko file, opens it, and invokes finit_module() (or init_module() on older systems).
  4. The kernel runs through validation, symbol resolution, memory allocation, relocation, and finally calls each module’s init function in sequence.
  5. Once the final nvidia module itself loads successfully, GPU functionality becomes available — new device nodes may appear under /dev, and userspace graphics libraries can now communicate with the hardware through the newly loaded driver stack.

Kernel Lockdown and Restricting Runtime Loading Further

Beyond the basic CAP_SYS_MODULE privilege check, Linux offers a more comprehensive hardening feature called Kernel Lockdown mode, which can be enabled either through kernel configuration or dynamically via the lockdown Linux Security Module. When active (particularly in its stricter “confidentiality” setting, as opposed to the more permissive “integrity” setting), lockdown mode restricts a whole range of kernel functionality that could otherwise be abused to load unauthorized code or read/modify kernel memory even by a privileged root user — including, notably, tightening the conditions under which module loading is permitted, generally requiring valid signatures with no exceptions once lockdown is engaged.

This matters increasingly in Secure Boot environments, where the entire point of the boot chain’s cryptographic verification (firmware verifying the bootloader, the bootloader verifying the kernel) would be undermined if, once running, that verified kernel could then simply load arbitrary unsigned code via a module load. Lockdown mode closes this gap, extending the chain of trust from the boot process into runtime kernel extensibility, ensuring that “verified boot” actually means something for the entire uptime of the system rather than just its initial moments.

Performance Considerations

Runtime module loading is fast — typically single-digit milliseconds for a small module, though larger, complex drivers (again, GPU drivers are a good example, given their sheer size) can take noticeably longer, particularly on the very first load after boot when relevant disk caches are cold. Systems that need extremely fast boot times (embedded systems, certain cloud instance types) sometimes opt to build critical drivers directly into the kernel image (statically) rather than as loadable modules, trading flexibility for a small amount of startup latency saved.

Security Considerations for Runtime Loading

Runtime module loading is a genuinely significant attack surface, which is exactly why so many of the steps above exist as safeguards rather than being optional conveniences:

  • Requiring CAP_SYS_MODULE (essentially root-only) to load modules at all, preventing unprivileged users from injecting arbitrary kernel code.
  • Signature enforcement, ensuring only modules signed by a trusted key can load on hardened systems.
  • Sysctl knobs like kernel.modules_disabled, which, once set to 1, permanently prevents any further module loading until the next reboot — a common hardening step for systems where the full set of needed drivers is already loaded and no further runtime flexibility is required.

Troubleshooting Runtime Load Failures

  • “Operation not permitted”: Check both your privilege level and whether module loading has been disabled via kernel.modules_disabled, or whether signature enforcement is rejecting an unsigned module.
  • “Invalid module format”: Kernel version mismatch — you likely have a module built for a different kernel version than the one you’re running.
  • “Unknown symbol”: A dependency isn’t loaded, or you’re missing a needed companion module; check with modinfo modulename to see the module’s declared dependencies.
  • Load succeeds but hardware still doesn’t work: The module loaded, but its init function may have failed to detect the specific hardware, or a firmware file it depends on (many drivers load a separate firmware blob at runtime, not embedded in the .ko itself) may be missing from /lib/firmware/.

Best Practices

  1. Prefer modprobe over raw insmod for anything beyond quick manual testing, since dependency resolution alone eliminates a large class of load failures.
  2. Keep your module tree up to date with depmod after installing new modules manually, so dependency resolution works correctly.
  3. Sign your out-of-tree modules if you’re deploying to systems with Secure Boot or strict module-signing policies enabled, rather than fighting the enforcement after the fact.
  4. Use modinfo liberally during development and troubleshooting — it surfaces a module’s declared dependencies, parameters, license, and version magic string without needing to load it.
  5. Log clearly from within your init function (pr_info, pr_err) so runtime load issues are diagnosable via dmesg rather than being a black box.

Summary

Runtime kernel module loading looks simple from the command line, but it’s a carefully layered process: reading the module into memory, crossing the syscall boundary into kernel space, validating format and signatures, resolving symbols against the kernel and other loaded modules, allocating and protecting memory appropriately, performing relocations, and finally executing the module’s own init logic. Every one of these steps exists as a deliberate safety checkpoint, because the cost of getting any of them wrong is a fully privileged code execution context — as serious a place for a bug (or a malicious actor) to land as exists anywhere in a computer system.

FAQs

What’s the difference between init_module() and finit_module()? init_module() takes a memory buffer containing the module’s contents, requiring userspace to have already read the file. finit_module() takes a file descriptor instead, letting the kernel read and validate the file directly — generally preferred on modern systems, particularly for integrity-verification workflows.

Do all kernel modules require dependencies? No, many standalone modules have no dependencies beyond the base kernel itself. Dependencies arise when a module relies on functionality exported by another module rather than the core kernel — common for driver “stacks,” like Wi-Fi drivers depending on a shared wireless configuration module.

Can module loading fail silently? Generally no — a failed load returns a nonzero error code and typically an accompanying dmesg message explaining why. However, an init function that “succeeds” (returns 0) without actually detecting expected hardware can look like a silent failure from a user’s perspective, even though technically the module load itself succeeded.

Is runtime loading slower than having a driver built into the kernel statically? The runtime loading process itself adds only a small, generally imperceptible amount of latency (milliseconds), but it does mean the driver isn’t available until explicitly loaded — which matters for boot-critical hardware like the disk controller hosting the root filesystem, which is why such drivers are often built statically or loaded very early via an initramfs.

What tool shows me what modules are currently loaded? lsmod on Linux, which reads from /proc/modules, showing each loaded module’s name, memory size, reference count, and what (if anything) depends on it.

Official References

  • Linux Kernel Module Programming Guide: https://www.kernel.org/doc/html/latest/kbuild/modules.html
  • Linux init_module()/finit_module() man pages: https://man7.org/linux/man-pages/man2/init_module.2.html
  • Linux Kernel Module Signing Documentation: https://www.kernel.org/doc/html/latest/admin-guide/module-signing.html
  • modprobe man page: https://man7.org/linux/man-pages/man8/modprobe.8.html
Total
0
Shares

Leave a Reply

Previous Post
Describe the role of the kernel module symbol table

Describe the role of the kernel module symbol table

Next Post
Discuss the steps involved in unloading a kernel module

Discuss the steps involved in unloading a kernel module

Related Posts