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

fhstatfs(2)/fhstatvfs(2) overwrite the vfs_getvfs()-held mount with vp->v_mount: nullfs pass-through FHTOVP causes mnt_hold underflow β€” KKASSERT panic on INVARIANTS, kfree() of a live struct mount (UAF) on release kernels

Field Value
ID DF-2668
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:H
CWE CWE-672 Operation on a Resource after Release
File sys/kern/vfs_syscalls.c
Lines 5074/5096 (fhstatfs), 5135/5149 (fhstatvfs)
Area kern
Confidence certain
Discovered 2026-08-29
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

sys_fhstatfs() takes a held mount (vfs_getvfs, :5063; mount_hold at vfs_mount.c:414-424) then executes mp = vp->v_mount; (:5074) and mount_drop()s that pointer at :5096; nullfs_fhtovp() is a pass-through returning a LOWER-filesystem vnode (null_vfsops.c:389-397), so vp->v_mount != the held mount. The vfs_getvfs() hold leaks on the nullfs mount while the live lower mount gets an unmatched mount_drop() driving mnt_hold 1β†’0. sys_fhstatvfs() repeats the identical defect. On INVARIANTS: panic: assertion "mp->mnt_refs == 0" failed in mount_drop. On release kernels the same path kfree()s a live mount still linked into the mountlist/fsid rb-tree with all its vnodes β€” heap-corruption-grade UAF.

Threat model & preconditions

Root-only (SYSCAP_RESTRICTEDROOT) β€” not an unprivileged escalation; realistic victims are mountd/NFS tooling following the documented nullfs-export workflow (sys_getfh's own comment, :4761-4768).

Proof of concept

Reproduced as root (findings/poc/DF-2668/fhstatfs_hold.c): mount -t null /tmp /tmp/nmtest; getfh(/tmp/nmtest/anchor_file); fhstatfs(fh) β†’ panic in mount_drop with sys_fhstatfs on the stack (panic.txt). Fix validated on a rebuilt kernel: valid statfs returned, no panic, nullfs unmounts cleanly.

--- a/sys/kern/vfs_syscalls.c
+++ b/sys/kern/vfs_syscalls.c
@@ sys_fhstatfs()
    if ((error = VFS_FHTOVP(mp, NULL, &fh.fh_fid, &vp)) != 0)
        goto done;
-   mp = vp->v_mount;
    sp = &mp->mnt_stat;
@@ sys_fhstatvfs()
    if ((error = VFS_FHTOVP(mp, NULL, &fh.fh_fid, &vp)))
        goto done;
-   mp = vp->v_mount;
    sp = &mp->mnt_vstat;

(nullfs VFS_STATFS already passes through lower stats)

Timeline

  • 2026-08-29 Discovered during pass-2 audit of vfs_syscalls.c (GLM 5.3); reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2668 Β· 12 files
FileTypeDescriptionSize
fhstatfs_hold.c β€” 1.9 KB view raw
build.sh β€” 88 B view raw
setup_and_run.sh β€” 315 B view raw
run.log β€” 1.2 KB view raw
panic.txt β€” 530 B view raw
env.txt β€” 358 B view raw
fix.diff β€” 543 B view raw
run.patched.log β€” 640 B view raw
fix_validation.log β€” 2.5 KB view raw
VERDICT.md β€” 3.7 KB ↓ raw
verdict.json β€” 3.6 KB view raw
manifest.json β€” 1.1 KB view raw
VERDICT.md
↓ download raw

DF-2668 VERDICT β€” REPRODUCED (mount hold-count underflow β†’ KKASSERT panic; live-mount UAF on release kernels)

Bottom line

fhstatfs(2) (and identically fhstatvfs(2)) on a file handle obtained from a nullfs-covered path (an operation sys_getfh() explicitly supports for NFS exports) makes sys_fhstatfs() call mount_drop() on the lower filesystem's live struct mount without ever holding it. On the stock INVARIANTS guest this is an immediate, 100%-reliable panic: assertion "mp->mnt_refs == 0" failed in mount_drop with sys_fhstatfs() on the stack (captured). On a release kernel the same path executes kfree(mp, M_MOUNT) on a live mount β€” a use-after-free of struct mount referenced by the mountlist, the fsid rb-tree, and every vnode of that filesystem. The vfs_getvfs() hold on the nullfs mount is leaked in the same call (permanent mount leak).

Why it happens (line-accurate)

  1. sys_fhstatfs() (sys/kern/vfs_syscalls.c:5041) takes a held mount: mp = vfs_getvfs(&fh.fh_fsid) (:5063). vfs_getvfs() (sys/kern/vfs_mount.c:414-424) does mount_hold(mp); the caller owes one mount_drop() (:393-405), which frees the mount when mnt_hold hits 0 (with KKASSERT(mp->mnt_refs == 0)).
  2. VFS_FHTOVP(mp, ...) on a nullfs mount is a pass-through returning a vnode of the lower filesystem: nullfs_fhtovp() at sys/vfs/nullfs/null_vfsops.c:389-397 calls VFS_FHTOVP(xmp->nullm_vfs, ...) and returns the lower vp, whose v_mount is the lower mount.
  3. sys_fhstatfs() then executes mp = vp->v_mount; (:5074): the held nullfs pointer is overwritten (its hold leaks), and the later mount_drop(mp) at :5096 decrements the lower mount's mnt_hold without a matching hold. A live, mounted filesystem has mnt_hold == 1 (set in mount_init(), consumed only by the final dounmount() drop at sys/kern/vfs_syscalls.c:1117), so the spurious drop drives it to 0 β†’ assert/kfree.
  4. sys_fhstatvfs() repeats the same two mistakes at :5135 and :5149. (sys_fhopen() :4850/sys_fhstat() :5021 keep the held pointer throughout β€” correct.)
  5. Confirmed end-to-end on the guest: mount -t null /tmp /tmp/nmtest, getfh("/tmp/nmtest/anchor_file") (nullfs fsid reported: 89630026,0000000b), fhstatfs(fh) β†’ panic in mount_drop called from sys_fhstatfs+0xd4.

Threat model

Triggering requires root (caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT) at :5056), so this is not an unprivileged escalation. The realistic victims are privileged daemons following the documented "nullfs mounts can be explicitly exported" workflow (mountd/NFS), plus any root tooling using fhstatfs on synthetic handles. The corruption itself (live mount freed while fully linked into the mountlist) is heap-corruption-grade on release kernels.

