Inconsistent locking of linker shared state: sys_kldload userrefs++/id read outside kld_lock (UAF-write window), unlocked refs mutations, and unlocked linker_files/found_modules traversals racing KPI loaders
| Field | Value |
|---|---|
| ID | DF-2740 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-362 Improper Synchronization |
| File | sys/kern/kern_linker.c |
| Lines | 814-821, 320-325, 518, 644-658, 909-917, 1128, 1134, 1162, 1529, 1636 |
| Area | kern |
| Confidence | likely |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
linker shared state is protected by two locks (kld_lock serializing syscalls, llf_lock guarding linker_files) but many accesses bypass the protocol: sys_kldload executes lf->userrefs++ and reads lf->id AFTER releasing kld_lock β a concurrent kldunload in that window can fully unload and kfree(lf), making userrefs++ a UAF write into a freed chunk and the id a stale value returned to userspace; linker_load_file's already-loaded refs++ and linker_load_dependencies' lfdep->refs++ take no llf_lock; the veto-path refs-- is unlocked (compounds DF-2739); linker_reference_module does a non-atomic refs++ under llf_lock SHARED; linker_load_module walks found_modules with no lock; sys_kldnext (unprivileged) and linker_file_lookup_symbol walk linker_files without llf_lock. The KPI entry points have their kld_lock acquisition commented out and are called without it by the deferred firmware thread, device-mapper, and the mount/netgraph autoload paths β so list/refcount mutation can run with no serialization at all against the unprivileged kldnext/kldstat readers (torn TAILQ traversal β UAF read β panic-class DoS).
Threat model & preconditions
Every realistic trigger needs a privileged or admin-configured actor (firmware-requesting driver, dm targets, mount/autoload, ng autoload β all root-gated), so this is hardening-class: lost-update refcount races (compounding DF-2739's underflow) and torn list walks by unprivileged kldnext(2)/kldstat(2) readers during privileged async module loads. Pure-syscall races are fully serialized by kld_lock and not winnable.
Recommended fix
Serialize all refs/userrefs RMWs and every linker_files/found_modules traversal with llf_lock (EXCLUSIVE for mutations); keep sys_kldload's userrefs++/lf->id inside the kld_lock critical section; either restore kld_lock in linker_reference_module/linker_release_module or make list/refcount operations safe under llf_lock alone. Advisory two-hunk fix.diff in findings/poc/DF-2740/ (not built).
Timeline
- 2026-08-30 Discovered during pass-2 audit of kern_linker.c (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2740 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.1 KB | β raw | |
| verdict.json | β | 3.2 KB | view raw |
DF-2740 β Inconsistent locking of linker shared state (refs / linker_files / found_modules)
What
sys/kern/kern_linker.c guards its shared state with two locks β kld_lock
(serializes the syscalls) and llf_lock (guards linker_files) β but a
large share of reads and writes of linker_file.refs, userrefs, the
linker_files list, and found_modules happen outside that protocol:
| site | access | protection | problem |
|---|---|---|---|
| sys/kldload :814-821 | lf->userrefs++, read lf->id |
after LK_RELEASE of kld_lock |
concurrent kldunload can tear down & kfree(lf) in the window β UAF write into freed chunk + stale id returned to userspace |
| linker_load_file :320-325 | lf->refs++ |
none (find_file_by_name released llf_lock) | lost update vs. refs-- under lock β premature teardown |
| linker_file_unload :517-518 | file->refs-- |
outside llf_lock (veto path; aggravates DF-2739) | lost update race |
| linker_load_dependencies :1636 | lfdep->refs++ |
kld_lock only | racy vs. non-syscall unloaders |
| linker_reference_module :1125-1131 | (*result)->refs++ |
llf_lock SHARED | non-atomic RMW under a shared lock β data race between CPUs |
| linker_load_module :1529 | modlist_lookup2() walk of found_modules |
none | traversal concurrent with TAILQ removal under llf_lock (unload) |
| sys_kldnext :909-923 | TAILQ_FIRST/TAILQ_NEXT over linker_files |
kld_lock only, no llf_lock | unprivileged reader racing mutators that don't take kld_lock |
| linker_file_lookup_symbol :644-658 | global TAILQ_FOREACH(linker_files) |
none | runtime symbol lookup racing list mutation |
The mutators that run without kld_lock are the exported KPIs
linker_reference_module() / linker_release_module() β their kld_lock
acquisition is commented out at kern_linker.c:1134 and :1162 β reached from:
sys/kern/subr_firmware.c:274and:460(deferred firmware-load thread),sys/dev/disk/dm/dm_target.c:75/:84(device-mapper target autoload),sys/netgraph/netgraph/ng_base.c:342-348,sys/netgraph7/socket/ng_socket.c:293(netgraph type autoload β root-gated at ngc_attach, SYSCAP_RESTRICTEDROOT),sys/kern/vfs_syscalls.c:324(mount fstype autoload β root-gated).
So e.g. an unprivileged kldnext(2) loop (no priv check β see DF-0025
context) walks linker_files under kld_lock only, while root-triggered
firmware/dm/mount autoload mutates the same list under llf_lock only β torn
traversal / use-after-free read β panic-class DoS.
Verification status (honest)
Static verification complete: every site above was traced to its callers and
lock context (path:line in the table). A guest race was NOT attempted as a
reliable win because:
- pure-syscall vs pure-syscall races (kldnext vs kldload/kldunload) are fully serialized by kld_lock β not winnable;
- every mutator that skips kld_lock requires a privileged actor on this guest (no firmware images, no dm targets configured, ng7 socket not loaded), so the race needs privileged setup and a tight window.
Fix validation was therefore not performed; the advisory fix.diff below is
code-reviewed only (fix_status: not_testable).
Advisory fix.diff (subset β the two highest-value hardenings)
sys_kldload: keep userrefs++/id read inside the kld_lock critical section; linker_load_file: take llf_lock around the already-loaded refs++.
See fix.diff in this directory. These two changes close the UAF-write window (sys_kldload) and the lost-update refcount race (linker_load_file) without introducing new lock orderings (llf_lock remains a leaf lock taken inside kld_lock, matching linker_file_unload).
Recommended complete fix
- All
refs/userrefsmutations underllf_lock(EXCLUSIVE for RMW). sys_kldnext/linker_file_lookup_symbollist walks underllf_lock.- Either take
kld_lockinlinker_reference_module/linker_release_module(uncomment, mind recursion via linker_load_moduleβlinker_load_file which itself doesn't take kld_lock) or make all list/refcount operations safe under llf_lock alone so the KPI callers don't need kld_lock.
Fix verification
not_testableAdvisory fix.diff (two highest-value hardenings) authored and code-reviewed; not built/tested because no reliable race harness exists on this guest without privileged async loaders.
[]
Confirmed kernel references
- sys/kern/kern_linker.c:814
- sys/kern/kern_linker.c:820
- sys/kern/kern_linker.c:324
- sys/kern/kern_linker.c:518
- sys/kern/kern_linker.c:1128
- sys/kern/kern_linker.c:1529
- sys/kern/kern_linker.c:910
- sys/kern/kern_linker.c:644
- sys/kern/kern_linker.c:1134
- sys/kern/kern_linker.c:1162
- sys/kern/subr_firmware.c:274
- sys/dev/disk/dm/dm_target.c:75
- sys/kern/vfs_syscalls.c:324
Detail
Evidence (decisive lines)
['README.md (site-by-site lock-context table)', 'fix.diff (advisory hardening: userrefs++/id inside kld_lock; refs++ under llf_lock)']
PoC changes
n/a β analysis-only verification
Verified recommended fix
Serialize all refs/userrefs mutations and linker_files/found_modules walks with llf_lock; move sys_kldload's userrefs++/id read inside kld_lock; restore kld_lock (or llf_lock-only safety) for the reference/release KPIs.
Verdict
Static verification only. kern_linker.c mutates/reads linker_file.refs, userrefs, the linker_files list and found_modules outside any consistent locking protocol: sys_kldload increments lf->userrefs and reads lf->id AFTER releasing kld_lock (kern_linker.c:814-821), linker_load_file's already-loaded refs++ (:320-325) and linker_load_dependencies' lfdep->refs++ (:1636) take no llf_lock, linker_reference_module does a non-atomic refs++ under llf_lock SHARED (:1128), linker_load_module walks found_modules unlocked (:1529), sys_kldnext (:909-923) and linker_file_lookup_symbol (:644-658) walk linker_files without llf_lock, and the KPI entry points linker_reference_module/linker_release_module have their kld_lock acquisition commented out (:1134, :1162) while in-tree callers (subr_firmware deferred thread, dm_target, mount fstype autoload, netgraph autoload) invoke them without kld_lock. These are genuine data races (UAF-write window in sys_kldload; torn list traversal from unprivileged kldnext vs privileged async loaders). No guest race was attempted: pure-syscall races are fully serialized by kld_lock, and every kld_lock-skipping mutator on this guest requires privileged setup, so no reliable unprivileged trigger exists. Low severity, hardening-class.
No comments yet.