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)
PoC verification
Evidence pack
findings/poc/DF-2923 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
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_testablenot_testable: race never landed on the baseline kernel within budget, so a patched comparison has nothing to diff against.
README.md result section
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.
No comments yet.