# DF-3607 — VERDICT

**Finding:** `ncr(4)`'s software timeout daemon (`ncr_timeout`) completes an
nccb that is still armed in the 53C8xx script processor's DSA. A subsequent
phase-mismatch on that connection is fixed up in `ncr_int_ma()` against the
dead (or already re-used) nccb: the chip is either wedged (returns without
restart — all further I/O on the HBA hangs) or restarted with a patch built
from the nccb's stale/re-used scatter descriptors, resuming host DMA into a
buffer the CAM periph has already released. The first form is a local DoS;
the second is kernel memory corruption via DMA.

**Status: inconclusive** — reproduced at source level (line-accurate trace
below); full runtime choreography requires an NCR 53C8xx HBA plus a SCSI
target that stalls mid-phase past the CCB timeout and then resumes. The
audit guest's QEMU-emulated 53C895A target is well-behaved and cannot be
made to stall on demand, so the race window itself could not be driven to
conclusion on the guest (attempts documented below).

## Why the code is wrong — line-accurate trace (sys/dev/disk/ncr/ncr.c)

1. `ncr_action()` XPT_SCSI_IO sets `cp->host_status = HS_BUSY`
   (ncr.c:4116) — and the host status **stays HS_BUSY for the whole
   connected life of the command**: the script only updates HS_REG
   (`scr1`) at completion-ish points (complete/status/cleanup).
   `cp->tlimit = time_uptime + timeout/1000 + 2` (ncr.c:4137-4138).

2. **DragonFly CAM never arms `ccb_h.timeout_ch`** — verified by grep:
   there is no `callout_reset` on `timeout_ch` anywhere under
   `sys/bus/cam/` (only `callout_init` at cam_xpt.c:3906 and alloc/free).
   CAM-level recovery is also unavailable for this SIM: `XPT_ABORT` and
   `XPT_RESET_DEV` are unimplemented and return `CAM_REQ_INVALID`
   (ncr.c:4170-4179). The driver's own daemon is therefore the *only*
   timeout enforcement.

3. `ncr_timeout()` (ncr.c:5101) walks all nccbs; for any with
   `host_status != 0` and `tlimit <= now` it:
   - disables the reselect pattern (`cp->jump_nccb.l_cmd = SCR_JUMP`,
     ncr.c:5159) — protects the *disconnected* case;
   - patches the start-queue entry to `skip` **only if** the job was
     never selected (`launch.l_paddr == select`, ncr.c:5160-5166) —
     after a successful selection the script has already overwritten
     `launch` with the idle-jump (`loadpos`, script at ncr.c:1599-1601),
     so a **connected, mid-transfer** job gets no protection at all;
   - `HS_BUSY/HS_NEGOTIATE/HS_DISCONNECT -> HS_TIMEOUT`,
     `cp->tag = 0`, `ncr_complete(np, cp)` (ncr.c:5168-5181).

4. `ncr_complete()` (ncb guarded at ncr.c:4428) sets `cp->ccb = NULL`
   (ncr.c:4452) and `ncr_free_nccb` clears the magic (ncr.c:6474) — but
   the nccb **stays in `np->link_nccb`** and the chip's DSA register
   still holds `CCB_PHYS(cp, phys)`. The CAM periph gets the CCB back
   and may free/reuse the data buffer.

5. The stalled target resumes/changes phase -> `MA` interrupt ->
   `ncr_int_ma()` (ncr.c:5640):
   - the nccb is found by physical DSA match **regardless of magic**
     (ncr.c:5692-5694); the sanity check that would notice the
     stale/dead nccb prints a mismatch but its `return` is commented
     out (ncr.c:5701-5707 `/* return; */`);
   - `PRINT_ADDR(cp->ccb)` with `cp->ccb == NULL` is harmless
     (`xpt_print_path(NULL)` is NULL-safe, cam_xpt.c) — no panic there;
   - on opcode mismatch it `return`s **without restarting the chip**
     (ncr.c:5775-5782): the script processor stays halted on the dead
     connection; every later I/O to this HBA hangs => local DoS;
   - on a data-phase match it builds `cp->patch[]` from the *stale*
     `cp->phys` scatter table — or, if `ncr_get_nccb()` has already
     handed the same nccb to a *new* XPT_SCSI_IO (freed nccbs are
     reused, ncr.c:6411-6454), from the *new* command's table at the
     *old* command's residual position — writes it (ncr.c:5798-5808)
     and restarts the script processor at `dispatch`/`checkattn`
     (ncr.c:5824-5828). The chip then DMAs residual data into
     whatever the stale (or mismatched new) descriptors point at:
     a buffer the periph released or reused => **use-after-free DMA
     write**.

The same completion-while-armed pattern exists for `HS_DISCONNECT`
(disconnected, never-resumed jobs are safe to complete because the
reselect chain no longer matches them) — the unprotected state is
exactly the connected one.

## What was executed on the guest

- Guest kernel `X86_64_GENERIC` contains the driver
  (`nm /boot/kernel/kernel`: `ncr_action`, `ncr_timeout`, `ncr_int_ma`,
  `ncr_attach` present; 45 ncr symbols).
- QEMU `lsi53c895a` added on the guest PCI bus (device 1000:0012 =
  `NCR_895A_ID` in ncr.c:1331). Default boot: **sym(4) wins the probe**
  (sym returns BUS_PROBE_DEFAULT=-20; newbus keeps the numerically
  greater probe result, sys/kern/subr_bus.c:1456 `result > pri`), so ncr
  never attaches out of the box.
- One-line guest kernel rebuild (`sym_conf.h`
  `SYM_SETUP_LP_PROBE_MAP 0x04` — the documented mechanism,
  sym_conf.h:270-292) makes sym probe the 895A at low priority so ncr
  claims it. See build.log / env.txt.
- With the rebuilt kernel the ncr driver attaches to the emulated HBA and
  the CAM scan drives real XPT_SCSI_IO traffic through `ncr_action`
  (see run.log). The emulated scsi-hd target completes every phase
  immediately; no mid-phase stall is available from `-device scsi-hd`,
  so the timeout daemon never fires on a *connected* job in these runs —
  the race window could not be entered with stock QEMU devices. The
  trigger program (ncr_timeout_race.c) documents how a programmable /
  stalling target would drive it (1 ms CCB timeouts against a slow
  target).

## Bottom line

Source-certain lifetime bug in the driver's only timeout/recovery path;
concrete kernel-memory-corruption primitive (stale scatter descriptors
programmed into the chip after the owning CCB was completed) gated on a
stalling SCSI target behind an NCR 53C8xx HBA. Recommend the fix.diff
(reject completions while DSA still names the nccb — force the
hard-recovery path; never fix up a phase mismatch against a dead nccb).

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