# DF-5240 — xpt_scan_bus() create-path failure branch frees the in-flight XPT_SCAN_LUN CCB and scan_info → kernel UAF (deterministic panic, heap-groomable)

## Where

`sys/bus/cam/cam_xpt.c`, `xpt_scan_bus()` case `XPT_SCAN_BUS`, the
`xpt_create_path()` failure branch (lines 5387–5398):

```c
for (i = 0; i <= max_target; i++) {
        if (i == initiator_id)
                continue;
        status = xpt_create_path(&path, xpt_periph,
                                 request_ccb->ccb_h.path_id, i, 0);
        if (status != CAM_REQ_CMP) {
                kprintf("xpt_scan_bus: xpt_create_path failed ...");
                kfree(scan_info, M_CAMXPT);            /* (1) */
                request_ccb->ccb_h.status = status;
                xpt_free_ccb(&work_ccb->ccb_h);        /* (2) */
                xpt_done(request_ccb);
                break;
        }
        work_ccb = xpt_alloc_ccb();                    /* reassigned here */
        ...
        work_ccb->ccb_h.ppriv_ptr0 = scan_info;        /* (3) */
        xpt_action(work_ccb);
}
```

## What is wrong

When a bus scan is run in **parallel** mode (`hba_misc & PIM_SEQSCAN == 0`)
and `xpt_create_path()` fails for target `i > 0` (e.g. the SIM is being
deregistered concurrently — USB umass unplug during boot rescan /
`camcontrol rescan` racing detach — which makes `xpt_alloc_device()`
return NULL via `CAM_SIM_DEREGISTERED`, cam_xpt.c:5083):

* (2) `work_ccb` **is not the PATH_INQ CCB any more** — it was reassigned
  at line 5400 and now *is* the XPT_SCAN_LUN CCB issued for target 0,
  which is **still owned by the probe peripheral** (queued in
  `probe_softc->request_ccbs`) and still in flight. Freeing it here is a
  use-after-free / double-ownership by construction. `xpt_free_ccb()`
  NULLs `ccb_h.timeout_ch` before the kfree (cam_xpt.c:4941-4947), so the
  freed CCB carries `timeout_ch == NULL`.
* (1) `scan_info` is freed while every already-issued SCAN_LUN CCB still
  references it through `ccb_h.ppriv_ptr0` (3). The completion path
  (`xpt_scan_bus()` case `XPT_SCAN_LUN`) then reads
  `scan_info->cpi->max_lun` (cam_xpt.c:5483), decrements
  `scan_info->counter` (cam_xpt.c:5499 — **UAF write**), and at scan
  termination would `xpt_free_ccb(&scan_info->cpi->ccb_h)` again.

## Reproduced

Trigger: a small kld (`evilsim.c` in this directory) registers a fake SIM
(3 targets, parallel scan), stalls the first INQUIRY of target 0's probe
and sets `CAM_SIM_DEREGISTERED` at that moment, so the scan loop's
`xpt_create_path()` for target 1 fails → the buggy branch frees the
in-flight CCB and `scan_info` → the module then completes the stalled
INQUIRY (spraying the freed 24-byte `scan_info` chunk with 0x41) → the
probe completes → `probedone()` pops the SCAN_LUN CCB from
`softc->request_ccbs` and `xpt_done()`s it → the SWI invokes
`xpt_scan_bus()` on the **freed** CCB.

Observed (3/3 runs, one with DDB backtrace):

```
[EVILSIM] stalled INQUIRY for target 0 lun 0, SIM marked DEREGISTERED
xpt_scan_bus: xpt_create_path failed with status 0x34, bus scan halted
[EVILSIM] back from XPT_SCAN_BUS: stall_seen=1 (scan_info has been freed ...)
[EVILSIM] sprayed 128 x 24-byte chunks with 0x41; scan_info->cpi is now 0x4141414141414141
[EVILSIM] completing stalled INQUIRY -> expect page fault at 0x41414141414141xx NOW
[EVILSIM] XPT_SCAN_BUS request completed (create_path failure branch taken)
(probe0:evilsim0:0:0:0): inquiry data fails comparison at DV1 step   <-- DF-5241 also fired here
(da0:evilsim0:0:0:0): got CAM status 0x6
...
Fatal trap 12: page fault while in kernel mode
fault virtual address	= 0x10
fault code		= supervisor write data, page not present
Stopped at      _callout_init:  movq    $0,0x10(%rdi)

db> trace
_callout_init() at _callout_init
xpt_setup_ccb() at xpt_setup_ccb+0x2e
xpt_scan_bus() at xpt_scan_bus+0x3f          <-- case XPT_SCAN_LUN, freed CCB
camisr_runqueue() at camisr_runqueue+0x141
swi_cambio() at swi_cambio+0x10f
ithread_handler() at ithhread_handler+0x2fe
```

`xpt_setup_ccb()` does `callout_init(ccb_h->timeout_ch)`
(cam_xpt.c:3906); the faulting write to `0x10(%rdi=NULL)` proves the CCB
being restarted is the one freed by the failure branch (`timeout_ch`
NULLed by `xpt_free_ccb`).

## Impact

Kernel use-after-free reachable whenever a bus scan races SIM
deregistration (unplug of a USB/FW/SCSI device during boot-time scan,
`camcontrol rescan` racing detach). Deterministic kernel panic with a
trivial reproducer; with heap grooming the freed `union ccb` (~large
M_CAMXPT chunk) and the 24-byte `scan_info` (counter write, dangling
`cpi` deref) give corruption primitives beyond the panic.

## Fix

See `fix.diff`: the failure branch must (a) free the PATH_INQ CCB /
`scan_info` only when nothing was issued yet (i == 0), and (b) when CCBs
are in flight, account the never-issued CCBs out of `scan_info->counter`
and NULL `scan_info->request_ccb` so the last completing CCB performs
the cleanup exactly once. Validated: with the fix applied, the same PoC
run completes without any trap (see VERDICT.md).

## Fix validation (2026-10-01, in-guest)

| Kernel | PoC outcome |
|---|---|
| `#0` stock (vm.sh snapshot) | **Fatal trap 12** (write va 0x10 in `_callout_init` via `xpt_setup_ccb <- xpt_scan_bus <- camisr_runqueue`), 3/3 runs — `run.log`, `panic.txt` |
| `#1` built with `fix.diff` (`make -j6 nativekernel`, rc=0, 35761 lines, 0 errors under -Werror; `make installkernel`, reboot) | **No trap.** All choreography markers print (stall, `xpt_create_path failed with status 0x34`, 128×0x41 spray, INQUIRY completion), then `[EVILSIM] survived 10s: NO PANIC (UAF did not trigger)` — `run.patched.log` |

The previously-observed bad behavior (kernel page fault on the freed CCB)
is gone with the fix and only the fix applied.


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