# DF-1051 — sgopen unlocks unheld lock + releases unacquired periph (REPRODUCED)

## Verdict
**REPRODUCED** — kernel panic observed on the unpatched `#0` baseline kernel; fix validated on the patched `#1` kernel.

## Mechanism (confirmed by runtime panic)

`sys/bus/cam/scsi/scsi_sg.c:sgopen` (the `/dev/sg*` open path) has two distinct bugs:

**(1) Unlocks a lock it never acquired.** At `:395-399`, the `securelevel > 1` error path calls `cam_periph_unlock(periph)` BEFORE `cam_periph_lock()` is called at `:400`. `cam_periph_unlock` calls `lockmgr(periph->sim->lock, LK_RELEASE)` which decrements the lwkt token reference count for `periph->sim->lock` — but no token was held. The token system panics with `lwkt_reltoken: illegal release` at `sys/sys/lwktoken.c` from inside `sgopen+0x85`. Trace captured in `panic.txt`:

```
REF CONTENT: tok=0 count=0000000000000000 owner=0xfffff8008e11d658
lwkt_reltoken: no tokens to release
panic: lwkt_reltoken: illegal release
cpuid = 1
Trace:
lwkt_reltoken() at lwkt_reltoken+0xda
lwkt_reltoken() at lwkt_reltoken+0xda
sgopen() at sgopen+0x85
dev_dopen() at dev_dopen+0x6c
devfs_spec_open() at devfs_spec_open+0x27d
vop_open() at vop_open+0x7c
```

**(2) Releases a periph reference it never acquired.** The sibling `ptopen` (`sys/bus/cam/scsi/scsi_pt.c:154`) calls `cam_periph_acquire(periph)` immediately after fetching `periph`; `sgopen` skips this. Every `cam_periph_release(periph)` call in `sgopen` (`:397`, `:414`) and `sgclose` (`:437`) therefore decrements `periph->refcount` (u_int, init 0 at `cam_periph.c:215`) below zero, underflowing to `0xFFFFFFFF` on the first call. The underflow permanently prevents the periph from being freed via `cam_periph_release` (the free path at `cam_periph.c:372` checks `refcount == 1`, unreachable after underflow).

## Trigger / preconditions
- Attacker position: local root with `SYSCAP_RESTRICTEDROOT` (the `caps_priv_check_self` at `:385` requires this).
- For (1): `sysctl kern.securelevel=2` then `open("/dev/sg0", O_RDWR)` → immediate panic.
- For (2): any `open`/`close` cycle on `/dev/sg*` at any securelevel silently underflows refcount; observable as CAM_periph allocation growth and broken hot-unplug cleanup.

## Reproduction steps (run as root on the guest)

```
cc -o sg_securelevel_panic sg_securelevel_panic.c
sysctl kern.securelevel=2
./sg_securelevel_panic
# Unpatched: kernel panics here (ssh dies, boot.log shows the trace).
# Patched:   open returns EPERM, guest stays up.
```

## Fix

`fix.diff` mirrors `ptopen`:
- Adds `if (cam_periph_acquire(periph) != CAM_REQ_CMP) return (ENXIO);` after fetching `periph`.
- Removes the spurious `cam_periph_unlock(periph)` from the `securelevel > 1` path (the lock was never acquired there).

Now every `cam_periph_release` in `sgopen`/`sgclose` corresponds to the new acquire.

## Fix validation (Phase 8)

- **Baseline** (`with-src` snapshot, `#0` build, unpatched): reproduced the panic. Captured in `panic.txt` and `run.log`.
- **Patched** (`#1` build at `Sun Jul 19 16:49:25 UTC 2026`, sha256 `bcfe20d5ad4accf44f6020bcb9aaf9162c9a4870a0b5d43022553fd6d2861238`):
  - With `securelevel=2` + `open("/dev/sg0")`: open returns `EPERM`, exit 1, guest stays up. ✅
  - With `securelevel=-1` + `open("/dev/sg0")`: open succeeds, exit 0 (normal path). ✅

Fix closes the bug deterministically.

## Kernel references
- `sys/bus/cam/scsi/scsi_sg.c:388-418` — sgopen (no acquire; unlock without lock)
- `sys/bus/cam/scsi/scsi_sg.c:421-441` — sgclose (release without acquire)
- `sys/bus/cam/scsi/scsi_pt.c:138-178` — ptopen (correct acquire-then-release pattern)
- `sys/bus/cam/cam_periph.c:215, 319-329, 341-378` — refcount init, acquire, release/free
- `sys/kern/lwkt_token.c` — `lwkt_reltoken` illegal-release panic (the actual fault)

## PoC changes
- `sg_securelevel_panic.c` written from scratch (no PoC existed in the folder).
  Adds `#include <string.h>` to silence the implicit-declaration warning that caused
  a userspace segfault on the error path (the kernel result was unaffected).
