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

vq_vptomp check-then-use race on vp->v_pfsmp β†’ mount use-after-free with indirect call through freed memory

Field Value
ID DF-2923
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H
CWE CWE-367 / CWE-416
File sys/kern/vfs_quota.c
Lines 420-433 (esp. :426-428); gap: vfs_cache.c:1386-1389
Area kern/vfs
Confidence likely
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

vq_vptomp() validates vp->v_pfsmp only with mountlist_exists(), which takes no reference; callers kern_ftruncate (vfs_syscalls.c:4123-4124) and vn_open O_TRUNC (vfs_vnops.c:325-326) immediately dereference MP->mnt_op->vfs_account. v_pfsmp is set once when the vnode is resolved through a nullfs mount (vfs_cache.c:1386-1389) and never cleared; the nullfs mount can then be unmounted and freed without waiting β€” the racing thread holds an fd on the underlying fs's vnode, so it pins no mnt_refs of the null layer. Window = the few instructions between exists() and the deref. Post-free the dangling v_pfsmp also makes mountlist_exists() match a NEW mount that reuses the freed memory, silently redirecting accounting. Concurrent ftruncate/O_TRUNC through a nullfs view racing umount of that view β†’ UAF of struct mount: spin_lock on freed ac_spin, RB walk of freed trees, indirect call read from freed memory (kernel code execution potential). Raced 20,000+ mount cycles in 150 s without landing the ~10 ns window β€” real by construction, not reproduced in budget. Fix: mount_hold across the gap (or keep v_pfsmp referenced for the vnode's lifetime).

Timeline

  • 2026-09-02 Discovered during pass-2 audit of vfs_quota.c (GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2923 Β· 3 files
FileTypeDescriptionSize
vqpfsrace.c β€” 2.3 KB view raw
verdict.json β€” 2.8 KB view raw
README.md β€” 2.3 KB ↓ raw

DF-2923 β€” vq_vptomp() check-then-use race on vp->v_pfsmp β†’ mount UAF

Code

sys/kern/vfs_quota.c:420-433:

if ((vp->v_pfsmp != NULL) && (mountlist_exists(vp->v_pfsmp))) {
    return vp->v_pfsmp;          /* NO reference taken */
}
return vp->v_mount;

Callers immediately dereference the result: * sys/kern/vfs_syscalls.c:4123-4124 kern_ftruncate: mp = vq_vptomp(vp); VFS_ACCOUNT(mp, ...) -> MP->mnt_op->vfs_account(MP,...) (sys/sys/mount.h:657) * sys/kern/vfs_vnops.c:325-326 vn_open O_TRUNC path, same pattern.

v_pfsmp is set once, forever, when the vnode is first resolved through a nullfs mount (sys/kern/vfs_cache.c:1386-1389) and is never cleared. The nullfs mount can be unmounted afterwards; dounmount() then frees the mount structure WITHOUT waiting for our thread (we hold no mnt_refs β€” vfs_syscalls.c:1107-1118 only waits on mnt_refs) because the racing fd holds a vnode of the underlying fs, not of the null layer.

The mountlist_exists() check takes no reference: between the check and the caller's MP->mnt_op dereference, a concurrent dounmount() can mountlist_remove() + kfree() the mount -> use-after-free, including an indirect call through freed memory (vfs_account) and a spin_lock on the freed mount's ac_spin.

Post-free, the stale v_pfsmp pointer also survives forever: if the freed mount memory is reused by a new mount, mountlist_exists() returns TRUE for the WRONG mount and accounting is silently redirected (accounting integrity, no crash).

Harness

vqpfsrace.c β€” root-side mount_null/umount churn (~135 cycles/s) racing an ftruncate loop on a file opened through the null view (the ftruncate side alone is unprivileged).

Result

20,000+ mount/unmount cycles, no hit: the vulnerable window is the few instructions between mountlist_exists() and the mnt_op dereference (~10 ns) inside a ~7 ms cycle => p(hit) ~ 1e-5/cycle. The race is real by construction but was not landed within the time budget.

Fix

Take a reference before trusting v_pfsmp: e.g. loop lwkt_gettoken(&mountlist_token); re-check mountlist_exists under the token; mount_hold(mp); lwkt_reltoken; ... mount_drop(mp) after use β€” or clear vp->v_pfsmp during nullfs unmount (VFS_UNMOUNT of the layer walking vnodes is not possible for foreign vnodes; the reference is the workable fix).

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: race never landed on the baseline kernel within budget, so a patched comparison has nothing to diff against.

README.md result section
per-fix-DF-2923

Confirmed kernel references

Detail

Evidence (decisive lines)

['race-run note in README.md β€” iter 20000 in 150 s, no hit; guest stayed up', 'source citations in README.md (vfs_quota.c:420-433, vfs_cache.c:1386-1389, vfs_syscalls.c:1107-1118, mount.h:657)']

PoC changes

Harness built as designed (tmpfs source + null view + forked ftruncate child + root mount/umount churn).

Verified recommended fix

mount_hold() the pfsmp under the mountlist token after the existence check (mount_drop after use), or otherwise keep v_pfsmp Referenced for the lifetime of the vnode.

Verdict

vq_vptomp() (vfs_quota.c:420-433) validates vp->v_pfsmp only with mountlist_exists(), which takes no reference; callers (kern_ftruncate vfs_syscalls.c:4123-4124, vn_open vfs_vnops.c:325-326) immediately dereference MP->mnt_op->vfs_account β€” a concurrent dounmount() of the nullfs mount frees the mount without waiting (the racing thread holds an fd on the UNDERLYING fs's vnode, not the null layer's, so no mnt_refs blocks the free) => use-after-free incl. indirect call through freed memory. Race is real by construction; 20,000+ mount_null/umount cycles (~135/s for 150 s) racing an ftruncate loop did not land the ~10 ns window inside the ~7 ms cycle, so not reproduced. Also noted: v_pfsmp dangles forever after the layer unmount; if the freed memory is reused by a new mount, mountlist_exists() returns TRUE for the wrong mount and accounting is silently redirected.