Kconfig, modules, symbols, and compatibility

Explain effective Kconfig values, module loading, symbol resolution and teardown.

14 min readChapter 10 of 168Beginner → intermediate
In this chapter

Before you start#

Read Kbuild and toolchains and the disposable QEMU lab. The source-only route needs no build or guest. The optional module experiment requires a matching chapter 08 x86-64 kernel, a newly assembled disposable initramfs and QEMU; never insert the example into the host. Module availability, signing and unloading depend on configuration and policy. Source descriptions refer to upstream v6.12.

Learning goals#

Predict the effective value of a Kconfig option from its type and dependencies; explain how a relocatable module is checked, linked and initialized at runtime; account for references through failure and unload; and distinguish module compatibility evidence from a guarantee of safety.

A configuration is a constrained solution#

Kconfig describes options and constraints, while the .config file records a particular solution. A boolean option is y or n; a tristate can also be m when module support and the option's constraints allow it. y generally selects built-in code and m module code when the Makefile uses the corresponding Kbuild idiom. A user request is not necessarily the final value: dependencies, enclosing menus, defaults and choices constrain visibility and effective values. olddefconfig resolves a configuration using the current source and defaults for new options. Inspect the resulting .config and generated files instead of assuming an editing command took effect.

depends on restricts an option's availability and value. A select imposes a reverse dependency and can force its target without recursively satisfying all of that target's own dependencies, which is why maintainers use it carefully. imply is a weaker reverse dependency whose target can remain disabled under relevant conditions. default supplies a value when applicable; it is not an unconditional command that overrides every user choice and dependency. A choice constrains alternatives according to its declared type and rules. The precise semantics are documented in the v6.12 Kconfig language reference. Do not treat a single line of a Kconfig file as the whole constraint graph: enclosing if blocks and other definitions also matter.

Source walkthrough#

A module request and its lifetime
A module request and its lifetime1. Guest loader to finit_module: request using a file descriptor. 2. finit_module to Module loader: validate policy and load file. 3. Module loader to Module loader: check ELF, symbols and relocations. 4. Module loader to Module init: invoke initialization on success. 5. Module init to Module loader (return): success or failure. 6. Module loader to finit_module (return): release or retain resources as appropriate. 7. finit_module to Guest loader (return): return result.Guest loaderfinit_moduleModule loaderModule initrequest using a filedescriptorvalidate policy andload filecheck ELF,symbols andrelocationsinvokeinitialization onsuccesssuccess or failurerelease or retainresources asappropriatereturn result
  1. Guest loaderfinit_module

    request using a file descriptor

  2. finit_moduleModule loader

    validate policy and load file

  3. Module loaderModule loader

    check ELF, symbols and relocations

  4. Module loaderModule init

    invoke initialization on success

  5. Module initModule loader

    Return. success or failure

  6. Module loaderfinit_module

    Return. release or retain resources as appropriate

  7. finit_moduleGuest loader

    Return. return result

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Guest loader → finit_module: request using a file descriptor
  2. finit_module → Module loader: validate policy and load file
  3. Module loader → Module loader: check ELF, symbols and relocations
  4. Module loader → Module init: invoke initialization on success
  5. Module init → Module loader (return): success or failure
  6. Module loader → finit_module (return): release or retain resources as appropriate
  7. finit_module → Guest loader (return): return result
Source map. Conditional source path, not a captured module load; policy, configuration and failures alter the sequence.

The opening of v6.12 kernel/module/Kconfig contains (SPDX GPL-2.0-only; help text omitted):

text
menuconfig MODULES
	bool "Enable loadable module support"
	modules
	select EXECMEM

The menu option itself is boolean; modules marks its special role in enabling tristate module choices. Selecting it also selects EXECMEM. This does not mean every driver becomes a module or every module can be unloaded. Its subsequent if MODULES block scopes other module options. For a concrete dependency, v6.12 drivers/usb/serial/Kconfig defines USB_SERIAL as tristate and dependent on TTY. If TTY=n, requesting USB_SERIAL=m cannot make that dependency true by itself; after configuration resolution inspect the result. If MODULES=n, an m selection is unavailable. Enclosing USB conditions can impose additional restrictions. This is source reasoning about a configuration, not a claim about your current kernel.

In v6.12 drivers/usb/serial/Kconfig, SPDX GPL-2.0, the start of USB_SERIAL (help text omitted):

text
menuconfig USB_SERIAL
	tristate "USB Serial Converter support"
	depends on TTY

The tristate line is what allows m at all, and depends on TTY is the constraint that a TTY=n configuration cannot satisfy. The excerpt does not show the enclosing if blocks, any select from another option, or the value that the resolved .config records.

