# DF-4027 VERDICT — sili deferred-timeout leaks a port-owned ccb back to the free list

**Status: NOT REPRODUCED (hardware absent).** The audit guest has no
SiLion Image 3124/3132 HBA (`sili(4)` never attaches), so the trigger
cannot be exercised live. The finding is **code-trace proven**:
every step below is line-verified in this tree; the only unverifiable
element is the device-side timing (a target that stalls a polled
command past its timeout while another command is active on the same
port — standard behavior for a broken/malicious SATA device behind a
port multiplier, exactly the crafted-disk threat model of the
DF-3690..3696 family).

## Root cause (line-verified)

`sili_poll()` documents its return contract as "Returns ATA_S_* state"
(sili.c:1294-1305) and every caller in sili_cam.c treats anything other
than `ATA_S_COMPLETE` as terminal and releases the xfer
(`sili_ata_put_xfer` at sili_cam.c:318/548/570/595/668). But
`sili_poll()` can return **`ATA_S_ONCHIP` — a non-terminal state**:

1. The polled command times out (sili.c:1346 `while (timeout > 0)`).
2. `timeout_fn(ccb)` = `sili_ata_cmd_timeout` → `sili_core_timeout(ccb, 0)`
   (sili.c:2440-2443).
3. `sili_core_timeout` marks the slot expired and, if **any other
   command is still active on the port**, defers:
   `ap->ap_expired |= 1 << slot; if (ap->ap_active != ap->ap_expired)
   return (1);` (sili.c:2505-2511) — the ccb stays `ATA_S_ONCHIP`,
   still owned by the port (`ap_active` bit set, callout state live).
4. `sili_poll` then falls out and returns `ccb->ccb_xa.state` —
   still `ATA_S_ONCHIP` (sili.c:1359).
5. sili_cam.c releases the xfer anyway:
   `sili_ata_put_xfer(xa)` → `sili_put_ccb()` (sili.c:2020-2030)
   pushes the **port-owned** ccb onto `ap_ccb_free` and bumps
   `serial`. (The `KKASSERT(ccb->ccb_xa.state == ATA_S_PUT)` in
   `sili_get_ccb` passes later because put set it.)

From this point the ccb has two owners.

## Consequences (trace)

* **ccb reuse while on-chip:** the next `sili_ata_get_xfer()` hands the
  same ccb/slot to a new command; `sili_issue_pending_commands` ORs the
  slot into `ap_active` a second time and increments `ap_active_cnt`
  again (sili.c:1532-1536) — accounting inflated; two logical commands
  share one hardware slot.
* **Stale completion unloads the wrong dmamap:** when the port drains,
  `sili_port_reinit`'s expired loop completes the stale request:
  `ccb->ccb_xa.state = ATA_S_TIMEOUT; ccb->ccb_done(ccb);`
  `ccb->ccb_xa.complete(&ccb->ccb_xa);` (sili.c:403-413).
  `sili_ata_cmd_done` → `sili_unload_prb` (sili.c:2415, 1271-1291)
  unloads whatever dmamap is currently loaded — the **new** command's —
  while the new command may still be on chip: DMA writeback onto
  memory the driver no longer owns (the DF-4014 mps shape).
* **Double CAM completion:** if the reissued ccb carries a CAM command,
  `complete` is `sili_ata_complete_disk_rw` (sili_cam.c:1331, 1638-…),
  which runs `xpt_done()` on `xa->atascsi_private` — the **new**
  request's CAM ccb — completing it out from under its own lifecycle:
  CAM UAF / double completion.
* **ap_active_cnt underflow / INVARIANTS panic:** the double-counted
  slot is decremented twice; `KKASSERT(ap->ap_active_cnt == 0)` in
  `sili_port_hardstop` (sili.c:1113) fires on INVARIANTS kernels; on
  production kernels the counter wraps and the port state machine
  corrupts.

## Trigger requirements (crafted-disk)

* sili HBA + port multiplier (or any second active command on the port)
  + a target that answers a POLLED probe/feature command
  (IDENTIFY / SET_FEATURES / SETXFER, 1000 ms timeout,
  sili_cam.c:313/541/563/588/655) slower than the timeout while another
  target's command is active. Poll users: probe path
  (sili_cam.c:315-321, 543-548, 565-570, 590-595, 661-668) and
  CAM_POLLED disk I/O (sili_cam.c:1333-1334).

## Why not DF-4014/2517/2518/1338/1339

* DF-4014 (mps_user.c) frees an in-flight command *itself* on wait
  timeout; here the driver's poll/timeout *state contract* hands a
  still-owned ccb to callers that release it — different file,
  different mechanism (deferral, not free-on-timeout).
* The sili knowns (SGE table bounds, NCQ tag OOB) are unrelated paths.

## Fix

`sili_poll()` must not return `ATA_S_ONCHIP`/`ATA_S_PENDING` to callers
whose contract is "state is terminal after return". Minimal fix: in
`sili_core_timeout`, when the timeout is deferred and the caller is the
poll path, the ccb must not be put back; better, sili_poll's callers
should put the xfer only on terminal states. Concrete one-line-ish
guard in sili_cam.c: replace the unconditional `sili_ata_put_xfer(xa)`
on the != ATA_S_COMPLETE paths with a check
`if (xa->state == ATA_S_ONCHIP || xa->state == ATA_S_PENDING) { do NOT
put; the port still owns the ccb; report failure and leave completion
to the port }`. (The CAM-polled path at sili_cam.c:1343-1354 already
behaves correctly — it ignores the return and lets the port complete.)

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

(No fix.diff for this item: documented recommendation only - see Fix above.)
