# DF-2460 — `_reject` dereferences `pq->mp` without a NULL check

## Verdict
**REPRODUCED** — remote kernel panic (DoS) from a malicious iSCSI target. The bug
is a NULL-pointer read (page fault), **not** a write primitive, so no privilege
escalation chain applies; the realistic impact ceiling is a remote denial of
service against any host running the iscsi_initiator module that connects to a
malicious/compromised iSCSI target. Fix **validated** on a rebuilt module.

## Mechanism (trigger → primitive → effect)
1. A root user starts an iSCSI session and hands a connected TCP socket to the
   kernel via the `ISCSISETSOC` ioctl. `i_setsoc()` (`sys/dev/disk/iscsi/initiator/iscsi.c:413`)
   immediately calls `isc_start_receiver()` (`iscsi.c:430`), which spawns the
   `isc_soc` receiver thread (`isc_soc.c:568`). The receiver runs **before**
   iSCSI login completes, so it processes whatever the target sends next.
2. The malicious target sends a 48-byte **REJECT** PDU (`opcode=0x3f`) with
   `AHSLength=0`, `DSLength=0` and no digests (the default, no CRC32C).
3. `so_recv()` (`isc_soc.c:365`) computes `len = 0` for such a PDU and therefore
   **skips** the `if(len){...}` block that assigns `pq->mp = sbp.sb_mb`
   (`isc_soc.c:428-481`); `pq->mp` stays `NULL` (set by `memset` in
   `pdu_alloc`, `iscsivar.h:321`).
4. `ism_recv()` dispatches the REJECT to `_reject()` (`isc_sm.c:448` →
   `isc_sm.c:79`). The very first statement dereferences the NULL mbuf:
   ```c
   pdu = mtod(pq->mp, pdu_t *);   /* isc_sm.c:87 */
   ```
   `mtod(m,t)` expands to `(t)((m)->m_data)`; `m_data` lives at offset `0x10`
   in `struct mbuf`, so the kernel reads from address `0x10` → page fault.

## Evidence (panic signature, unpatched `#0` kernel)
```
Fatal trap 12: page fault while in kernel mode
fault virtual address	= 0x10
fault code		= supervisor read data, page not present
Stopped at      ism_recv+0xee:  movq    0x10(%rdx),%rdx
db>
```
The faulting instruction (`movq 0x10(%rdx),%rdx` with `%rdx==0`) is exactly the
`mtod(NULL,...)` read of `m_data` at offset `0x10`. Reproduced by
`mtarget reject` (sends the REJECT) + `idrv` (passes a connected socket to the
kernel via `ISCSISETSOC`).

## Threat model / privilege boundary
- The attacker is the **iSCSI target** (a malicious or compromised storage
  server). The victim is the kernel of any host whose `iscsi_initiator` connects
  to it. No authentication is required by the iSCSI default (RFC 3720), and the
  bug fires **before login completes**, so the malicious PDU needs no valid
  session state.
- Triggering requires `ISCSISETSOC`, which needs `open("/dev/iscsi")` — the
  device is `UID_ROOT, GID_WHEEL, 0600` (`iscsi.c:641,762`), so a privileged
  user must initiate the connection. That is the normal operational model
  (`iscontrol(8)` runs as root). Once a session exists, the attacker (target)
  controls the damage; the boundary crossed is network → kernel.

## Why no escalation (Phase 6 hard blocker)
The primitive is a **read from a fixed low address (`NULL + 0x10`)** — it is a
NULL-pointer dereference, not an attacker-controlled write. There is no
attacker-influenced destination, no content control, and no freed/reused object.
This is the "genuinely read-only / no write primitive" valid hard blocker from
Phase 6: the only achievable effect is crashing the kernel. (The controlled
fault address `0x10` is not mappable on this kernel and conveys no primitive.)

## PoC changes
- `mtarget.c` (new): minimal TCP server that injects a crafted PDU per mode.
  For DF-2460, sends a 48-byte REJECT BHS (`opcode=0x3f, F=1, AHS=0, DS=0`).
- `idrv.c` (new): minimal initiator driver — `open /dev/iscsi`,
  `ISCSISETSES`, `open /dev/iscsiN`, `socket()+connect()`, `ISCSISETSOC` — to
  start the kernel receiver without performing iSCSI login, exactly the
  precondition `i_setsoc()` creates.
- `fix.diff` (new): NULL-check `pq->mp` in `_reject` and drop the PDU.

## Fix (`fix.diff`)
Add a NULL guard at `sys/dev/disk/iscsi/initiator/isc_sm.c:87` (`_reject`):
if `pq->mp == NULL` (data-less REJECT), log via `xdebug` and `pdu_free()` the
PDU instead of `mtod()`-dereferencing it. Without the data segment the rejected
task tag cannot be recovered, so dropping the PDU is the correct safe action.
**Supersedes** the finding markdown's proposal (the markdown suggested a NULL
check; this implements it at the exact faulting line with the drop semantics).

## Fix validation (rebuilt single module, not a full kernel)
- Baseline (unpatched `#0` module): PoC panics the guest — `Fatal trap 12`,
  `fault virtual address = 0x10`, `ism_recv+0xee`. (panic.txt)
- Patched module (rebuilt `iscsi_initiator.ko`, reinstalled, `kldload`ed,
  sha256 `b1e16868...`): PoC runs clean — `idrv` reports
  *"still alive after 6s (no panic)"*; `dmesg` shows
  *"`>>> _reject: 0] REJECT with NULL mbuf (no data segment) - dropping`"*.
- The module rebuild was used because this is a loadable KLD (`bsd.kmod.mk`),
  so a full nativekernel rebuild is unnecessary; the patched `.c` is compiled
  with the same flags/KERNCONF and linked into the module.
