โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0008

vfs_setpublicfs() use-after-vput of root vnode + refcount leak on VFS_VPTOFH error

Field Value
ID DF-0008
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:H
CWE CWE-416 Use After Free; CWE-401 Missing Release of Memory after Effective Lifetime
File sys/kern/vfs_subr.c
Lines 2255-2290
Area kern
Confidence likely
Discovered 2026-06-29
Reported pending

Summary

In vfs_setpublicfs(), the root vnode rvp obtained from VFS_ROOT() (which returns it referenced and locked) is vput() at vfs_subr.c:2261, dropping its usecount and lock โ€” but is then dereferenced again at vfs_subr.c:2269 by vn_get_namelen(rvp) โ†’ VOP_PATHCONF(rvp) when ex_indexfile is set. After vput, rvp is unlocked and, if VFS_ROOT's was the only reference, eligible for asynchronous reclamation by the vnlru thread โ€” a use-after-free. Even absent a free, VOP_PATHCONF is invoked without the vnode lock, a definite lock-protocol violation. Separately, the error path at vfs_subr.c:2258-2259 returns on VFS_VPTOFH failure without vput(rvp), permanently leaking one usecount (the vnode is never reclaimable).

Root cause

sys/kern/vfs_subr.c:2255-2290:

if ((error = VFS_ROOT(mp, &rvp)))           /* rvp referenced + locked */
    return (error);

if ((error = VFS_VPTOFH(rvp, &nfs_pub.np_handle.fh_fid)))
    return (error);                          /* :2259  LEAK: no vput(rvp) */

vput(rvp);                                   /* :2261  drops ref + lock    */

...
if (argp->ex_indexfile != NULL) {
    int namelen;

    error = vn_get_namelen(rvp, &namelen);   /* :2269  USE AFTER vput      */
    if (error)
        return (error);
    ...
}

vn_get_namelen() (sys/kern/vfs_subr.c:2552) calls VOP_PATHCONF(rvp), which requires a live, locked vnode. At :2269 rvp is neither. Two defects:

  1. Refcount leak (:2259): the VFS_VPTOFH error path returns without vput(rvp), leaking the usecount VFS_ROOT added โ€” the vnode is pinned indefinitely (vnode leak / DoS).
  2. Use-after-vput / use-after-unlock (:2261 then :2269): vput drops the ref and the lock; vn_get_namelen then touches rvp unlocked. If VFS_ROOT's was the only reference, rvp may have been reclaimed (vgonel/vclean) and reused by the time VOP_PATHCONF dispatches, causing a use-after-free. At minimum, VOP_PATHCONF races other vnode operations on v_data.

Threat model & preconditions

  • Attacker position: a context that can configure a WebNFS public export (mount(2) with MNT_EXPUBLIC). Mount/export is privileged (priv_check), so the trigger is root-only โ€” this limits direct privilege impact.
  • Privileges gained or impact: kernel memory-safety defect. A legitimate admin enabling the feature, or a root context in a setup where mount is partially delegated (e.g. a confined/jail-like or setuid mount helper), can trip a kernel panic (UAF on a reclaimed/reused vnode) or vnode corruption from the unlocked VOP_PATHCONF. The leak variant is an availability issue (vnode exhaustion).
  • Required config or capabilities: privilege to mount with MNT_EXPUBLIC; an indexfile set (for the UAF) or a filesystem whose vptofh can fail (for the leak).
  • Reachability: mount(2) โ†’ vfs_setpublicfs() (via the export path).

Proof of concept

PoC source: findings/poc/DF-0008/expub.c

Repeatedly issues mount(2) with MNT_EXPUBLIC and an ex_indexfile to widen the race window against vnlru reclamation of the just-vput() root vnode.

Build & run (as root)

cc -o expub findings/poc/DF-0008/expub.c
mkdir -p /mnt
./expub /dev/adx0s1a /mnt

Expected output

On a DEBUG/INVARIANTS kernel racing vnlru, a panic from VOP_PATHCONF on a reclaimed/locked vnode. The VFS_VPTOFH-error variant deterministically leaks a vnode (vnode-count growth that never decreases across failed mounts).

Impact

