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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2936 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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):
kldload ./boom.koβ rc=0 (success) even though MOD_LOAD returned 5.kldstatstill listsboom.koas loaded (Refs 1).kldstat -m boomβ "can't find module boom" β the module was unregistered and freed while its code stays resident.- dmesg order:
MOD_LOAD -> deliberately failingβMOD_UNLOAD dispatchedβmodule_register_init: MOD_LOAD (...) error 5β MOD_UNLOAD is dispatched at load time. 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.
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).
kldload ./boom.koβ rc=0 β load reported successful.kldstatlistsboom.ko(id 8, Refs 1) β file resident.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.- dmesg:
MOD_LOAD -> deliberately failingthenMOD_UNLOAD dispatchedthenmodule_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). kldunload boomβ rc=0, no second MOD_UNLOAD (module no longer onfile->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 failingthen 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 boomafterwards β not found (registration correctly dropped bylinker_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
fixedPatched 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
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.
No comments yet.