# DF-2285 — scsi_targ_bh teardown UAF — VERDICT

**Status: REPRODUCED (mechanical, verbatim driver code, stock INVARIANTS
guest kernel). Impact: kernel use-after-free confirmed as a live
primitive; observable as a deterministic kernel panic (DoS) with a
groomable, attacker-window-controlled write/read-after-free core.
Reachability is root-gated (no unprivileged path on the stock guest),
so this closes as Medium — consistent with the filed severity.**

## 1. Line-accurate teardown-path analysis (what targbhdtor actually does)

`targbhdtor` (sys/bus/cam/scsi/scsi_targ_bh.c:402-424):

1. `softc->state = TARGBH_STATE_TEARDOWN` (:409).
2. `targbhdislun(periph)` (:411) which, in order:
   - walks `softc->accept_tio_list` (:347-354) issuing `XPT_ABORT` for
     every ATIO the driver ever armed (targbhenlun:267-286). NOTE:
     `accept_tio_list` is only a resource inventory — a SELECTED ATIO
     (handed by the SIM to the periph with a CDB, its CTIO dispatched
     by targbhstart) is still linked there, but the abort is sent to
     the SIM for a ccb the SIM no longer owns;
   - walks `softc->immed_notify_slist` (:356-363) aborting the INs;
   - disables the LUN via `XPT_EN_LUN(enable=0)` (:368-371).
   **Nothing in the driver enumerates, aborts, or drains CTIOs
   (`XPT_CONT_TARGET_IO`) submitted by `targbhstart` via
   `xpt_action(start_ccb)` (:497).** The author left two XXX markers:
   `XXX Block for Continue I/O completion` (:342) and
   `XXX Wait for callback of targbhdislun()` (:419).
3. falls through to `sim_lock_sleep(softc, 0, "targbh", hz/2,
   periph->sim->lock)` (:420) — **a fixed 500 ms timed sleep with no
   wakeup source** (a tree-wide grep shows the only `wakeup` calls in
   the file are at :444 and :526, neither targets `softc`), then
   `kfree(softc, M_SCSIBH)` (:421).

After the dtor returns, `camperiphfree` (sys/bus/cam/cam_periph.c,
`camperiphfree`) frees `periph->path` and `periph` itself — so a late
SIM completion that still remembers `cbfcnp = targbhdone` and the
periph runs entirely on freed memory (in the real kernel xpt_done
would deref the freed path even before targbhdone).

`targbhdone` (XPT_CONT_TARGET_IO case, :638-695) on a late completion
executes, against the freed softc: `softc = periph->softc` (:522),
`TAILQ_REMOVE(&softc->pending_queue, &atio->ccb_h, ...)` (:648) —
which READS `*atio->tqe_prev` (a pointer into the freed chunk) and
WRITES two qwords through/into the freed chunk (queue.h TAILQ_REMOVE
semantics) — `desc->data_resid -= desc->data_increment` (:676), the
`softc->state` read (:678) steering the `xpt_action((union ccb*)atio)`
re-arm (:689) versus the cleanup branch (:692-693).

## 2. Mechanical demonstration (verbatim driver compiled into the kernel)

Rig: pre/post interposition harness around a byte-for-byte verified
copy of scsi_targ_bh.c (sha256
4bb4bca6b3a2b0eb84a737d2251752f4c3ebd0c8d5e7c16ffb7e99411e49307b,
check_verbatim.sh: `VERBATIM CHECK: embedded copy == sys source`),
fabricated periph/path/sim with a REAL lockmgr sim-lock so the REAL
`sim_lock_sleep` runs; fake SIM parks ATIO/IN resources, holds the
dispatched CTIO (stalled initiator), aborts idle CCBs, cannot abort a
SELECTED ATIO (CAM_REQ_CMP_ERR — conservative SIM behaviour); the dtor
thread is CPU-pinned to the ctor so the just-freed softc chunk is
deterministically reclaimed as a 0x41 canary victim immediately after
`targbhdtor` returns.

Guest: DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC with
`opt_global.h: #define INVARIANTS 1` (stock), harness kernel builds
#1 (baseline) and #1 03:32:40 (fixed).

### S2 MAIN (baseline) — the UAF

