# DF-5592 VERDICT — REPRODUCED (use-after-free, deterministic on production symbols)

## Bottom line

`cam_sim_release()` (sys/bus/cam/cam_sim.c:241-256) frees a `struct cam_sim`
that the CAM completion softint (`swi_cambio → camisr`, cam_xpt.c:7389-7407)
still holds a raw pointer to — and, in the second variant, that a lock-free
`xpt_done()` from interrupt context (cam_xpt.c:4901-4917) can still be
inserting into. Reproduced two ways on the stock guest kernel
(6.5-DEVELOPMENT X86_64_GENERIC) with a KLD rig that uses only production
symbols:

1. **Silent UAF with control-flow proof (mode 1).** A marker ccb was queued on
   the dying sim's `sim_doneq` immediately before `cam_sim_free()` (exactly
   what a late interrupt-context `xpt_done()` does at cam_xpt.c:4904-4907).
   After the free + lock release, `camisr_runqueue` popped it and executed
   `(*ccb_h->cbfcnp)(...)` (cam_xpt.c:7506) — a kernel function-pointer call
   dispatched out of a `kfree()`'d object. `marker_hit=1`; the guest kept
   running (silent corruption).

2. **Crash proof (mode 2).** The freed chunk's `sim_doneq.tqh_first` was set
   to `0x00dead000` (freed memory is still mapped — the standard post-UAF
   attacker write). After the lock release, camisr faulted:
   `[Stopped at camisr_runqueue+0x53: movq 0x20(%r15),%rax]` — guest in DDB,
   down. (panic.txt = serial console capture.)

The interleaving is 100% the in-tree detach protocol (ahci_cam.c:210-227:
`lockmgr(ap_sim_lock); xpt_freeze_simq(); xpt_bus_deregister(); cam_sim_free();
lockmgr release`): the rig merely makes the softint's arrival deterministic
(it blocks on the sim lock the detacher holds, which is precisely what happens
in-tree whenever the swi is scheduled after the detach thread acquires the
lock — e.g. under load, on a busy softint CPU, or kldunload during I/O).

## Primitive / exploitation ceiling

* UAF **write** in softint context: `sim->flags &= ~CAM_SIM_ON_DONEQ`
  (cam_xpt.c:7403) is a read-modify-write on the freed chunk (bit clear at
  offset of `flags`).
* `lockmgr()` on the freed embedded `struct lock` (7402) — lock-count/owner
  writes into freed memory.
* `spin_lock(&sim->sim_spin)` + TAILQ walk of the freed `sim_doneq` (7415-17).
* **Function-pointer dispatch** `(*ccb_h->cbfcnp)(path->periph, ccb)` (7506):
  whoever reallocates the 168-byte M_CAMSIM chunk controls `cbfcnp`, `path`,
  and the whole ccb image → arbitrary kernel call with two controlled pointer
  args. On this guest (no SMAP/SMEP/KASLR) that is a straight code-execution
  primitive; the natural trigger class is controller detach (root kldunload /
  virtio device destroy / surprise removal) racing completion, so the
  attacker model is "root-adjacent or physical/hardware event + timing",
  not unprivileged-local. Impact recorded as panic (deterministic crash
  proven); the escalation chain is documented but not weaponized.

## Why this is cam_sim.c's bug

The free decision lives in `cam_sim_release()`. It takes no lock, checks no
`CAM_SIM_ON_DONEQ`, never unlinks from `cam_simq`, and uses a non-atomic
`if (refcount == 1) ... kfree` protocol (the acquisition side,
`xpt_bus_register()`, uses `atomic_add_int` at cam_xpt.c:4322 — the asymmetry
itself). Every neighbor finalizer in cam_xpt.c
(`xpt_release_bus` 4979-5008, `xpt_release_target` 5048-…, `xpt_release_device`
5192-5232) uses atomic cmpset loops precisely because these objects die
concurrently; cam_sim.c predates that discipline.

## Fix validation

* `fix.diff`: (a) `xpt_done()` pins the sim (`atomic_add_int` under
  `cam_simq_spin`) when linking it on `cam_simq`; (b) `camisr()` clears
  `CAM_SIM_ON_DONEQ` for popped sims under the same spin and drops the pin
  with `cam_sim_release(sim, 0)` only after processing+unlock; (c)
  `cam_sim_release()` rewritten as an atomic cmpset loop; the final 1→0
  transition happens in `xpt_sim_finalize()` under `cam_simq_spin` so it
  cannot interleave with the pin acquisition; (d) `cam_dead_sim.refcount`
  pinned high (0x40000000) so the static dead sim can never be finalized.
* Built as `make nativekernel KERNCONF=X86_64_GENERIC` on the guest
  (fix_build.log), installed, rebooted.
* Mode 4 comparison (same rig, same choreography):
  - baseline: `cam_sim_free() returned; chunk refcount now reads 0` (already
    kfree'd while camisr blocked) and `marker_hit=1` on freed memory.
  - fixed: `chunk refcount now reads 1` (pin held ⇒ sim NOT freed),
    `marker_hit=1` with the sim valid+locked, and the chunk only reaches
    kfree after camisr drops the pin — the UAF window is gone; no trap.
  - (fix_run.log / serial capture.)

## Attempt log

1. First build: rig panicked in its own boot scan (`LK_CANRECURSE` missing) —
   rig bug, fixed.
2. First deregistration attempt with 8 attached da devices wedged inside
   xpt_bus_deregister teardown (destroy_dev under sim lock) — rig redesign:
   noattach mode (INQUIRY → CAM_REQ_INVALID), scan-on-demand, probe ccbs
   parked; teardown surface eliminated. (Mode-3 leak run then completed.)
3. Mode 1 first run: race executed but heap-spray reclaim missed the chunk;
   replaced reclaim-marker with the marker-ccb callback (stronger proof, no
   allocator dependence) → reproduced.
4. Mode 2: spray missed again; replaced with direct write of the freed chunk
   (post-UAF standard step) → camisr_runqueue fault, DDB stop.
5. Fix build: `buildkernel` refused (no buildworld stamps on snapshot) →
   `nativekernel` (same as DF-5550's validation).

## Cross-references

* The rig logs incidentally show `cam_devq_release: WARNING active allocations
  8 active send 8!` firing during the detach — the DF-5552 / DF-5594 class
  (devq dropped with active I/O), corroborating that provenance on a real
  deregistration flow.
* DF-5593 (same run): the sim's ccb pool (`ccb_freeq`, 8 ccbs) is orphaned by
  `cam_sim_free()` — measured leak, separate finding.

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