# 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_REMOVE`s 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.
