β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2936

module_register_init MOD_LOAD-failure path destroys the module registry entry while kldload(2) reports success β€” module lifecycle desynchronized from linker-file lifecycle (zombie module, teardown-order inversion)

Field Value
ID DF-2936
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:L/A:L
CWE CWE-755 / CWE-667
File sys/kern/kern_module.c
Lines 110-116
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

When a module's MOD_LOAD handler returns an error, module_register_init() dispatches MOD_UNLOAD and calls module_release(), dropping the registration reference created by module_register to zero — the module is removed from both the global modules TAILQ and its file's module list and freed. But the failure is unobservable upstream: the SYSINIT framework discards return values, linker_load_file() sets LINKER_FILE_LINKED and returns 0, and sys_kldload returns the file id with rc=0. Result: a driver KLD whose probe fails becomes a zombie — code/sysctls/SYSUNINITs resident forever, invisible to modstat(2)/kldstat, with inverted teardown ordering (SYSUNINIT runs at kldunload AFTER MOD_UNLOAD already ran at load time; MOD_UNLOAD ran BEFORE sibling SYSINITs completed). Any module whose SYSUNINIT assumes a completed MOD_LOAD, or whose MOD_UNLOAD already freed its state, crashes or double-frees at kldunload. Privileged trigger (kldload or boot-time static-module failure, e.g. absent hardware — silently removes the module from modstat); no unprivileged reach; ceiling is module-dependent panic. Not a memory-corruption primitive; no user→root route.

Proof of contest

VERIFIED (findings/poc/DF-2936/): boom.ko with MOD_LOAD returning 5 + SYSUNINIT tag. Root kldload β†’ rc=0 (bug); kldstat lists boom.ko; kldstat -m boom β†’ 'can't find module boom' (bug); dmesg shows MOD_UNLOAD dispatched at load time; kldunload β†’ no second MOD_UNLOAD, SYSUNINIT runs for the destroyed module. Fix validated on rebuilt kernel #1 (leave registration in place on MOD_LOAD failure; linker_file_unload does the teardown once): module stays registered, symmetric teardown.

Validated fix.diff (remove the load-time destroy; comment documents the lifecycle contract) in findings/poc/DF-2936/.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of kern_module.c (GLM 5.3); reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2936 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 4.0 KB ↓ raw
boom.c β€” 1.7 KB view raw
Makefile β€” 133 B ↓ download
build.sh β€” 181 B view raw
run.sh β€” 559 B view raw
build.log β€” 1.0 KB view raw
run.log β€” 1.2 KB view raw
run.fix.log β€” 590 B view raw
env.txt β€” 473 B view raw
fix.diff β€” 1.1 KB view raw
verdict.json β€” 3.7 KB view raw

DF-2936 β€” module_register_init MOD_LOAD-failure path desynchronizes module lifecycle from linker-file lifecycle

What this demonstrates (baseline, stock kernel)

module_register_init() (sys/kern/kern_module.c:110-116) destroys the module registry entry β€” module_unload() + module_release() β€” when the module's MOD_LOAD handler returns an error. But the caller chain cannot observe the failure: the SYSINIT framework discards the error, so linker_load_file() (sys/kern/kern_linker.c:346-352) marks the file LINKED and reports success, and sys_kldload (kern_linker.c:820-826) returns the file id with rc=0.

