Linux Kernel Module Stacking — Part 2
GPL enforcement, module reference counting, symbol namespaces, and the full build-to-runtime dependency pipeline — Linux 6.x
Quick Recap from Part 1
In Part 1 you learned what module stacking is, how to use EXPORT_SYMBOL and EXPORT_SYMBOL_GPL, why loading order matters with insmod, and how to read the lsmod output. In this part we go deeper into how the kernel actually enforces all of this at runtime and at build time.
Module Reference Counting — How the Kernel Tracks “In Use”
Every loaded kernel module has an internal reference count. This count tells the kernel how many things are currently using that module. The kernel will not allow a module to be unloaded as long as its reference count is greater than zero.
The reference count you see in lsmod is not just about other modules. It can be incremented by:
- Another module depending on and using this module’s exported symbols
- A process that has opened a device file backed by this module
- A subsystem that holds an internal reference (e.g. a registered driver entry)
The two kernel functions that manage the reference count are try_module_get() and module_put(). These are defined in include/linux/module.h.
/* Increment the reference count of a module.
* Returns true (non-zero) on success.
* Returns false (zero) if the module is being unloaded — do not use it.
*/
bool try_module_get(struct module *module);
/* Decrement the reference count. Must be called after try_module_get succeeds. */
void module_put(struct module *module);
|
provider_module loaded refcount = 0 |
→ |
consumer_module loaded refcount = 1 |
→ |
consumer_module unloaded refcount = 0 |
→ |
provider_module unloaded refcount was 0 ✓ |
When the consumer module is loaded and uses a symbol from the provider, the kernel internally calls try_module_get(provider_module). This increments the provider’s reference count from 0 to 1. When the consumer is unloaded, module_put(provider_module) is called and the count drops back to 0, making the provider removable.
You can check the current reference count of any module directly:
$ cat /sys/module/provider_module/refcnt
1
# See which modules are listed as holders (dependents)
$ ls /sys/module/provider_module/holders/
consumer_module
When Do You Call try_module_get Yourself?
Most driver developers never call try_module_get directly. The kernel subsystems handle it for you. For example, when a process opens a character device, the kernel calls fops_get() internally which does try_module_get(fops->owner). Your driver just sets .owner = THIS_MODULE in its file_operations structure and the reference counting works automatically.
static const struct file_operations ep_fops = {
.owner = THIS_MODULE, /* kernel uses this for ref counting */
.open = ep_open,
.release = ep_release,
.read = ep_read,
.write = ep_write,
};
You only need to call try_module_get and module_put manually when you store a function pointer from another module and call it later — for example in a plugin or callback architecture inside the kernel.
GPL License Enforcement — Two Layers of Defense
The kernel enforces GPL-only symbol access at two separate points. Getting blocked at either one means you cannot use the symbol.
|
① Build Time — modpost
ERROR: modpost: GPL-incompatible module consumer.ko uses GPL-only symbol 'ep_show_platform_info' |
→ |
② Load Time — kernel/module/main.c
|
What MODULE_LICENSE Does Exactly
The kernel checks your declared license string against a fixed list inside include/linux/license.h. The function license_is_gpl_compatible() returns true only for these exact strings:
MODULE_LICENSE("GPL"); /* GNU General Public License v2 or later */
MODULE_LICENSE("GPL v2"); /* GNU General Public License v2 only */
MODULE_LICENSE("GPL and additional rights");
MODULE_LICENSE("Dual BSD/GPL");
MODULE_LICENSE("Dual MIT/GPL");
MODULE_LICENSE("Dual MPL/GPL");
Anything else — including "Proprietary", "MIT", "BSD" alone, or any custom string — is treated as non-GPL. The kernel still loads the module but sets the P taint flag on the kernel.
| Flag Letter | Meaning | Triggered By |
|---|---|---|
| P | Proprietary module loaded | Non-GPL MODULE_LICENSE string |
| O | Out-of-tree module loaded | Any module not from the kernel source tree |
| F | Module force-loaded | insmod --force bypassed version check |
| E | Unsigned module loaded | Module loaded without valid signature (when signing enabled) |
| C | Staging driver loaded | Driver from drivers/staging/ |
You can check the current taint status of your running kernel at any time:
$ cat /proc/sys/kernel/tainted
0 # Zero means no taint — clean kernel
# After loading a proprietary module:
$ cat /proc/sys/kernel/tainted
4096 # Non-zero — kernel is tainted
Taint is sticky for the entire boot session. Even if you later unload the proprietary module, the taint bit remains set until the next reboot. This is important when reporting kernel bugs — a tainted kernel means the maintainers cannot fully trust the crash reports.
What Happens at the Boundary — GPL Module Using a Proprietary Module
The GPL enforcement also works in the other direction. If a GPL-licensed module tries to use symbols from a proprietary module, the kernel prints a warning and marks the GPL module as contaminated:
dmesg output:
consumer_module: module using GPL-only symbols uses symbols from proprietary module provider_module.
This is the kernel’s way of saying: you cannot wrap a GPL interface around a proprietary core. The intent is to prevent a common trick where a thin GPL shim module is used to expose proprietary functionality to the kernel.
Symbol Namespaces in Linux 6.x — A Newer Layer of Access Control
Starting with Linux 5.4, the kernel added a feature called symbol namespaces. This lets subsystem maintainers group their exported symbols under a named namespace and require consuming modules to explicitly declare that they are importing from that namespace.
Think of it as a second layer of access control on top of the GPL check. Even if your module is GPL-compatible, you still have to declare which namespaces you are importing from.
Exporting Into a Namespace
/* Linux 6.13+ — namespace is a quoted string */
EXPORT_SYMBOL_NS(ep_add, "EP_MATH");
EXPORT_SYMBOL_NS_GPL(ep_show_platform_info, "EP_INTERNAL");
EXPORT_SYMBOL_NS(ep_add, EP_MATH). From Linux 6.13 onwards (commit cdd30ebb1b9f by Peter Zijlstra), the namespace must be a quoted string literal: EXPORT_SYMBOL_NS(ep_add, "EP_MATH"). Code written for older kernels using the bare token form will not compile on 6.13+. Always use the quoted string form for any new code targeting Linux 6.x.
Importing a Namespace in the Consumer
/* In consumer_module.c — must declare namespace imports */
MODULE_IMPORT_NS("EP_MATH");
MODULE_IMPORT_NS("EP_INTERNAL");
If the consumer does not declare the import, the kernel refuses to load it with EINVAL and dmesg shows:
consumer_module: module EP_MATH is not imported
During development, you can use make nsdeps in your kernel build directory and it will automatically add the correct MODULE_IMPORT_NS lines to your source files based on what your module actually uses.
Provider ModuleEXPORT_SYMBOL_NS(fn_a, "NS_PUBLIC");EXPORT_SYMBOL_NS_GPL(fn_b, "NS_PRIVATE");fn_a is in namespace NS_PUBLIC fn_b is in namespace NS_PRIVATE and also GPL-only |
→ |
Consumer A (GPL)
MODULE_LICENSE("GPL");MODULE_IMPORT_NS("NS_PUBLIC");MODULE_IMPORT_NS("NS_PRIVATE");✓ Can use both fn_a and fn_b
Consumer B (Proprietary)
MODULE_LICENSE("Proprietary");MODULE_IMPORT_NS("NS_PUBLIC");✗ Cannot use fn_b (GPL-only) ⚠ Can use fn_a if namespace declared |
The Full Dependency Pipeline — From Build to Runtime
Understanding the complete flow from writing code to a module running on the kernel helps you debug problems faster. Here is how the whole system works end to end:
Step 1: makeGCC compiles .c files into .o object files LD links .o files into a relocatable .ko file |
| ↓ |
Step 2: modpost (automatic during make)Reads Module.symvers — checks all external symbols are exported Verifies GPL compliance for EXPORT_SYMBOL_GPL symbols Checks namespace imports Embeds a ____versions CRC table into the .ko for ABI checking
|
| ↓ |
Step 3: make modules_install or manual copy to /lib/modules/<ver>/.ko files placed under /lib/modules/$(uname -r)/extra/ or kernel/ |
| ↓ |
Step 4: sudo depmod -aScans all .ko files under /lib/modules/$(uname -r)/ Builds the dependency graph Writes /lib/modules/$(uname -r)/modules.dep and modules.dep.bin Also writes modules.symbols and modules.alias |
| ↓ |
Step 5: modprobe consumer_moduleReads modules.dep.bin Loads provider_module first (dependency resolved) Then loads consumer_module Kernel does final symbol resolution + GPL + namespace checks at load time |
What modules.dep Looks Like
After running depmod -a, the file /lib/modules/$(uname -r)/modules.dep contains lines like this:
kernel/extra/consumer_module.ko: kernel/extra/provider_module.ko
This means consumer_module.ko depends on provider_module.ko. When you run modprobe consumer_module, modprobe reads this file and loads provider_module first. Dependencies are listed right-to-left (rightmost must load first).
# Inspect dependencies of an installed module
$ modinfo consumer_module
filename: /lib/modules/6.x.x/extra/consumer_module.ko
license: GPL
depends: provider_module
vermagic: 6.x.x SMP preempt mod_unload
import_ns: EP_MATH
The depends: field shows what modules must be loaded first. The import_ns: field (Linux 6.x) shows which symbol namespaces this module imports from.
Linux 6.x Specific Changes to Module Infrastructure
If you have used older tutorials or books written for kernel 4.x or 5.x, there are a few important changes to be aware of when working on Linux 6.x.
| Change | Old Behavior | New Behavior (6.x) |
|---|---|---|
| Source tree location | kernel/module.c |
kernel/module/main.c (split into multiple files in 5.18+) |
| Namespace string syntax | EXPORT_SYMBOL_NS(fn, MYNS) — bare token |
EXPORT_SYMBOL_NS(fn, "MYNS") — quoted string (6.13+) |
| Module compression | gzip or xz only | zstd added; in-kernel decompression supported (CONFIG_MODULE_COMPRESS_ZSTD) |
| Module signing algorithms | RSA, ECDSA | RSA, NIST P-384 ECDSA, and ML-DSA (post-quantum, needs OpenSSL 3.5+) |
| symbol_get() behavior | Could resolve non-GPL symbols from any module | Only resolves GPL-only symbols; warns and fails for non-GPL symbols |
| New export macro | Not available | EXPORT_SYMBOL_FOR_MODULES(sym, "mod1,mod2") — restricts symbol to named in-tree modules only |
EXPORT_SYMBOL_FOR_MODULES — New in 6.x
This is a new macro in 6.x that allows a symbol to be exported but restricted to a specific list of in-tree modules. It is mainly used by KVM and other subsystems that want tight control over who can use a particular internal function.
/* Only kvm and kvm-intel can use this symbol */
EXPORT_SYMBOL_FOR_MODULES(kvm_internal_helper, "kvm,kvm-intel");
/* Tail glob — kvm and any module starting with kvm- */
EXPORT_SYMBOL_FOR_MODULES(another_helper, "kvm,kvm-*");
These symbols are placed in a special namespace that cannot be imported by out-of-tree modules. They are GPL-only by design and cannot be used by external code at all. You will not need this for typical driver development, but it is useful to know when reading kernel source code.
Common Mistakes and How to Fix Them
Mistake 1: Forgetting EXPORT_SYMBOL
Symptom: Unknown symbol function_name (err -2) in dmesg when loading the consumer.
Cause: The function exists in the provider module but was never exported. Without EXPORT_SYMBOL, it is invisible to other modules.
Fix: Add EXPORT_SYMBOL(function_name); after the function definition in the provider, rebuild, and reload.
/* Missing — causes Unknown symbol error */
int ep_helper(void) { return 42; }
/* Correct */
int ep_helper(void) { return 42; }
EXPORT_SYMBOL(ep_helper);
Mistake 2: Loading Consumer Before Provider
Symptom: insmod: ERROR: could not insert module: Unknown symbol in module
Cause: Using insmod and loading the consumer module before the provider is in memory.
Fix: Always load the provider first with insmod, or use modprobe which handles this automatically.
Mistake 3: Trying to Remove the Provider While Consumer Is Loaded
Symptom: rmmod: ERROR: Module provider_module is in use by: consumer_module
Cause: The provider’s reference count is non-zero because the consumer is still loaded and using its symbols.
Fix: Remove modules in reverse order — consumer first, then provider. Or use modprobe -r consumer_module which does this automatically.
Mistake 4: Non-GPL Module Trying to Use EXPORT_SYMBOL_GPL Symbol
Symptom at build time: ERROR: modpost: GPL-incompatible module uses GPL-only symbol
Symptom at load time: Unknown symbol function_name (err -2)
Cause: Your MODULE_LICENSE is not GPL-compatible but your code calls a GPL-only exported function.
Fix: Either change your module license to MODULE_LICENSE("GPL") or MODULE_LICENSE("Dual MIT/GPL"), or do not use that GPL-only function. You cannot use EXPORT_SYMBOL_GPL symbols in a non-GPL module — this is enforced at both build and load time.
Mistake 5: modprobe Cannot Find the Module
Symptom: modprobe: FATAL: Module consumer_module not found in directory /lib/modules/6.x.x
Cause: The .ko file was not installed into the correct location, or depmod -a was not run after installation.
Fix:
# Copy modules to the right place
$ sudo cp *.ko /lib/modules/$(uname -r)/extra/
# Rebuild the dependency database
$ sudo depmod -a
# Now modprobe works
$ sudo modprobe consumer_module
Mistake 6: Namespace Not Imported (Linux 6.x)
Symptom: Module fails to load with EINVAL and dmesg shows module NAMESPACE_NAME is not imported.
Cause: The provider exported a symbol into a namespace using EXPORT_SYMBOL_NS but the consumer did not declare MODULE_IMPORT_NS.
Fix: Add the import declaration to the consumer, or run make nsdeps to auto-generate missing imports.
/* Add this to consumer_module.c */
MODULE_IMPORT_NS("PROVIDER_NAMESPACE");
Best Practices for Module Stacking
| Practice | Why It Matters |
|---|---|
Always declare MODULE_LICENSE("GPL") |
Avoids taint, gives access to all exported symbols, and is effectively required for in-tree development |
| Use modprobe / modprobe -r for stacked modules | Handles dependency order automatically — both for loading and removal |
Always run depmod -a after installing .ko files |
modprobe will not see your module without this step |
| Create a shared header for exported symbols | Prevents declaration mismatches between provider and consumer |
Use .owner = THIS_MODULE in file_operations |
Lets the kernel manage reference counting for you automatically |
| Use quoted strings for namespaces (Linux 6.13+) | Bare token form no longer compiles on 6.13+ kernels |
Check /sys/module/<name>/holders/ before removing |
Shows exactly which other modules currently hold a reference to this one |
Useful Commands for Debugging Module Dependencies
# Show all loaded modules, their size, and their dependents
$ lsmod
# Show detailed info about a .ko file (before loading)
$ modinfo ./consumer_module.ko
# Show live kernel symbol table — grep for your symbol
$ sudo grep ep_add /proc/kallsyms
# Check reference count of a loaded module
$ cat /sys/module/provider_module/refcnt
# List modules that hold a reference to a module
$ ls /sys/module/provider_module/holders/
# Check the dependency file generated by depmod
$ cat /lib/modules/$(uname -r)/modules.dep | grep consumer
# See which processes have a device file open (if refcount is stuck > 0)
$ sudo lsof /dev/your_device
# Check kernel taint status
$ cat /proc/sys/kernel/tainted
# Show all namespaces imported by a module
$ modinfo consumer_module | grep import_ns
# Verbose modprobe — shows what is loaded and in what order
$ sudo modprobe -v consumer_module
Key Takeaways — Part 2
Frequently Asked Questions — Part 2
Q: Can I check if a symbol is GPL-only before trying to use it?
Yes. Check /proc/kallsyms for the symbol. If the type letter is lowercase t it is not exported at all. If it is uppercase T it is exported. To know if it is GPL-only, look at the kernel source: search for the function name followed by EXPORT_SYMBOL_GPL. Alternatively use grep -r "EXPORT_SYMBOL_GPL(function_name)" kernel_source_directory/.
Q: What is Module.symvers and when do I need it?
Module.symvers is a file generated during a kernel or module build. It maps each exported symbol to its CRC checksum (used to detect ABI changes), its export type (GPL or not), and which module owns it. You need it when building one out-of-tree module that depends on another out-of-tree module — you point the consumer’s build to the provider’s Module.symvers so the build system knows the symbol is available and what its CRC is.
Q: The reference count is stuck at 1 even after I unloaded the consumer. What happened?
This usually means something else is still holding a reference. Check if a process has the device file open (lsof /dev/yourdevice). Check /sys/module/provider/holders/ to see if any module is listed. If the module is a platform or bus driver, the subsystem may hold an internal reference until all registered devices are removed. Sometimes a bug in the consumer’s exit function forgets to release a resource, leaving a dangling reference.
Q: Does module signing affect stacking?
Module signing and module stacking are independent mechanisms. Signing controls whether the kernel loads a .ko file at all (based on cryptographic signature). Stacking controls whether one module can use another’s symbols (based on load order and export macros). If CONFIG_MODULE_SIG_FORCE is enabled, every module in the stack must be signed — the consumer and the provider both. On most development systems this is not enforced unless you are running a distribution kernel with Secure Boot.
Q: Can I have a circular dependency between two modules?
No. The kernel does not support circular module dependencies. If module A depends on module B and module B also depends on module A, there is no valid loading order — you cannot load either one first. This is a design flaw and must be resolved by refactoring the shared code into a third module that both A and B depend on, or by combining the functionality into a single module.
Q: What is the difference between softdeps and hard dependencies?
A hard dependency means the consumer’s symbols cannot be resolved without the provider — it physically cannot load without it. A softdep (soft dependency) is a hint to modprobe: load module X before module Y if both are being loaded, but Y does not actually call any symbol from X. Softdeps are declared in modprobe configuration files (/etc/modprobe.d/) using softdep modulename pre: otherdep. They handle ordering requirements that are not about symbol resolution — for example, firmware loading order.
Q: I changed my provider module and rebuilt it. Do I need to reload the consumer too?
Yes — if you unloaded and reloaded the provider, the consumer that was loaded against the old version is now pointing at addresses that no longer exist. You must unload the consumer first, reload the new provider, then reload the consumer. The kernel vermagic and CRC checks in the .ko file help catch ABI mismatches at load time and will refuse to load an incompatible combination.
Authoritative References
Free Linux Kernel Programming Course
All lectures on EmbeddedPathashala are completely free. No subscription, no signup. Learn Linux kernel programming, Linux device drivers, and embedded systems at your own pace.
Visit EmbeddedPathashala