Discuss the steps involved in unloading a kernel module

Discuss the steps involved in unloading a kernel module

Loading a kernel module tends to get all the attention in tutorials and documentation, but unloading is arguably the trickier half of the equation. Getting code into the kernel safely is one challenge; getting it back out cleanly — without leaving dangling references, leaked memory, or a system that crashes the moment something tries to use freed resources — is a whole different level of difficulty. Let’s go through exactly what happens when a kernel module is unloaded.

Why Unloading Is Harder Than Loading

When you load a module, you’re adding something new to a system that didn’t depend on it before. When you unload a module, you’re removing something that other parts of the running kernel — and possibly userspace — might currently be actively using, referencing, or waiting on. The kernel has to be absolutely certain nothing will try to call into freed code or touch freed memory the instant after the module is gone, because doing so would mean executing garbage instructions or corrupting memory — a near-certain kernel panic or, worse, an exploitable vulnerability.

Step 1: The Unload Request

Unloading typically begins with a user command: rmmod modulename or modprobe -r modulename (the latter also removing now-unused dependency modules automatically, mirroring modprobe‘s smarter loading behavior). Both tools ultimately invoke the delete_module() system call, handing control to the kernel.

Step 2: Reference Count Checking

This is the single most important safety check in the entire unloading process. Every kernel module maintains a reference count, tracked by the kernel’s module subsystem, incremented whenever something depends on it — another module referencing its exported symbols, a device using a driver it provides, an open file handle pointing to functionality it implements, and so on.

If this reference count is non-zero, the kernel refuses to unload the module, returning an “in use” error (commonly surfaced to the user as something like “module is in use” from rmmod). This check exists specifically to prevent the disaster scenario described above — you simply cannot remove code that something else is actively relying on.

This is why, if you’ve ever tried to unload a network driver while a network interface it manages is still up and configured, or unload a filesystem module while a filesystem of that type is still mounted, you get a firm refusal rather than a crash. The kernel is protecting you (and itself) from a use-after-free scenario at the most fundamental level.

Step 3: Forcing an Unload (and Why You Almost Never Should)

Linux does technically support force-unloading via rmmod -f, which bypasses reference count checks. This is explicitly documented as dangerous and taints the kernel (marks it as running unsupported/potentially unstable configurations, visible in crash reports and dmesg) when used. In virtually all legitimate scenarios, force-unloading indicates something has gone wrong elsewhere — a reference leak in a driver, for instance — rather than being an appropriate everyday tool. Production systems should essentially never rely on forced unloading.

Step 4: Running the Module’s Exit Function

Assuming the reference count check passes, the kernel calls the module’s registered exit/cleanup function — conventionally registered via the module_exit() macro in Linux kernel module source code:

static void __exit my_module_exit(void)
{
    // Unregister everything registered during init
    misc_deregister(&my_device);
    free_irq(my_irq_number, NULL);
    kfree(my_allocated_buffer);
    pr_info("my_module: exiting cleanly\n");
}

module_exit(my_module_exit);

This function is the mirror image of the init function, and it needs to systematically undo everything the init function set up, in roughly reverse order:

Step 5: Ensuring No In-Flight Operations Remain

Beyond the reference counting described in Step 2, well-written modules also need to internally track and quiesce any asynchronous operations they’ve started — outstanding I/O requests, running kernel threads spawned by the module, or timers that haven’t fired yet. The exit function typically needs to signal these to stop, and then actually wait (block) until they’ve genuinely finished, rather than assuming a cancellation request completes instantly.

This is a genuinely tricky area of kernel programming, and it’s a common source of subtle unload-related bugs — a module might pass all the “obvious” reference counting checks and still crash on unload because some background kernel thread it spawned tries to access module memory a moment after the module’s exit function returns and the memory is freed.

Step 6: Memory Deallocation

Once the exit function returns, the kernel proceeds to free the memory regions that held the module’s code and data — the same regions that were carefully allocated and populated during the loading process described in the initialization sequence. This memory is returned to the kernel’s general memory allocator, available for reuse elsewhere.