The runtime loader's finit_module entry in v6.12 kernel/module/main.c includes this excerpt (SPDX GPL-2.0-or-later; validation and other statements omitted):

c
f = fdget(fd);
err = idempotent_init_module(fd_file(f), uargs, flags);
fdput(f);
return err;

The function first checks whether module loading is permitted and rejects unsupported flag bits. It then acquires the descriptor reference, passes its file to the loader helper, and releases the descriptor reference before returning the helper's result. A bad or unreadable descriptor is an error; a pointer passed to the helper is not a promise that the supplied file is a valid module. The separate init_module entry copies module data from userspace memory. Both enter the loader under their documented checks rather than accepting an arbitrary userspace pointer as executable kernel code.

Within load_module, the same source checks the signature when configured, validates ELF and sections, performs early checks, allocates module memory and reserves its place in the module list. A later excerpt shows a symbol-resolution error branch (same file and SPDX notice; surrounding stages omitted):

c
err = simplify_symbols(mod, info);
if (err < 0)
	goto free_modinfo;

The return value determines whether the loader continues or goes through cleanup. The named cleanup label is one step in a chain that releases resources acquired earlier; the exact label depends on how far loading progressed. Relocations, symbol resolution, version checks and initialization are distinct stages. On the successful path the loader eventually calls do_init_module. If the init function reports failure, the module does not become an ordinary live module and its failure path must clean up. Do not compress all failure cases into “the module was never allocated”: some fail after memory or references were acquired.

In v6.12 kernel/module/main.c, SPDX GPL-2.0-or-later, the tail of load_module moves from formation to the init call (intervening statements omitted):

c
	/* Finally it's fully formed, ready to start executing. */
	err = complete_formation(mod, info);
	if (err)
		goto ddebug_cleanup;

	err = prepare_coming_module(mod);
	if (err)
		goto bug_cleanup;

A failing stage jumps to the cleanup label for the stage it reached, which is the chain described above. prepare_coming_module runs the coming notifiers before init, and only then does do_init_module run the module's init function. The excerpt does not show the cleanup labels themselves or the relocation and symbol stages before it.

What is inside a module?#

A .ko is an ELF relocatable object containing code, data, sections, symbol and relocation information, plus generated module metadata. Kbuild and modpost arrange the final module and collect version and exported-symbol information. At load time addresses must be resolved against permitted kernel and module exports; a missing required export can prevent loading. EXPORT_SYMBOL and EXPORT_SYMBOL_GPL are not interchangeable promises that an arbitrary module can use any internal symbol. License metadata affects which exports are permitted and can affect taint reporting. Consult v6.12 include/linux/module.h and the external module guide.

vermagic records a string of relevant build properties; matching it does not prove full ABI compatibility. With CONFIG_MODVERSIONS, symbol-version checks can provide additional evidence that expected exported interfaces match. They do not validate object lifetime, concurrency, semantics, device behavior or every compiler assumption. Upstream does not promise a stable internal module ABI across arbitrary kernels. Build the module against the exact configured output tree used for the guest, including the matching Module.symvers where needed. A modules_prepare run alone does not produce every artifact needed for symbol versioning; a complete kernel build may be required.

Module signatures and enforcement are another independent dimension. CONFIG_MODULE_SIG enables checking; policy such as CONFIG_MODULE_SIG_FORCE can require a trusted signature. When enforcement is off, unsigned modules may load and taint the kernel. A signature mismatch for a key the kernel has, or an unparsable signature, is rejected; enforcement can also reject unsigned modules. Taint records conditions useful for diagnosis; it is not a security verdict and removing a module does not necessarily erase the historical taint. See module signing and tainted kernels.

Build-time and load-time symbol evidence#

Suppose a module calls an exported helper named by its relocation table. At build time, modpost uses the symbol information available from the kernel output and other supplied module builds. With versioning enabled, that information includes checksums associated with exported interfaces. At load time, the running kernel resolves the reference against exports actually available there and applies relocations to the module's allocated sections. A missing symbol, restricted GPL-only export or incompatible version can prevent formation before the module's init function is called. The successful build therefore establishes a relationship to the build tree, while a successful load establishes additional checks against one running kernel. Neither shows that the helper's semantics match the caller's assumptions or that an asynchronous callback cannot outlive the module.

Lifetime, unloading and a worked state#

Imagine a small diagnostic module with an init function that prints one message and returns zero, and an exit function that prints another. Initially its file is just bytes in the guest initramfs. An authorized guest loader opens it and asks the kernel to load it. After validation, allocation, relocation and successful initialization, the module can appear in /proc/modules. That entry is evidence of the guest's observed state, not proof that all module paths were exercised. A second insertion of the same name can fail because the module is already present; the exact diagnostic depends on userspace and kernel.

