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

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)

No comments yet.

PoC verification

Evidence pack

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

  1. VFS_UNMOUNT() β€” ffs_unmount() frees the struct ufsmount, sets mp->mnt_data = 0 (sys/vfs/ufs/ffs_vfsops.c:862-866)
  2. … journal cleanup, vnode-op teardown, cache drops …
  3. 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:

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 mounted
  • race.sh β€” vn(4) + newfs UFS setup, N fhstat spinners, mount/umount -f loop
  • build.sh β€” host-side: push + compile in guest
  • run.sh β€” host-side: run the race, watch for the panic
  • panic.txt β€” serial console capture of the fatal trap (baseline)
  • build.log β€” kernel build log for the fix validation
  • run.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 the vfs_busy(mp, LK_NOWAIT) interlock (shared mnt_lock excludes the whole dounmount body) to sys_fhstat and nfsrv_fhtovp, keeps the vfs_getvfs filter, and NULL-guards ufs_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).
VERDICT.md
↓ download raw

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)

  1. sys_fhstat / nfsrv_fhtovp resolve a user/wire fsid with vfs_getvfs() (sys/kern/vfs_mount.c:413-424), which only RB-looks-up the mounttree β€” it does not check MNTK_UNMOUNT (stock kernel).
  2. dounmount() order: dounmount_interlock sets MNTK_UNMOUNT (vfs_syscalls.c:831) β†’ VFS_UNMOUNT frees ump, mnt_data = 0 (vfs_syscalls.c:1006 β†’ ffs_vfsops.c:862-866) β†’ then mountlist_remove (vfs_syscalls.c:1040). Throughout that span the mount is still fsid-resolvable.
  3. ffs_fhtovp (ffs_vfsops.c:1216-1218) and ufs_check_export (ufs_vfsops.c:242-246) deref VFSTOUFS(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_export crashes the same way (NE_LOCK on 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 at ffs_fhtovp+0xb within ~20 s: the check-and-use is a TOCTOU β€” an fhstat thread can pass the flag check (flag still clear), be preempted at lwkt_reltoken, and a full umount -f completes before the thread loads mnt_data.
  • fix v2 (validated) β€” the airtight interlock is vfs_busy(): it checks MNTK_UNMOUNT under mnt_token AND takes mnt_lock shared, which excludes the entire dounmount body (exclusive mnt_lock from 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.c sys_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.c nfsrv_fhtovp β€” same vfs_busy/unbusy wrap around the CHECKEXP+FHTOVP pair (the unauthenticated threat path). 4. sys/vfs/ufs/ufs_vfsops.c ufs_check_export β€” NULL mnt_data guard returning EACCES. 5. sys/vfs/ufs/ffs_vfsops.c ffs_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) and kern_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_unmount never calls vfs_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

fixed
baseline reproduced→ patch + rebuild →patched clean

Validated 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']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 9 05:56:10 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.