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

Unsynchronized vfsconf registry: sys_mount vs kldunload TOCTOU β€” UAF write/call through unloaded module (reproduced as irrecoverable kernel deadlock of the mount/linker subsystem)

Field Value
ID DF-2918
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H
CWE CWE-367 / CWE-362 / CWE-416
File sys/kern/vfs_init.c
Lines 264-284, 458-480 (racing side: vfs_syscalls.c:313, :359)
Area kern/vfs
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

The vfsconf registry has zero synchronization: no lock, no token, non-atomic vfc_refcount. vfs_unregister() checks vfsp->vfc_refcount != 0 (:469) and removes the entry (:478) with nothing serializing it against sys_mount()'s unlocked vfsconf_find_by_name() followed by vfc_refcount++ β€” a window containing an M_WAITOK kmalloc and a syncer-kthread creation, crossed by every mount(2) attempt, including failing ones. A concurrent kldunload can unregister and free/unmap the module while a mount thread is mid-window: the mounter then executes vfc_refcount++ into freed module address space and dereferences mp->mnt_op/vfc_vfsops into the unloaded module. Second edge in the same envelope: sys_mount's in-syscall auto-load enters the linker while holding the mountpoint vnode exclusively locked, AB-BA against kldunload holding the linker lock through module teardown; vn_syncer_thr_stop()'s unconditional wait on a syncer thread whose kthread_create result is ignored supplies the permanent-block primitive. Not uid0: the unload side requires root (privileged conspirator); the fully-unpriv mounter variant is blocked on stock GENERIC because every user-mountable fstype is compiled in and modular fuse is cap-gated by the get_fscap("fusefs") vs vfc_name "fuse" mismatch (recorded as follow-up).

Proof of contest