Module state transitions
  1. UNFORMED

    The enum value for a module still being set up; the loader checks it at several points.

  2. COMING

    Set in complete_formation so strong references ignore the module while it is still forming.

  3. LIVE

    do_init_module sets it after init returns without a failure.

  4. GOING

    A failed init (the fail: label) or try_stop_module during unload marks the module as dying.

Source map. Source map of state assignments in v6.12 kernel/module/main.c; not a captured module load.

If unloading is configured and no use prevents it, removal transitions the module toward teardown and calls its exit function. Real modules must first stop new work, unregister interfaces, cancel or flush deferred work and timers as appropriate, and ensure no callback or reference can still enter freed code or data. A module reference count helps manage some uses, but it is not a substitute for each subsystem's synchronization and lifetime protocol. Forced unloading, when enabled, can be dangerous; this exercise never requires it. If a module is busy, treat refusal as evidence to investigate references, not a reason to force removal. If its init function fails after registering something, it must undo the registrations it already made before returning the error.

The loader's own failure path is separate from the module's cleanup. In v6.12 kernel/module/main.c, SPDX GPL-2.0-or-later, the fail: label of do_init_module reads (lines omitted before the label):

c
fail:
	/* Try to protect us from buggy refcounters. */
	mod->state = MODULE_STATE_GOING;
	synchronize_rcu();
	module_put(mod);
	blocking_notifier_call_chain(&module_notify_list,
				     MODULE_STATE_GOING, mod);
	klp_module_going(mod);
	ftrace_release_mod(mod);
	free_module(mod);

The excerpt shows the module moved to the going state, a wait for RCU readers, a reference drop, and the call to free_module. It does not show what the module's own init function undid before returning an error, and it does not show the fail_free_freeinit path, which runs before this label.

A built-in driver can use related initialization interfaces but cannot be unloaded as an independently loaded .ko. Its init section can have a different lifetime from persistent code. Do not retain pointers into init-only memory after that memory is freed. The configuration and linker determine which path is present; merely reading a module_exit declaration does not prove an exit callback is usable for a built-in object.

Practice#

Source-only route: use the following state table as a worked example. With MODULES=n, no loadable modules are supported. With MODULES=y, TTY=n, a requested USB_SERIAL=m is constrained by its dependency and must not be assumed effective. With MODULES=y, TTY=y, a matching guest kernel and a compatible module, loading may proceed to further signature, ELF and symbol checks. For each row identify the first condition that can reject the requested state; this is a prediction, not an observed load.

Requested versus effective module state
MODULES=n
No loadable module support; an m selection is unavailable.
MODULES=y, TTY=n
A requested USB_SERIAL=m is constrained by its TTY dependency.
MODULES=y, TTY=y
Loading may proceed to signature, ELF and symbol checks.
Conceptual model. Conceptual state table from v6.12 Kconfig dependencies and loader checks; a prediction, not an observed load.

Optional guest route: start with chapter 08's private LAB on the host. This rebuilds the lab kernel, not the host kernel. It assumes the chapter 08 tools, static BusyBox, kernel source and working QEMU. Enable module support and unloading; leave enforcement disabled only for this isolated teaching guest. Rebuild the kernel and its module metadata after configuration changes:

sh
cd "$LAB" || exit 1
"$LAB/linux/scripts/config" --file "$LAB/build/.config" \
  -e MODULES -e MODULE_UNLOAD -d MODULE_SIG_FORCE
make -C "$LAB/linux" O="$LAB/build" olddefconfig || exit 1
grep -E '^(CONFIG_MODULES|CONFIG_MODULE_UNLOAD|CONFIG_MODULE_SIG_FORCE)=' "$LAB/build/.config"
make -C "$LAB/linux" O="$LAB/build" -j4 bzImage modules || exit 1

The resolved config must show CONFIG_MODULES=y and CONFIG_MODULE_UNLOAD=y; CONFIG_MODULE_SIG_FORCE should not be enabled for this disposable unsigned example. A missing or disabled prerequisite is a stop condition. If other guest policy enforces signatures, the load can still be refused. The kernel and module must use this same output tree and compatible compiler. No physical device is required.

Create the synthetic teaching module on the host in a fresh subdirectory inside the lab. It does not access hardware:

