Module Loading insmod modprobe and kmod

Getting a loadable kernel module from a .ko file on disk into the running kernel happens through one of two system calls — init_module(2) (copy a buffer) or, today almost universally, finit_module(2) (hand the kernel a file descriptor so it reads and verifies the file itself). Userspace tools sit on top: insmod loads a single named file with no smarts, while modprobe resolves dependencies via modules.dep, honors aliases/blacklists/options from /etc/modprobe.d/, and loads everything in order. The kernel can also load modules for itself: when it needs a driver or filesystem it does not have, the in-kernel kmod helper (request_module()) spawns /sbin/modprobe out in userspace to fetch it. Whatever the trigger, the heavy lifting — ELF validation, symbol resolution, relocation, and running module_init — happens inside the kernel’s load_module() (per kernel/module/main.c, Linux 6.12 LTS). This note owns the loading mechanism; what a module is and its lifecycle states live in Loadable Kernel Modules.

Mental Model

flowchart TB
  subgraph US["Userspace"]
    INS["insmod foo.ko<br/>(single file, no deps)"]
    MP["modprobe foo<br/>(resolves deps via<br/>modules.dep, aliases,<br/>blacklist, options)"]
  end
  subgraph K["Kernel needs a module"]
    RM["request_module('fs-ext4')<br/>or MODALIAS uevent"]
    UMH["call_usermodehelper:<br/>spawn /sbin/modprobe"]
    RM --> UMH
  end
  UMH -->|"runs as a userspace process"| MP
  INS -->|"init_module / finit_module"| LM
  MP -->|"finit_module(fd, args, flags)<br/>once per module, in dep order"| LM
  subgraph SC["Syscall + load_module()"]
    LM["sig check &rarr; ELF validate &rarr;<br/>alloc exec mem &rarr; resolve symbols &rarr;<br/>apply relocations &rarr; COMING &rarr;<br/>parse params &rarr; sysfs &rarr; init() &rarr; LIVE"]
  end

The three entry paths into one loader. What it shows: insmod and modprobe are userspace tools that ultimately call finit_module; modprobe adds dependency/alias resolution before doing so. The kernel itself can be the originator — request_module() (the kmod path) spawns modprobe as a userspace process, which loops right back into the same syscall. The insight: there is exactly one in-kernel loader (load_module); every tool and the kernel’s own autoloader are just different ways of arriving at the same finit_module call. Learn load_module once and you understand all of them.

The Two Syscalls: init_module and finit_module

At the bottom, two system calls load a module; both require the CAP_SYS_MODULE capability. The older init_module(2) takes the module image as a user-space buffer:

int syscall(SYS_init_module, void *module_image, unsigned long len,
            const char *param_values);

Per the man page it “loads an ELF image into kernel space, performs any necessary symbol relocations, initializes module parameters to values provided by the caller, and then runs the module’s init function” (init_module(2)). The param_values string is space-delimited name=value settings for module parameters. Its kernel signature is SYSCALL_DEFINE3(init_module, void __user *, umod, unsigned long, len, const char __user *, uargs) (kernel/module/main.c).

The modern call, finit_module(2) (added in Linux 3.8), takes a file descriptor instead of a buffer:

int syscall(SYS_finit_module, int fd, const char *param_values, int flags);

It is “like init_module(), but reads the module to be loaded from the file descriptor fd” (init_module(2)). This is the crucial design difference: with init_module, userspace reads the file into a buffer and the kernel must trust that buffer; with finit_module, the kernel holds the fd and reads the file directly, so it can authenticate the file by its on-disk identity. The man page spells out the motivation — it is “useful when the authenticity of a kernel module can be determined from its location in the filesystem; in cases where that is possible, the overhead of using cryptographically signed modules … can be avoided.” It is also what lets IMA appraisal and the kernel’s own signature check operate on the real file rather than a copy. This is why every modern kmod-based tool (insmod, modprobe) prefers finit_module.

finit_module’s flags argument accepts three bits (init_module(2), matching the kernel’s SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)):

  • MODULE_INIT_IGNORE_MODVERSIONS“Ignore symbol version hashes” (skip the modversion CRC check).
  • MODULE_INIT_IGNORE_VERMAGIC“Ignore kernel version magic” (skip the vermagic match). These two together are what --force sets, and they taint the kernel TAINT_FORCED_MODULE.
  • MODULE_INIT_COMPRESSED_FILE (since Linux 5.17) — “Use in-kernel module decompression.” When set, the kernel decompresses the .ko.gz/.ko.xz/.ko.zst itself rather than relying on userspace, controlled by CONFIG_MODULE_DECOMPRESS. (zstd support arrived in 5.17, with xz also from 5.17 and the broader set per the man page.)

Uncertain

