# DF-2461 — `i_prepPDU` truncates PDU length to `u_int`; `maxBurstLength` accepts negative

## Verdict
**REPRODUCED** — a root-only kernel memory-corruption primitive via the
`ISCSISEND` ioctl. The two cited integer defects are both real and confirmed:
`i_setopt()` accepts a **negative** `maxBurstLength` (only `!= 0` is guarded),
which disables `i_prepPDU()`'s E2BIG safety net, and `i_prepPDU()` stores a
64-bit `size_t` length into the 32-bit `pq->len` without an overflow check.
On the default GENERIC kernel (INVARIANTS ON) the resulting corruption
manifests as a deterministic kernel panic. Fix **validated** on a rebuilt module.

### Privilege / impact ceiling (Phase 6 honesty)
- **Trigger requires root.** `ISCSISEND` is dispatched from `iscsi_ioctl()`
  (`iscsi.c:234`) with **no internal privilege check**, but `/dev/iscsi*` is
  created `UID_ROOT, GID_WHEEL, 0600` (`iscsi.c:641,762`), so only root can
  issue it. Root→kernel is game-over by definition (root can `kldload`), so
  this is **not** an unprivileged→root escalation; it is a hardening /
  defense-in-depth defect. There is no unprivileged path: the ISCSISEND
  overflow needs a root-opened fd, and `i_prepPDU`'s outgoing-PDU truncation
  only matters for attacker-controlled `ahs_len`/`ds_len` (the CAM-generated
  SCSI-CMD path uses small fixed sizes). This is the "write reachable only from
  an already-root context" valid hard blocker for an *escalation* claim.
- The demonstrated effect on GENERIC is therefore **panic (DoS)** from a
  privileged user; on a noinv kernel it would be a silent heap/mbuf overflow
  (still root-only).

## Mechanism (the negative-maxBurstLength bypass → mbuf overflow)
1. `i_setopt()` (`isc_subr.c:99`) stores `maxBurstLength` with only an
   `if(opt->maxBurstLength != 0)` guard, so `maxBurstLength = -1` is accepted.
2. `i_prepPDU()` (`isc_sm.c:293`) computes `len` as `size_t`, then tests
   `if(sp->opt.maxBurstLength && (len > sp->opt.maxBurstLength))`. With
   `maxBurstLength == -1` (int) and `len` (size_t), the int is converted to
   `(size_t)0xFFFFFFFFFFFFFFFF`, so the comparison is always false — **E2BIG is
   never returned** for any size.
3. `i_send()` (`iscsi.c:461`) proceeds; `isc_qout()` queues the PDU;
   `proc_out()` → `isc_sendPDU()` (`isc_soc.c:120`) does
   `bcopy(pp->ahs, mh->m_data + mh->m_len, pp->ahs_len)`. The mbuf header has
   only `~MHLEN` bytes of room, so a 70000-byte AHS overflows it.

### The truncation (separate, same routine)
`i_prepPDU()` (`isc_sm.c:291`) does `pq->len = len;` where `len` is `size_t`
(64-bit) and `pq->len` is `u_int` (32-bit, `iscsivar.h:182`). Choosing
`ahs_len=200, ds_len=0xFFFFFF68` makes `len = 0x100000060`, which truncates to
`pq->len = 0x60 = 96`; then `i_send()` does `kmalloc(96-48=48)` (kmalloc-64
chunk) but `copyin(pp->ahs, bp, 200)` — a 152-byte heap overflow. On GENERIC
this is silently corrupted into adjacent slab chunks (copyin traps any
slab-page-boundary fault and returns EFAULT, so no immediate panic unless the
neighbour chunks are later alloc/freed and INVARIANTS catches them).

## Evidence
**Baseline (unpatched `#0`) — negative-maxBurstLength bypass → mbuf overflow panic:**
```
>>> i_prepPDU: 0] pdu len=70000 > 65536     <- test (A): default maxBurst -> E2BIG (correct)
panic: overflowed mbuf 0xfffff801187af400    <- test (A2): maxBurst=-1 -> bypass -> isc_sendPDU overflow
cpuid = 3
m_free() at m_free+0x351
m_freem() at m_freem+0x15
sosend() at sosend+0xf1
isc_sendPDU() at isc_sendPDU+0xa12
ism_proc() at ism_proc+0x3ee
Stopped at Debugger+0x7c
```
(The `(A)` line is the default-maxBurst case correctly returning E2BIG; `(A2)`
sets `maxBurstLength=-1`, bypasses E2BIG, and the oversized AHS overflows the
mbuf header in `isc_sendPDU`.)

Reproduced by `df2461` (root ioctl driver) + `mtarget hold` (keeps the session
socket connected so `i_send` does not short-circuit with ENOTCONN). See
`panic.txt` for the full trace.

## PoC changes
- `mtarget.c` (new): malicious-target harness; `hold` mode keeps the TCP
  connection open so `ISCSISETSOC` sets `sp->soc` for the ISCSISEND path.
- `df2461.c` (new): root ioctl driver demonstrating (A) E2BIG with default
  maxBurst, (A2) the negative-maxBurst bypass, and (B) the size_t→u_int
  truncation. Includes the kernel `iscsi.h` (matches `sizeof(pdu_t)=96`,
  `sizeof(isc_opt_t)=208`) for correct ioctl encoding.
- `fix.diff` (new): two-part fix.

## Fix (`fix.diff`) — two logical changes
1. `isc_subr.c:99` `i_setopt()`: change the guard from `!= 0` to `> 0`, so a
   negative `maxBurstLength` is rejected (the default 65536 is kept). This also
   makes the `len > sp->opt.maxBurstLength` comparison in `i_prepPDU` correct
   (no sign-extension to a huge size_t).
2. `isc_sm.c:291` `i_prepPDU()`: reject `len > 0xffffffff` (E2BIG) before the
   `pq->len = len` truncation, so a wrapped length cannot produce an undersized
   `kmalloc` followed by a full-length `copyin`.

**Matches** the finding markdown's `## Recommended fix` proposal (which
identified both the truncation and the negative-acceptance); this implements
both at the exact lines with return-E2BIG semantics.

## Fix validation (rebuilt single module)
- Baseline (unpatched `#0`): test (A2) → `panic: overflowed mbuf` in
  `isc_sendPDU` (panic.txt).
- Patched module (sha256 `b5181f50...`): all three tests return **E2BIG** cleanly
  — (A) oversized/default-maxBurst → E2BIG; (A2) negative `maxBurstLength`
  rejected (stays default) → E2BIG; (B) wrapped length → E2BIG, dmesg shows
  `">>> i_prepPDU: 0] pdu len=4294967392 exceeds 32 bits"`. No panic, guest up.
