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).
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2918 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 insyncing 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.
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 forceddebug.panic=1hangs insyncing disks...(crash-dump sync deadlocked on the same corrupted state; serial log captured in run.log). Unprivilegedsysctl vfs.genericenumeration 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
sys_mount's auto-load path (vfs_syscalls.c:315-342) bumpslf->userrefsonly on the autoload path; mounts of a module that root kldloaded manually are protected only by the racy refcount check β the same envelope.vfs_register()'s sysctl renumber loop (vfs_init.c:338-343) iteratessysctl__vfs_childrenwithout SYSCTL_XLOCK while unregistering/re-registering the iterator itself mid-loop (filed as DF-2919).- The wedge also blocked crash-dump sync β a panic on a raced system cannot complete its dump.
vfs_register()ignores the return value ofvfs_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 whosekthread_create()result is ignored (vfs_sync.c:318-322). - v2 (adds the in-flight-mount veto), built in-guest
(
make nativekernel KERNCONF=X86_64_GENERICRC=0,make installkernelRC=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
fixedBaseline 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']
Confirmed kernel references
- sys/kern/vfs_init.c:458-480
- sys/kern/vfs_init.c:469
- sys/kern/vfs_init.c:264-284
- sys/kern/vfs_init.c:240-308
- sys/kern/vfs_init.c:338-343
- sys/kern/vfs_syscalls.c:313
- sys/kern/vfs_syscalls.c:315-342
- sys/kern/vfs_syscalls.c:354-359
- sys/kern/vfs_syscalls.c:468
- sys/kern/vfs_syscalls.c:1079
- sys/kern/vfs_mount.c:340-351
- sys/kern/vfs_sync.c:318-322
- sys/kern/kern_linker.c:477-569
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.
No comments yet.