Genuine kernel memory-safety defect (UAF + refcount leak + lock-protocol violation), but gated behind mount/export privilege, so rated Low. It is a correctness/robustness fix a maintainer can action immediately; in a delegated-mount threat model it is more severe.

Keep rvp referenced+locked across vn_get_namelen, and ensure every error path vputs it exactly once. Sketch (keeps a single vput on success and one on each error path):

--- a/sys/kern/vfs_subr.c
+++ b/sys/kern/vfs_subr.c
@@ -2255,14 +2255,15 @@
    if ((error = VFS_ROOT(mp, &rvp)))
        return (error);
    if ((error = VFS_VPTOFH(rvp, &nfs_pub.np_handle.fh_fid)))
-       return (error);
-
-   vput(rvp);
+       goto out;

    if (argp->ex_indexfile != NULL) {
        int namelen;

-       error = vn_get_namelen(rvp, &namelen);
+       error = vn_get_namelen(rvp, &namelen);  /* rvp still locked+ref'd */
        if (error)
-           return (error);
+           goto out;
        nfs_pub.np_index = kmalloc(namelen, M_TEMP, M_WAITOK);
        error = copyinstr(argp->ex_indexfile, nfs_pub.np_index,
            namelen, NULL);
@@ -2286,9 +2287,11 @@
        if (error) {
            kfree(nfs_pub.np_index, M_TEMP);
-           return (error);
+           goto out;
        }
    }
+   vput(rvp);

    nfs_pub.np_mount = mp;
    nfs_pub.np_valid = 1;
    return (0);
+out:
+   vput(rvp);
+   return (error);
 }

References

  • sys/kern/vfs_subr.c:2255-2290 โ€” vfs_setpublicfs (leak + UAF).
  • sys/kern/vfs_subr.c:2552 โ€” vn_get_namelen โ†’ VOP_PATHCONF (needs locked vp).
  • CWE-416 Use After Free; CWE-401 Missing Release of Memory after Effective Lifetime.

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/vfs_subr.c.
  • pending Reported to DragonFlyBSD security contact.

PoC verification

Evidence pack

findings/poc/DF-0008 ยท 12 files
FileTypeDescriptionSize
expub.c trigger-source root-only UPDATE-mount trigger of vfs_setpublicfs :2261->:2269 UAF path (rewritten: correct struct ufs_args + MNT_UPDATE) 4.1 KB view raw
VERDICT.md verdict full narrative: real+reachable, line-by-line proof, why no panic on non-DEBUG kernel 8.9 KB โ†“ raw
README.md readme human build/run/expected summary 3.1 KB โ†“ raw
build.sh build-script cc -o expub expub.c 253 B view raw
run.sh run-script as root: ./expub /boot N (UPDATE mount w/ MNT_EXPUBLIC+indexfile) 714 B view raw
build.log build-log final successful build, full output 63 B view raw
run.log run-log decisive run (/boot 16): path reached 16/16, no panic, guest up 423 B view raw
run.stress.log run-log stress run (/boot 512) with vnode churn: path reached 512/512, no panic 383 B view raw
dmesg.txt dmesg post-trigger dmesg: no vnode/lock/witness/panic warnings 597 B view raw
env.txt environment uname, cc version, kernel config (non-DEBUG, no INVARIANTS/WITNESS), /boot=ufs 461 B view raw
fix.diff suggested-fix keep rvp locked across vn_get_namelen; vput exactly once per path (git apply --check passes) 1.4 KB view raw
manifest.json manifest this catalog 2.8 KB view raw
README.md readme human build/run/expected summary
โ†“ download raw

DF-0008 โ€” PoC

expub.c โ€” root-only trigger of the vfs_setpublicfs() use-after-vput of the root vnode (and the VFS_VPTOFH-error refcount leak on a filesystem whose vptofh can fail).

Verdict