Verify: the exact kernel versions where each finit_module compression algorithm became available (man page says MODULE_INIT_COMPRESSED_FILE “since Linux 5.17”, and lists gzip/xz/zstd, but the per-algorithm “since” mapping is muddled across sources — one source said zstd “since 6.2”). Reason: sources disagreed on the zstd availability version, and the brief pins to 6.12 where all three exist anyway. To resolve: check MODULE_COMPRESS choices in kernel/module/Kconfig history and the finit_module(2) CHANGES section. uncertain

Note

glibc provides no wrappers for these two syscalls — callers must use syscall(2) directly. The userspace tooling lives in libkmod (the kmod package), which insmod/modprobe link against (init_module(2)).

Inside load_module() — What the Kernel Does at Load

Both syscalls converge on load_module(). The sequence in 6.12 (kernel/module/main.c), step by step:

  1. module_sig_check(info, flags) — verify the appended signature (deferred to Module Signing and Kernel Lockdown).
  2. elf_validity_cache_copy(info, flags) — validate the ELF. It checks the type is relocatable and the architecture matches:
    if (info->hdr->e_type != ET_REL) { ... goto no_exec; }
    if (!elf_check_arch(info->hdr)) { ... goto no_exec; }
    A .ko must be ET_REL (a relocatable object), and elf_check_arch rejects a module built for a different CPU — loading an ARM module on x86 fails here with ENOEXEC. Section-header bounds and the section-name string table are sanity-checked to defend against a malformed file.
  3. layout_and_allocate(info, flags) — compute where each section lands and allocate kernel memory for it. Executable text comes from execmem_alloc(execmem_type, size), the modern allocator (post-5.x) for executable kernel memory, separate from data:
    ptr = execmem_alloc(execmem_type, size);
    if (!ptr) return -ENOMEM;
  4. add_unformed_module(mod) — link the module into the global list in state MODULE_STATE_UNFORMED, and reject a duplicate name.
  5. simplify_symbols(mod, info)symbol resolution. The module’s relocatable object refers to kernel functions and other modules’ exports by name; every undefined symbol (SHN_UNDEF) must be bound to a real address:
    case SHN_UNDEF:
        ksym = resolve_symbol_wait(mod, info, name);
        if (ksym && !IS_ERR(ksym)) {
            sym[i].st_value = kernel_symbol_value(ksym);
    resolve_symbol does a binary search across the kernel’s exported-symbol table and the RCU-protected list of other modules’ exports, also checking GPL compatibility and version CRCs — this is where a proprietary module is denied a EXPORT_SYMBOL_GPL symbol, and where a modversion CRC mismatch produces the famous disagrees about version of symbol error. If a symbol belongs to a not-yet-loaded module, resolve_symbol_wait can briefly wait (the EBUSY “Timeout while trying to resolve a symbol” case). The export side is owned by Symbol Export and Module Namespaces.
  6. apply_relocations(mod, info)relocation. With every symbol now bound to an address, the loader patches the placeholder offsets the compiler left in the code and data so they point at the real load addresses:
    if (info->sechdrs[i].sh_type == SHT_REL)
        err = apply_relocate(...);
    else if (info->sechdrs[i].sh_type == SHT_RELA)
        err = apply_relocate_add(...);
    apply_relocate/apply_relocate_add are architecture-specific (each arch/*/kernel/module.c implements them) because relocation encodings differ per CPU. This is the step that makes a relocatable object usable at whatever address it happened to be allocated.
  7. complete_formation(mod, info) — apply memory protections (text becomes read-only + executable, rodata read-only) and advance the state to MODULE_STATE_COMING.
  8. prepare_coming_module(mod) — enable ftrace records for the module, notify livepatch, and fire the MODULE_STATE_COMING notifier so subsystems can react.
  9. parse_args(...) — apply the param_values (the parameters the user or modprobe passed), wiring them into the module’s parameter variables.
  10. mod_sysfs_setup(...) — create /sys/module/<name>/ and its parameters/, sections/, holders/ entries.
  11. do_init_module(mod) — run the module’s constructor: do_one_initcall(mod->init), then on success set mod->state = MODULE_STATE_LIVE, fire the LIVE notifier, and schedule the freeing of __init sections. A non-zero return from init unwinds the entire load.

The whole flow is the mechanical answer to “what relocation/symbol-resolution does the kernel do at load” — it is steps 5 and 6, bracketed by ELF validation and memory allocation, ending in running init. The lifecycle states it walks through (UNFORMED → COMING → LIVE) are detailed in Loadable Kernel Modules.

insmod vs modprobe

These are the two userspace front-ends, and the distinction matters constantly in practice.

insmod is, per its man page, “a trivial program to insert a module into the kernel” (insmod(8)). You give it a full path to one .ko file and it calls finit_module on it — nothing more. It does not resolve dependencies, does not search /lib/modules/, and does not consult aliases or modprobe.d. If the module needs another module’s symbols and that module is not already loaded, insmod simply fails with an unresolved-symbol error. The man page itself says “Most users will want to use modprobe(8) instead, which is more clever and can handle module dependencies.” insmod exists mainly for the rare case of loading a single out-of-tree .ko by explicit path during development.

modprobe is the dependency-aware tool. You give it a module name (no path, no .ko suffix), e.g. modprobe ext4, and it:

  • looks in /lib/modules/$(uname -r)/ for the module and “expects an up-to-date modules.dep.bin file as generated by the corresponding depmod utility” (modprobe(8));
  • reads modules.dep to find every module the target depends on, and loads them in order before the target (the construction of modules.dep is owned by Module Dependencies and depmod);
  • applies configuration from /etc/modprobe.d/*.conf (aliases, blacklists, options, install/remove hooks);
  • then finit_modules each module in turn.

Critically, modprobe is “dumb” about the module itself: “the work of resolving symbols and understanding parameters is done inside the kernel” — modprobe only orchestrates which files to load and in what order; the actual linking is load_module’s job. Errors surface in dmesg.

/etc/modprobe.d/ — Aliases, Blacklist, Options

modprobe’s behavior is shaped by directives in /etc/modprobe.d/*.conf (and the read-only /usr/lib/modprobe.d/) (modprobe.d(5)):

  • alias <wildcard> <modulename> — give a module alternate names, with shell-style wildcards: alias my-mod* really_long_modulename means that modprobe my-mod-something has the same effect.” This is the backbone of autoloading — modules declare device aliases (e.g. pci:v00008086d...) via MODULE_DEVICE_TABLE, depmod compiles them into modules.alias, and modprobe matches a requested alias to a module.
  • blacklist <modulename>“indicates that all of that particular module’s internal aliases are to be ignored.” This stops a module from autoloading through device-alias matching; it does not prevent loading it directly by name. A common point of confusion: blacklist foo will not stop modprobe foo or a hard dependency from loading foo — to truly forbid it, use install foo /bin/true.
  • options <modulename> <opt>=<val>“add options to the module modulename every time it is inserted into the kernel.” These accumulate with options from aliases and the command line.
  • install <modulename> <command>“run your command instead of inserting the module … The command can be any shell command,” with $CMDLINE_OPTS substitution. Used to truly blacklist (install foo /bin/true) or to set up prerequisites.
  • remove <modulename> <command> — the symmetric hook invoked on modprobe -r.
  • softdep <modulename> pre: ... post: ... — declare optional ordering dependencies: pre-deps are loaded before the module, post-deps after, even though they are not hard symbol dependencies.

Common modprobe flags: -r/--remove removes a module and its now-unused dependencies (the key advantage over rmmod); -a inserts several named modules; -f/--force strips version checks (tainting the kernel); -n/--dry-run shows what would happen without doing it; -q/--quiet suppresses “module not found” noise.

The kmod Autoload Path — request_module()

The kernel frequently discovers it needs code it does not currently have loaded: a mount() of an ext4 filesystem when ext4 is modular, a hot-plugged USB device whose driver is a module, an AF_* socket family that lives in a module. Rather than fail, the kernel asks userspace to load the module for it. This is the kmod mechanism, fronted by the request_module() macro (include/linux/kmod.h):

#define request_module(mod...) __request_module(true, mod)
#define request_module_nowait(mod...) __request_module(false, mod)

__request_module(bool wait, const char *fmt, ...) takes a printf-style name. The canonical example is filesystem mounting: get_fs_type() in fs/filesystems.c calls request_module("fs-%.*s", ...) for an unknown type, so mount -t ext4 triggers request_module("fs-ext4"), which maps (via modules.alias) to the ext4 module (rwmj, “How does mount load the right kernel module?”). The device model uses the same path: when a new device appears, its uevent carries a MODALIAS= key, and either udev (in userspace) or the in-kernel request_module for the device’s modalias drives the load — see Device-Driver Matching and Uevents and the Kernel-Userspace Netlink Channel.

__request_module does not load the module itself — it cannot, because module loading is a userspace policy (which paths, which config). Instead it spawns /sbin/modprobe as a userspace process. Its core flow (kernel/module/kmod.c):

  1. Bail with -ENOENT if modprobe_path is empty (autoload disabled).
  2. Refuse synchronous loading from an async context — “We don’t allow synchronous module loading from async” — to avoid deadlock.
  3. Call security_kernel_module_request() so an LSM can veto the request.
  4. Acquire a slot from a concurrency semaphore (kmod_concurrent_max, MAX_KMOD_CONCURRENT = 50), with a 5-second all-busy timeout that warns about a “recursive module dependency creating a loop.”
  5. call_modprobe() builds the argv and runs the helper.

call_modprobe() constructs the command and uses the usermode helper infrastructure:

static int call_modprobe(char *orig_module_name, int wait)

It builds argv = { modprobe_path, "-q", "--", module_name, NULL }, sets a minimal environment (HOME, TERM, PATH), and calls call_usermodehelper_setup() + call_usermodehelper_exec() to spawn the process from a clean kernel context (a kworker), waiting for it (or not) per the wait flag. The -q keeps it quiet; the spawned modprobe then does its normal dependency resolution and finit_modules the result — re-entering load_module from userspace. This is the loop the diagram shows.

modprobe_path defaults to CONFIG_MODPROBE_PATH ("/sbin/modprobe") and is runtime-tunable via /proc/sys/kernel/modprobe; setting it to the empty string disables autoloading entirely“avoids the overhead of an attempted execve() and potential deadlocks” (kernel sysctl docs). This is a real hardening knob: forbidding autoload shrinks the kernel’s attack surface against malicious modalias-triggered loads.

Idempotent Loading — Deduplicating Concurrent Loads

A subtle race: two CPUs (say, two udev workers reacting to two devices that need the same driver) may call finit_module on the same file at the same time. Without coordination, both would run load_module, one would win and one would get EEXIST after wasting work — and historically this caused real boot-time problems. Linux 6.x added idempotent loading: idempotent_init_module() keys on the file’s inode so only the first caller actually loads, and the rest wait for its result (kernel/module/main.c):

if (!idempotent(&idem, file_inode(f))) {
    int ret = init_module_from_file(f, uargs, flags);
    return idempotent_complete(&idem, ret);
}
return idempotent_wait_for_completion(&idem);

The first caller inserts an entry into a hash table keyed by the file inode and does the load; concurrent callers find the entry and block in idempotent_wait_for_completion until the loader calls idempotent_complete, then return that same result. The effect is that N parallel finit_module(same_file) calls do the work once and all return the same success — no spurious EEXIST, no wasted relocation passes.

Failure Modes and Common Misunderstandings

  • insmod foo.ko fails with Unknown symbol, but modprobe foo works. insmod does not pull in dependencies; the symbol lives in a module insmod never loaded. Use modprobe, which reads modules.dep.
  • blacklist doesn’t actually stop the module. Blacklisting only suppresses alias-driven autoload; a direct modprobe/insmod or a hard dependency still loads it. To hard-block, use install foo /bin/true and rebuild the initramfs if the module loads early.
  • Autoload silently does nothing. Check /proc/sys/kernel/modprobe — if empty, autoload is off. Also check that modprobe.blacklist= was not passed on the kernel command line, and that an LSM/lockdown policy is not vetoing security_kernel_module_request.
  • finit_module rejected under Secure Boot / lockdown. When the kernel is in lockdown or requires signatures, init_module (the buffer form) may be disabled outright and only signed files loaded via finit_module are accepted — see Module Signing and Kernel Lockdown.
  • EBUSY “Timeout while trying to resolve a symbol.” A dependency module is taking too long to appear; usually a dependency ordering or load-loop problem.
  • Loaded but wrong version → memory corruption. Forcing past vermagic with -f taints TAINT_FORCED_MODULE and can crash unpredictably; rebuild against the running kernel instead.

Alternatives and When to Choose Them

For day-to-day use modprobe is always the right tool — by name, dependency-aware, config-aware. Reach for insmod only to load a specific out-of-tree .ko by path during development, knowing you must pre-load its dependencies yourself. The kmod autoload path is not something you invoke directly; it is the kernel’s own mechanism, and your only lever is /etc/modprobe.d/ policy plus the /proc/sys/kernel/modprobe switch. As for the syscalls, finit_module supersedes init_module for everything: it lets the kernel authenticate the file, supports in-kernel decompression, and is what libkmod uses; init_module survives only for the buffer-only edge cases and is the form most likely to be disabled under hardening.

Production Notes

Boot is one giant module-load cascade: the initramfs contains just enough modules to mount the real root, udev fires MODALIAS uevents for every detected device, and modprobe (or kmod) loads the matching drivers — which is why a missing modules.dep/modules.alias or a stale initramfs manifests as “device not detected” or “cannot mount root.” The idempotent-loading work directly addressed boot-time storms where dozens of identical-driver loads raced. On hardened/Secure-Boot systems, operators disable init_module, require signed modules via finit_module, and sometimes zero out /proc/sys/kernel/modprobe to forbid autoload entirely; conversely, a malicious or buggy device whose modalias matches an exploitable driver is a known attack vector that autoload-disabling mitigates. When debugging “why did this module load?”, modprobe --show-depends <name> prints the exact dependency chain and modprobe -nv <name> dry-runs the finit_module sequence without touching the kernel.

See Also