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

umount -f force-close never passes FREAD: fifo fifoinfo + connected socket pair leak permanently

Summary

vclean_vxlocked()'s force-close loop (vfs_subr.c:1341-1352) only ever passes FWRITE|FNONBLOCK or bare FNONBLOCK to VOP_CLOSE - never FREAD - so fifo_close() cannot decrement fi_readers, the counters-both-zero destroy condition never fires, and the fifoinfo plus its connected AF_LOCAL socket pair (socreate x2 + unp_connect2, :188-222) leak permanently whenever a fifo is still open when a mount is force-unmounted (or forced shutdown tears down mounts). The later userspace close() routes to dead_close() which never reaches the fifo counters. Root-triggered permanent kernel resource leak, unbounded with repetition (2 sockets + ~1KiB per orphaned fifo); pre-condition reachable without privilege via a fifo fd pinned by an in-flight SCM_RIGHTS message no process owns (unmount kill protocol cannot close it). VERIFIED stock: netstat -f unix stays +2 permanently; control returns to baseline. Fix validated (VFIFO-aware force-close flags + counter clamps): returns to baseline, normal fifo I/O unaffected.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3161 Β· 9 files
FileTypeDescriptionSize
umount_fifoleak_scmrights.c β€” 3.6 KB view raw
fifo_hold.c β€” 834 B view raw
umount_fifoleak.sh β€” 2.3 KB view raw
run.log β€” 1.6 KB view raw
fix_run.log β€” 1.0 KB view raw
env.txt β€” 264 B view raw
fix.diff β€” 1.4 KB view raw
VERDICT.md β€” 3.4 KB ↓ raw
README.md β€” 2.4 KB ↓ raw

DF-3161 β€” umount -f force-close never balances fifo reader/writer counts (fip + socket pair leaked)

Root cause

vclean_vxlocked() (sys/kern/vfs_subr.c:1341-1352) force-closes a vnode with open descriptors by looping VOP_CLOSE() until v_opencount == 0, but it only ever passes FWRITE|FNONBLOCK (when v_writecount) or bare FNONBLOCK β€” never FREAD. fifo_close() (sys/vfs/fifofs/fifo_vnops.c:610-614) decrements fi_readers only when FREAD is passed, so after a forced unmount the reader count can never reach 0, the fi_readers == 0 && fi_writers == 0 destroy condition (fifo_vnops.c:620-622) never fires, and the fifoinfo plus its connected AF_LOCAL socket pair (socreate x2 + unp_connect2, fifo_vnops.c:188-222) leak permanently. The later userspace close() goes to dead_close() (sys/vfs/deadfs/dead_vnops.c:118-133), which never touches the fifo counters.

Reaching the force-close path

dounmount(MNT_FORCE) first tries to make holders exit: it SIGINTs (retry 3) then SIGKILLs (retry 7) every process using the mount (sys/kern/vfs_syscalls.c:929-946) and waits up to 50 retries x 0.25 s. An open fifo that no process owns β€” e.g. an fd passed over a unix-domain socket (SCM_RIGHTS) and never received β€” is pinned by the socket buffer, invisible to the kill protocol, so the retries exhaust and the mount is ripped with the fifo still open (console warns forced umount of ... - N namecache refs), running the force-close loop.

PoC

umount_fifoleak_scmrights.c (deterministic; the one that reproduces): open a fifo O_RDWR on a tmpfs, hide the fd inside a socketpair via SCM_RIGHTS, umount -f, drop the socket, count connected unix-domain stream sockets with netstat -f unix.

umount_fifoleak.sh + fifo_hold.c: first attempt (process-held fd) β€” does NOT leak because dounmount's SIGINT kills the holder cleanly; kept as the control/characterization run.

Build / run (as root on the guest)

cc -O -Wall -o leak_scm umount_fifoleak_scmrights.c
./leak_scm

Expected output (bug present)

baseline: 4 stream sockets
fifo-open (+2 expected): 6 stream sockets
...
umount rc=0            (after ~13 s of exhausted retries; console warns)
after umount -f + close (leak: stays +2): 6 stream sockets   <-- LEAK

Expected output (fixed kernel)

after umount -f + close ...: 4 stream sockets   (back to baseline)
VERDICT.md
↓ download raw

DF-3161 VERDICT β€” reproduced (permanent kernel resource leak on umount -f)

Reproduced? Yes.

