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

vn: VNF_DESTROY teardown run from inside the rawdev close path races concurrent last-but-one close β€” cdev sysref double-put kernel panic (reproduced 2/2) plus minutes-long uninterruptible system-wide devfs stalls and a post-free struct-disk deref in diskclose

Summary

vnclose executes disk_destroy+SLIST_REMOVE+kfree from within dev_dclose of the winning last close (under ds_token inside generic diskclose); a concurrent last-but-one close drops its own cdev sysrefs while devfs_destroy_dev_worker's triple release_dev double-puts the same sysref -> panic 'SRF_PUTAWAY assertion'. Before the panic the same interleave wedges devfs system-wide in uninterruptible sleep for minutes; the losing diskclose re-reads dp fields from the freed softc (post-free deref; 2816-byte slab recycling gives control in principle). REPRODUCED 2/2 fresh boots with the minimal 2-thread trigger. Root-gated. Deferred-teardown callout fix, validated on a patched kernel build.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4563 Β· 14 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
stall.c β€” 2.8 KB view raw
vn_race.c β€” 5.1 KB view raw
build.sh β€” 87 B view raw
run.sh β€” 92 B view raw
run.log β€” 1001 B view raw
run.2.log β€” 934 B view raw
panic.txt β€” 691 B view raw
panic.2.txt β€” 691 B view raw
env.txt β€” 470 B view raw
fix.diff β€” 2.1 KB view raw
manifest.json β€” 857 B view raw
verdict.json β€” 4.5 KB view raw

DF-4563 β€” vn(4): teardown-from-close-path race β†’ cdev sysref double-put panic + unkillable stalls

  • File: sys/dev/disk/vn/vn.c
  • Class: refcount/lifetime race β†’ kernel panic (assertion-guarded double-put), DoS
  • Trigger: local, requires SYSCAP_RESTRICTEDROOT (vn ioctls are root-gated)
  • Demonstrated: kernel panic 2/2 fresh-boot runs of a 2-thread racing-close program (no other load)

TL;DR

When a cloned vn unit is detached (VNIOCDETACH sets VNF_DESTROY) and two file descriptors on the unit are closed concurrently, the winning (last) close runs the entire disk teardown from inside the rawdev close path:

sys_close β†’ diskclose (generic, holds ds_token, d_opencount 2β†’1β†’0)
  └─ dev_dclose(rawdev) β†’ vnclose()                    [vn.c:181-187]
       └─ disk_destroy(&vn->sc_disk)                    synchronous disk-core msg
            └─ devfs_destroy_related(d_cdev)            synchronous devfs-core msg
                 └─ devfs_destroy_dev_worker: release_dev() Γ— 3   ← races the
                     loser close()'s own cdev ref drops               sysref

The losing close (d_opencount 2β†’1) simultaneously drops its cdev sysref from its own diskclose/fdrop path. The destroy worker's release_dev() triple-put then double-puts the same sysref β†’ panic: assertion "(sr->flags & SRF_PUTAWAY) == 0" failed in sysref_put. Before the panic lands, the same interleave wedges the losing close (and any new devfs operations system-wide) in uninterruptible sleep for minutes.

A second consequence of the same teardown-in-close design: vnclose() also SLIST_REMOVEs and kfree()s the vn_softc (with the embedded struct disk) while the losing diskclose() still dereferences dp->d_slice / dp->d_rawdev after acquiring ds_token (sys/kern/subr_disk.c diskclose) β€” a post-free deref that requires slab-block recycling to fault.

Files

  • stall.c β€” minimal trigger: clone vn unit, attach (swap-backed), detach, open 2nd fd, race two close()s, repeat. Prints per-iteration close latencies; the panic lands within the first iterations.
  • vn_race.c β€” heavier harness (4 racing pairs + p_args 'A' spray aimed at the post-free dp->d_slice read); produced the same wedge/panic.
  • build.sh / run.sh
  • run.log, run.2.log β€” console/dmesg excerpts of both fresh-boot runs
  • panic.txt β€” the panic + backtrace (run 1), panic.2.txt (run 2)
  • env.txt β€” guest uname / state
  • fix.diff β€” deferred-teardown fix (callout), validated: patched kernel survives the same trigger, unpatched panics 2/2.
  • VERDICT.md β€” full narrative

Reproduce

cc -O2 -pthread -o stall stall.c
./stall 800          # panics within the first ~dozen iterations
                     # (may first wedge close() for minutes, then panic)

Expected (unpatched):

