# DF-2462 PoC — ism_stop frees the session while CAM/ic_action and the per-session sysctl handlers can still dereference it (UAF)

* File under test: `sys/dev/disk/iscsi/initiator/isc_sm.c:724-777` (`ism_stop`),
  with the victim paths `isc_cam.c:201-309` (`ic_action`) and
  `isc_sm.c:630-676` (`isc_dump_stats`, `isc_sysctl_targetName/Address`).
* Root cause: `ism_stop()` tears a session down IN PLACE. Between the CAM
  path teardown (`ic_lost_target`, isc_sm.c:752) and `kfree(sp)` (:777) the
  session stays reachable: `sc->sessions[sp->sid]` is cleared only at :772
  and the per-session sysctl OIDs only at :774 (`sysctl_ctx_free`) — and
  neither `ic_action()` nor the sysctl handlers hold a lock or reference
  across their lookup→use, so any reader that already holds `sp` keeps
  dereferencing freed memory. The `xpt_freeze_devq` drain that would stop
  in-flight CAM traffic is `#if 0`'d out in `ic_freeze` (isc_cam.c:57-63).
* Windows (line-accurate):
  * **W-CAM** — from entry into `ism_stop` (the :740-744 xmitter wait loop
    can sleep up to `hz` per iteration; `iscsi_cleanup` does logout I/O)
    until `sessions[sid]=NULL` (:772): `ic_action()`'s unlocked
    `sp = isp->sessions[target_id]` (isc_cam.c:211) returns the dying
    session. After :772 any reader that already loaded `sp` still holds it
    (no refcount) — unbounded.
  * **W-SYSCTL** — the OIDs `...iscsiN.{targetname,targeaddress,stats,douio}`
    stay registered until :774, and their handlers hold raw `sp`
    (`stats`: arg1 = `sp`) or raw `&sp->opt.targetName` pointers; a handler
    entered any time before removal dereferences `sp` with no lifetime
    guarantee. `i_freeopt` (:771) frees the option strings BEFORE
    `sysctl_ctx_free` (:774) removes the OIDs that resolve through them.
  * **W-AGGRAVATOR** — if `sysctl_ctx_free` fails under concurrent readers
    it returns an error that `ism_stop` merely logs (:774-775); the session
    is still kfree'd at :777 with the OIDs left REGISTERED pointing into
    the freed chunk (observed live on the guest: "can't re-use a leaf
    (stats)!" on the next session's registration while an unprivileged
    user hammered the OID).
* Trigger preconditions (real driver): a configured iSCSI session being
  torn down (iscontrol exits / is killed → `iscsi_close` → `ism_stop`,
  iscsi.c:176; module unload → `iscsi_stop` → `ism_stop`, iscsi.c:851)
  while (a) CAM commands to the iscsi bus are in flight, or (b) ANY user
  reads the per-session sysctl OIDs (world-readable, no privilege needed —
  proven as uid 1001 in scenario S6).

## Rig

Kernel-compiled verbatim interposition: `df_pre.c + <verbatim isc_sm.c>
+ <verbatim isc_cam.c: ic_lost_target/_inq/_scsi_encap/ic_action>
+ <verbatim iscsi_subr.c: dwl/scsi_encap> + <verbatim isc_subr.c:
i_freeopt> + df_post.c` compiled into X86_64_GENERIC as
`dev/disk/iscsi/initiator/dfiscsi_harness.c` (sha256-gated against the
audit tree by build.sh).  All interposition is `#define`-renames of
external functions (xpt_async/xpt_free_path/xpt_done shims,
isc_stop_receiver/iscsi_cleanup/i_pdu_flush/release_dev/destroy_dev
shims, `kfree` → free+reclaim-and-poison, `ksnprintf`/`kprintf` →
stall hooks); the driver text is byte-for-byte the gated source.
Everything else is real: lockmgr, TAILQ ops, mtx_init/mtx_uninit,
sysctl_ctx_free, kmalloc/kfree(M_ISCSI), i_freeopt, pdu_alloc, and the
real userland sysctl path for the per-session OIDs.

Stall points (deterministic scheduling of the race):
 * teardown parked inside `ic_lost_target` (the xpt_async shim at
   isc_cam.c:90) — in-window;
 * CAM victim parked in `ic_action` between the sessions[] lookup
   (:211/:215) and the dereferences (:254 / _scsi_encap) — via the
   `debug(4,"func_code...")` kprintf at :217;
 * sysctl victim parked inside `isc_dump_stats` before touching sp — via
   the first `ksnprintf` (isc_sm.c:642).

