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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2740 Β· 2 files
FileTypeDescriptionSize
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:

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).

  1. All refs/userrefs mutations under llf_lock (EXCLUSIVE for RMW).
  2. sys_kldnext/linker_file_lookup_symbol list walks under llf_lock.
  3. Either take kld_lock in linker_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_testable
baseline no→ patch + rebuild →patched clean

Advisory 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.

[]
↓ fix.diffper-fix-DF-2740

Confirmed kernel references

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.