# DF-2281 — scsi_sg.c sgwrite size_t->int truncation of uio_resid

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

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

### Why not reproduced (the gate)

`sgwrite()` is the write(2) handler of the **sg(4) SCSI-passthrough peripheral**.
The guest has an sg node (`/dev/sg0`, mode `0600 root:operator`) on the QEMU DVD-ROM,
but it is **root-only**. The unprivileged actor `maxx` (uid 1001, in **no** privileged
group — not `operator`, not `wheel`) cannot open `/dev/sg0`, so the `sgwrite` path is
unreachable. (The finding's own threat model is the `operator` group via mode 0660;
this guest uses 0600, closing even that.)

Gate proof (this guest):
```
$ ls -la /dev/sg0
crw-------  root operator  /dev/sg0      # mode 0600 — root only
$ id maxx   ->  uid=1001(maxx) ... groups=1001(maxx)   [not operator]
$ (as maxx) camcontrol devlist -> couldn't open /dev/xpt0: Permission denied
```

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

`sys/bus/cam/scsi/scsi_sg.c`, `sgwrite()`:
- `int error = 0, cdb_len, buf_len, dir;` (`scsi_sg.c:648`) — `buf_len` is `int`.
- `buf_len = uio->uio_resid;` (`scsi_sg.c:698`) — **size_t (64-bit) silently
  truncated into int**. A `write()` whose residual after the 37-byte header+CDB has
  bit 31 set yields a negative `buf_len`.
- `buf = kmalloc(buf_len, M_DEVBUF, M_WAITOK | M_ZERO);` (`:700`) — `kmalloc` takes
  `size_t`, so the negative int is sign-extended to a huge size. The `M_ZERO`
  fast-path in `sys/sys/malloc.h` unconditionally calls `__builtin_memset(_, 0, _)`
  when `(M_WAITOK|M_NULLOK)==M_WAITOK`, so a NULL/oversized allocation panics inside
  `memset` before the caller ever checks. (The `nbytes` clamp in
  `sys/kern/sys_generic.c:336-337` is overwritten at `:349`; `writev` bypasses it
  entirely.)
- Same defect on the `else if (hdr->reply_len != 0)` branch (`:705-707`): the
  user-controlled `int hdr->reply_len` is passed unsanitized to `kmalloc`.

Attacker model (per finding): a member of the `operator` group on a system where
`/dev/sgN` is 0660. On this guest the node is 0600 root-only, so the unprivileged
path is closed.

### Exploit chain

Not developed — `/dev/sg0` is 0600; `maxx` cannot open it, so there is no
unprivileged trigger. A root trigger would be root→kernel (game-over by definition),
not an escalation. Valid hard blocker for the unprivileged model: the device node is
not openable by the actor.

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

`fix.diff` rejects `uio_resid > INT_MAX` and `buf_len < 0` before the `kmalloc`, and
bounds the user-controlled `hdr->reply_len` to `[0, MAXPHYS]`. Validated in the
combined `nativekernel` build (`fix_build.log`).

### Reproduce

No runnable PoC for the unprivileged actor (`/dev/sg0` is 0600). Artifacts:
`env.txt`, `fix.diff`, `fix_build.log`.