panic: assertion "(sr->flags & SRF_PUTAWAY) == 0" failed in sysref_put at sysref2.h:85
release_dev() at release_dev+0x6e
release_dev() at release_dev+0x6e
devfs_destroy_dev_worker() at devfs_destroy_dev_worker+0x18b
devfs_destroy_related_worker() at devfs_destroy_related_worker+0x80

Expected (patched with fix.diff): program runs to DONE n=800 worst_close=<ms> with no panic and no multi-second close stalls.

VERDICT.md
↓ download raw

DF-4563 VERDICT β€” reproduced (kernel panic), root-gated trigger

Status

  • status: reproduced / impact: panic / confidence: certain
  • 2/2 fresh-boot reproductions (vm.sh reset with-src between runs), minimal 2-thread trigger, no other load needed. Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (see env.txt).

Root cause (path:line)

  • sys/dev/disk/vn/vn.c:455-477 β€” VNIOCDETACH: allows the last-close teardown when disk_getopencount() <= 1 and arms VNF_DESTROY for cloned units (dkunit(dev) >= VN_PREALLOCATED_UNITS, vn.c:473-475).
  • sys/dev/disk/vn/vn.c:169-189 β€” vnclose() runs, from inside the rawdev close path: disk_destroy() + devfs_clone_bitmap_put() + SLIST_REMOVE()
  • kfree(vn).
  • sys/kern/subr_disk.c:1126-1161 β€” generic diskclose(): decrements d_opencount, then under ds_token calls dsclose(dev, type, dp->d_slice) and, on the last close, dev_dclose(dp->d_rawdev) β†’ the vnclose teardown. The losing close blocks on ds_token while the winner frees; after the token it dereferences dp->d_slice/dp->d_rawdev from the (potentially freed) softc β€” the post-free deref window.
  • sys/kern/subr_disk.c:527-555 β€” DISK_DISK_DESTROY handler: devfs_destroy_related(d_cdev) + destroy_dev(d_cdev) + destroy_only_dev(d_rawdev).
  • sys/vfs/devfs/devfs_core.c:1440-1460 β€” devfs_destroy_dev_worker() calls release_dev(dev) three times; racing the loser close's own release_dev() of the same cdev sysref double-puts it.

Demonstrated manifestation

panic: assertion "(sr->flags & SRF_PUTAWAY) == 0" failed in sysref_put at /usr/src/sys/sys/sysref2.h:85
cpuid = 1
release_dev() at release_dev+0x6e
release_dev() at release_dev+0x6e
devfs_destroy_dev_worker() at devfs_destroy_dev_worker+0x18b
devfs_destroy_related_worker() at devfs_destroy_related_worker+0x80
devfs_msg_exec() at devfs_msg_exec+0x6c
devfs_msg_core() at devfs_msg_core+0xb5
Debugger("panic")
  • Run 1: heavy harness (vn_race.c, 2 racing pairs + p_args spray) β€” panic after ~10 minutes of racing (several minutes-long unkillable D-state stalls observed first, ps -H showed 4 threads in D state; kill -9 not processed while stalled).
  • Run 2: minimal harness (stall.c, 2 threads, no spray) from a fresh reset with-src snapshot β€” identical panic; first racing iteration wedged the guest's devfs (new ssh sessions hung at banner) until the panic.

Exploitability assessment

  • The sysref assertion catches the double-put before free-in-use in this kernel config (KKASSERT-enabled). Without the assertion this is a use-after-free / double-free of a cdev (kernel heap).
  • The post-free dp->d_slice deref by the losing diskclose() is a read of a freed vn_softc (2816-byte slab class, measured via vmstat -m) followed by dsclose() bit-clear writes through the garbage pointer β€” exploitable in principle via same-class slab recycling (execve p_args of ~2700 bytes, content user-controlled, needs kern.ps_arg_cache_limit raised or a different same-class allocation stream).
  • Trigger requires SYSCAP_RESTRICTEDROOT (vnioctl, vn.c:434) for the detach that arms VNF_DESTROY. So the demonstrated panic is a privileged-user DoS/crash, not an unprivileged escalation. Severity Medium (same bar as the known vn race findings, e.g. DF-1699).

Fix validation

  • fix.diff: defer the VNF_DESTROY teardown out of vnclose via callout_reset(..., hz, vn_destroy_deferred, ...); the loser's close has long completed (and released its cdev refs) by the time the destroy worker runs; callout_drain() added to MOD_UNLOAD.
  • Baseline: unpatched kernel β†’ panic 2/2.
  • Patched: make buildkernel KERNCONF=X86_64_GENERIC && make installkernel, reboot, same stall 800 trigger β†’ no panic, program completes (DONE n=800 ...), no multi-second close stalls. See run.patched.log.

