Linux Kernel Module Stacking — Part 2

Linux Kernel Module Stacking Part 2 – GPL Enforcement, Reference Counting, Symbol Namespaces (Linux 6.x) | Free Linux Kernel Programming Course
Free Linux Kernel Programming Course  |  EmbeddedPathashala

Linux Kernel Module Stacking — Part 2

GPL enforcement, module reference counting, symbol namespaces, and the full build-to-runtime dependency pipeline — Linux 6.x

Linux 6.x
Updated Content
Free
No Signup Needed
Part 2 / 2
This Lecture

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 internals try_module_get / module_put GPL enforcement — build time and load time MODULE_LICENSE taint flags Symbol namespaces in Linux 6.x Full dependency pipeline: modpost → depmod → modprobe Linux 6.x specific module infrastructure changes Common mistakes and how to fix them

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);
Module Reference Count Lifecycle
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.

GPL Enforcement — Two Checkpoints
① Build Time — modpost
  • Runs automatically after make
  • Checks the consumer’s declared license against every symbol it references
  • If consumer uses an EXPORT_SYMBOL_GPL symbol but declares a non-GPL license, it emits:
ERROR: modpost: GPL-incompatible module consumer.ko uses GPL-only symbol 'ep_show_platform_info'
② Load Time — kernel/module/main.c
  • Runs when you do insmod or modprobe
  • The kernel walks the symbol table looking for each symbol the module needs
  • For GPL-only symbols it checks the gplok flag of the requesting module
  • If the module is tainted proprietary, gplok = false, the symbol lookup returns nothing
  • Result: Unknown symbol ep_show_platform_info (err -ENOENT) and load fails

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.

Common Kernel Taint Flags
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");
Important Linux 6.13 Change: Before 6.13, the namespace argument was a bare token without quotes: 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.

Symbol Namespace Access Control — Linux 6.x
Provider Module

EXPORT_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:

Build-to-Runtime Dependency Pipeline
Step 1: make
GCC 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 -a
Scans 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_module
Reads 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.

Linux 6.x Module Infrastructure Changes
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

Best Practices Summary
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

Reference count tracks how many things use a module try_module_get increments, module_put decrements .owner = THIS_MODULE handles ref counting for char drivers GPL enforcement happens at both build time and load time Non-GPL modules cannot access EXPORT_SYMBOL_GPL symbols Taint flag P means a proprietary module was loaded Namespaces add a third layer of export access control Linux 6.13+ requires quoted string namespace syntax depmod builds the dependency graph modprobe reads kernel/module/ directory replaced kernel/module.c in 5.18+

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.

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

Leave a Reply

Your email address will not be published. Required fields are marked *