ufs_check_export/ufs_fhtovp (via ffs_fhtovp) reachable with mnt_data==NULL in the dounmount fsid window: vfs_getvfs() hands out mounts that VFS_UNMOUNT already tore down β deterministic NULL-deref kernel panic (fatal trap 12 @ 0x18 in ffs_fhtovp), unauthenticated via nfsrv_fhtovp on exported UFS, root-gated via fhstat/fhopen
Summary
dounmount() runs VFS_UNMOUNT() - ffs_unmount frees the struct ufsmount and clears mp->mnt_data - long before mountlist_remove() takes the mount off the fsid RB-tree. vfs_getvfs() never checks MNTK_UNMOUNT, so during that window every fsid-based consumer receives a mount whose mnt_data is NULL: ufs_check_export computes &((struct ufsmount*)0)->um_export and NE_LOCKs it, and ffs_fhtovp loads um_fs at NULL+0x18. VERIFIED on-guest: vn-backed newfs UFS, getfh captures the stable root fh, 4-8 root processes spin fhstat(2) while a loop mounts/umounts -f: 'Fatal trap 12, fault VA = 0x18, Stopped at ffs_fhtovp+0xb: movq 0x18(%rax),%r9' within 3 rounds (~35s). Unauthenticated remote variant whenever an exported UFS exists and nfsd runs (nfsrv_fhtovp resolves the wire fsid into the same window; NFS traffic needs no credentials; MNT_EXPORTED survives ffs_unmount). Fix-v1 (vfs_getvfs MNTK_UNMOUNT filter only) still panicked ~20s - proving the preemptible check-and-use TOCTOU; the validated 5-hunk fix (vfs_busy LK_NOWAIT interlock in sys_fhstat + nfsrv_fhtovp + vfs_getvfs filter + NULL guards) survived 6000/6000 rounds. NULL read of a zeroed field - DoS ceiling, no uid0 route.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3241 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.8 KB | β raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| getfh.c | β | 462 B | view raw | |
| fhbomb.c | β | 1.4 KB | view raw | |
| fhstat_once.c | β | 722 B | view raw | |
| race.sh | β | 1.9 KB | view raw | |
| build.sh | β | 172 B | view raw | |
| run.sh | β | 231 B | view raw | |
| env.txt | β | 381 B | view raw | |
| run.log | β | 1.8 KB | view raw | |
| run.fix.log | β | 366 B | view raw | |
| panic.txt | β | 2.1 KB | view raw | |
| build.log | β | 5.6 MB | β download | |
| fix.diff | β | 2.9 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 4.9 KB | view raw |
DF-3241 β ufs_check_export / ufs_fhtovp reachable with mnt_data == NULL in the dounmount fsid window
File under audit: sys/vfs/ufs/ufs_vfsops.c (pass 2)
Kernel: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), stock in-guest build
What the bug is
dounmount() (sys/kern/vfs_syscalls.c) tears a mount down in this order:
VFS_UNMOUNT()β ffs_unmount() frees thestruct ufsmount, setsmp->mnt_data = 0(sys/vfs/ufs/ffs_vfsops.c:862-866)- β¦ journal cleanup, vnode-op teardown, cache drops β¦
mountlist_remove(mp)(sys/kern/vfs_syscalls.c:1040) β only NOW does the mount leave the fsid RB-tree.
Between (1) and (3) the mount is still resolvable by fsid through
vfs_getvfs() (sys/kern/vfs_mount.c:413-424), which does not check
MNTK_UNMOUNT. Every fsid consumer then feeds the still-listed mount into
the UFS file-handle path:
nfsrv_fhtovp()(sys/vfs/nfs/nfs_subs.c:1235-1246) β unauthenticated remote/local whenever an exported UFS exists and nfsd is running:VFS_CHECKEXPβufs_check_export()derefsVFSTOUFS(mp)->um_exporton a NULLmnt_data(sys/vfs/ufs/ufs_vfsops.c:242-246), thenVFS_FHTOVPβffs_fhtovp()readsVFSTOUFS(mp)->um_fsat NULL+0x18 (sys/vfs/ufs/ffs_vfsops.c:1216-1218).fhopen/fhstat/fhstatfs/fhstatvfs(sys/kern/vfs_syscalls.c:4856, 5024, 5072, 5133) β root-gated in-kernel repro path (used by this PoC).kern_checkpoint(sys/kern/kern_checkpoint.c:496).
Result: deterministic Fatal trap 12, supervisor read at 0x18 in
ffs_fhtovp (kernel NULL-deref panic). This is the UFS twin of DF-3236
(devfs fhtovp NULL lock) and DF-3207 (nullfs mnt_data).
Files
getfh.cβ capture an fhandle_t (root; getfh(2))fhbomb.cβ spin fhstat(2) on the captured handle (root; fhstat is root-gated)fhstat_once.cβ sanity: fh must resolve while mountedrace.shβ vn(4) + newfs UFS setup, N fhstat spinners, mount/umount -f loopbuild.shβ host-side: push + compile in guestrun.shβ host-side: run the race, watch for the panicpanic.txtβ serial console capture of the fatal trap (baseline)build.logβ kernel build log for the fix validationrun.fix.logβ patched-kernel rerun (must survive)fix.diffβ root-cause fix: vfs_getvfs() refuses mounts with MNTK_UNMOUNT
Reproduce
# build ./build.sh # run (as root, in guest): ROUNDS=4000 NBOMB=8 sh /root/poc/race.sh # host-side driver: ./run.sh
Expected (stock kernel): panic within seconds-to-minutes:
Fatal user address access from kernel mode from fhbomb at ... Fatal trap 12: page fault while in kernel mode fault virtual address = 0x18 ... Stopped at ffs_fhtovp+0xb: movq 0x18(%rax),%r9 # rax == mnt_data == NULL
Expected (patched kernel): the race survives all rounds; fhstat gets ESTALE during the window instead of a torn-down mount.
Notes
- The panic reproduced on the first driver run (ROUNDS=3, NBOMB=4) within ~35 s β the window is hit easily because every in-window fhstat traps.
- Fix engineering result: a
vfs_getvfs()MNTK_UNMOUNT filter ALONE is NOT sufficient β the fix-v1 kernel panicked identically within ~20 s (check-and-use is preemptible). The validated fix (fix.diff) adds thevfs_busy(mp, LK_NOWAIT)interlock (sharedmnt_lockexcludes the whole dounmount body) tosys_fhstatandnfsrv_fhtovp, keeps the vfs_getvfs filter, and NULL-guardsufs_check_export/ffs_fhtovp. Patched kernel: 6000/6000 rounds survived, zero panics. - The unprivileged-real-world variant needs nfsd + an exported UFS mount (NFS traffic is unauthenticated); the fh* syscalls used here are root-gated and only demonstrate the kernel-side primitive (same methodology as DF-3236's accepted repro).
DF-3241 VERDICT β ufs_check_export / ufs_fhtovp NULL mnt_data window (dounmount)
Status: REPRODUCED (panic) β fix validated (6000/6000 rounds clean)
What was reproduced
On the stock INVARIANTS kernel (DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC),
an fhstat(2) that resolves a UFS mount's fsid inside the dounmount() window
between VFS_UNMOUNT() (which frees struct ufsmount and clears
mp->mnt_data, sys/vfs/ufs/ffs_vfsops.c:862-866) and mountlist_remove(mp)
(sys/kern/vfs_syscalls.c:1040) faults deterministically:
Fatal user address access from kernel mode from fhbomb at ffffffff80910b3b Fatal trap 12: page fault while in kernel mode fault virtual address = 0x18 Stopped at ffs_fhtovp+0xb: movq 0x18(%rax),%r9 ; rax = mnt_data = NULL
0x18 == offsetof(struct ufsmount, um_fs) (ufsmount.h:69-74). Reproduced on
the first driver run (ROUNDS=3, NBOMB=4) within ~35 s; the guest sat in
DDB (db>).
Why it happens (path:line)
sys_fhstat/nfsrv_fhtovpresolve a user/wire fsid withvfs_getvfs()(sys/kern/vfs_mount.c:413-424), which only RB-looks-up the mounttree β it does not checkMNTK_UNMOUNT(stock kernel).dounmount()order:dounmount_interlocksets MNTK_UNMOUNT (vfs_syscalls.c:831) βVFS_UNMOUNTfrees ump,mnt_data = 0(vfs_syscalls.c:1006 β ffs_vfsops.c:862-866) β thenmountlist_remove(vfs_syscalls.c:1040). Throughout that span the mount is still fsid-resolvable.ffs_fhtovp(ffs_vfsops.c:1216-1218) andufs_check_export(ufs_vfsops.c:242-246) derefVFSTOUFS(mp)unguarded β NULL read.
Reachability / threat
- PoC path (used here):
fhstat(2)β root-gated (same accepted methodology as the DF-3236 devfs twin). - Real-world unprivileged path:
nfsrv_fhtovp()(sys/vfs/nfs/nfs_subs.c: 1235-1246) β unauthenticated NFS traffic to a running nfsd while any exported UFS is force-unmounted;VFS_CHECKEXPβufs_check_exportcrashes the same way (NE_LOCKon NULL+0x78 when MNT_EXPORTED sticks, since ffs_unmount never clears it, ffs_vfsops.c:867). kern_checkpoint(kern_checkpoint.c:496) has the same shape.
Impact ceiling: kernel NULL-deref panic (DoS) β not a corruption primitive; no escalation chain exists for this bug class (fixed-address read of a zeroed pointer field).
Fix iteration (important engineering result)
- fix v1 β
vfs_getvfs()refuses mounts with MNTK_UNMOUNT: INSUFFICIENT. The rebuilt kernel still trapped atffs_fhtovp+0xbwithin ~20 s: the check-and-use is a TOCTOU β an fhstat thread can pass the flag check (flag still clear), be preempted atlwkt_reltoken, and a fullumount -fcompletes before the thread loadsmnt_data. - fix v2 (validated) β the airtight interlock is
vfs_busy(): it checks MNTK_UNMOUNT undermnt_tokenAND takesmnt_lockshared, which excludes the entire dounmount body (exclusivemnt_lockfrom vfs_syscalls.c:842 through 1091, including VFS_UNMOUNT). 1. sys/kern/vfs_mount.c β keep the vfs_getvfs MNTK_UNMOUNT filter (semantics: a dying mount must not be handed out; defense in depth). 2. sys/kern/vfs_syscalls.csys_fhstatβ vfs_busy(mp, LK_NOWAIT) across the VFS_FHTOVP; ESTALE on failure; vfs_unbusy+mount_drop on the way out. 3. sys/vfs/nfs/nfs_subs.cnfsrv_fhtovpβ same vfs_busy/unbusy wrap around the CHECKEXP+FHTOVP pair (the unauthenticated threat path). 4. sys/vfs/ufs/ufs_vfsops.cufs_check_exportβ NULL mnt_data guard returning EACCES. 5. sys/vfs/ufs/ffs_vfsops.cffs_fhtovpβ NULL mnt_data guard returning ESTALE.
Baseline vs patched, identical driver:
* stock kernel: Fatal trap 12 @ 0x18 in ffs_fhtovp β€ 3 rounds (twice:
stock and fix-v1).
* fix-v2 kernel (#1 Wed Sep 9 05:56:10): 6000/6000 mount+umount -f
rounds with 8 fhstat spinners, SURVIVED, no panic; fhstats return
ESTALE during teardown instead of crashing.
Residual notes
- The same fsid-window pattern exists for
fhopen/fhstatfs/fhstatvfs(root-gated) andkern_checkpoint; fix v2's vfs_busy pattern should be applied there too (mechanical, same shape). This PoC validated the pattern on sys_fhstat + the NFS path is protected by the same patch. ffs_unmountnever callsvfs_free_addrlist(&ump->um_export)β radix heads leak on unmount of an exported fs (root-gated; separate low-sev issue, ffs_vfsops.c territory).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedValidated on-guest: baseline (stock #0 kernel) panicked at ffs_fhtovp+0xb within 3 rounds; a first fix attempt (vfs_getvfs MNTK_UNMOUNT filter only, kernel built and booted) STILL panicked within ~20 s, proving the preemptible check-and-use TOCTOU; the final 5-hunk fix (vfs_busy interlock in sys_fhstat + nfsrv_fhtovp + vfs_getvfs filter + NULL guards in ufs_check_export/ffs_fhtovp, fix.diff) survived 6000/6000 mount+umount -f rounds with 8 fhstat spinners and zero panics (run.fix.log).
['fix.diff (git apply --check clean, 6 hunks across 5 files)', 'run.fix.log: SANITY_OK, round 6000 done, SURVIVED 6000 rounds without panic', 'build.log: full nativekernel build of the patched kernel (3756 objects relinked)', 'serial log of fix-v1 panic captured in session (same ffs_fhtovp+0xb signature) documented in VERDICT.md']
Confirmed kernel references
Detail
Evidence (decisive lines)
["panic.txt: 'Fatal trap 12 ... fault virtual address = 0x18' + 'Stopped at ffs_fhtovp+0xb: movq 0x18(%rax),%r9' from process fhbomb (stock kernel, <=3 rounds)", 'run.log: baseline host transcript and window analysis (0x18 == offsetof(struct ufsmount, um_fs))', "run.fix.log: fix-v2 kernel 'SANITY_OK' + 'round 6000 done' + 'SURVIVED 6000 rounds without panic'", 'VERDICT.md: why fix v1 (vfs_getvfs flag filter alone) still panicked and why the vfs_busy() interlock closes it', 'fix.diff: 5-hunk validated fix (git-apply clean)']
PoC changes
No seed PoC existed; written from scratch for this finding. Fixes during bring-up: ESTALE needs sys/errno.h on DragonFly; guest sh choked on a stray ' * ' comment line in race.sh; DFly vnconfig takes no -s size arg (size comes from the backing file, created with dd); truncate -s 64m unsupported, used dd; newfs on /dev/vn0 auto-creates the compatibility slice (mounted as /dev/vn0s0).
Verified recommended fix
Interlock fsid-based file-handle resolution against unmount: vfs_busy(mp, LK_NOWAIT) around VFS_CHECKEXP/VFS_FHTOVP in nfsrv_fhtovp() and sys_fhstat() (and the remaining fh* syscalls), bail with ESTALE on failure; additionally filter MNTK_UNMOUNT mounts in vfs_getvfs() and NULL-guard mnt_data in ufs_check_export()/ffs_fhtovp().
Verdict
Deterministic kernel NULL-deref panic reproduced on the stock INVARIANTS kernel: an fhstat(2) resolving a UFS mount's fsid inside the dounmount() window between VFS_UNMOUNT() (ffs_unmount frees the ufsmount, mp->mnt_data=0 at ffs_vfsops.c:862-866) and mountlist_remove() (vfs_syscalls.c:1040) faults at ffs_fhtovp+0xb reading 0x18(%rax) with rax=mnt_data=NULL. Baseline hit within 3 mount/umount -f rounds. vfs_getvfs() (vfs_mount.c:413-424) hands out mounts in that window because it never checks MNTK_UNMOUNT. Unauthenticated reachability exists via nfsrv_fhtovp() (nfs_subs.c:1235-1246) against any exported UFS with nfsd running; the fh* syscalls used for the PoC are root-gated (same accepted methodology as the DF-3236 devfs twin). Impact ceiling is a kernel DoS panic (fixed-address read of a NULL pointer field), not a corruption primitive; no uid0 chain exists for this bug.
No comments yet.