# DF-5499 — passioctl() takes no periph reference across blocking CAMIOCOMMAND paths

## What the finding is

`passioctl()` (sys/bus/cam/scsi/scsi_pass.c:451-456) looks a pass periph up
with `cam_extend_get()` and immediately `cam_periph_lock()`s it **without any
`cam_periph_acquire()`** — unlike `passopen()` (scsi_pass.c:319) and unlike the
sibling drivers' ioctl entries that use `cam_periph_hold()` (cd/ch/sa family,
cf. DF-5273/DF-5327).  The ioctl then blocks with the sim lock dropped and no
reference held at:

- `cam_periph_getccb()` → `sim_lock_sleep(&periph->ccb_list, ...)`
  (cam_periph.c:846-855), unkillable (no PCATCH);
- `cam_periph_runccb()` → `cam_periph_ccbwait()` → `sim_lock_sleep(...)`
  (cam_periph.c:863-872), unkillable;
- the `cam_periph_mapmem()` window (scsi_pass.c:577-579).

If the periph refcount could reach 0 while a thread is blocked there, the
next `cam_periph_invalidate()` (cam_periph.c:573-578) runs `camperiphfree()`,
freeing `softc` (passcleanup, scsi_pass.c:168-183), the periph itself
(cam_periph.c:630) **and `periph->path`** (cam_periph.c:629) — dangling every
outstanding CCB's `ccb_h.path`, the error-recovery `saved_ccb_ptr` into
`softc->saved_ccb` (cam_periph.c:1616), and the sleep address itself.

## Why it is filed as Info / hardening (NOT reproduced as a UAF)

The open-file reference held by an in-flight `ioctl()` syscall keeps
`devfs_spec_close()` from ever firing `d_close` under the ioctl:

1. Normal `close()`: deferred until every in-flight syscall on the file
   returns — the release strictly orders after the ioctl.
2. `umount -f` of a secondary devfs mount (the forced-reclaim door used by
   this PoC): `vflush(FORCECLOSE)` **refuses** busy device vnodes —
   proven live on the guest: with T1 blocked inside CAMIOCOMMAND and
   `umount -f /mnt/pw` executed, the console printed
   `vflush: Warning, cannot destroy busy device vnode` / `vfs refused to
   unmount, error 16`, and the rig's `stat` sysctl showed
   **`pass periph ... refcount=1 flags=0 valid`** — the release never ran.
   See console.log + run_full.log in this directory.
3. DragonFly has no `revoke(2)` for devfs (no VOP_REVOKE / vgonel path
   reachable from userspace).

Residual risk (why the hardening is still worth it): devfs's own
`opencount = count_dev(dev)` is documented `XXX NOT SMP SAFE`
(devfs_vnops.c:1146-1148); a miscount with the same cdev open through two
devfs mounts could fire `d_close` early, and `passioctl`'s missing reference
is exactly what would convert that into a premature free / UAF.  A future
revoke implementation would do the same.

## Reproduce (negative-evidence rig)

- build.sh — compiles the `passsim` KLD rig (fake CAM SIM with a
  direct-access LUN, hang/lost/poison/complete/stat sysctls) and the
  stage-driven PoC inside the guest.
- run.sh — loads the rig, opens the new /dev/passN through a secondary
  devfs mount, blocks a thread in CAMIOCOMMAND, then walks the stages
  (forced umount → stat → AC_LOST_DEVICE → stat → poison → complete),
  printing the periph refcount at each step.
- Expected (and observed): refcount stays 1 across the forced unmount;
  periph freed only via the normal invalidate-after-close ordering; no
  UAF, no panic.  This is the documented negative result.

## Recommended hardening

Take a periph reference for the duration of the ioctl (see fix.diff):

```diff
 	periph = cam_extend_get(passperiphs, unit);
 	if (periph == NULL)
 		return(ENXIO);
+	if (cam_periph_acquire(periph) != CAM_REQ_CMP)
+		return(ENXIO);
 
 	cam_periph_lock(periph);
 	...
 	cam_periph_unlock(periph);
+	cam_periph_release(periph);
 	return(error);
```

## Artifacts

- passsim.c, poc5499.c, sset.c, Makefile, build.sh, run.sh
- build.log, run_full.log (stage output), console.log (kernel side:
  refcount proof), run.log (process state post-run)
- verdict.json, manifest.json