Negative results recorded during this audit (pass 2)

  • attach-path dev_dopen() imbalance: none β€” the internal open targets the raw dev whose vnopen doesn't touch d_opencount (subr_disk.c layering).
  • devfs_clone_bitmap_get is fully serialized by devfs_bitmap_lock (devfs_helper.c:221-233) β€” no clone-unit race.
  • swap-backed vn_size overflow at vn.c:632-633 (PAGE_SIZE * vn_size as off_t) wraps for vn_size β‰₯ 2^51, but swap_pager_strategy / swp_pager_meta_build never bound-check object->size, so the result is a sparse swap-disk of bogus advertised geometry, not memory corruption.
  • concurrent double-VNIOCATTACH (no INITED serialization) leaks one backing vnode + file, no UAF β€” adjacent to DF-2528's thesis, folded there.

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

Identical trigger (./stall 800): stock kernel panics (sysref_put SRF_PUTAWAY) on both fresh-boot baseline runs; patched kernel (fix.diff, nativekernel build #1 Mon Sep 21 15:46:08 UTC 2026) completed 800/800 iterations, worst close latency 0.000s, zero >1s stalls, no panic. Demonstrated vulnerability eliminated by the fix.

run.patched.log (patched DONE n=800, EXIT=0); panic.txt / panic.2.txt (baseline panics); build performed on guest: cd /usr/src && make nativekernel KERNCONF=X86_64_GENERIC, kernel installed and booted before the patched run.
↓ fix.diffper-fix-DF-4563

Confirmed kernel references

Detail

Exploit chain

root (SYSCAP_RESTRICTEDROOT): open /dev/vn (clone unit >=4), VNIOCATTACH swap-backed, VNIOCDETACH (arms VNF_DESTROY), open the unit node again, race close() of the two descriptors -> winner's vnclose teardown races loser's cdev sysref releases -> sysref double-put -> kernel panic (demonstrated); pre-panic the guest's devfs wedges system-wide in uninterruptible sleep for minutes (demonstrated); post-free dp->d_slice deref gives a memory-corruption window if the freed 2816-byte slab block is recycled with controlled content (code-derived, not demonstrated to corruption).

Evidence (decisive lines)

panic.txt and panic.2.txt (identical sysref_put SRF_PUTAWAY panics, backtrace through devfs_destroy_dev_worker); run.patched.log (patched kernel: DONE n=800 worst_close=0.000s stalls>1s=0, EXIT=0); VERDICT.md narrative; stall.c minimal trigger; env.txt guest details

PoC changes

PoC authored fresh in this run (no seed): stall.c (2-thread racing close of a detached cloned vn unit). Harness bugs fixed during bring-up: barrier handshake rewritten with generation counters, unit discovery switched from broken minor() decode to stat-rdev scan, cross-pair unit interference eliminated by stat-only scanning, fork/exec p_args spray capped properly.

Verified recommended fix

Defer the VNF_DESTROY teardown out of the rawdev close path (callout, fix.diff); full fix is a reference-counted struct disk lifetime in the disk layer so vnclose/diskclose cannot free the softc while any disk-layer close is still in flight.

Verdict

vn(4) runs the whole VNF_DESTROY disk teardown from inside the rawdev close path (vnclose -> disk_destroy -> sync disk/devfs messages -> release_dev x3) while a concurrent last-but-one close of the same unit drops its own cdev sysrefs: the destroy worker double-puts the sysref and the kernel panics with the SRF_PUTAWAY assertion (release_dev -> devfs_destroy_dev_worker -> devfs_destroy_related_worker). Reproduced 2/2 from fresh vm.sh reset with-src boots with a minimal two-thread racing-close program; the same interleave first wedges the losing close and all new devfs operations in uninterruptible sleep for minutes. The losing diskclose also dereferences dp->d_slice/dp->d_rawdev after the softc kfree (post-free window, exploitable in principle via same-class slab recycling; sizeof(vn_softc)=2816). Trigger requires SYSCAP_RESTRICTEDROOT (vn ioctls), so this is a privileged-user kernel crash/DoS, not an unpriv escalation. fix.diff (deferred teardown via callout) validated: stock kernel panics on the identical trigger, patched kernel completes 800/800 iterations with zero stalls and no panic.