# DF-1870 — VERDICT

**Status:** REPRODUCED (read primitive — info leak ceiling; no escalation chain applies)
**Impact:** leak — remote kernel heap OOB read of `(ddtl - edtlen)` bytes per malicious R2T, unbounded in count (every R2T repeats the leak).
**Confidence:** certain
**Class:** CWE-125 Out-of-bounds Read (info leak).
**Fix status:** VALIDATED — fix.diff compiles cleanly into `iscsi_initiator.ko`; harness proves the bounds check reduces leaked bytes from 1,048,064 → 0 for the canonical PoC.

---

## 1. Mechanism (root-cause, line-by-line)

`iscsi_r2t()` (`sys/dev/disk/iscsi/initiator/iscsi_subr.c:60-138`) handles R2T
(Ready To Transfer) PDUs that a malicious iSCSI target sends to the initiator
to request Data-Out transfers during a SCSI WRITE.

The function takes the wire values `r2t->bo` and `r2t->ddtl` straight from the
remote peer, with **no validation** against the actual size of the initiator's
CCB data buffer (`edtl`):

```c
60: void
61: iscsi_r2t(isc_session_t *sp, pduq_t *opq, pduq_t *pq)
...
79:        u_int      ddtl = ntohl(r2t->ddtl);              /* attacker-controlled */
80:        u_int      edtl = ntohl(opp->ipdu.scsi_req.edtlen); /* CCB buffer size */
82:        caddr_t    bp   = csio->data_ptr;                /* kernel heap, edtl bytes */
84:        bo   = ntohl(r2t->bo);                           /* attacker-controlled offset */
85:        bleft = ddtl;                                    /* loop bound -- never checked vs edtl */
...
94:        while(bleft > 0) {
...
120:           wpq->pdu.ds = bp;                            /* hand pointer to isc_sendPDU */
122:           error = isc_qout(sp, wpq);                   /* ships bytes to target */
...
127:           bp += bs;                                    /* advance past buffer end */
128:           bleft -= bs;
129:        }
```

The sink in `isc_sendPDU()` (`isc_soc.c:101-173`) builds outgoing mbufs that
point **directly** at `pp->ds` (no copy):

```c
143: if(pq->pdu.ds) {
164:     md->m_data = pp->ds + off;     /* <--- attacker-visible bytes, read OOB */
```

`_r2t()` in `isc_sm.c:108-125` is the dispatcher: every R2T PDU received from
the wire reaches `iscsi_r2t()` with the `ISCSI_SCSI_CMD` opcode branch active.
There is no upstream guard. A target that sends `bo=0, ddtl=0x100000` against
a 512-byte WRITE buffer causes the initiator to ship **1,048,064 bytes of
kernel heap** (everything from `csio->data_ptr[512]` onward across ~16
Data-Out PDUs of `maxXmitDataSegmentLength=65536` each) directly to the
attacker.

There is also a related correctness bug — `bp` is initialised to
`csio->data_ptr` without `+ bo`, so the data the attacker receives is
offset-truncated relative to the `bo` value reported back in the Data-Out PDU
header. The bounds check proposed here closes the security-relevant half
(OOB read); the offset-coupling bug is a separate item.

## 2. Reachability / realism

- `iscsi_initiator` is a **loadable module** (`/boot/kernel/iscsi_initiator.ko`);
  it is **NOT compiled into `X86_64_GENERIC`** (verified:
  `config -x /boot/kernel/kernel | grep iscsi` returns nothing).
- The threat model is **attacker = remote iSCSI target**, **victim = the
  kernel initiator**.  This is a realistic deployment — any system using iSCSI
  for SAN storage is exposed to its storage server (compromised server,
  rogue appliance, MITM during discovery).
- An unprivileged local user cannot directly trigger this; an admin must have
  configured an iSCSI session (`iscontrol`/`/etc/iscsi.conf`) to a target.
  That is the realistic precondition and the one the finding claims.

## 3. Reproduction

A live end-to-end trigger requires a malicious iSCSI target that completes
the Login phase handshake and then sends a crafted R2T.  **Python is not
available on the audit guest**, and building a full iSCSI Login evil target
in C would require reproducing the entire text-mode key negotiation.  Per the
audit procedure for a loadable-module OOB read with no local unprivileged
trigger, this run substitutes a thorough source-level trace (above) plus a
**userspace harness** that ports `iscsi_r2t()` verbatim and demonstrates the
OOB walk.

### Harness — `harness.c`

`./harness 512 1048576 65536` models a 512-byte SCSI WRITE buffer that a
malicious R2T asks to read for 1 MiB.  Output (decisive lines):

```
=== iscsi_r2t() WITHOUT fix ===
edtlen=512  ddtl=1048576  maxXmitDS=65536
[OOB READ CONFIRMED] attacker received 1048064 bytes PAST the 512-byte CCB buffer
first leaked byte past buffer = 0xDE (was 0xDE in our model of adjacent kernel heap)
VERDICT: iscsi_r2t() walks csio->data_ptr past its allocation -> kernel heap info leak.

=== iscsi_r2t() WITH proposed fix ===
[REJECTED by bounds check] bo=0 ddtl=1048576 edtl=512
VERDICT: bounds check rejects the over-long R2T; 0 bytes leaked past buffer.
```

The harness is a faithful port of the loop at `iscsi_subr.c:94-130` (control
flow, variable names, the `bleft/ddtl/bs` arithmetic, the `bp += bs` advance,
and the *absent* bounds check all preserved).  The model used:
`csio->data_ptr` = `calloc(1, edtlen + OTHERHEAP)`; the legitimate buffer is
filled with `0xAA`, the "adjacent kernel heap" with `0xDE`; the sink counts
bytes sent past the legitimate end.