```
DFH: sim: CTIO 0xfffff801183d23c0 ACCEPTED AND HELD at fake SIM (stalled initiator; ticks=...)
DFH: dtor thread: calling targbhdtor
DFH: sim: XPT_ABORT idle ATIO ... -> completed CAM_REQ_ABORTED   (x7)
DFH: sim: XPT_ABORT SELECTED ATIO 0xfffff8008db00cc0 -> CAM_REQ_CMP_ERR (SIM cannot abort a ccb owned by the periph; in-flight CTIO untracked)
DFH: sim: XPT_ABORT IN ... -> completed CAM_REQ_ABORTED           (x8)
DFH: sim: XPT_EN_LUN enable=0
DFH: kfree(softc=0xfffff8004f296c20 size=280) (CTIO still HELD at fake SIM)   <-- free with outstanding CTIO
DFH: dtor thread: targbhdtor RETURNED after 65 ticks (hz/2=50)                <-- fixed 500ms, no wakeup protocol
DFH: reclaim: canary victim 0xfffff8004f296c20 (0x41-filled, size 280) == freed softc chunk (SAME CHUNK reclaimed)
DFH: dump[cmd] damaged_qwords(out of 35)=0                                     <-- nothing touched it yet
DFH: complete: firing held CTIO 0xfffff801183d23c0 into targbhdone (outstanding 699 ticks, hz/2=50) softc_freed=1
panic: Bad link elm 0xfffff8008db00cc0 prev->next != elm
targbhdone() at targbhdone+0x439
```

The panic is `QMD_TAILQ_CHECK_PREV` (sys/sys/queue.h:528-534, enabled
by `_KERNEL && INVARIANTS`) firing inside the verbatim `TAILQ_REMOVE`
at scsi_targ_bh.c:648: `atio->tqe_prev` points into the freed softc
chunk — now the live 0x41 canary victim — and the read of
`*(atio->tqe_prev)` returned `0x4141414141414141` != the elm. That is
a driver dereference of freed-then-reclaimed heap memory, caught by
the INVARIANTS gate. On a production (non-INVARIANTS) kernel the same
code proceeds to WRITE through those pointers (TAILQ_REMOVE writes
`*tqe_prev = tqe_next` and `head->tqh_last = tqe_prev`, queue.h:646-663)
— i.e. an offset-deterministic write-after-free (NULL + self-pointer
into whatever object now occupies the chunk) plus the
`softc->state`-from-freed-memory control-flow read at :678 that would
take the re-arm branch (:689) and push the stale ATIO back into a dead
periph. The timing is fully attacker-controlled: the held CTIO was
outstanding 699 ticks (6.99 s) versus the fixed 500 ms budget — the
initiator simply keeps stalling the DATA phase while teardown runs
(hot-remove / camcontrol / SIM dereg).

### Controls (baseline)

- S0 CONTROL-1 (ctor -> dtor, no I/O): aborts 8 ATIOs + 8 INs,
  sleeps 66 ticks, frees softc; canary victim intact
  (`damaged_qwords=0`); no panic.
- S1 CONTROL-2 (ctor -> inquiry -> complete BEFORE dtor): the CTIO
  completes while the softc is alive (`softc_freed=0`,
  `postfree_actions=0`), the ATIO re-arms normally, teardown is then
  clean; no panic.

So the panic appears exactly and only when a dispatched CTIO is still
outstanding at teardown — the precise condition the finding claims.

### Harness-artifact disclosure

An intermediate harness revision scribbled 0xDE over freed chunks to
make UAF touches visible; DragonFly chains free chunks through the
chunk itself (kern_slaballoc.c `z_LChunks`/`c_Next`), so that scribble
delay-panicked `chunk_mark_free` in `slab_cleanup` — a harness bug,
not the finding. It was replaced by the same-chunk canary victim (a
LIVE allocation; structurally safe) before the decisive runs recorded
here. No other harness-induced anomalies.

## 3. A/B fix validation (fix.diff)

fix.diff ports the maintainer-shaped teardown discipline:
`u_int active_ctios` in the softc, incremented in `targbhstart`
BEFORE `xpt_action(start_ccb)` (the SIM may complete synchronously),
decremented + `wakeup(&softc->active_ctios)` in the `targbhdone`
XPT_CONT_TARGET_IO case, and `targbhdtor` now drains
(`while (softc->active_ctios > 0) sim_lock_sleep(&softc->active_ctios,
0, "targbhctio", 0, periph->sim->lock)`) before the existing hz/2
sleep and `kfree`. Divergence note: FreeBSD CURRENT still carries the
same fixed `hz/2` sleep + free in its scsi_targ_bh.c (their
`cam_periph_sleep(periph, softc, PRIBIO, "targbh", hz/2)`), i.e. the
defect is inherited from the 2003 FreeBSD base and persists upstream;
FreeBSD scsi_target.c (targ(4)) by contrast frees queued CCBs and
clears `periph->softc` in its dtor.

