# DF-2459 — Re-queued NOP-IN PDU in `_nop_in` causes `bcopy(NULL)` in `isc_sendPDU`

## Verdict
**REPRODUCED** — remote kernel panic (DoS) from a malicious iSCSI target. The bug
is a NULL-pointer read (page fault @0x0) inside `bcopy`/`memmove`, **not** a
write primitive; no escalation chain applies (Phase 6 read-only hard blocker).
Fix **validated** on a rebuilt module.

## Mechanism (trigger → primitive → effect)
1. Same precondition as DF-2460: a privileged user passes a connected iSCSI
   socket to the kernel via `ISCSISETSOC`, starting the receiver thread
   *before* login (`iscsi.c:430`).
2. The malicious target sends a **NOP-IN** PDU (`opcode=0x20`) with
   `itt = 0xffffffff` and `ttt != 0xffffffff`, and a non-zero `AHSLength`
   (e.g. `AHSLength=1` ⇒ 4 bytes of AHS).
3. `so_recv()` (`isc_soc.c:387-389`) sets `pp->ahs_len = bhs->AHSLength*4`
   and reads the AHS bytes into the mbuf chain `pq->mp` — it **never** sets
   `pp->ahs`, which stays `NULL` (from `pdu_alloc`'s `memset`).
4. `_nop_in()` (`isc_sm.c:195`) takes the `itt==-1 && ttt!=-1` branch, mutates
   the BHS into a NOP-OUT (`isc_sm.c:217-227`) and re-queues the *same* PDU via
   `isc_qout()` (`isc_sm.c:228`). Because `pq->len` is non-zero (set by
   `so_recv`), `isc_qout` does **not** call `i_prepPDU` (`isc_sm.c:309`), so the
   stale `pp->ahs_len` survives.
5. `proc_out()` dequeues the NOP-OUT and calls `isc_sendPDU()` (`isc_sm.c:530`),
   whose active (mbuf) implementation does:
   ```c
   if(pp->ahs_len) {                 /* isc_soc.c:120 -- true */
       bcopy(pp->ahs, ..., pp->ahs_len); /* isc_soc.c:125 -- pp->ahs == NULL */
   ```
   `bcopy(NULL, ...)` faults reading address `0x0`.

## Evidence (panic signature, unpatched `#0` kernel)
```
Fatal trap 12: page fault while in kernel mode
fault virtual address	= 0x0
fault code		= supervisor read data, page not present
instruction pointer	= 0x8:0xffffffff80bca9b5
...
Stopped at      memmove+0xb5:   movl    (%rsi),%edx
db>
```
`memmove` (called by `bcopy`) with `%rsi==0` is the `bcopy(pp->ahs==NULL,...)`.
Reproduced by `mtarget nopin` + `idrv`.

## Threat model / privilege boundary
Identical to DF-2460: the attacker is the iSCSI target; the bug fires pre-login,
no auth needed; the privileged `ISCSISETSOC` (root, `/dev/iscsi` is `0600
root:wheel`) is the only thing the victim must do (normal `iscontrol` usage).

## Why no escalation (Phase 6 hard blocker)
NULL-pointer *read* (`bcopy` from `NULL`). No attacker-controlled write
destination, no content control, no object reuse — the read-only / no-write
hard blocker applies; the only effect is a kernel panic (DoS).

## PoC changes
- `mtarget.c`, `idrv.c`: same harness as DF-2460; `nopin` mode sends the
  52-byte NOP-IN (`opcode=0x20, F=1, AHSLength=1, itt=0xffffffff, ttt=1` + 4 AHS bytes).
- `fix.diff` (new): in `_nop_in`'s re-queue branch, clear the stale AHS/data
  length fields (`pp->ahs_len=0`, `pp->ds_len=0`, pointers NULL) and force
  `i_prepPDU` to re-prepare the outgoing NOP-OUT (`pq->len=0`).

## Fix (`fix.diff`)
Root cause: a received PDU is re-queued for sending with `pp->ahs_len` set from
the wire but `pp->ahs == NULL` (AHS lives in `pq->mp`). The outgoing NOP-OUT
keep-alive carries no AHS/data, so the fix drops the stale length fields and
lets `isc_qout→i_prepPDU` recompute `pq->len`/BHS lengths for the NOP-OUT.
**Supersedes** the finding markdown's proposal (which identified the
`bcopy(pp->ahs)` line; this fixes the root cause at the re-queue site).

## Fix validation (rebuilt single module)
- Baseline (unpatched `#0`): PoC panics — `Fatal trap 12 va=0x0
  Stopped at memmove+0xb5` (panic.txt).
- Patched module (sha256 `f71cd799...`): PoC runs clean — `idrv` reports
  *"still alive after 6s (no panic)"*, guest up, no panic in serial log.