Fix validation

fix.diff deletes both mp = vp->v_mount; reassignments (sys/kern/vfs_syscalls.c:5074, :5135) so the mount that was actually held by vfs_getvfs() is the one statfs'd (nullfs VFS_STATFS already passes through to the lower filesystem's stats) and dropped exactly once. Applied to the guest's /usr/src together with DF-2667's fix, kernel rebuilt (make nativekernel KERNCONF=X86_64_GENERIC, INVARIANTS), rebooted: - baseline (stock kernel): panic (above); - patched kernel: identical setup, fhstatfs returns 0 with valid f_mntonname/f_type, no panic, subsequent mount/unmount of the nullfs and tmpfs lifecycle normal (run.patched.log, fix_validation.log).

Artifacts

fhstatfs_hold.c (PoC), build.sh, setup_and_run.sh, run.log, panic.txt, env.txt, fix.diff, run.patched.log, fix_validation.log, verdict.json, manifest.json.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel: fhstatfs on the nullfs-covered handle succeeds with valid data, no KKASSERT, and the nullfs mount unmounts cleanly afterwards; mount hold bookkeeping correct.

['fix_validation.log', 'run.patched.log', 'fix.diff']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Aug 30 10:16:19 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.log (getfh fsid + freeze)', 'panic.txt (mount_drop KKASSERT panic with sys_fhstatfs in backtrace)', 'fix_validation.log + run.patched.log (patched kernel: fhstatfs ok, clean unmount)', 'fix.diff']

PoC changes

PoC written fresh (no seed); raw syscalls getfh(161)/fhstatfs(297) with fhandle_t from sys/mount.h; anchor path passed as argv[1] after the first attempt used a path covered by the nullfs (getfh ENOENT) β€” fixed by pointing at the nullfs view of /tmp/anchor_file.

Verified recommended fix

Delete the 'mp = vp->v_mount;' reassignments in sys_fhstatfs()/sys_fhstatvfs() so the vfs_getvfs()-held mount is dropped exactly once (see fix.diff).

Verdict

Reproduced on the stock INVARIANTS guest as root: getfh() on a nullfs-covered path (explicitly supported for NFS exports), then fhstatfs() on that handle panics with 'panic: assertion "mp->mnt_refs == 0" failed in mount_drop' and the backtrace mount_drop() <- sys_fhstatfs() <- syscall2(). Root cause: sys_fhstatfs() takes a held mount from vfs_getvfs() (sys/kern/vfs_syscalls.c:5063) but overwrites the pointer with vp->v_mount (:5074) and mount_drop()s that at :5096; nullfs VFS_FHTOVP is a pass-through returning a LOWER-filesystem vnode (sys/vfs/nullfs/null_vfsops.c:389-397), so the vfs_getvfs hold leaks on the nullfs mount while the live lower mount receives an unmatched mount_drop() driving mnt_hold 1->0. On INVARIANTS kernels this trips the KKASSERT (captured); on release kernels the same path kfree()s a live struct mount β€” a use-after-free of a mount still linked into the mountlist/fsid rb-tree with all its vnodes. sys_fhstatvfs() has the identical defect at :5135/:5149. Root-only trigger, so impact ceiling is privileged-user-triggered kernel corruption, not unprivileged escalation. fix.diff deletes both reassignments; validated on a rebuilt kernel (#1): identical setup returns valid statfs data, no panic, and the nullfs unmounts cleanly afterwards (no leaked hold).