# DF-4237 — sili(4): unchecked `sili_ata_get_xfer()` NULL dereference in probe / NCQ-renegotiation (kernel panic)

## What

`sili_cam_probe()` (sys/dev/disk/sili/sili_cam.c) calls `sili_ata_get_xfer()`
at sili_cam.c:289 (IDENTIFY) and sili_cam.c:371 (NCQ-renegotiation loop) and
dereferences the returned `struct ata_xfer *` without a NULL check. The core
helper explicitly returns NULL when the port's ccb free list is exhausted
(sili.c:2318-2327, `DPRINTF("... NULL ccb")`), which happens whenever the CAM
command queue is saturated at reprobe time. Result: write/read through NULL
(`xa->complete = ...` at sili_cam.c:290; `xa->tag` read at sili_cam.c:372) →
kernel page fault → panic.

## Threat model / reachability

* **Physical disk, local user (race):** heavy NCQ I/O (queue saturated, 30 of
  31 slots held) concurrent with any event that re-probes the port
  (link error recovery softreset — sili.c:845-849 sets
  `ap_probe = ATA_PROBE_NEED_IDENT` *without* revoking in-flight ccbs —
  hot-plug glitch, `camcontrol reset`). Next `XPT_SCSI_IO` runs the probe
  (sili_cam.c:944-945); the renegotiation loop permanently holds every free
  slot with `tag >= at_ncqdepth` (sili_cam.c:370-376) and drains the pool →
  NULL deref. `XPT_PATH_INQ` (wildcard) / `XPT_ENG_EXEC` / the port-thread
  state machine call `sili_cam_probe` with **no devq credit**
  (sili_cam.c:846-850, 878-881; sili.c:511-512), so with a saturated queue
  even the IDENTIFY itself NULL-derefs (sili_cam.c:289-290).
* **Crafted SATA disk (deterministic, same threat model as DF-3690../DF-4027):**
  the device feeds identify word 75 (`qdepth`); reporting `qdepth=1` makes
  `at_ncqdepth = 2` (sili_cam.c:365) so only tag 0 cycles in the loop, making
  the pool drain near-certain after the device forces an error-recovery
  reprobe with I/O in flight.

Impact: NULL-pointer kernel panic (local DoS). Not a memory-corruption
primitive (fault on the NULL direct-map entry, not attacker-mappable memory).

## Evidence in this pack

* `sili_ncq_pool_model.c` — userspace model encoding the exact pool rules
  (31 slots, slot 1 reserved as err ccb, TAILQ head-pop/tail-push, devq
  opening 30, renegotiation loop semantics). Built and run **on the audit
  guest** with DragonFly cc 8.3:
  - shipped-kernel mode: Scenario A prints the panic site
    (`sili_cam.c:372 xa->tag read through NULL xa after loop held 1 slots
    (iteration 2/31)`), Scenario C (idle port) is the negative control;
  - `guard` mode (models fix.diff): both scenarios defer, no panic.
* `run.log` — full untrimmed guest output of both modes.
* `VERDICT.md` — full line-cited trace, honest classification.
* `fix.diff` — NULL guards at the two reachable probe sites + defensive
  guards on the I/O-path `sili_ata_get_xfer` calls.

## Reproduce

```
# on the DragonFly guest (no sili HBA needed for the model)
cc -O2 -Wall -o sili_model sili_ncq_pool_model.c
./sili_model          # shipped-kernel semantics -> PANIC emulated at :372
./sili_model guard    # fix.diff semantics -> defers, no panic
```

Live kernel trigger requires a SiI 3124/3132 HBA with a disk (or crafted
SATA device); the audit guest has none (see `env.txt`), so the finding is
classified **not_reproduced / hardware-gated**, code-trace + model proven —
same discipline as DF-4027.
