# DF-2282 — scsi_sg.c sgopen missing cam_periph_acquire (+ unlock-without-lock)

## Verdict: NOT REPRODUCED (permission-gated) — source bug CONFIRMED real; fix.diff compiles.

**Classification:** `not_reproduced` / permission-gated for the unprivileged actor /
impact=none. The refcount/lifetime bug is **real in source**; the defense-in-depth
`fix.diff` **compiles cleanly** (`nativekernel rc=0`, `-Werror`, `-DINVARIANTS`).

### Why not reproduced (the gate)

`sgopen()` is the open(2) handler of the **sg(4) peripheral**. `/dev/sg0` on this
guest is **mode 0600 root:operator** (root-only); `maxx` (uid 1001, in **no**
privileged group) cannot open it, so the open/close refcount churn and the
securelevel error path are unreachable by the unprivileged actor. A root trigger
would be root→kernel (game-over by definition).

Gate proof (this guest):
```
$ ls -la /dev/sg0   ->  crw------- root operator /dev/sg0   (0600)
$ id maxx           ->  uid=1001(maxx) ... groups=1001(maxx)   [not operator]
```

### The source bug (real, cited `path:line`)

`sys/bus/cam/scsi/scsi_sg.c`, `sgopen()` (`:375-418`):
- Unlike every other CAM passthrough driver (compare `scsi_pass.c:319`
  `cam_periph_acquire` + `:322` `cam_periph_lock`), `sgopen` **never calls
  `cam_periph_acquire`**. `sgclose()` (`:421-440`) **unconditionally** calls
  `cam_periph_release` (`:437`). So every open/close pair nets **one decrement** of
  `periph->refcount` with no matching increment.
- The `securelevel > 1` error path (`:395-398`) calls `cam_periph_unlock` and
  `cam_periph_release` **before** any `cam_periph_lock`/`acquire` —
  unlock-without-lock (`lockmgr LK_RELEASE` on an unheld lock) and
  release-without-acquire (refcount decrement with no matching increment).
- When the periph is later marked `CAM_PERIPH_INVALID` (device removal),
  `cam_periph_release` frees it, but `cdev_t si_drv1` still points at the freed
  periph → subsequent `open`/`ioctl` is a **use-after-free**.

Attacker model (per finding): the `operator` group on a host where `/dev/sgN` is
group-accessible. On this guest the node is 0600 root-only, closing the unprivileged
path.

### Exploit chain

Not developed — `/dev/sg0` is 0600; `maxx` cannot exercise the open/close churn.
Valid hard blocker for the unprivileged model: the device node is not openable by the
actor. (The UAF would additionally require device removal timing, but that is moot
without an open path.)

### Fix (defense-in-depth, validated to compile)

`fix.diff` mirrors `scsi_pass.c`: adds `cam_periph_acquire` + `cam_periph_lock` at
the top of `sgopen`, makes the `securelevel`/`INVALID` error paths symmetric
(unlock+release), and removes the bogus "else release" workaround. Validated in the
combined `nativekernel` build (`fix_build.log`).

### Reproduce

No runnable PoC for the unprivileged actor. Artifacts: `env.txt`, `fix.diff`,
`fix_build.log`.