Poison/reclaim: on the session `kfree`, the harness immediately
reclaims the chunk (same size/type — the zone returns the same address)
and fills it as an attacker-controlled object (FIXUP: valid
mutexes/queues + chosen field values; FAULT: valid mutexes + garbage
queue heads).

## Build / run

```
./build.sh          # gate, push, generate TU, nativekernel, install, reboot
./run.sh            # baseline battery S0..S6, S7, then S5 (kills guest)
./make_fixed.sh     # apply fix.diff on the guest, rebuild, reboot
./run_fixed.sh      # A/B battery on the fixed kernel (S5 must survive)
```

Scenario map:
 * S0 control (live session; values are the A/B reference)
 * S1 in-window concurrent CAM read (teardown parked after ic_lost_target)
 * S2 UAF READ via verbatim ic_action (PATH_INQ reads sp->opt.maxluns
   from the freed chunk → cpi.max_lun)
 * S3 UAF READ/LEAK via verbatim isc_dump_stats (freed-chunk contents
   exported through SYSCTL_OUT)
 * S4 UAF WRITE via verbatim ic_action→scsi_encap→dwl/isc_qout/
   i_nqueue_csnd (writes INTO the freed, attacker-reclaimed chunk)
 * S5 deterministic fault: same as S4 but the reclaim leaves garbage
   queue heads / lun table → kernel page fault (guest dies; baseline
   `Fatal trap 12 ... scsi_encap+0x445`, the `dwl` indexed write
   `sp->target_lun[sp->target_nluns++] = lun` with an
   attacker-controlled 32-bit index)
 * S6 unprivileged userland reads of the session OIDs while the
   teardown is parked in-window (uid 1001)
 * S7 natural race: unpriv userland sysctl hammer against a
   create/teardown loop with garbage-isc reclaim

## Expected on the verbatim kernel

* S0: `PATH_INQ: status=0x1 max_lun=2`, stats `recv=111 sent=222 ...`,
  clean teardown.
* S1: S6: reads succeed against the being-torn-down session
  (`sessions[7]` still set; live values returned to uid 1001).
* S2: `*** UAF READ PROVEN ***` — `cpi.max_lun=0x51515150`
  (0x51515151 planted in the reclaimed chunk).
* S3: `*** UAF READ/LEAK PROVEN ***` — planted values
  (`flags=0x50505050`, `cws/cmd/exp/max/stat/itt = 0x65..-0x6a..`)
  surface in the handler output.
* S4: `*** UAF WRITE PROVEN ***` — ncsnd incremented, csnd.tqh_first
  linked, target_lun[0]/target_nluns written inside the freed chunk.
* S5: guest dies: `Fatal trap 12 ... supervisor write data ...
  Stopped at scsi_encap+0x445: movl %r13d,0x9c(%rbx,%rdx,4)`.
* S7: no natural hit in the runs performed (probabilistic); the
  ctx_free-failure aggravator (stale registered OIDs pointing at the
  freed session) was observed live.

## Expected on the fixed kernel (A/B)

* S0 bit-identical to baseline (same status, same stats string).
* S1: `PATH_INQ: status=0x8 (CAM_DEV_NOT_THERE)` during the window.
* S2/S3/S4: `teardown state after 3s: stop_done=0 free_seen=0` —
  ism_stop is HELD in the reference drain while the victim uses the
  session; victims observe LIVE values only; no freed chunk exists to
  read or corrupt while they run.
* S5: `GUEST SURVIVED` (baseline: Fatal trap 12).
* S6: the unprivileged reads surface no session data (handler returns
  ENODEV via the sid lookup; the sysctl tool prints no value).

## Fix

`fix.diff` (4 files): detach `sc->sessions[sid]=NULL` to the top of
`ism_stop` under `sc->lock`; add `sp->refs` + `isc_sess_lookup()/isc_sess_rele()`;
`ic_action` looks up under `sc->lock` and takes a reference (released
before returning); the per-session sysctl handlers take the sid as
arg1 and resolve the session through `isc_sess_lookup()` (returning
ENODEV once detached); `ism_stop` drains `sp->refs` before destroying
the session and runs `sysctl_ctx_free` before `i_freeopt`.