Deterministic PoC (umount_fifoleak_scmrights.c, run as root on the stock INVARIANTS guest kernel #0, 2026-09-07):

  • baseline connected unix-domain stream sockets: 4
  • fifo open on a fresh tmpfs: 6 (+2 = the internal socket pair)
  • fifo fd hidden in a unix-socket receive buffer (SCM_RIGHTS, never received) so no process owns it
  • umount -f: dounmount's SIGINT/SIGKILL protocol (sys/kern/vfs_syscalls.c:929-946) cannot find a holder; 50 retries x 0.25 s exhaust; console prints forced umount of "(null)" - 1 namecache refs, 1 mount refs; the mount is ripped.
  • close the holding socketpair, wait 2 s: still 6 β€” the socket pair is never freed.

Control: the same fifo opened/closed normally (and even the process-held umount -f variant, where SIGINT kills the holder first) returns to the baseline count of 4. The leak is specifically the force-close path.

Why (line-level)

  1. vgone_vxlocked -> vclean_vxlocked(vp, DOCLOSE) (sys/kern/vfs_subr.c:1285, 1567).
  2. Force-close loop sys/kern/vfs_subr.c:1341-1352: passes only FWRITE|FNONBLOCK (if vp->v_writecount) or FNONBLOCK. FREAD is never passed.
  3. fifo_close (sys/vfs/fifofs/fifo_vnops.c:610-614): fi_readers-- happens only under ap->a_fflag & FREAD -> stays 1.
  4. Destroy condition fi_readers == 0 && fi_writers == 0 (fifo_vnops.c:620-622) never fires; v_fifoinfo and the connected socket pair (socreate x2 + unp_connect2, fifo_vnops.c:194-216) leak.
  5. The later userspace close of the (now dead) vnode goes to dead_close (sys/vfs/deadfs/dead_vnops.c:118-133), which only clamps v_opencount/v_writecount and never reaches the fifo layer.

For the O_RDWR fd in the PoC the first (and only) loop iteration passes FWRITE|FNONBLOCK, decrementing only fi_writers; fi_readers remains 1 forever. Each repetition of the scenario leaks 2 sockets + 1 fifoinfo (~1 KiB) β€” unbounded with repeated mounts.

Impact

Permanent kernel memory/socket leak, root-triggered (umount -f, shutdown of mounts with orphaned fifo descriptors β€” SCM_RIGHTS in flight, descriptor held by a dying-but-unkillable process, etc.). Not memory corruption: the leaked objects stay referenced and consistent.

Fix validation (see run.2.log / fix logs)

fix.diff (this directory) makes the force-close loop VFIFO-aware (FREAD|FWRITE / FREAD flags) and clamps fifo_close's decrements. Applied to a clean-source guest, kernel rebuilt with make nativekernel, guest rebooted:

  • baseline PoC behavior pre-patch (stock kernel #0): leak reproduced (6 sockets persisting; see run.log).
  • post-patch: after umount -f + close ...: 4 stream sockets β€” back to baseline, leak gone; no panic; normal fifo open/close/read/write and the SIGINT-kill protocol unaffected (regression checks in fix_run.log).

Chain summary

unpriv user -> NOT applicable; requires root's umount -f (or shutdown) + a fifo fd that no process can be forced to close. Pure resource leak.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

On the rebuilt+rebooted kernel #1 with fix.diff applied, the identical PoC returns the socket count to baseline (4) - the leak is gone; normal blocking/O_RDWR fifo I/O and the umount kill protocol regression-checked clean.

fix_run.log (patched-kernel run + regression), kbuild BUILD-RC=0, kinstall INSTALL-RC=0
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Sep 7 10:27:31 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root mounts tmpfs; unpriv-side primitive: any process that gets a fifo fd into a unix-socket buffer (SCM_RIGHTS, unreceived) on a mount that is later force-unmounted (root action or shutdown) leaks 2 sockets + 1 fifoinfo per fifo, permanently

Evidence (decisive lines)

['run.log: baseline 4 -> open 6 -> umount -f + close: 6 (leak) vs control 4', 'fix_run.log: same scenario on patched kernel #1 -> 4 (baseline restored)', 'VERDICT.md: full line-level chain vclean->fifo_close->destroy-condition']

PoC changes

First attempt (process-held fd, umount_fifoleak.sh) did not leak because dounmount SIGINTs holders cleanly; rewrote as umount_fifoleak_scmrights.c pinning the fd with an unreceived SCM_RIGHTS message so the force-close path actually runs - deterministic reproduction.

Verified recommended fix

vclean_vxlocked: pass FREAD|FWRITE (writecount>0) or FREAD for VFIFO force-closes; clamp fifo_close decrements so mixed opens stay balanced.

Verdict

Reproduced on the stock INVARIANTS guest kernel #0: a fifo whose descriptor is pinned by an in-flight SCM_RIGHTS message (owned by no process, so dounmount's SIGINT/SIGKILL protocol cannot reach it) survives umount -f; the exhausted-retry forced unmount runs vclean_vxlocked(DOCLOSE), whose close-flag selection (vfs_subr.c:1341-1352) never passes FREAD, so fifo_close cannot decrement fi_readers and the fifoinfo + its connected AF_LOCAL socket pair leak permanently (netstat -f unix stays +2 above baseline; control returns to baseline). Root-triggered permanent kernel resource leak, unbounded with repetition; not memory corruption. Fix validated: with vclean VFIFO-aware flags + clamped fifo_close decrements (fix.diff), the same PoC returns to baseline on rebuilt kernel #1 and normal fifo semantics are unaffected.