# DF-5273 evidence pack — cdioctl() refcount burn on interrupted hold

## Finding (one line)
`cdioctl()` (sys/bus/cam/scsi/scsi_cd.c:1930-1934) reacts to a
`cam_periph_hold(periph, PCATCH)` failure — which `cam_periph_hold()`
itself already unwinds internally (cam_periph.c:395 acquire, :402 release)
— with an **extra, unmatched `cam_periph_release(periph)`**.  Every
signal-interrupted ioctl attempt therefore decrements the peripheral
refcount by one: the refcount can be driven to 0 and negative on a valid,
open device, and — when the periph is later invalidated — the
`CAM_PERIPH_INVALID && refcount == 1` branch of `cam_periph_release()`
(cam_periph.c:352-357, 371-374) frees the periph (softc, disk, sysctls)
while file descriptors are still open → use-after-free window in
`cdioctl`/`cdclose` between `cam_extend_get()` and `cam_periph_lock()`.

## Reproduce (guest, root; evilcd kld loaded)
```
cd /root/df5271 && cc -O -o poc_ref poc_ref.c
timeout 45 usched :1 ./poc_ref /dev/cd1
```

## Expected (vulnerable kernel)
```
[poc] initial cd periph refcount: 1
[poc] round 0 burn 0: ioctl rc=-1 errno=4(Interrupted system call)  refcount=1
[poc] round 0 burn 1: ioctl rc=-1 errno=4(Interrupted system call)  refcount=0
[poc] round 0 burn 2: ioctl rc=-1 errno=4(Interrupted system call)  refcount=-1
```
One burned reference per EINTR-interrupted ioctl; **no close() anywhere**.
(The holder child keeps the periph held via a device-stalled CDIOCPAUSE;
the parent's CDIOCPLAYMSF blocks in `cam_periph_hold` and is
self-interrupted with SIGALRM after 100 ms — sigaction installed without
SA_RESTART so the sleep returns EINTR.)  Across earlier exploratory runs
the refcount reached -3 and -5 the same way; `debug.evilcd.refcnt` reads
the live periph's `refcount` field directly.

## Why it matters
`cam_periph_hold()`'s acquire/release is balanced by design; the extra
release in cdioctl's error path is the only unbalanced drop in the driver's
ioctl path (contrast `cdopen()` scsi_cd.c:1017/1028-1031, which *does*
acquire before holding, making its failure path balanced).  Effects:
- refcount underflow (demonstrated: 1 → 0 → -1);
- with the device invalidated (unplug) and refcount == 1 at release time,
  `camperiphfree()` runs with open fds → subsequent ioctl/close race the
  free between `cam_extend_get()` (scsi_cd.c:1918) and `cam_periph_lock`
  (scsi_cd.c:1922) → UAF on `periph`/`softc`.

Not escalated further here: the trigger set (privileged open + concurrent
holder + timed signal + hot-unplug) makes the full UAF racy; the refcount
underflow itself is deterministic and is demonstrated.

## Files
- `poc_ref.c` — self-contained fork-based PoC (holder child + timed
  SIGALRM burns + refcount reads).
- `evilcd.c` — the fake CD (stalls CDIOCPAUSE; `debug.evilcd.refcnt`,
  `debug.evilcd.unstall`).
- `run.log` (see DF-5271 pack for the shared rig logs),
  `refcount_burn.log` — the decisive output above.
- `fix.diff` — one-line fix (delete the unmatched release).

## Fix
```diff
--- a/sys/bus/cam/scsi/scsi_cd.c
+++ b/sys/bus/cam/scsi/scsi_cd.c
@@ -1931,7 +1931,6 @@ cdioctl(struct dev_ioctl_args *ap)
 	if ((error = cam_periph_hold(periph, PCATCH)) != 0) {
 		cam_periph_unlock(periph);
-		cam_periph_release(periph);
 		return (error);
 	}
```
(cam_periph_hold already drops its own acquire on failure.)
