DF-2918 / verdict.json
{ "finding_id": "DF-2918", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "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.", "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": [ "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" ], "kernel_refs": [ "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" ], "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.", "attempts": 5, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline); fix kernel #1 2026-09-03 10:40:23; fix kernel #2 2026-09-03 10:56:41", "runtime_sec": 5400, "guest_dirty": 0, "build_cmd": "cc -O2 -o /root/mountloop mountloop.c && cc -O2 -o /root/kldloop kldloop.c && cc -O2 -o /root/sysctlloop sysctlloop.c", "run_cmd": "MODE=race GAP=10000 sh race.sh 3 600", "code_hash": "mountloop.c 065d5e4fb7c2015a0c8366829e32bbc15055b09243b3c608b56dc4200aa1015d; kldloop.c b83939645c993d4407896026b44286294461c815013e6e8b31983ea79709285e; sysctlloop.c 9e388f6f7e5d2d25c911f61c7b0a2c7af7251fbf7f374d1218399a675b23b700; race.sh 8cc8eb30dc316cf92878b9b82869b69e684901d7108a5dde79ef3dfafcad99ee", "notes": "Severity honest-basis: Medium (not High) because the unload side of the race requires a privileged actor (kldunload), so this is corruption-class/DoS with a privileged conspirator, not unpriv->root. The write-side UAF (refcount++/vfsops deref into freed module pages) is established by construction and closes under fix.diff; the dominant observed manifestation is the deadlock. Residual items discovered and recorded: (1) sys_mount's autoload holds the mountpoint vnode lock across linker_load_file (AB-BA with kldunload) - closed by the in-flight-mount veto in fix v2; (2) vn_syncer_thr_create ignores kthread_create failure while vn_syncer_thr_stop waits unconditionally (vfs_sync.c:318-322) - independent latent hang worth its own ticket; (3) get_fscap whitelists 'fusefs' but the fuse module registers vfc_name 'fuse', functionally disabling unprivileged fuse mounts on stock systems; (4) sys_mount's autoload path only takes lf->userrefs when IT autoloads - mounts of manually-kldloaded modules rely solely on the (formerly racy) refcount. Guest was reset to the clean-source snapshot after validation.", "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)", "fix_status": "fixed", "fix_kernel_uname": "DragonFly 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", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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.", "fix_evidence": [ "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" ] } |