# DF-2285 — `scsi_targ_bh.c` teardown UAF — evidence pack

Finding: Use-after-free in target teardown — `targbhdtor()` frees
`softc` after a fixed `hz/2` (500 ms) sleep with no wakeup protocol,
while CTIOs dispatched by `targbhstart()` are neither tracked nor
aborted (`targbhdislun()` aborts only idle ATIOs / immediate notifies).
A CTIO still owned by the SIM completes into `targbhdone()` which
dereferences the freed `softc` (`TAILQ_REMOVE` on `pending_queue` at
scsi_targ_bh.c:648 — read AND write through `&softc->pending_queue` —
plus `softc->state` read at :678 steering the re-arm branch at :689).

## Verdict (short)

REPRODUCED on the stock (INVARIANTS) guest kernel, mechanically, with
the verbatim driver code compiled into the kernel: teardown with a
held CTIO frees the softc after exactly the fixed 500 ms, the late
CTIO completion then executes `targbhdone` against the freed (and
same-chunk-reclaimed, 0x41-canary-filled) softc and the kernel panics
with `Bad link elm ... prev->next != elm` from inside `targbhdone+0x439`
(the INVARIANTS `QMD_TAILQ_CHECK_PREV` gate catching the
use-after-free read at scsi_targ_bh.c:648). Controls (no CTIO in
flight; CTIO completed before teardown) are clean. The A/B fixed
kernel (fix.diff: outstanding-CTIO counter + wakeup drain) blocks the
dtor until the CTIO completes, runs the completion with the softc
alive, and shows ZERO post-free driver actions with the canary fully
intact — no panic.

Reachability is root-gated (see VERDICT.md): `scsi_targ_bh.c` is
`optional targbh` (sys/conf/files:46), absent from X86_64_GENERIC, no
`/boot/kernel/targbh.ko` on the stock guest, and requires a
target-mode-capable SIM (`PIT_PROCESSOR`) — so no unprivileged path;
impact is a demonstrated kernel UAF/panic primitive in
root-configured target-mode deployments (CVSS AV:A/AC:H).

## Rig

The established CAM verbatim-interposition kernel-compiled harness
(same pattern as the DF-2270/scsi_ch rig):

- `rig/df_pre.c` — preprocessor-interposes every external the verbatim
  driver calls (`xpt_action`, `xpt_alloc_ccb`, `xpt_setup_ccb`,
  `kmalloc`/`kfree`, ...). Nothing in the driver is edited.
- verbatim `sys/bus/cam/scsi/scsi_targ_bh.c` (sha256-gated by
  `rig/check_verbatim.sh`; audit-tree hash
  `4bb4bca6b3a2b0eb84a737d2251752f4c3ebd0c8d5e7c16ffb7e99411e49307b`;
  fixed-build hash `4103058df2c3bdccecb87a5ee57251b1008a0cc0ffb1b6d511a5d2fe7de97fc3`).
- `rig/df_post.c` — implements the shims: fabricated periph / path /
  sim (with a REAL lockmgr sim-lock so the REAL `sim_lock_sleep()` in
  `targbhdtor` executes), a fake target-mode SIM that parks resource
  ATIOs/INs, **holds a dispatched CTIO indefinitely** (stalled
  initiator), aborts idle CCBs, and refuses to abort a SELECTED ATIO
  it no longer owns; and a dtor thread pinned to the ctor CPU that,
  immediately after `targbhdtor()` returns, reclaims the just-freed
  softc chunk with a 0x41 canary victim (same per-CPU slab zone,
  deterministic LIFO) so post-free driver touches are visible and any
  write lands in a live replacement object.
  Note: an earlier harness revision poisoned freed chunks with
  `memset(0xDE)` after `kfree`; DragonFly chains free chunks through
  the chunk itself (kern_slaballoc.c), so that scribble is a harness
  artifact (delayed `chunk_mark_free` assertion) and was removed —
  post-free evidence is the canary victim + the panic backtrace.
- `rig/gen.sh` concatenates pre + verbatim + post into
  `bus/cam/scsi/dftbh_harness.c` and registers it as a `standard`
  kernel file (`device targbh` is NOT in X86_64_GENERIC, so the
  verbatim TU is the only compiled instance).
- `rig/build.sh` builds/installs/reboots X86_64_GENERIC on the guest
  (INVARIANTS kernel, `opt_global.h: #define INVARIANTS 1`).

Commands on the guest: `sysctl debug.dftbh.cmd=<ctor|inquiry|dtor|
dtorwait|complete|reclaim|periphfree|dump|status|abortmode=N>`.

## Build & run

```
# from findings/poc/DF-2285/rig on the audit host, guest up:
./build.sh                  # full log -> build.log (guest side: /root/build.log)
./run.sh                    # S0, S1 controls + S2 MAIN -> ../run.log + ../panic.txt
# A/B fix validation:
#   (reset guest to with-src, re-push rig, apply ../fix.diff)
./run_fixed.sh              # same scenarios on fixed kernel   -> ../fix_run.log
```

## Observed (baseline, stock INVARIANTS kernel)

- S0 CONTROL-1 (no I/O): teardown aborts 8 idle ATIOs + 8 INs, sleeps
  65 ticks, `kfree(softc)` — canary victim intact, no panic.
- S1 CONTROL-2 (CTIO completed BEFORE teardown): normal re-arm, then
  clean teardown — no panic.
- S2 MAIN: CTIO held at the fake SIM; `targbhdtor` returns after the
  fixed 65 ticks with the CTIO STILL HELD (`kfree(softc=... size=280)
  (CTIO still HELD at fake SIM)`); the freed chunk is reclaimed
  same-CPU as a 0x41 canary victim (SAME CHUNK); pre-completion dump:
  fully 0x41; the completion fires 699 ticks after dispatch
  (>> hz/2=50, attacker-controlled) and the kernel panics:
  `panic: Bad link elm 0xfffff8008db00cc0 prev->next != elm` with
  backtrace `targbhdone+0x439` — panic.txt.

## Observed (fixed kernel, fix.diff applied)

- S2 MAIN: dtor thread blocks in the `targbhctio` drain; the
  completion runs with `softc_freed=0` (TEARDOWN branch, no re-arm,
  desc/atio freed cleanly); dtor returns after 192 ticks (drained);
  post-free canary dump: `damaged_qwords=0`, `postfree=0`,
  `rearm=0` — no panic.
- S0/S1 controls bit-identical to baseline (65/66 ticks, clean).

## Files

- `rig/` — harness sources + gen/build/run scripts (see above)
- `fix.diff` — maintainer-shaped fix (outstanding-CTIO counter +
  wakeup drain in targbhdtor; increment in targbhstart before
  xpt_action; decrement+wakeup in targbhdone CTIO completion)
- `build.log`, `installkernel.log` — baseline build/install (host side)
- `fix_build.log`, `fix_installkernel.log`, `fix_gen.log` — fixed build
- `run.log` — full untrimmed baseline scenario dmesg (S0/S1/S2)
- `panic.txt` / `panic.serial.log` — baseline S2 panic (serial console)
- `fix_run.log` — full untrimmed fixed-kernel scenario dmesg
- `env.txt`, `patched_env.txt` — guest environment (baseline/fixed)
- `VERDICT.md`, `manifest.json`, `verdict.json` — verdict + machine record