Step 7: Removal from Kernel Module Bookkeeping

Finally, the kernel removes the module from its internal module list — no longer visible via lsmod or /proc/modules — and its previously exported symbols become unavailable to any future module loads. If other modules had listed this one as a dependency, that relationship is cleaned up as part of this bookkeeping step as well.

A Worked Example: Unloading a Network Driver

Let’s trace through a realistic scenario — unloading the driver for a USB Wi-Fi adapter you’re about to unplug:

  1. You run sudo ip link set wlan0 down to bring the interface down first (good practice, reduces the chance of in-flight operations at unload time).
  2. You run sudo modprobe -r rtl8188eu.
  3. The kernel checks the module’s reference count. Since you brought the interface down and it’s not otherwise in use, the count should be zero.
  4. The module’s exit function runs: it deregisters the network device, frees the interrupt handler tied to the USB device’s data endpoint, cancels any pending USB transfer requests, and frees allocated buffers.
  5. modprobe -r also automatically unloads now-unused dependency modules, like cfg80211, if nothing else on the system still needs them.
  6. The kernel frees the module’s memory and removes it from the module list.
  7. lsmod no longer shows the module; you can safely unplug the device.

RCU and Delayed Freeing During Unload

One additional wrinkle worth understanding, connecting back to the RCU (Read-Copy-Update) synchronization mechanism used heavily in performance-sensitive kernel subsystems: if a module’s data structures are accessed via RCU by other parts of the kernel, freeing them immediately inside the exit function can be genuinely unsafe, even after all “known” references have apparently been released. This is because RCU readers deliberately avoid taking any lock at all when reading, meaning the kernel can’t simply check a reference count to know when it’s safe to free the underlying memory — a reader might still be in the middle of an RCU-protected read section, invisible to any counter-based tracking.

The correct approach is to use synchronize_rcu() (or its callback-based sibling, call_rcu()) during the exit path, which blocks (or schedules a deferred callback) until the kernel can guarantee every CPU has passed through what’s called a “quiescent state” — a checkpoint guaranteeing no pre-existing RCU reader could still be in flight. Only after this guarantee is satisfied is it actually safe to free the underlying memory. Skipping this step in a module that uses RCU-protected data structures is a subtle but serious bug, since it can appear to work correctly in casual testing and only manifest as a crash under specific timing conditions involving concurrent readers — precisely the kind of intermittent, hard-to-reproduce failure that makes concurrent kernel programming so notoriously difficult to get right.

What Happens If Unloading Goes Wrong

Allowing or Blocking Unloading Entirely

Not every module is even designed to support unloading. A module author can simply choose not to define an exit function at all — in this case, once loaded, the module is permanently resident until the next reboot, and rmmod will refuse to even attempt removal, reporting that the module doesn’t support unloading. This is a legitimate design choice for modules where safe teardown genuinely isn’t practical or worth the engineering effort — some early-boot-critical modules or certain classes of security-sensitive modules (which might not want to offer any code path that removes their protections at runtime) take exactly this approach deliberately.

Separately, the kernel configuration option CONFIG_MODULE_UNLOAD controls whether unloading is even compiled into the kernel as a capability at all. Some highly locked-down kernel builds (certain embedded systems, security-hardened server images) disable this entirely, meaning no module — regardless of how well-written its exit function is — can ever be unloaded on that system. This is a deliberate hardening measure: if an attacker who somehow gains the ability to load kernel code can’t also unload legitimate security-monitoring modules, that closes off one avenue of covering their tracks.

The Role of try_module_get() and module_put()

Digging a bit deeper into how reference counting actually works mechanically: any code path that wants to safely call into a module’s functionality first calls try_module_get(), which atomically increments the module’s reference count and returns success — but critically, it returns failure instead if the module is already in the process of being unloaded, preventing a nasty race condition where code might otherwise start using a module the instant after its exit function has already begun running. Once the caller is done with whatever it needed from the module, it calls module_put() to decrement the count again.

