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)
PoC verification
Evidence pack
findings/poc/DF-4563 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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-freedp->d_sliceread); produced the same wedge/panic.build.sh/run.shrun.log,run.2.logβ console/dmesg excerpts of both fresh-boot runspanic.txtβ the panic + backtrace (run 1),panic.2.txt(run 2)env.txtβ guest uname / statefix.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.
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-srcbetween 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 whendisk_getopencount() <= 1and armsVNF_DESTROYfor 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β genericdiskclose(): decrementsd_opencount, then underds_tokencallsdsclose(dev, type, dp->d_slice)and, on the last close,dev_dclose(dp->d_rawdev)β the vnclose teardown. The losing close blocks onds_tokenwhile the winner frees; after the token it dereferencesdp->d_slice/dp->d_rawdevfrom 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()callsrelease_dev(dev)three times; racing the loser close's ownrelease_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 -Hshowed 4 threads in D state;kill -9not processed while stalled). - Run 2: minimal harness (
stall.c, 2 threads, no spray) from a freshreset with-srcsnapshot β 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_slicederef by the losingdiskclose()is a read of a freedvn_softc(2816-byte slab class, measured viavmstat -m) followed bydsclose()bit-clear writes through the garbage pointer β exploitable in principle via same-class slab recycling (execvep_argsof ~2700 bytes, content user-controlled, needskern.ps_arg_cache_limitraised or a different same-class allocation stream). - Trigger requires
SYSCAP_RESTRICTEDROOT(vnioctl, vn.c:434) for the detach that armsVNF_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 viacallout_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, samestall 800trigger β 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 whosevnopendoesn't touchd_opencount(subr_disk.c layering). devfs_clone_bitmap_getis fully serialized bydevfs_bitmap_lock(devfs_helper.c:221-233) β no clone-unit race.- swap-backed
vn_sizeoverflow at vn.c:632-633 (PAGE_SIZE * vn_sizeas off_t) wraps forvn_size β₯ 2^51, butswap_pager_strategy/swp_pager_meta_buildnever bound-checkobject->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
fixedIdentical 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.
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.
No comments yet.