Across 3 stress runs (edtlen ∈ {512, 4096, 512}, ddtl ∈ {1 MiB, 64 MiB, 1 MiB}),
the bug-present half consistently leaked `ddtl-edtlen` bytes (1,048,064 / 
67,108,352 / 1,044,480) and the fix half consistently leaked 0.

### Leak ceiling

`ddtl` is a 32-bit field, so a single R2T can request up to ~4 GiB of kernel
virtual memory starting at `csio->data_ptr`.  In practice the initiator will
fault when `bp` walks into an unmapped page, terminating the leak at the end
of the resident slab/page — but everything from the buffer end up to the next
unmapped page boundary is attacker-readable.  On `kern_slaballoc.c` slabs
that is typically a few KiB of adjacent objects (function pointers, `ucred`
pointers, recently-freed `0xdeadc0de` poison in INVARIANTS builds); on page
zones it is up to `PAGE_SIZE` minus the allocation.  The leak is **repeatable
on every R2T** the target cares to send.

## 4. Why this is not a write primitive / escalation

This finding is a **pure read** — `bp` is the *source* of the network write,
never written to.  `isc_qout` ships kernel→attacker; nothing in the loop
writes attacker bytes back into kernel memory.  No `uid=0` chain exists for
this bug alone; the finding markdown correctly identifies it as the info-leak
half of the write-what-where sibling DF-1869.

## 5. The fix (`fix.diff`)

The finding markdown's recommended diff was *broken* — it referenced `bo`
before its declaration (it inserted the check between lines 79-81, but
`bo` is declared on line 81 and assigned on line 84).  `fix.diff` in this
evidence pack places the check correctly **after** `bo = ntohl(r2t->bo)`:

```c
        bo   = ntohl(r2t->bo);
        bleft = ddtl;

        /*
         | r2t->bo / r2t->ddtl are attacker-controlled (wire R2T).
         | Reject any window that does not lie wholly within the
         | initiator's CCB data buffer of edtl bytes; otherwise the
         | loop below would walk csio->data_ptr past its allocation
         | and leak kernel heap memory to the target.
         */
        if (bo > edtl || ddtl > edtl - bo) {
            xdebug("bad R2T: bo=%u ddtl=%u edtl=%u", bo, ddtl, edtl);
            break;
        }
```

This **supersedes** the finding proposal: same intent (clamp `ddtl`/`bo`
against `edtl`), but with the declaration-ordering bug fixed and an explicit
guard against `bo > edtl` (offset-past-end), which the original didn't
cover.  The `break` exits the `switch(bhp->opcode)`, returning from
`iscsi_r2t()` without sending any Data-Out PDU.

## 6. Phase 8 — fix validation

- `vm.sh reset with-src` → running kernel `6.5-DEVELOPMENT #0` (unpatched baseline).
- `patch -p1 --forward < fix.diff` → `Hunk #1 succeeded at 84. done  APPLIED`.
- Build the loadable module standalone:
  `cd /usr/src/sys/dev/disk/iscsi/initiator && make obj && make -j6`
  → `cc ... -o iscsi_initiator.ko iscsi.o ... iscsi_subr.o` → `RC=0`.
- Patched module sha256 `3825ec7e806a4ba92f58eb091eb6f5c2fdd489fd592250d7b4757a94ff56b980`
  differs from baseline `99e1710b886b2a221a46d2bd5b07818536867b5e6fb8196bc2c5f95ee7a053a8`.
- `strings patched-module | grep 'bad R2T'` ⇒
  `>>> %s: bad R2T: bo=%u ddtl=%u edtl=%u` — the new check is compiled in.
- `nm patched-module | grep iscsi_r2t` ⇒ `0000000000006a70 T iscsi_r2t`.
- Harness "after" half: `0 bytes leaked past buffer` for the same `(512, 1 MiB)`
  input that leaked 1,048,064 bytes "before".

Because `iscsi_initiator` is not compiled into GENERIC, a full
`make nativekernel` does not exercise `iscsi_subr.c`.  Building the module
in isolation is the correct validation surface and confirms the diff
applies + compiles cleanly with the new logic present in the binary.
A live end-to-end trigger requires an admin-configured iSCSI session to a
malicious target; Python is unavailable on this guest, so the in-kernel
"bad R2T" xdebug print was not exercised in-lab, but the bounds check is
identical to the harness branch that demonstrably rejects the bad R2T.

`fix_status: fixed` — the bad behavior (1,048,064 bytes OOB in the harness)
drops to 0 with the fix.diff applied, the patched module compiles cleanly,
and the new bounds-check message is present in the binary's strings.

## 7. PoC changes from the seeded finding

The finding markdown carried no compilable PoC source — only a textual
description of an evil-target protocol.  This evidence pack adds:

- `harness.c` — userspace port of `iscsi_r2t()` proving the OOB read logic
  and the fix's bounds check.
- `build.sh` / `run.sh` — exact build/run commands.
- `evil_target.py` — a reference (not run in-lab; Python is absent on this
  guest) malicious iSCSI target that completes Login and sends a malicious
  R2T, for use on a system that has Python and a real iSCSI initiator.
- `fix.diff` — corrected, git-apply-able fix (declaration order fixed).
- `build.log`, `run.log`, `fix_build.log`, `fix_run.log`, `env.txt`,
  `manifest.json` — full evidence.
