# DF-3017 — devfs node/cdev lifetime race: unpriv /dev/pts stat vs pty clone teardown → kernel panic (sysref_get on freed cdev)

## What

`devfs_vop_nresolve()` / `devfs_vop_getattr()` use a `struct devfs_node *`
(and its `node->d_dev` cdev) **without any reference and outside the
protection that keeps that memory alive**, while an unprivileged user can
make devfs free both concurrently:

* **Window A (proven panic)** — `devfs_allocv()` (sys/vfs/devfs/devfs_core.c:303)
  is called from `devfs_vop_nresolve()` (devfs_vnops.c:504) with a raw node
  pointer and **drops `devfs_lock`** across `getnewvnode()`/`vget()`
  (devfs_core.c:319/336). While the lock is dropped, the devfs core thread
  can run `destroy_dev()` teardown (`devfs_destroy_dev_worker` →
  `devfs_propagate_dev` → `devfs_destroy_device_node` → `devfs_unlinkp` +
  `devfs_freep`) which **frees the node** (`objcache_put`) and drops the
  cdev sysref. `devfs_allocv` then writes `node->v_node = vp` into freed
  memory and calls `v_associate_rdev(vp, node->d_dev)` →
  `reference_dev()` on the freed/terminating cdev.

* **Window B (same family)** — `devfs_vop_getattr()` (devfs_vnops.c:548)
  dereferences `DEVFS_NODE(vp)` and `node->d_dev` with **no vnode lock**
  (its major callers deliberately run unlocked: `vn_stat()` "vp already has
  a ref and is validated, can call unlocked", and `naccess()` via
  `VOP_GETATTR_LITE` → `vop_stdgetattr_lite` → `VOP_GETATTR`) and no
  `devfs_lock`. `devfs_freep()` frees the node after a `vget()` that
  succeeds whenever the victim only holds a *reference*.

## Trigger (unprivileged, 2 racing threads, no setup)

* Thread A: `open("/dev/ptmx")` + `close()` in a loop. Every iteration
  `ptyclone()` creates pts+ptm cdevs and devfs nodes under `/dev/pts`;
  last close runs `pti_done()` → `destroy_dev()` ×2 (sys/kern/tty_pty.c:281).
* Thread B: `lstat("/dev/pts/N")` (+ `access()`) in a loop →
  `devfs_vop_nresolve` → `devfs_allocv` (window A) and
  `VOP_GETATTR_LITE` → `devfs_vop_getattr` (window B).

Observed result (twice, minutes into the run, stock INVARIANTS kernel):

```
panic: assertion "(sr->flags & SRF_PUTAWAY) == 0" failed in sysref_get at /usr/src/sys/sys/sysref2.h:70
cpuid = 4
reference_dev() at reference_dev+0xa9
v_associate_rdev() at v_associate_rdev+0x31
devfs_allocv() at devfs_allocv+0x1a0
devfs_vop_nresolve() at devfs_vop_nresolve+0x185
vop_nresolve() at vop_nresolve+0x53
Debugger("panic")
```

`sysref_get` on a cdev already in SRF_PUTAWAY teardown = **use-after-free
write (refcount resurrect) on freed kernel memory**. On a non-INVARIANTS
kernel the assertion is absent and the resurrected/underflowed sysref
proceeds silently into a double-free / dangling cdev — the classic
exploitable refcount-corruption primitive (see VERDICT.md for the
weaponization analysis).

## Build

```
cc -O2 -pthread -o df3017 df3017.c
```

## Run (as any unprivileged user; /dev/ptmx is mode 0666)

```
./df3017 300
```

3 churn threads (`open`/`close` /dev/ptmx) + 8 stat threads
(`lstat`+`access` /dev/pts/0..7). Baseline: kernel panic (db> prompt,
guest down) within ~1–4 minutes (2/2 runs).

## Expected

* Baseline: `panic: assertion "(sr->flags & SRF_PUTAWAY) == 0" failed in
  sysref_get` with `reference_dev ← v_associate_rdev ← devfs_allocv ←
  devfs_vop_nresolve` on the console; ssh dies; `vm.sh status` → down.
* With fix.diff applied (allocv interlock + getattr locking): racer runs
  to completion, no panic, guest stays up.

## Files

* `df3017.c` — racer PoC (also detects UAF-read anomalies leaking into
  `struct stat`: wrong `st_mode`/`st_nlink`/`st_size`/`st_ino`)
* `build.sh`, `run.sh` — exact commands
* `panic.txt`, `panic.run2.txt` — serial-console panic transcripts (2 runs)
* `fix.diff` — git-apply-able fix (validated in-guest: rebuilt kernel, rerun → no panic)
* `verdict.json`, `manifest.json`, `VERDICT.md`