This pairing is what actually backs the “module is in use” protection described earlier. If you look inside real driver code, you’ll see this pattern constantly — a network driver’s packet transmit function, a filesystem driver’s read function, a character device’s file operations — all typically wrapped with this get/put pairing (often implicitly, through higher-level kernel framework code that handles it on the module’s behalf) precisely so the reference count accurately reflects genuine, active usage at every point in time, not just at initial registration.

Windows and macOS Comparison

Windows drivers have an analogous unload sequence — the I/O Manager and Plug and Play Manager coordinate to call the driver’s Unload routine (referenced via the DriverObject->DriverUnload field), which must similarly release all resources, cancel pending I/O, and free memory before the driver’s code is unmapped. Windows also enforces its own reference counting for drivers currently handling active device I/O, refusing removal of drivers still in active use, mirroring Linux’s approach conceptually.

macOS’s approach with kernel extensions worked similarly, though Apple’s deprecation of third-party kexts in favor of user-space DriverKit sidesteps much of this complexity for newer drivers — a user-space driver crashing or being terminated doesn’t carry the same catastrophic, whole-system risk that a botched kernel-space unload does, which is a big part of why Apple made that architectural shift.

Best Practices for Writing Clean Module Exit Functions

  1. Write your exit function as a careful mirror of your init function, undoing every registration and allocation in reverse order.
  2. Never assume asynchronous operations have completed just because you’ve signaled them to stop — explicitly wait for confirmation where the kernel API provides a mechanism to do so.
  3. Test the load/unload cycle repeatedly and under load (with the device or subsystem actively in use, then properly quiesced) during development, not just on an idle system.
  4. Use kernel memory debugging tools (like kmemleak on Linux) during development to catch leaks that might not be obvious from casual testing.
  5. Never rely on forced unloading (rmmod -f) as a normal workflow step — treat needing it as a bug signal, not a routine tool.
  6. Document any known limitations around unloading (for instance, if your module genuinely cannot be safely unloaded under certain conditions) rather than leaving future maintainers to discover this the hard way.

Summary

Unloading a kernel module safely requires far more care than it might initially appear. The kernel’s reference counting system provides a critical first line of defense, refusing to remove modules still in active use, but the real work happens inside the module’s own exit function — carefully unregistering every resource, canceling every pending operation, and freeing every allocation that its init function set up, all without leaving any dangling references that could be touched a moment later by code that no longer exists. Getting this wrong is one of the most common sources of kernel instability in custom or poorly-maintained drivers, which is exactly why disciplined, symmetrical init/exit design is considered a core skill in kernel-level programming.

FAQs

Why does Linux refuse to unload some modules? Because their reference count is non-zero — something else in the kernel (another module, an open device, a mounted filesystem) still depends on them. This is a safety mechanism preventing use-after-free crashes.

Is it ever safe to force-unload a kernel module? Rarely, and it should generally be treated as a debugging tool rather than a normal operational step. Forced unloads bypass safety checks and taint the kernel, and are far more likely to lead to instability than to resolve the underlying issue causing the “in use” state.

What happens to memory a module allocated if the exit function doesn’t free it? It leaks permanently — kernel memory isn’t automatically reclaimed the way a terminated userspace process’s memory is. This memory remains unusable until the next system reboot, making memory leaks in kernel module exit paths a genuinely serious, cumulative problem.

Can a kernel module prevent itself from being unloaded? Indirectly, yes — by maintaining a non-zero reference count for legitimate reasons (active device usage, open handles, etc.), a module effectively blocks unloading until those conditions clear. There’s also a historical mechanism, try_module_get()/module_put(), that modules and their dependents use precisely to manage this.

Does unloading a module affect other modules that depend on it? The kernel won’t let you unload a module that other loaded modules currently depend on — you’d need to unload the dependents first (or let a tool like modprobe -r handle the correct order automatically).

Official References

Exit mobile version