Observed on the guest (kernel #0, stock):

  1. kldload ./boom.ko β†’ rc=0 (success) even though MOD_LOAD returned 5.
  2. kldstat still lists boom.ko as loaded (Refs 1).
  3. kldstat -m boom β†’ "can't find module boom" β€” the module was unregistered and freed while its code stays resident.
  4. dmesg order: MOD_LOAD -> deliberately failing β†’ MOD_UNLOAD dispatched β†’ module_register_init: MOD_LOAD (...) error 5 β€” MOD_UNLOAD is dispatched at load time.
  5. kldunload boom β†’ rc=0, no second MOD_UNLOAD (the registry entry is gone; linker_file_unload's module loop at kern_linker.c:505-522 never sees it), but the module's SYSUNINIT runs at kldunload for the module that was destroyed at load time (linker_file_sysuninit, kern_linker.c:208).

Impact: a KLD whose MOD_LOAD fails (e.g. driver probe failure on hardware-absent systems) silently becomes a zombie: its code, sysctls and SYSUNINITs stay resident forever, invisible to modstat(2)/kldstat module enumeration, and its teardown ordering is inverted (SYSUNINIT after the MOD_UNLOAD that already ran). Any module whose SYSUNINIT assumes MOD_LOAD completed (or whose MOD_UNLOAD already cleaned up) can crash or corrupt state at kldunload time. Trigger requires kldload privilege (SYSCAP_NOKLD) or a boot-time MOD_LOAD failure, hence Low severity.

Build (in guest, as root)

cd /tmp/df2936 && make       # boom.ko β€” needs /usr/src + /usr/obj kernel tree

Run (in guest, as root)

kldload ./boom.ko            # rc=0 (bug)
kldstat | grep boom          # file loaded
kldstat -m boom              # module ABSENT (bug)
kldunload boom               # SYSUNINIT for dead module (bug)
dmesg | tail                 # event ordering proof

build.sh / run.sh encode the exact commands.

Expected output (bug present)

See run.log β€” decisive lines: KLDLOAD_RC=0, kldstat: can't find module boom, dmesg triple MOD_LOAD -> deliberately failing / MOD_UNLOAD dispatched / error 5, then SYSUNINIT running (module destroyed at load time!) after kldunload.

Fix validation

fix.diff removes the unload+release from the failure path. After make nativekernel + reboot (kernel #1, run.fix.log):

  • kldstat -m boom β†’ module PRESENT (Id 325, Refs 1)
  • no MOD_UNLOAD at load time (dmesg)
  • kldunload β†’ MOD_UNLOAD dispatched once at unload time + SYSUNINIT, module properly released (registry clean afterwards)

The remaining "kldload reports success despite MOD_LOAD failure" is a separate kern_linker.c issue (SYSINIT errors cannot propagate through linker_file_sysinit) β€” noted in the finding, out of scope for this file.

VERDICT.md
↓ download raw

DF-2936 VERDICT β€” reproduced / fixed

Finding: module_register_init() MOD_LOAD-failure path destroys the module registry entry (module_unload + module_release, sys/kern/kern_module.c:112-113 in the stock tree) while every caller up the chain reports success: SYSINIT return values are discarded by linker_file_sysinit() (sys/kern/kern_linker.c:176-189), so linker_load_file() (kern_linker.c:346-352) sets LINKER_FILE_LINKED and returns 0, and sys_kldload (kern_linker.c:820-826) hands back the file id.

How it was reproduced (baseline, kernel #0)

Built boom.ko in-guest: DECLARE_MODULE(boom, ...), MOD_LOAD returns 5, plus a SYSUNINIT kprintf tag (sources: boom.c, Makefile).

  1. kldload ./boom.ko β†’ rc=0 β€” load reported successful.
  2. kldstat lists boom.ko (id 8, Refs 1) β€” file resident.
  3. kldstat -m boom β†’ "can't find module boom" β€” module freed and removed from both TAILQs (module_release, kern_module.c:177-183) at load time.
  4. dmesg: MOD_LOAD -> deliberately failing then MOD_UNLOAD dispatched then module_register_init: MOD_LOAD (boom, ffffffff82600000, 0) error 5 β€” the handler address is also printed to the log (kern_module.c:114-115, error-path KVA print, gated on privileged trigger; the kldstat/kldsym pointer-leak family is DF-0025).
  5. kldunload boom β†’ rc=0, no second MOD_UNLOAD (module no longer on file->modules, kern_linker.c:505-522 never dispatches to it), but the module's SYSUNINIT runs for the module destroyed at load time (linker_file_sysuninit, kern_linker.c:208-243).

Full output: run.log.

Root cause (path:line)

  • sys/kern/kern_module.c:110-116 β€” failure path tears the registration down behind the linker's back; the registration reference created by module_register (kern_module.c:134,141,146) belongs to the file, whose teardown (linker_file_unload β†’ module_release, kern_linker.c:521) is the only legitimate dropper.
  • sys/kern/kern_linker.c:350 β€” linker_file_sysinit() return value ignored; load reported successful regardless of SYSINIT/MOD_LOAD failure.

Impact

Privileged trigger only (kldload, caps_priv_check_self(SYSCAP_NOKLD), kern_linker.c:806, or a boot-time MOD_LOAD failure of a static module β€” in the static case the module vanishes from modstat silently, which is merely cosmetic). For KLD modules: permanent zombie state β€” code/sysctls/SYSUNINITs resident, module invisible to modstat(2)/kldstat, teardown-order inversion (SYSUNINIT runs after the already-dispatched MOD_UNLOAD; MOD_UNLOAD runs before other SYSINITs of the same file have executed). A driver whose SYSUNINIT assumes successful MOD_LOAD, or whose MOD_UNLOAD already freed everything, crashes or double-frees at kldunload. No unprivileged reach; no memory corruption demonstrated in-tree β†’ severity Low, bucket base:kern.

Fix validation (kernel #1, fix.diff applied in-guest, make nativekernel)

run.fix.log:

  • kldload ./boom.ko β†’ rc=0 (unchanged β€” success-reporting is the separate kern_linker.c defect), module stays registered: kldstat -m boom β†’ Id 325 Refs 1 boom.
  • dmesg: MOD_LOAD -> deliberately failing then the error kprintf β€” no MOD_UNLOAD at load time.
  • kldunload boom β†’ rc=0: MOD_UNLOAD dispatched (once, at unload time), SYSUNINIT running (symmetric), kldstat -m boom afterwards β†’ not found (registration correctly dropped by linker_file_unload β†’ module_release, kern_linker.c:521).

All three baseline-observed bad behaviors are gone on the patched kernel: registry entry survives the failed MOD_LOAD; MOD_UNLOAD is dispatched exactly once at file-unload time; SYSUNINIT runs for a registered module. fix_status = fixed.

Classification

  • status: reproduced (baseline behavior confirmed exactly as traced)
  • impact: none direct (privileged-trigger lifecycle/logic flaw; no memory corruption reachable from the registry side in-tree)
  • confidence: certain
  • exploit chain: n/a (not a memory-corruption primitive; ceiling is module-dependent crash at kldunload)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel eliminates the registry desync: module remains visible (kldstat -m boom -> Id 325), MOD_UNLOAD dispatched exactly once at kldunload, SYSUNINIT symmetric, registration released by linker_file_unload.

findings/poc/DF-2936/run.fix.log; findings/poc/DF-2936/fix.diff
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Thu Sep 3 17:08:57 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["findings/poc/DF-2936/run.log: KLDLOAD_RC=0 + kldstat lists boom.ko id 8 while 'kldstat -m boom' -> can't find module boom", 'findings/poc/DF-2936/run.log dmesg: MOD_LOAD failing -> MOD_UNLOAD dispatched (at load time) -> module_register_init error 5 -> SYSUNINIT runs at kldunload', 'findings/poc/DF-2936/run.fix.log (patched kernel #1): module stays registered (Id 325 Refs 1), no MOD_UNLOAD at load, kldunload dispatches MOD_UNLOAD once + SYSUNINIT then registry entry properly released']

PoC changes

Wrote the KLD from scratch: DECLARE_MODULE with a MOD_LOAD handler returning 5 plus a SYSUNINIT kprintf tag to expose dispatch ordering; Makefile uses SYSDIR=/usr/src/sys and KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC (guest has full kernel obj tree).

Verified recommended fix

Do not module_unload+module_release in module_register_init's MOD_LOAD-failure path; leave the registration to be dropped by linker_file_unload so the module lifecycle stays symmetric with the linker file lifecycle.

Verdict

module_register_init (sys/kern/kern_module.c:110-116) destroys the module registry entry (module_unload + module_release) when MOD_LOAD fails, but the failure is invisible up the chain: linker_file_sysinit discards SYSINIT errors, linker_load_file marks the file LINKED and returns 0, and kldload(2) reports success. Reproduced in-guest with boom.ko (MOD_LOAD returns 5): kldload rc=0, boom.ko stays loaded while 'kldstat -m boom' says no such module, MOD_UNLOAD is dispatched at load time, and kldunload later runs the module's SYSUNINIT for the long-destroyed module without a second MOD_UNLOAD. Privileged trigger (SYSCAP_NOKLD kldload, or boot-time static-module MOD_LOAD failure); impact ceiling is module-dependent crash/double-free at kldunload via teardown-order inversion, so severity Low, direct impact none.