# DF-2264 — saioctl control-device early returns leak cam_periph_lock (SIM-wide wedge)

File: `sys/bus/cam/scsi/scsi_sa.c`
Claim under verification: `saioctl()` takes the SIM-wide CAM lock
unconditionally at :849 (`cam_periph_lock(periph)`) but three early
returns inside the `SA_IS_CTRL(dev)` switch bypass the single
unlocking exit at the function tail (:1345-1348):

- :878 `return (error)` — MTIOCERRSTAT with `cam_periph_hold(periph, PCATCH)` interrupted,
- :914 `return (error)` — MTIOCTOP/MTIOCSETEOTMODEL with the hold interrupted,
- :920 `return (EINVAL)` — unknown ioctl on the control device.

Every subsequent CAM operation on that SIM (any xpt_action, any other
ioctl on any device of the same HBA) blocks forever: SIM-wide deadlock.

## Rig

Same kernel-compiled harness as DF-2263 (see ../DF-2263/): verbatim
scsi_sa.c inside the harness TU, only external CAM symbols interposed.
`saioctl()` itself is the REAL function. Fabricated state: a real
`struct lock` (lockinit), a `cam_sim` pointing at it, a `cam_periph`
with `periph->softc`, and a `struct cdev` with `si_uminor = 1<<29`
(`SA_IS_CTRL`, unit 0) registered through the REAL `cam_extend_set()`
into the driver's own `saperiphs` array. `saioctl` then runs its real
lock discipline on a real lockmgr lock.

* **t2264a**: `ap.a_cmd = 0xfffffffe` (unknown ioctl), control device.
  After the real `saioctl()` returns: `lockstatus(&lk, curthread)` and a
  wedge probe (kernel thread doing `cam_sim_lock()` on the same lock,
  1s watchdog).
* **t2264b**: `ap.a_cmd = MTIOCSETEOTMODEL`, `periph->flags |=
  CAM_PERIPH_LOCKED` so the unconditional `cam_periph_hold()` at :912
  sleeps in `caplck`; the caller is a userland `sysctl` process which is
  signalled with SIGUSR1 after 1s (`sh -c "(sleep 1; kill -USR1 $$) &
  exec sysctl debug.df226x.t2264b=1"`), making the PCATCH sleep return
  EINTR — the exact "hold interrupted" event of the finding.

## Results

UNPATCHED (run_unfixed.dmesg):

```
DFH2264A: saioctl(unknown ioctl, control device) returned 22 (EINVAL=22);
          SIM lock status AFTER return: 2 (LK_EXCLUSIVE=2)
DFH2264A: *** LOCK LEAKED: returned to caller with the SIM-wide CAM lock
          held (scsi_sa.c:849 vs :920 early return)
DFH2264: 2264A: *** probe thread still BLOCKED after 1s: SIM-wide wedge
          confirmed (thread parked forever)

DFH2264B: saioctl returned 4 after interrupted cam_periph_hold
          (4 = EINTR); SIM lock status AFTER return: 2
DFH2264B: *** LOCK LEAKED via interrupted hold (scsi_sa.c:912-914 early return)
DFH2264: 2264B: *** probe thread still BLOCKED after 1s: SIM-wide wedge
          confirmed (thread parked forever)
```

The leaked lock is held by a thread that has already returned to
userland (t2264a) or died from the signal (t2264b) — nothing will ever
release it. On a real system the same leak wedges every future CAM
operation on the HBA (xpt_action paths all take the SIM lock), turning a
single Ctrl-C during `mt status`/`mt setblk` into a durable,
system-reboot-required hang of the whole tape chain.

PATCHED (run_fixed.dmesg, fix2264.diff = `goto out_locked` for the three
returns + `out_locked: cam_periph_unlock(periph); return (error);`):

```
DFH2264A: ... returned 22 (EINVAL=22); SIM lock status AFTER return: 0
DFH2264: probe thread ACQUIRED the SIM lock
DFH2264: 2264A: probe acquired the lock (NOT leaked)
```

A/B clean: same EINVAL to the caller, lock released, probe acquires.
(On the patched side the t2264b signal raced the hold in the recorded
run — hold succeeded and the normal exit released the lock; the
interrupted-hold path itself is proven on the unpatched kernel and uses
the identical out_locked fix validated by the 2264A A/B.)

## Reachability / severity honesty

The control-device ioctls sit behind `caps_priv_check_self(SYSCAP_RESTRICTEDROOT)`
(scsi_sa.c:451) at open time — root/operator only. A privileged local
user converting a signal into a durable SIM-wide CAM wedge: availability
impact on tape-attached systems, no memory corruption, no info leak, no
privesc. Matches Low severity.

## 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.
