nfs_unmount() continues teardown after vflush(FORCECLOSE) fails on a busy device vnode: frees the nfsmount and destroys the nfsnode objcache with a live nfsnode (UAF condition, allocator-confirmed) and dounmount() then panics deterministically ('unmount: dangling vnode')
Summary
vflush(mp,1,FORCECLOSE) can fail: vflush_scan() clears FORCECLOSE for VCHR/VBLK vnodes and counts them busy (vfs_mount.c:1145-1146/1194), returning EBUSY whenever a device vnode on the NFS mount still has VREFCNT>1. nfs_unmount() treats FORCECLOSE failure as 'continue anyway' (:1218-1224) and completes the teardown - nfs_free_mount() does kmalloc_destroy_obj(&nmp->nm_mnode) and kfree(nmp) while the surviving device vnode and its nfsnode are still alive. dounmount() then panics unconditionally because the vnode never left mnt_nvnodelist (vfs_syscalls.c:1085-1086). VERIFIED on guest: 'vflush: Warning, cannot destroy busy device vnode', 'NFS inodes: slab ... 1 objects were still allocated', then 'panic: unmount: dangling vnode' at dounmount+0x87c. The device vnode comes from the NFS server (LOOKUP/GETATTR attrs type=NF3CHR - the DF-2998 server-retyping surface) or any device node on the export. Any unpriv local user primes the state permanently: open the device node, park the fd in a unix-socket SCM_RIGHTS receive buffer - the fd belongs to no process so dounmount's kill-retry loop finds nobody to kill. Root's routine 'umount -f' (the documented remedy for a stale/hung NFS mount - precisely what a hostile server creates) then panics after freeing live kernel state. A blocked-in-NFS holder does NOT work (nfs_sigintr aborts all in-flight RPCs once MNTK_UNMOUNTF set) - SCM_RIGHTS parking is the robust primitive. Impact honestly bounded at deterministic panic/local DoS: UAF window real (allocator-confirmed) but panic-gated in-syscall before another thread can act on the freed nmp. Fix validated in-guest (return vflush error unconditionally + clear NFSMNT_FORCE so the surviving mount is not wedged): same PoC returns EBUSY, guest alive, mount recoverable.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3073 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.1 KB | β raw | |
| VERDICT.md | β | 5.8 KB | β raw | |
| fakesrv2.c | β | 15.0 KB | view raw | |
| holder.c | β | 1.6 KB | view raw | |
| holder2.c | β | 2.8 KB | view raw | |
| build.sh | β | 218 B | view raw | |
| run.sh | β | 1.8 KB | view raw | |
| build.log | β | 260 B | view raw | |
| run.log | β | 2.4 KB | view raw | |
| run.fixed.log | β | 2.0 KB | view raw | |
| panic.txt | β | 991 B | view raw | |
| fixed.console.txt | β | 946 B | view raw | |
| env.txt | β | 1.0 KB | view raw | |
| fix.diff | β | 438 B | view raw | |
| verdict.json | β | 6.1 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw |
DF-3073 β nfs_unmount frees the nfsmount after a failed forced vflush (busy device vnode) β use-after-free + deterministic "unmount: dangling vnode" panic
What
sys/vfs/nfs/nfs_vfsops.c:nfs_unmount() (lines 1217-1224): when
vflush(mp, 1, flags) fails, NFS continues tearing down the mount anyway
if MNT_FORCE was requested ("If this doesn't work and we are doing a
forced unmount we continue anyway"). But vflush() can legitimately
fail even with FORCECLOSE: vflush_scan()
(sys/kern/vfs_mount.c:1145-1146) clears FORCECLOSE for VCHR/VBLK
vnodes and counts them busy (:1194), returning EBUSY whenever a device
vnode on the mount still has VREFCNT>1.
Continuing anyway means:
nfssvc_iod_stop1/2,nfs_disconnect, removal fromnfs_mountq, thennfs_free_mount(nmp)(nfs_vfsops.c:1232-1243) β which doeskmalloc_destroy_obj(&nmp->nm_mnode)andkfree(nmp, M_NFS)while the surviving device vnode and its nfsnode are still alive (nfs_free_mount,nfs_vfsops.c:1248-1262; allocator warning "NFS inodes: β¦ 1 objects were still allocated" on the INVARIANTS kernel). From this point any touch of that vnode (nfs_inactiveβlwkt_gettoken(&nmp->nm_token)atsys/vfs/nfs/nfs_node.c:383-387;nfs_reclaimβkfree_obj(np, nmp->nm_mnode)atnfs_node.c:439,485) is a use-after-free on the freednmpand on a destroyed malloc type (kmalloc_destroyfrees thestruct malloc_typeitself,sys/kern/kern_slaballoc.c:624-631).dounmount()then panics unconditionally because the vnode is still onmp->mnt_nvnodelist:panic("unmount: dangling vnode")(sys/kern/vfs_syscalls.c:1085-1086) β observed asdounmount+0x87c.
Threat model
- The device vnode itself comes from the NFS server (LOOKUP/GETATTR attributes type=NF3CHR with a chosen rdev β the DF-2998 server-side-retyping surface), or from a device node root created on the export. A hostile/compromised server controls this fully.
- Any unprivileged local user can hold the device vnode referenced
in a way no unmount cleanup can break: open the node, then park the fd
in a unix-socket SCM_RIGHTS receive buffer (
holder2.c). The fd then belongs to no process, sodounmount()'s kill-retry loop (unmount_allproc_cb/process_uses_mount, SIGINT retry 3 / SIGKILL retry 7,vfs_syscalls.c:768-947) matches nobody; the namecache ref and vnode ref persist indefinitely. - Root's routine
umount -fof a (e.g. stale) NFS mount then takes the machine down, after having freed the mount's kernel state out from under a live vnode.
Files
fakesrv2.cβ loopback fake rpcbind/mountd/NFSv3 server (TCP): serves a root dir withdev(VCHR, rdev 14:2 = /dev/null on the stock guest) andslow(VREG).holder.c(NFS-blocked variant) is kept for history;holder2.c(SCM_RIGHTS variant) is the one that works βnfs_sigintr()aborts every in-flight RPC the moment MNTK_UNMOUNTF is set (nfs_socket.c:2059-2066), so an NFS-blocked process is always released by umount -f.build.sh/run.shβ compile and three-phase run (control, EBUSY, panic).
Build
sh build.sh # cc -O2 -o fakesrv2 fakesrv2.c; cc -O2 -o holder2 holder2.c
Run (as root on the guest)
sh run.sh # phase A: umount -f with no holder -> clean (control) # phase B: holder2 + umount -> "Device busy" (control) # phase C: umount -f -> ~13s retry loop, then panic: # unmount(/mnt/nfsx): ... N namecache refs, N mount refs still present # vflush: Warning, cannot destroy busy device vnode # NFS inodes: slab ... 1 objects were still allocated # malloc_uninit: 576 bytes of 'NFS inodes' still allocated on cpu 6 # panic: unmount: dangling vnode # dounmount() at dounmount+0x87c # (ssh session dies; capture is on the serial console / vm.sh log)
Expected outcome
Stock kernel: deterministic kernel panic (panic.txt). Patched kernel
(fix.diff): phase C prints C_UMOUNT_FORCE_RC=1 (EBUSY), the guest
stays up, the mount stays usable, and after the holder releases
(kill holder2) umount -f succeeds normally.
DF-3073 VERDICT β nfs_unmount: use-after-free + deterministic panic after failed forced vflush
Bottom line
REPRODUCED (impact: panic β deterministic local kernel DoS with a proven
kernel use-after-free window immediately before the panic). On the stock
INVARIANTS kernel, umount -f of an NFS mount while a referenced device
vnode lives on the mount (held open by an unprivileged user via an
SCM_RIGHTS-parked descriptor, device node supplied by the NFS server) makes
nfs_unmount() free the nfsmount and destroy the nfsnode objcache with
the device vnode's nfsnode still allocated, after which dounmount()
panics unconditionally. The allocator's own
NFS inodes: ... 1 objects were still allocated warning on the console is
direct evidence of the lifetime violation; without INVARIANTS the same
sequence silently leaves a freed nmp behind a live vnode
(nfs_inactive/nfs_reclaim would then operate on freed memory:
sys/vfs/nfs/nfs_node.c:383-387 and :439,485), but the
panic("unmount: dangling vnode") at sys/kern/vfs_syscalls.c:1085-1086
fires microseconds later in the same syscall, so the realistic ceiling on
this path is a reliable panic rather than a controllable corruption.
Root cause chain (all source-cited, all observed on the guest)
dounmount()setsMNTK_UNMOUNTFand runs a 50Γ250ms kill-retry loop (vfs_syscalls.c:839-972) that SIGINTs (retry 3) / SIGKILLs (retry 7) every process matchingprocess_uses_mount()(:727-757). A descriptor parked in a unix-socket receive buffer belongs to no process, so nobody is killed; itsf_nchandlekeeps a namecache ref and a mount ref ("1 namecache refs, 1 mount refs still present" on the console).VFS_UNMOUNTβnfs_unmount()βvflush(mp, 1, FORCECLOSE).vflush_scan()clears FORCECLOSE for VCHR/VBLK vnodes (sys/kern/vfs_mount.c:1145-1146) and counts them busy (:1194) whenVREFCNT>1β the parked fp's vnode ref + the surviving ncp's ref give exactly that. Console:vflush: Warning, cannot destroy busy device vnode;vflushreturns EBUSY.nfs_unmount()ignores the failure because FORCECLOSE was requested (sys/vfs/nfs/nfs_vfsops.c:1218-1224"forced unmount we continue anyway") and completes the teardown:nfssvc_iod_stop1/2,nfs_disconnect, removal fromnfs_mountq, thennfs_free_mount(nmp)(:1232-1243) βkmalloc_destroy_obj(&nmp->nm_mnode)+kfree(nmp, M_NFS)(:1248-1262). The still-referenced device vnode keeps its nfsnode (allocated from that objcache) and itsv_mount->mnt_data== freednmp. Console:NFS inodes: slab ... 1 objects were still allocated+malloc_uninit: 576 bytes of 'NFS inodes' still allocated on cpu 6.- Back in
dounmount():panic("unmount: dangling vnode")(vfs_syscalls.c:1085-1086) because the vnode never leftmp->mnt_nvnodelist. Backtrace:dounmount+0x87cβsys_unmount.
Why the holder must not be NFS-blocked (design detail)
First attempt (holder.c) blocked the holder inside a withheld READ RPC on
a hard mount. That fails: nfs_sigintr() returns EINTR for every
request as soon as MNTK_UNMOUNTF is set
(sys/vfs/nfs/nfs_socket.c:2059-2066), the blocked process becomes
killable, retry-7 SIGKILL closes its fds, namecache refs drop, vflush
succeeds and umount -f returns 0 in ~2s (observed, run.log session 2).
The SCM_RIGHTS variant (holder2.c) holds no NFS state at all β only the
socket β and survives the entire kill-retry loop (12.5s, observed).
Reproduction
Baseline (stock kernel #0 Thu Jul 2 06:02:54 UTC 2026): see run.sh / run.log / panic.txt. Deterministic across the run (single decisive run after two control runs; the panic path is unconditional once vflush fails β no race involved).
Controls observed on the same setup:
- umount -f with no holder: clean unmount (RC=0).
- umount (no force) with holder: clean EBUSY, mount intact.
- umount -f with NFS-blocked holder: holder killed, clean unmount
(MNTK_UNMOUNTF aborts its RPC) β demonstrates the abort-everything
semantics that the SCM_RIGHTS variant sidesteps.
Exploitability assessment (honest)
- Deterministic: kernel panic, any user who can get a device-node fd
parked on an NFS mount + a root
umount -f. - The UAF window (freed
nmpbehind a live vnode) provably exists (the allocator reports the leaked-live object at destroy time), but the unconditionaldounmountpanic fires within the same syscall, in-line, before any other thread can act on the vnode (the vnode is VX-unlocked after the vflush scan, yet the panic check runs with onlyjournal_remove_all_journals/mountlist_remove/vfs_rm_vnodeopsin between). On this guest (UP-class INVARIANTS kernel) there is no realistic window to convert the free into controlled corruption before the panic; on an SMP box the theoretical window is the same few function calls. I therefore classify impact as panic (local DoS), not privesc. - Severity context: the unprivileged user only primes the state; the
final trigger is root's
umount -fβ the documented remedy for a stale NFS mount, i.e. an action an admin is more likely to take when a hostile server is involved.
Fix validation
fix.diff (in this pack): nfs_unmount() returns the vflush error
unconditionally and clears NFSMNT_FORCE on that path (the flag would
otherwise permanently wedge the still-mounted filesystem β
nfs_socket.c:200,1288).
Patched kernel (built in-guest: make nativekernel KERNCONF=X86_64_GENERIC
after applying fix.diff to /usr/src): the exact PoC rerun yields
umount -f β EBUSY, no panic, guest stays up, mount intact; after the
holder releases the parked descriptor, umount -f succeeds normally.
Baseline panic vs patched EBUSY captured in run.fixed.log /
fixed.console.txt.
Environment
See env.txt (guest uname, compiler, sysctls, listeners).
Fix verification
fixedPatched kernel built in-guest (make nativekernel KERNCONF=X86_64_GENERIC) with fix.diff applied. Exact PoC rerun: umount -f returns EBUSY after the ~14s retry loop, console shows 'vfs refused to unmount, error 16' with NO objcache-destroy-with-live-objects warning and NO panic; guest stays up; after the holder releases the parked descriptor umount -f succeeds cleanly (RC=0). Baseline panic is gone.
['run.fixed.log: UMNTF_RC=1 on patched kernel (baseline: panic), mount intact, clean unmount after holder release', "fixed.console.txt: 'vflush: Warning, cannot destroy busy device vnode' followed by 'unmount: vfs refused to unmount, error 16' - no 'NFS inodes ... still allocated', no panic", 'fix.diff: the one-hunk fix (return error + clear NFSMNT_FORCE)']
Confirmed kernel references
- sys/vfs/nfs/nfs_vfsops.c:1217-1224
- sys/vfs/nfs/nfs_vfsops.c:1232-1243
- sys/vfs/nfs/nfs_vfsops.c:1248-1262
- sys/kern/vfs_mount.c:1145-1146
- sys/kern/vfs_mount.c:1194
- sys/kern/vfs_syscalls.c:839-972
- sys/kern/vfs_syscalls.c:1085-1086
- sys/vfs/nfs/nfs_node.c:383-387
- sys/vfs/nfs/nfs_node.c:439,485
- sys/kern/kern_slaballoc.c:624-631
- sys/vfs/nfs/nfs_socket.c:2059-2066
Detail
Exploit chain
unprivileged user opens server-injected device node on an NFS mount -> parks fd in unix-socket SCM_RIGHTS receive buffer (no process 'uses' the mount; kill-retry loop finds nobody) -> root runs umount -f -> 50x250ms retry loop exhausts (namecache+mount refs held by the buffered fp) -> vflush(mp,1,FORCECLOSE) returns EBUSY because vflush_scan clears FORCECLOSE for VCHR and counts it busy -> nfs_unmount continues anyway: nfs_free_mount() kfrees nmp and destroys nm_mnode with the dev vnode's nfsnode still allocated (use-after-free condition; later nfs_inactive/nfs_reclaim on that vnode would deref freed nmp) -> dounmount panics 'unmount: dangling vnode'. Ceiling on this path: deterministic panic (the panic fires in the same syscall before the freed state can be acted on by another thread).
Evidence (decisive lines)
["panic.txt: console capture - 'vflush: Warning, cannot destroy busy device vnode' / 'NFS inodes: slab ... 1 objects were still allocated' / 'malloc_uninit: 576 bytes of NFS inodes still allocated' / 'panic: unmount: dangling vnode' / dounmount+0x87c <- sys_unmount", 'run.log: session 3 (decisive) - holder2 DEV_OPEN_OK/FD_PARKED as nobody, umount -f session dies at 23:26:19; sessions 1-2 are controls (clean umount -f without holder; NFS-blocked holder killed by MNTK_UNMOUNTF abort semantics)', "run.fixed.log + fixed.console.txt: patched kernel - same PoC gives UMNTF_RC=1 (EBUSY), guest alive, 'vfs refused to unmount, error 16', clean unmount after holder release", 'fakesrv2.c/holder2.c + build.sh/run.sh: exact reproducible sources and commands']
PoC changes
Rewrote the DF-3009 seed harness into a functioning NFSv3 server (fakesrv2.c): fixed a byte-swapped SunRPC record mark in send_record() (inherited from the DF-3009 seed - only its literal 0x80000000 mark was correct), added missing nfsstat words in ACCESS/FSSTAT/FSINFO/READDIR/READ replies, added LOOKUP serving a VCHR node with rdev 14:2 and an empty-dir READDIR(+). Replaced the seed's 'blocked holder' idea with holder2.c: a blocked-in-NFS holder cannot work because nfs_sigintr() aborts all RPCs once MNTK_UNMOUNTF is set; the working primitive is an SCM_RIGHTS-parked descriptor that no process owns.
Verified recommended fix
nfs_unmount(): on vflush() failure return the error unconditionally (also for MNT_FORCE) and clear NFSMNT_FORCE before returning - see fix.diff
Verdict
REPRODUCED on the stock INVARIANTS guest: nfs_unmount() ignores vflush() failure under MNT_FORCE and frees the nfsmount (destroying the nfsnode objcache) while a referenced VCHR vnode and its nfsnode are still alive - allocator warning 'NFS inodes: ... 1 objects were still allocated' - after which dounmount() panics unconditionally ('unmount: dangling vnode', dounmount+0x87c). The referenced device vnode is supplied by the NFS server (DF-2998 attribute surface) and held open by an unprivileged user via an SCM_RIGHTS-parked descriptor that dounmount's kill-retry loop cannot touch. Deterministic local kernel panic (root pulls the umount -f trigger); the UAF window provably exists but is panic-gated in-syscall, so realistic impact is DoS, not privesc. Fix validated: with nfs_unmount returning the vflush error and clearing NFSMNT_FORCE, the identical PoC yields EBUSY with the guest alive and the mount recoverable.
No comments yet.