sh
mkdir "$LAB/demo-module" || exit 1
cat > "$LAB/demo-module/demo.c" <<'C'
#include <linux/init.h>
#include <linux/module.h>
static int __init demo_start(void) { pr_info("demo: init\n"); return 0; }
static void __exit demo_end(void) { pr_info("demo: exit\n"); }
module_init(demo_start);
module_exit(demo_end);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Disposable teaching module");
C
printf 'obj-m := demo.o\n' > "$LAB/demo-module/Makefile"
make -C "$LAB/linux" O="$LAB/build" M="$LAB/demo-module" modules || exit 1
file "$LAB/demo-module/demo.ko"

The module's source and license declaration are educational code written for this exercise, not an upstream excerpt. obj-m requests a module; the external-module build uses the configured output tree. file should identify a relocatable ELF object for the target architecture. Build errors, missing Module.symvers, unresolved symbols or a wrong architecture are not reasons to attempt a host load.

Before rebuilding the chapter 08 archive, verify your BusyBox has the relevant applets: "$LAB/busybox-1.36.1/busybox" --list | grep -E '^(insmod|rmmod|dmesg)$'. All three lines must be present. The commands below append the module and loader links to the chapter 08 manifest once. Start from its original manifest each time to avoid duplicate archive entries. LAB must be the absolute space-free lab path from chapter 08.

sh
cat >> "$LAB/initramfs.list" <<'EOF_LINKS'
slink /bin/insmod /bin/busybox 0777 0 0
slink /bin/rmmod /bin/busybox 0777 0 0
slink /bin/dmesg /bin/busybox 0777 0 0
EOF_LINKS
printf 'file /demo.ko %s/demo-module/demo.ko 0644 0 0\n' "$LAB" >> "$LAB/initramfs.list"
set -o pipefail
"$LAB/build/usr/gen_init_cpio" "$LAB/initramfs.list" | gzip -9 > "$LAB/initramfs-modules.cpio.gz" || exit 1
gzip -t "$LAB/initramfs-modules.cpio.gz" || exit 1
gzip -dc "$LAB/initramfs-modules.cpio.gz" | cpio -it

Inspect the listing for demo.ko and the applets before boot. The archive contains the module bytes; the host filesystem is not mounted into the guest. Launch the chapter 08 QEMU command with the newly rebuilt kernel and substitute "$LAB/initramfs-modules.cpio.gz" for its -initrd argument. The guest is disposable; its archive is distinct from the original chapter 08 archive.

Only at the guest prompt run:

sh
insmod /demo.ko
cat /proc/modules
insmod /demo.ko
rmmod demo
dmesg

A successful first insertion should place demo in /proc/modules; the second insertion is a deliberate failure because the same module is already present. The final removal should succeed if unloading is enabled and no reference prevents it; the guest log should include the module's messages. Exact wording depends on kernel and BusyBox versions. If the first insertion fails, inspect its error, guest kernel log, configuration, signature policy and matching build before proceeding. Do not interpret an earlier /proc/modules entry or a copied host module as success. Shut down the guest as in chapter 08; discard the lab archive and build only after retaining your notes. No host insmod or rmmod is part of this exercise. This optional module lab has not been runtime verified by the author.

Knowledge check#

  1. Why can a requested USB_SERIAL=m become unavailable when TTY=n?
  2. What references does the shown finit_module excerpt acquire and release, and what additional work remains before init runs?
  3. Why does matching vermagic not establish safe compatibility?
  4. What must a real module do before freeing data on unload, and what does a busy refusal tell you?
  5. How can signature policy affect an otherwise valid unsigned module?

Answers#

  1. USB_SERIAL depends on TTY, and its enclosing configuration may add more constraints. A requested value is resolved against the complete Kconfig graph, not accepted unconditionally.
  2. fdget acquires the file-descriptor reference, the helper consumes the file for loading, and fdput releases that reference. Signature, ELF, layout, symbol, relocation and initialization checks remain on the applicable path.
  3. Vermagic is limited metadata. Symbol versions add checks when configured, but neither proves semantic correctness, all ABI details, locking, lifetime or device behavior.
  4. It must prevent new entry, unregister interfaces and synchronize outstanding callbacks, work and references according to each subsystem. A busy refusal means the kernel detected a condition preventing ordinary unload; investigate rather than force it.
  5. Enforcement can reject an unsigned module. Under a permissive configuration it may load with a taint; malformed or invalid signatures and other security policy can produce different failures.

Checkpoint#

Deliver the three-row Kconfig state table and a loader timeline with one failure cleanup, plus guest logs if you performed the optional experiment. Pass when you explain requested versus effective values, separate ELF relocation from init, and identify a concrete unload race. If uncertain, trace finit_module through load_module and revise the table; never retry by loading a module into the host. The checkpoint is not certification.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.