Real and reachable, but NOT reproduced as an observable fault on the audited non-DEBUG GENERIC kernel. The vulnerable sequence VFS_ROOT โ†’ vput(rvp) โ†’ vn_get_namelen(rvp) (i.e. VOP_PATHCONF on the already-unlocked/unref'd rvp) ran end-to-end 520/520 times across test runs with no panic, because (1) the X86_64_GENERIC kernel has no INVARIANTS/WITNESS to catch the unlocked-VOP_PATHCONF lock-protocol violation, and (2) the UFS mount-root vnode retains references through the race window so vnlru does not reclaim it (the actual UAF-free never happens on this vnode). The deterministic refcount-leak variant needs a filesystem whose VFS_VPTOFH fails; UFS ffs_vptofh always returns 0, and none of the export-capable fs types on this guest have a failing vptofh. The bug IS genuine (master DEV unchanged since 2026-06-22) and the fix applies โ€” see VERDICT.md for the full line-by-line proof.

The bug

vfs_setpublicfs() (sys/kern/vfs_subr.c:2255-2295):

if ((error = VFS_ROOT(mp, &rvp)))      /* rvp referenced + locked        */
    return (error);
if ((error = VFS_VPTOFH(rvp, ...)))
    return (error);                    /* :2259  LEAK: no vput(rvp)      */
vput(rvp);                             /* :2261  drops ref AND lock      */
...
if (argp->ex_indexfile != NULL) {
    int namelen;
    error = vn_get_namelen(rvp, &namelen);  /* :2269  USE AFTER vput      */

vn_get_namelen() โ†’ VOP_PATHCONF(rvp) (vfs_subr.c:2552) runs on a vnode that has already been vput() (unlocked, usecount decremented). If VFS_ROOT's was the only reference, rvp is reclaimable by vnlru between :2261 and :2269 โ€” use-after-free. At minimum it is a definite vnode lock-protocol violation.

Reachability

mount(2) (UPDATE mount of an already-mounted ufs fs with fspec=NULL and export.ex_flags = MNT_EXPUBLIC|MNT_EXPORTED, ex_indexfile set) โ†’ ffs_mount export path (ffs_vfsops.c:270-273) โ†’ vfs_export โ†’ vfs_setpublicfs. Mount/export is privilege-gated (root only) โ€” a genuine kernel memory-safety defect (Low), not an unprivileged escalation.

Build

./build.sh        # cc -o expub expub.c

Run (as root)

./run.sh                 # defaults: /boot, 16 loops
# or: ./run.sh /boot 512

Expected output

On a DEBUG/INVARIANTS kernel racing vnlru: a panic from VOP_PATHCONF on a reclaimed/unlocked vnode. On the default GENERIC kernel (this guest): the run completes with vfs_setpublicfs UAF path reached ok=N and no panic โ€” the lock-protocol violation races silently. The VFS_VPTOFH-error leak variant is deterministic but needs a vptofh-failing export fs (not available on this guest).

Impact

Genuine CWE-416 (UAF) + CWE-401 (refcount leak) + vnode-lock-protocol violation, gated behind mount/export privilege (root). No panic/leak/uid0 obtained on this non-DEBUG kernel; the finding's value is the real, reachable code defect and the targeted fix in fix.diff.

VERDICT.md verdict full narrative: real+reachable, line-by-line proof, why no panic on non-DEBUG kernel
โ†“ download raw

DF-0008 โ€” vfs_setpublicfs() use-after-vput of root vnode + refcount leak

Verdict

NOT REPRODUCED as an observable fault on this kernel โ€” but the defect is REAL and REACHABLE, and the vulnerable code path is CONFIRMED to execute on the audited master DEV kernel. This is not a false positive, not already-fixed: master DEV (committed 2026-06-22) contains the exact buggy lines unchanged. The vulnerable sequence VFS_ROOT โ†’ VFS_VPTOFH โ†’ vput(rvp) โ†’ vn_get_namelen(rvp) ran end-to-end 520/520 times across my test runs with no panic, because the audited X86_64_GENERIC kernel is a non-DEBUG build (no INVARIANTS/WITNESS to catch the unlocked-VOP_PATHCONF), and the UFS mount-root vnode retains references through the race window so vnlru does not reclaim it. The deterministic refcount-leak variant (:2259) needs a filesystem whose VFS_VPTOFH fails, and none of the export-capable filesystem types on this guest (hammer2/devfs/ufs/null/tmpfs) have a failing vptofh. The fix (fix.diff) is warranted โ€” the defect is genuine.

Classification: not_reproduced (no panic/leak/uid0/dos observed), impact: none, confidence: likely. The code-level defect itself is certain; the absence of a live fault is a property of this kernel configuration, not of the bug.

The bug โ€” confirmed line-by-line in master DEV

vfs_setpublicfs() (sys/kern/vfs_subr.c:2255-2295):

2255:   if ((error = VFS_ROOT(mp, &rvp)))
2256:       return (error);
2257:
2258:   if ((error = VFS_VPTOFH(rvp, &nfs_pub.np_handle.fh_fid)))
2259:       return (error);            /* LEAK: no vput(rvp) */
2260:
2261:   vput(rvp);                     /* drops ref AND lock */
2262:
2266:   if (argp->ex_indexfile != NULL) {
2267:       int namelen;
2269:       error = vn_get_namelen(rvp, &namelen);   /* USE AFTER vput */

vn_get_namelen() (sys/kern/vfs_subr.c:2547-2557) unconditionally calls VOP_PATHCONF(vp, _PC_NAME_MAX, retval):

2552:   error = VOP_PATHCONF(vp, _PC_NAME_MAX, retval);

Lock/refcount semantics confirmed against the audited source:

  • VFS_ROOT(mp, &rvp) โ†’ ufs_root (sys/vfs/ufs/ufs_vfsops.c:58-68) โ†’ VFS_VGET โ†’ vget, which returns rvp referenced and LK_EXCLUSIVE-locked.
  • vput(vp) (sys/kern/vfs_lock.c:703-706) is vn_unlock(vp); vrele(vp); โ€” it drops both the lock and the usecount.

So at :2269/:2552, VOP_PATHCONF is dispatched on rvp which is now unlocked and usecount-decremented. Two defects:

  1. Refcount leak (:2259) โ€” the VFS_VPTOFH error return does so without vput(rvp), leaking the usecount VFS_ROOT added (vnode pinned indefinitely). Not reachable on UFS (ffs_vptofh, sys/vfs/ufs/ffs_vfsops.c:1228-1239, always returns 0); reachable on any filesystem whose vptofh fails (e.g. one using the default vfs_stdvptofh โ†’ EOPNOTSUPP, sys/kern/vfs_default.c:1562-1566).
  2. Use-after-vput / lock-protocol violation (:2261 then :2269) โ€” VOP_PATHCONF runs on an unlocked vnode; if VFS_ROOT's was the only reference, rvp may have been reclaimed (vgonel/vclean) by vnlru in the window, yielding a use-after-free in the VOP dispatch.

Reachability โ€” CONFIRMED on the audited kernel

vfs_setpublicfs() is reachable from userspace via the export path:

mount(2)  โ†’  ffs_vfsops.c:179 (copyin ufs_args)
           โ†’  ffs_vfsops.c:187 (MNT_UPDATE branch)
           โ†’  ffs_vfsops.c:270-273 (fspec==NULL โ†’ vfs_export)
           โ†’  vfs_subr.c:2201-2204 (ex_flags & MNT_EXPUBLIC โ†’ vfs_setpublicfs)
           โ†’  vfs_subr.c:2255-2295  โ† the buggy sequence

The trigger (expub.c) issues an UPDATE mount of /boot (ufs) with struct ufs_args { .fspec=NULL, .export.ex_flags=MNT_EXPORTED|MNT_EXPUBLIC, .export.ex_indexfile="index.html" }. Each successful mount proves the entire buggy path ran (the :2266 indexfile branch is taken because ex_indexfile != NULL, so :2269 vn_get_namelen(rvp) executes on the already-vput rvp).

Result on the audited kernel (/root/expub /boot N):

run loops path reached panic guest
run.log 16 16/16 ok none up
run.stress 512 512/512 ok none up

(Total 520/520 successful triggering mounts.) The mount returns 0 each time, so vfs_setpublicfs set nfs_pub.np_valid=1 and mp->mnt_flag |= MNT_EXPUBLIC โ€” i.e. the buggy :2261โ†’:2269 sequence completed end-to-end 520 times.

Why no panic on this kernel (honest assessment)

  1. No INVARIANTS/WITNESS. sysctl kern.conftxt shows no INVARIANTS/WITNESS/DEBUG/VFS_DEBUG in the X86_64_GENERIC config. A WITNESS build would flag the unlocked VOP_PATHCONF (vnode-lock protocol violation); an INVARIANTS build asserts vn_lock state in VOP entry and would panic. This kernel has neither, so the violation races silently.
  2. Mount-root vnode is not reclaimed in the window. The /boot root vnode is held by the mount structure (mp keeps a reference to its root), so after vput(rvp) decrements the VFS_ROOT-added usecount, the vnode's usecount remains โ‰ฅ1 and vnlru does not reclaim it between :2261 and :2269. The lock-protocol violation occurs, but the actual UAF-free never happens on this vnode. Concurrent vnode churn (the stress run) does not change this โ€” vnlru only reclaims vnodes with usecount 0.

So on a default kernel the UAF-half is a silent correctness/robustness defect; on a DEBUG/INVARIANTS kernel (or with a sufficiently adversarial vnode whose only ref was VFS_ROOT's) it panics. The leak-half is deterministic but needs a vptofh-failing export-capable filesystem, which this guest does not provide.

Impact

Genuine kernel memory-safety defect (CWE-416 use-after-free + CWE-401 refcount leak + vnode-lock-protocol violation), but gated behind mount/export privilege (sys_mount โ†’ caps_priv_check_td for the fs cap, plus a separate check when MNT_EXPORTED is set, vfs_syscalls.c:164-168). Direct exploitation requires root (or a delegated-mount / setuid-mount-helper threat model). Rated Low. No panic/leak/uid0 was obtained on this kernel; the value of the finding is the real, reachable code defect and the fix.

Exploit chain

None developed โ€” the primitive is gated behind root and does not produce a corruption primitive on this non-DEBUG kernel. A root attacker who can trigger the UAF-free variant (a vptofh-failing export fs, or a DEBUG kernel) would get a vnode UAF that could in principle be groomed into corruption, but that is not achievable in the current guest configuration.

PoC changes

Rewrote expub.c substantially. The original passed a bare struct export_args (384 B) as the mount(2) data argument, but ffs_mount does copyin(data, &args, sizeof(struct ufs_args)) with struct ufs_args = 448 B ({ char *fspec; struct export_args export; }). The original therefore fed the kernel misaligned garbage and never reached the export path. The rewrite:

  • Uses the correct struct ufs_args (#include <vfs/ufs/ufsmount.h>), fspec=NULL, .export.ex_flags = MNT_EXPORTED|MNT_EXPUBLIC, .export.ex_indexfile = "index.html".
  • Issues an UPDATE mount (MNT_UPDATE) of an already-mounted ufs fs (/boot), which is the only path in ffs_mount that processes export args (ffs_vfsops.c:187,270-273).
  • Clears the public export with MNT_DELEXPORT between iterations so the singleton nfs_pub can be re-set (vfs_subr.c:2246).
  • Loops and reports how many times the buggy path was reached.

Added the repro glue (build.sh, run.sh) and the full evidence logs (build.log, run.log, run.stress.log, env.txt, manifest.json, fix.diff).

How to reproduce

./build.sh                 # as maxx: cc -o expub expub.c
# as root:
cp expub /root/expub
/root/expub /boot 16       # reaches the buggy :2261->:2269 path 16 times

On a DEBUG/INVARIANTS kernel this panics in VOP_PATHCONF on the unlocked vnode; on a default GENERIC kernel it races silently (the run reports vfs_setpublicfs UAF path reached ok=N).

References

Confirmed kernel references

Detail

Exploit chain

none. No corruption primitive obtained on this non-DEBUG kernel: the UAF-free never occurs on the UFS mount-root vnode (mount holds a ref), and there are no INVARIANTS to convert the lock-protocol violation into a fault. The bug is gated behind mount/export privilege (sys_mount -> caps_priv_check_td; vfs_syscalls.c:164-168 requires privilege for MNT_EXPORTED), so it is not an unprivileged escalation. A root attacker on a DEBUG kernel (or with a vptofh-failing export fs) could in principle get a vnode UAF groomable into corruption, but that configuration is not present on this guest.

Evidence (decisive lines)

[*] target=/boot loops=16
[*] sizeof(ufs_args)=448 export_args=384
[+] done: vfs_setpublicfs UAF path reached ok=16 fail=0 (of 16)
[+] vfs_setpublicfs() ran the buggy :2261-vput -> :2269-VOP_PATHCONF path 16 time(s); on a non-DEBUG
    kernel the unlocked-VOP races silently -- check serial console for a vnlru-reclaim UAF panic.
RUN_EXIT=0
--- guest alive ---
 1:06AM  up 25 mins, 0 users, load averages: 0.00, 0.00, 0.00

(stress run /boot 512 with vnode churn: ok=512/512, STRESS_EXIT=0, guest up; dmesg post-trigger: no vnode/lock/witness/panic warnings)

PoC changes

Rewrote expub.c substantially. The original passed a bare struct export_args (384 B) as the mount(2) data, but ffs_mount does copyin(data,&args,sizeof(struct ufs_args)) with struct ufs_args=448 B ({char *fspec; struct export_args export;}), so the original fed misaligned garbage and never reached the export path. The rewrite: (1) uses correct struct ufs_args (#include ), fspec=NULL, .export.ex_flags=MNT_EXPORTED|MNT_EXPUBLIC, .export.ex_indexfile="index.html"; (2) issues an UPDATE mount (MNT_UPDATE) of an already-mounted ufs fs (/boot), the only ffs_mount path that processes export args (ffs_vfsops.c:187,270-273); (3) clears the public export with MNT_DELEXPORT between iterations so the singleton nfs_pub (vfs_subr.c:2246) can be re-set; (4) loops and reports how many times the buggy path was reached. Added build.sh, run.sh, VERDICT.md, refreshed README.md, build.log, run.log, run.stress.log, dmesg.txt, env.txt, manifest.json, fix.diff.

Verified recommended fix

Keep rvp referenced+locked across vn_get_namelen() and vput exactly once on every path (success and each error). Concretely in vfs_setpublicfs (vfs_subr.c:2255-2295): (a) on the VFS_VPTOFH error at :2259 add vput(rvp) before return (closes the refcount leak); (b) remove the unconditional vput(rvp) at :2261 and instead vput once on the success path after the indexfile block, and convert the two error returns inside the indexfile block (:2270, :2288) to goto out; add an out: label that does vput(rvp); return error (closes the UAF/unlocked-VOP). fix.diff implements exactly this; git apply --check passes. Matches the finding proposal's intent (the proposal sketched the same goto-out shape); the verified diff makes the hunk line-numbers exact against master and adds the missing vput on the VPTOFH-error path explicitly.

Verdict

NOT REPRODUCED as an observable fault on this kernel, but the defect is REAL, REACHABLE, and the vulnerable code path is CONFIRMED to execute on the audited master DEV kernel. This is NOT a false positive and NOT already-fixed: master DEV (committed 2026-06-22, blame 6cc80ee9) contains the exact buggy lines unchanged. The vulnerable sequence VFS_ROOT(rvp ref'd+locked, vfs_subr.c:2255 via ufs_vfsops.c:58->vget) -> VFS_VPTOFH -> vput(rvp) (vfs_subr.c:2261; vput=vn_unlock+vrele, vfs_lock.c:703, drops BOTH lock and ref) -> vn_get_namelen(rvp)->VOP_PATHCONF(rvp) (vfs_subr.c:2269/2552) on the now-unlocked/unref'd rvp ran end-to-end 520/520 times across my runs with NO panic. No panic because: (1) the X86_64_GENERIC kernel has NO INVARIANTS/WITNESS (sysctl kern.conftxt confirms) to catch the unlocked-VOP_PATHCONF lock-protocol violation; (2) the UFS mount-root vnode retains references through the race window so vnlru never reclaims it (the actual UAF-free never occurs on this vnode). The deterministic refcount-leak variant (:2259, VFS_VPTOFH error return without vput) needs a filesystem whose vptofh fails, but UFS ffs_vptofh (ffs_vfsops.c:1228) always returns 0, and none of the export-capable fs types on this guest (hammer2/devfs/ufs/null/tmpfs) have a failing vptofh; procfs lacks vptofh (vfs_stdvptofh->EOPNOTSUPP, vfs_default.c:1562) but does not process export args, so the leak variant is unreachable here. On a DEBUG/INVARIANTS kernel this panics in VOP_PATHCONF on the unlocked vnode; on a default GENERIC kernel it races silently. The fix.diff is warranted.