Fixed-kernel observations (fix_run.log):

```
DFH: complete: firing held CTIO ... (outstanding 269 ticks, hz/2=50) softc_freed=0   <-- completion now lands BEFORE the free
DFH: complete: targbhdone returned; postfree_actions=0 rearm_after_free=0
DFH: kfree(softc=0xfffff8004f296c20 size=288) (CTIO still not held)
DFH: dtor thread: targbhdtor RETURNED after 192 ticks (hz/2=50)                      <-- dtor BLOCKED in the drain, then freed
DFH: dump[cmd] damaged_qwords(out of 36)=0                                           <-- canary fully intact after free
status: ... ctio_held=0 softc_freed=1 postfree=0 rearm=0 victim=1
```

- S2 MAIN on the fixed kernel: no panic, zero post-free driver
  actions, canary intact, the TEARDOWN branch (softc alive) frees the
  desc/atio cleanly, and the dtor demonstrably WAITED for the CTIO
  (192/319 ticks vs the fixed 65 in baseline).
- S0/S1 controls: bit-identical to baseline (65/66 ticks, clean) —
  the normal path is unchanged apart from the counter bookkeeping.

## 4. Threat model / reachability (honest)

- `sys/conf/files:46` — `bus/cam/scsi/scsi_targ_bh.c optional targbh`;
  NOT in X86_64_GENERIC; no `targbh` symbols in the stock kernel
  (`nm /boot/kernel/kernel`), no `/boot/kernel/targbh.ko` on the
  guest. Reaching the driver requires the root admin to build/load it
  (`device targbh` / kldload) AND a target-mode-capable SIM
  (`PIT_PROCESSOR`, targbhasync:203) — none of the QEMU-emulated HBAs
  expose that in DragonFly. So: **no unprivileged reachability on the
  stock system**; no uid0 chain exists to demonstrate. Per protocol
  (root-gated), this closes with the primitive characterization.
- In a root-configured target-mode deployment the trigger divides as:
  the remote initiator keeps one CTIO in flight (stall the DATA
  phase), then teardown happens (SIM dereg / hot-remove / admin).
  The 500 ms budget is attacker-influenced only insofar as the
  initiator controls the stall — the window is trivially > 500 ms.
- Primitive: offset-deterministic read+write-after-free on a 280-byte
  M_SCSIBH chunk (pending_queue head at +0/+8; state at +248),
  groomable (same-chunk canary reclaim demonstrated; M_SCSIBH is a
  dedicated type with a small, driver-controllable population),
  followed by use of the freed `periph` (camperiphfree frees it right
  after the dtor). On INVARIANTS kernels this is a reliable panic
  (remote-triggerable DoS from the initiator position); on
  production kernels it is silent cross-object corruption. The
  write values through TAILQ_REMOVE are NULL/self-pointer (not
  directly arbitrary), but the re-arm branch (:689) then re-submits a
  stale ATIO whose descriptor was freed in the teardown branch of a
  concurrent completion — additional use/double-free surface. Full
  code-execution escalation was NOT developed: reachability is
  root-gated, which caps the severity at Medium (DoS/corruption).

## 5. Residual notes

- abortmode=2 (a SIM that would complete an abort for a ccb it
  already handed to the periph) is a theoretical amplifier not
  exercised by the verdict runs; the conservative SIM behaviour
  (abort refused) is what any conforming SIM does for a ccb it does
  not own.
- The harness `periphfree` command models `camperiphfree` poisoning
  the periph/path after the dtor (S4 amplification); it was not
  needed for the verdict since the in-driver panic already
  demonstrates the UAF at :648.
- Baseline guest-kernel build log was overwritten by the fixed build
  on the guest (`/root/build.log`); the baseline build artifacts in
  this pack are build.log (host-side driver of the same build),
  installkernel.log, and the booted-kernel evidence in run.log.
  The fixed build kept its full 35k-line log (fix_build.log).

## Bottom line

The claim is verified end-to-end on the verbatim driver: the fixed
500 ms sleep is the ONLY teardown barrier, dispatched CTIOs are
invisible to it, the softc (and in the real kernel the periph and
path) are freed while a CTIO is still SIM-owned, and the late
completion dereferences the freed chunk — panicking the stock
INVARIANTS kernel inside targbhdone+0x439 with the chunk reclaimed by
a canary victim. The four-line-counter fix eliminates it (drain
before free), with controls unchanged.

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