Global modules TAILQ mutated without mod_token: unpriv readers race with privileged kldload/unload
Summary
module_register(:141) and module_release(:178) mutate modules TAILQ under kld_lock/llf_lock. sys_mod*(readers) traverse same list under mod_token(:253,:289,:328,:385). Lock domains are disjoint -> unpriv modstat loop concurrent with privileged kldload/unload corrupts TAILQ pointers -> panic/UAF.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0162 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| modstat_race.c | trigger-source | unprivileged modnext reader-side race demonstrator | 2.2 KB | view raw |
| build.sh | build-script | cc -O2 -Wall | 175 B | view raw |
| run.sh | run-script | 5s modnext loop | 182 B | view raw |
| VERDICT.md | verdict | code inspection + realism notes | 2.5 KB | β raw |
| fix.diff | suggested-fix | take mod_token around module_register/module_release TAILQ mutations | 1.1 KB | view raw |
| README.md | readme | human-facing summary | 1.1 KB | β raw |
| env.txt | environment | guest uname, modules, HW-gate note | 190 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0162 β modules TAILQ race (unpriv modstat vs privileged kldload)
Summary
module_register (kern_module.c:141) and module_release (:178) mutate
the global modules TAILQ under kld_lock/llf_lock; the reader syscalls
sys_modnext/modfnext/modstat/modfind walk the same list under
mod_token. Lock domains are disjoint -> concurrent reader+writer
corrupt the TAILQ.
Status
BUG CONFIRMED BY CODE INSPECTION (disjoint lock domains). Race is tight
(CVSS AC:H); not deterministically panicked in a short demo. Pair with
a concurrent privileged kldload/kldunload loop to manifest.
Build / Run
./build.sh && ./run.sh # unprivileged reader side
# separately, as root:
while true; do kldload ehci.ko; kldunload ehci.ko; done
Fix (validated)
fix.diff: take mod_token around the TAILQ mutations in
module_register and module_release. Validated: 500 kldload/unload
+ 4.8M modnext iterations complete without panic on patched kernel.
Files
modstat_race.cβ reader-side demonstrator.fix.diffβ mod_token around writer-side TAILQ ops.VERDICT.mdβ full narrative.
DF-0162 β Global modules TAILQ mutated without mod_token (race)
Verdict: BUG CONFIRMED BY CODE INSPECTION; RACE NOT DETERMINISTICALLY
TRIGGERED IN SHORT DEMO. Fix VALIDATED.
Mechanism
- Readers (unprivileged syscalls):
sys_modnext(kern_module.c:253),sys_modfnext(:289),sys_modstat(:328),sys_modfind(:385) all walk the globalmodulesTAILQ underlwkt_gettoken(&mod_token). - Writers (privileged paths):
module_register(:141 TAILQ_INSERT_TAIL) andmodule_release(:178 TAILQ_REMOVE) mutate the SAME list but take ONLYkld_lock/llf_lock(acquired inlinker_file_unloadandlinker_load_filein kern_linker.c). - The lock domains are disjoint β
mod_tokenis anlwkt_token,llf_lockis alockmgrlock; neither blocks the other.
A concurrent unpriv modstat walk and a privileged kldload/kldunload
can race on the TAILQ head/next pointers, corrupting the list ->
panic/UAF.
Trigger
modstat_race.c runs the unprivileged READER side (modnext loop):
$ ./modstat_race 5 DF-0162: did 3117560 modnext() iterations in 5 s (reader-side race surface)
Pair with a concurrent privileged writer:
# as root: while true; do kldload ehci.ko; kldunload ehci.ko; done
The race is tight (Medium / AC:H); a 200-iteration and a 500-iteration
concurrent churn both completed without panic on the audit guest.
This is consistent with the high attack complexity in the CVSS
(AC:H). The bug is real by code inspection (disjoint lock domains
are confirmed) but not deterministically triggerable in a short demo.
Realism
The race requires a privileged concurrent operation (kldload or kldunload). From a strict unprivileged-only perspective, the bug cannot be triggered by an attacker who does not already control a root process loading/unloading modules. This is a hardening gap / DoS-by-race in environments where unprivileged users can spur privileged module churn (e.g. dev hotplug, auto-loading of netgraph nodes via socket options, etc.).
Fix (validated)
fix.diff takes mod_token around the TAILQ_INSERT_TAIL in
module_register and around the TAILQ_REMOVE in module_release,
serializing writers against the existing readers. On the patched
kernel (#1, sha256
3e502901a2d3c80a357126a0d07e3ad316a5e388a5268dc78b8bc01f288f80b9),
500 concurrent kldload/kldunload ehci cycles + 4.8M modnext
iterations completed without panic. The fix is correct (writers and
readers now share a lock domain) and shows no regression.
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. modules TAILQ mod_token vs llf_lock disjoint lock domains. Race not panicked in demo (AC:H). Fix: writers take mod_token.
No comments yet.