VERIFIED (findings/poc/DF-2918/): race wedged 4/4 in 15-100 s on the stock INVARIANTS guest (all mounters D-state wchan ncplk/syncexit/vnode, fuse.ko pinned, mount(2)/kldload/kldunload dead system-wide, SIGKILL ineffective, shutdown and crash-dump sync hang β€” even debug.panic wedges at 'syncing disks...'); control run (no unload churn) clean at 600k+ iterations/mounter. Fix validated in-guest (kernel #2, 21-hunk registry-serialization diff + in-flight-mount counter breaking the AB-BA): identical 600 s workload, 16.4M mount attempts, zero wedges. uid0 route: none β€” root participation required on the unload side (documented hard blocker); primitive characterized (refcount write into freed module pages + indirect call through freed vfsops).

Validated fix.diff (registry lockmgr lock, vfsconf_acquire/release, in-flight-mount counter) in findings/poc/DF-2918/. Follow-ups: handle kthread_create failure in vn_syncer_thr_create/stop; reconcile get_fscap("fusefs") with fuse's vfc_name "fuse"; take a linker reference for mounts of manually-kldloaded modules.

Timeline

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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2918 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 7.3 KB ↓ raw
mountloop.c β€” 1.5 KB view raw
kldloop.c β€” 1.9 KB view raw
sysctlloop.c β€” 1.0 KB view raw
race.sh β€” 1.3 KB view raw
run.log β€” 4.2 KB view raw
run.2.log β€” 2.7 KB view raw
panic.txt β€” 1.2 KB view raw
env.txt β€” 1.1 KB view raw
fix.diff β€” 9.0 KB view raw
manifest.json β€” 1.7 KB view raw
verdict.json β€” 7.9 KB view raw

DF-2918 β€” Unynchronized vfsconf registry: mount/kldunload race β†’ hard kernel deadlock (and by-construction UAF)

File: sys/kern/vfs_init.c (pass 2)

Build (in guest)

cc -O2 -o /root/mountloop mountloop.c
cc -O2 -o /root/kldloop   kldloop.c     # optional gap arg, default 10000 usec
cc -O2 -o /root/sysctlloop sysctlloop.c

Guest setup (root):

pw useradd -m -n poc ; pw usermod -n poc -s /bin/sh
mkdir -p /home/poc/mnt ; chown -R poc:poc /home/poc
cp /root/sysctlloop /home/poc/ ; chown poc /home/poc/sysctlloop
sysctl vfs.usermount=1          # only needed for the (currently
                                # stock-unreachable) unpriv mounter variant
kldload /boot/kernel/fuse.ko

Run

sh race.sh 3 600                    # MODE=race default; also starts 2
                                    # unpriv sysctl readers as user poc
# control (no unload churn β€” must stay healthy):
MODE=control sh race.sh 4 90

Artifacts: /root/race.log, /tmp/mloop., /tmp/kld.out, /tmp/sloop.

Expected (stock INVARIANTS kernel, observed 2026-09-03)

  • CONTROL run: mounters complete >600k mount(2) attempts each in 90 s (all EFAULT/EPERM β€” the mounts are supposed to fail; the vulnerable window vfs_syscalls.c:313β†’:359 is crossed on every attempt) with zero adverse effects. Guest stays healthy.
  • RACE run: within 15–25 s every mounter thread wedges permanently in kernel D-state (wchan ncplk / syncexit / vnode), kldloop wedges inside the linker, fuse.ko is pinned loaded, mount(2) and kldload/ kldunload are dead system-wide, wedged threads survive SIGKILL, the guest can no longer shut down cleanly ("guest not answering"), and a forced debug.panic hangs in syncing disks.... Only a hard reset recovers. Reproduced 3/3 race runs; control clean 1/1.

Key observation about the failure shape

kldloop immediately reloads the module after each unload, so the freed module address range is typically re-mapped (likely at the same address) before a mounting thread dereferences its stale vfsp/vfsops pointer β€” the stale pointer hits valid memory of the fresh module copy and the corruption manifests as corrupted lock/refcount state β†’ the observed irrecoverable deadlock, rather than a page-fault panic. With a post-unload gap (GAP=10000/50000) the range stays unmapped longer, but the lock cascade still wins the race on this kernel.

VERDICT.md
↓ download raw

DF-2918 β€” VERDICT

Finding: sys/kern/vfs_init.c β€” the vfsconf registry (the list walked by every mount, every sysctl vfs.generic enumeration) has zero synchronization: no lock, no token, non-atomic vfc_refcount. vfs_unregister() (vfs_init.c:458-480) checks vfsp->vfc_refcount != 0 (:469) and then removes the entry (:478) with nothing to serialize it against sys_mount()'s unlocked vfsconf_find_by_name() at vfs_syscalls.c:313 followed by vfsp->vfc_refcount++ at :359 β€” a window that contains an M_WAITOK kmalloc and a syncer kthread creation (vfs_syscalls.c:354-356), i.e. hundreds of microseconds, and is crossed by every mount(2) attempt, including ones that ultimately fail. A concurrent kldunload can therefore unregister + unload a filesystem module while a mount thread is mid-window: the mounter then writes vfc_refcount++ into freed module address space and dereferences mp->mnt_op / vfsp->vfc_vfsops pointers into the unloaded module (UAF write + UAF indirect call).

Reproduced? YES β€” hard kernel deadlock of the mount/linker subsystem

(status=reproduced, impact=dos on this kernel; the UAF write/call is established by construction β€” see "why no page-fault panic" below)

Guest: DragonFly 6.5-DEVELOPMENT #0 x86_64 (stock INVARIANTS kernel), single-tenant QEMU/KVM rig (dfbsd-qemu/vm.sh).

PoC: mountloop hammers mount("fuse", "/home/poc/mnt", 0, NULL) (each attempt crosses the vulnerable window; the mount itself is expected to fail EFAULT after the window), kldloop (root) cycles kldload/kldunload("/boot/kernel/fuse.ko") β€” the unload keeps winning the racy refcount check whenever it lands in a mounter's pre-increment window.

  • Control (mounters only, module loaded, no unload churn): 4 mounters Γ— 600,000+ iterations in 90 s, zero adverse effects, clean exit, guest healthy (run.log, Run 3).
  • Race (mounters + kldunload churn): 3/3 runs wedged irrecoverably within 15 s – 4 min: every mounter thread permanently D-state with wchan ncplk / syncexit / vnode, the kldloop wedged inside the linker, fuse.ko pinned loaded, mount(2) and kldload/kldunload dead system-wide. Wedged threads ignore SIGKILL. The guest can no longer be shut down ("guest not answering" β€” QEMU hard-kill required) and even a forced debug.panic=1 hangs in syncing disks... (crash-dump sync deadlocked on the same corrupted state; serial log captured in run.log). Unprivileged sysctl vfs.generic enumeration loops (84M+ reads) kept running throughout, so the wedge is owned by the mount/linker path.
  • Mechanism correlation: dmesg shows fuse_init(mountloop|1367) β€” the mounter's own sys_mount autoload (:324) interleaved with kldloop's explicit loads/unloads of the same module, exactly the unsynchronized registration envelope described above.

Why no page-fault panic (and why that doesn't weaken the finding)

kldloop reloads the module immediately after each successful unload, so the freed module address range is re-mapped (typically at the same address) before a mounter dereferences its stale vfsp; the stale pointer hits valid memory of the fresh module instance β†’ silent state corruption (refcount/lock state) β†’ the observed irrecoverable deadlock. With a post-unload gap (GAP=10000/50000) the range stays unmapped longer, but the lock cascade still wins on this kernel (run 5/6). The write-side UAF is nonetheless provable by construction: between vfs_syscalls.c:313 and :359 nothing prevents vfsconf_remove() (vfs_init.c:276) + module unload; every subsequent access in that window (mount_init(mp, vfsp->vfc_vfsops) at :355, vfsp->vfc_refcount++ at :359) is a use of freed module memory.

Why not uid0 (documented hard blocker)

The unload side requires root (kldunload is privileged), so this is not an unprivileged→root primitive; escalation would require a privileged conspirator, at which point root already exists. The fully-unprivileged mounter variant (vfs.usermount=1) is real but was not reachable on this stock guest because every user-mountable fstype (null/devfs/procfs/tmpfs) is compiled into the kernel and the one modular user-facing type, fuse, registers vfc_name "fuse" (sys/vfs/fuse/fuse_vfsops.c:490) while get_fscap() (vfs_syscalls.c:5385-5395) only whitelists "fusefs", so an unprivileged mounter is EPERM-gated before the window. On a kernel with a modular user-mountable filesystem (or if that fuse/get_fscap mismatch is fixed), the identical race is drivable end-to-end by an unprivileged user against a root-initiated (or automated) kldunload.

Secondary observations recorded during verification

  1. sys_mount's auto-load path (vfs_syscalls.c:315-342) bumps lf->userrefs only on the autoload path; mounts of a module that root kldloaded manually are protected only by the racy refcount check β€” the same envelope.
  2. vfs_register()'s sysctl renumber loop (vfs_init.c:338-343) iterates sysctl__vfs_children without SYSCTL_XLOCK while unregistering/re-registering the iterator itself mid-loop (filed as DF-2919).
  3. The wedge also blocked crash-dump sync β€” a panic on a raced system cannot complete its dump.
  4. vfs_register() ignores the return value of vfs_init(vfc) (vfs_init.c:445) β€” no in-tree filesystem fails init today (hardening note only).

Fix validation (two stages β€” see run.2.log)

fix.diff (final, v2) adds to sys/kern/vfs_init.c a lockmgr registry lock (vfsconf_lk): lookups/iteration take it shared; new vfsconf_acquire() does lookup + atomic_add_int(&vfc_refcount, 1) under shared (excluding unregister's exclusive section); vfsconf_release() drops atomically; vfs_register() does duplicate-check+insert under exclusive; vfs_unregister() does find + refcount-check + uninit + remove as ONE exclusive critical section, and additionally returns EBUSY while any mount(2) syscall is in flight (vfsconf_mount_begin/end() bracket sys_mount). sys_mount / dounmount / vfs_rootmountalloc converted to acquire/release (vfs_syscalls.c, vfs_mount.c).

  • v1 (registry lock + acquire/release only), built in-guest, kernel #1: closed the vfsconf lifetime TOCTOU (mounters survived ~10Γ— longer) but the deadlock still formed at ~150 s via the second edge β€” sys_mount's in-syscall autoload enters the linker while holding the mountpoint vnode lock (AB-BA against kldunload holding the linker lock), and vn_syncer_thr_stop() waits unconditionally for a syncer thread whose kthread_create() result is ignored (vfs_sync.c:318-322).
  • v2 (adds the in-flight-mount veto), built in-guest (make nativekernel KERNCONF=X86_64_GENERIC RC=0, make installkernel RC=0), booted as kernel #2: the IDENTICAL race workload ran the full 600 s with 6.6M/4.8M/5.0M mount attempts across the three mounters, 161M unprivileged sysctl enumerations, kldloop cycling the whole time, zero wedged D-threads, no panic, guest healthy β€” === survived 600 seconds ===. Baseline wedged in ≀100 s on 3/3 runs.

Follow-ups recommended beyond this diff: (a) audit kthread_create failure handling in vn_syncer_thr_create/stop (independent latent hang, exposed by this churn); (b) reconcile get_fscap("fusefs") with the fuse module's registered vfc_name "fuse" (currently functionally disables unprivileged fuse mounts); (c) take lf->userrefs (or equivalent) for mounts of manually-kldloaded modules, not just the autoload path.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline stock kernel wedged irrecoverably in <=100 s under the race workload (3/3 full runs + 2 partials). fix.diff v1 (registry lock + acquire/release) closed the vfsconf lifetime TOCTOU but the deadlock persisted (~150 s) via the autoload/vnode vs linker AB-BA edge. fix.diff v2 (adds vfsconf_mount_begin/end bracketing sys_mount and an in-flight-mount EBUSY veto in vfs_unregister) was built in-guest (make nativekernel RC=0, installkernel RC=0), booted as kernel #2, and the IDENTICAL workload ran the full 600 s with 16.4M total mount attempts, 161M unpriv sysctl enumerations, zero wedged D-threads, no panic - bad behavior GONE.

['findings/poc/DF-2918/run.2.log (baseline wedge timing, v1 partial result, v2 600 s survival with counters)', 'findings/poc/DF-2918/fix.diff (21 hunks, git-apply clean against sys/)', 'guest /root/build3.log RC=0, /root/install3.log RC=0, uname #2 after reboot']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #2: Thu Sep 3 10:56:41 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root: kldloop cycles kldload/kldunload of a modular filesystem (fuse.ko) || racer: mount(2) loop crosses vfs_syscalls.c:313->:359 on every attempt; when kldunload's racy refcount read observes 0 during a mounter's pre-increment window, vfsconf_remove() unlinks the entry and the linker frees/unmaps the module while the mounter still holds vfsp/mp->mnt_op into it -> vfc_refcount++ write into freed module pages + indirect calls through freed vfsops; observed outcome: permanent kernel deadlock (mount/linker subsystem), unkillable threads, host requires hard reset

Evidence (decisive lines)

["run.log: control clean (iter=600000 ok=0 fail=600001 x4 mounters, 90 s) vs 3/3 race wedges with ps D-state/wchan captures (ncplk/syncexit/vnode), fuse pinned (kldstat id 4), 'guest not answering' resets", "run.log: dmesg fuse_init(mountloop|1367) proving sys_mount's in-syscall autoload interleaved with kldunload churn of the same module", "panic.txt: forced debug.panic on the wedged guest hangs in 'syncing disks...' - crash-dump sync deadlocked on the corrupted state, no vmcore produced", 'run.2.log: fix validation - stock wedge <=100 s; fix v1 (registry lock+acquire/release) still wedges at ~150 s (2nd edge identified); fix v2 (+in-flight-mount veto) survives the full 600 s with 6.6M/4.8M/5.0M mount attempts and 161M unpriv sysctl enumerations, zero wedges']

PoC changes

PoC authored from scratch (the finding had no seed pack): mountloop (unpriv-mountable mount(2) hammer; mounts intentionally fail after the vulnerable window), kldloop (root kldload/kldunload churn with EBUSY veto-retry and post-unload gap), sysctlloop (unpriv vfs.generic enumeration hammer), race.sh (control/race driver). Iterations: ext2fs -> tmpfs -> nullfs -> fuse (only genuinely modular user-relevant fs type on the stock guest); added post-unload gap after observing silent re-mapping of freed module addresses; fixed sysctl oid vfs.generic.0 -> vfs.generic; escaped nested-su quoting by pushing a driver script.

Verified recommended fix

Serialize the vfsconf registry: lockmgr registry lock (shared lookups/iteration/acquire, exclusive register/unregister), atomic vfc_refcount via vfsconf_acquire()/vfsconf_release() used by sys_mount/dounmount/vfs_rootmountalloc, and an in-flight-mount counter vetoing vfs_unregister with EBUSY while any mount(2) syscall runs (see fix.diff)

Verdict

REPRODUCED. The vfsconf registry in sys/kern/vfs_init.c has no synchronization of any kind: vfs_unregister() (vfs_init.c:469) checks vfc_refcount != 0 and removes the entry with nothing serializing it against sys_mount()'s unlocked vfsconf_find_by_name() (vfs_syscalls.c:313) followed by vfc_refcount++ (:359) - a window containing an M_WAITOK kmalloc and a syncer-kthread creation, crossed by every mount(2) attempt. A concurrent kldunload can unregister and unload a filesystem module while a mount thread is mid-window; by construction the mounter then writes refcount into freed module address space and dereferences vfsops pointers into the unloaded module (UAF write + UAF indirect call). On the stock INVARIANTS guest the observable manifestation (reproduced 4/4, in 15-100 s) is an irrecoverable kernel deadlock of the mount/linker subsystem: all mounters D-state (wchan ncplk/syncexit/vnode), fuse.ko pinned, mount(2)/kldload/kldunload dead system-wide, SIGKILL ineffective, clean shutdown impossible, and even a forced panic wedges in 'syncing disks...' so no crash dump completes. The control run (identical mount loop, no unload churn) is clean (600k+ iterations/mounter, 90 s). A contributing second edge in the same envelope is sys_mount's in-syscall auto-load (vfs_syscalls.c:324) which enters the linker while holding the mountpoint vnode lock, deadlocking against kldunload holding the linker lock (AB-BA), plus vn_syncer_thr_stop()'s unconditional wait on a syncer thread whose creation result is ignored (vfs_sync.c:318-322). NOT uid0: the unload side requires root (privileged conspirator), so this is a DoS/memory-corruption race, not an unpriv->root primitive; the fully-unprivileged mounter variant is additionally blocked on stock GENERIC because every user-mountable fstype (null/devfs/procfs/tmpfs) is compiled in and modular fuse is cap-gated out by the get_fscap('fusefs') vs vfc_name('fuse') mismatch.