# DF-1869 — VERDICT

| Field          | Value                                                        |
|----------------|--------------------------------------------------------------|
| **Verdict**    | REPRODUCED (primitive confirmed; remote-only reachability)   |
| **Status**     | reproduced                                                   |
| **Impact**     | corruption (write-what-where; remote RCE ceiling in threat model) |
| **Confidence** | certain                                                      |
| **Class**      | CWE-787 Out-of-bounds Write                                  |
| **Kernel**     | DragonFly 6.5-DEVELOPMENT #0 (master DEV, X86_64_GENERIC, INVARIANTS ON) |

## 1. The bug (line-by-line source confirmation)

In `sys/dev/disk/iscsi/initiator/iscsi_subr.c`, `scsi_decap()`, the
`ISCSI_READ_DATA` branch handles a SCSI Data-In PDU coming back from the
iSCSI **target**. The relevant lines:

```c
566:  if(ntohl(cmd->edtlen) >= pq->pdu.ds_len) {     // ONLY check: segment length
567:       int     offset, len = pq->pdu.ds_len;     // signed int offset
...
572:       offset = ntohl(rcmd->bo);                 // bo = attacker-controlled (u_int, iscsi.h:256)
573:       dp = csio->data_ptr + offset;             // NO bounds check vs edtlen
574:       i_mbufcopy(pq->mp, dp, len);              // writes len attacker bytes at offset
```

- `cmd->edtlen` is set at `iscsi_subr.c:516` to `htonl(csio->dxfer_len)` — the size
  of the initiator's kernel-side CCB data buffer (`csio->data_ptr`).
- `rcmd->bo` is the iSCSI Data-In PDU's "Data Offset" field (`data_in_t.bo`,
  `u_int`, `iscsi.h:256`), fully controlled by the target on the wire.
- The only guard is `edtlen >= ds_len` (segment length fits) — there is **no**
  `offset + ds_len <= edtlen` check.
- `offset` is declared `int` (`iscsi_subr.c:567`); assigning `u_int bo` whose
  high bit is set sign-extends to a negative ptrdiff_t, so `dp = csio->data_ptr + offset`
  writes **below** the buffer. Large positive `bo` writes **above** it.
- `i_mbufcopy()` (`iscsivar.h:565`) then copies `len = ds_len` attacker bytes
  from the PDU's mbuf chain into `dp` — a deterministic **write-what-where**
  with both offset and content controlled by the remote target.

## 2. Reproduction — userspace harness (faithful port)

Because `iscsi_initiator` is a *loadable* module (NOT in `X86_64_GENERIC`) and
the trigger requires an established iSCSI session (which an unprivileged user
cannot create — see §4), the primitive is reproduced with a faithful userspace
port of the exact kernel arithmetic in `harness.c`. It:

1. Allocates an `edtlen`-sized buffer (`csio->data_ptr` analogue) flanked by
   4096-byte REDZONE (0xA5) canaries on each side.
2. Replays the kernel `if (edtlen >= ds_len)` check, the `offset = ntohl(bo)`
   assignment, the `dp = buf + offset` arithmetic, and `i_mbufcopy`.
3. Counts every redzone byte that is no longer 0xA5 after the copy (= an
   attacker byte written outside the legitimate CCB buffer).

### Build & run

```
sh build.sh        # cc -O2 -Wall -o harness harness.c  (+ harness_fixed)
sh run.sh          # runs both, compares
```

### Result (decisive)

UNFIXED (verbatim kernel logic):

```
[in_bounds_bo=0_ds=512]          -> in-bounds (no OOB)        # legal case still works
[OOB_pos_bo=0x200_ds=0x100]      -> OOB WRITE  oob_bytes=256  # positive overrun
[OOB_pos_bo=0x300_ds=0x80]       -> OOB WRITE  oob_bytes=128  # large positive overrun
[OOB_neg_bo=0xFFFFFC00_ds=16]    -> OOB WRITE  oob_bytes=16   # NEGATIVE offset (sign-ext)
[OOB_pos_bo=0x200_ds=16_pattern] -> OOB WRITE  oob_bytes=16   # crafted bytes (slab-groom)
SUMMARY mode=UNFIXED  total_oob_bytes=416  exit=1
```

FIXED (`offset+len <= edtlen` guard):

```
[in_bounds_bo=0_ds=512]          -> in-bounds (no OOB)        # legal case unaffected
[OOB_pos_bo=0x200_ds=0x100]      -> REJECTED by fix
[OOB_pos_bo=0x300_ds=0x80]       -> REJECTED by fix
[OOB_neg_bo=0xFFFFFC00_ds=16]    -> REJECTED by fix           # negative offset also rejected
[OOB_pos_bo=0x200_ds=16_pattern] -> REJECTED by fix
SUMMARY mode=FIXED  total_oob_bytes=0  exit=0
```

All 4 attacker offset/byte combinations write 416 bytes outside the CCB
buffer when unfixed; the proposed fix rejects all four while leaving the
legal in-bounds case untouched. Reproduced 3× — fully deterministic.

## 3. PHASE 6 — escalation analysis (the honest answer)

This is a **memory-corruption primitive** (write-what-where), so the
audit's primary question is: *can unprivileged maxx turn this into uid=0?*

### Primitive characterization

- **Write capability**: arbitrary `len` bytes (len = `ds_len`, ≤ edtlen),
  attacker-controlled content (PDU data segment), at attacker-controlled
  offset (`bo`), into a kernel heap allocation of size `edtlen` whose start
  address is `csio->data_ptr`.
- **Allocation**: `csio->data_ptr` is a CAM-periph kmalloc of size
  `csio->dxfer_len` (e.g. `scsi_da.c` uses `M_SCSIDA`/`M_DEVBUF`).
  A 512-byte SCSI read → kmalloc-512 bucket; a 4 KB read → page zone, etc.
- **Negative-offset variant**: because `offset` is `int` and `bo` is `u_int`,
  a `bo` with the high bit set yields a negative `dp`, writing **below**
  `csio->data_ptr` — into whatever earlier slab object or slab metadata
  precedes it.
- **Slab victim candidates in kmalloc-512** (same bucket, controllable
  fields): `struct file` (function-pointer `f_ops`), `struct ucred`-adjacent
  objects, `struct pipe`, `struct socket`/`so_options` vectors, various
  periph softc blobs. On this guest (no SMAP / no SMEP / no KASLR,
  INVARIANTS ON), an ideal chain would:
  1. Groom kmalloc-512 so a `struct file` lands immediately after a
     predictable CCB allocation.
  2. Issue a SCSI read whose Data-In PDU returns `bo = +0x200`, `ds_len = 64`,
     bytes crafted to overwrite `file->f_ops` with the address of a forged
     `fileops` in userspace (no SMAP → kernel reads user pages).
  3. Trigger a `read()` on the victim fd; the forged `fo_read` jumps to
     userspace shellcode (no SMEP → user page executable from ring 0) which
     calls `commit_creds(prepare_kernel_cred(NULL))` and returns.
  4. Back in userspace, `setresuid(0,0,0)` → uid=0.

### Why uid=0 is NOT delivered here — a VALID hard blocker

The chain above cannot be exercised by the unprivileged user `maxx` on the
default GENERIC guest, for a concrete reason:

> **`iscsi_initiator` is not in `X86_64_GENERIC`; it is a loadable module
> (`/boot/kernel/iscsi_initiator.ko`) and loading it requires root.**

Verified on the guest (see `env.txt`):

```
maxx$ kldload iscsi_initiator
kldload: can't load iscsi_initiator: Operation not permitted

maxx$ iscontrol            # the userland initiator helper (tries to kldload)
iscsi_initiator: Error while handling kernel module: Operation not permitted
```

Without the module loaded there is no `/dev/iscsi*` device, no active session,
and the buggy `scsi_decap()` function is not even resident in kernel memory.
This matches the agent's "valid hard blocker" rule:

> The write is reachable **only from an already-root context** (kldload /
> wheel-only ioctl / devfs root:operator node with no group membership), so
> there is no privilege boundary to cross.

A `kldload` from a setuid-root helper or a custom-built module would be
circular and is explicitly disallowed by the bright-line rule.

### What this finding IS (the realistic threat model)

A **remote** kernel-corruption bug:

- **Attacker position**: the iSCSI **target** (server). The victim kernel
  runs the *initiator* code.
- **Precondition (realistic, NOT circular)**: an admin has established an
  iSCSI session to an attacker-controlled or compromised target. This is
  the normal operating mode of any DragonFlyBSD host using iSCSI storage.
- **Impact under that precondition**: the malicious target sends one crafted
  Data-In PDU per SCSI read; the write-what-where lands; on this guest the
  chain above achieves **ring-0 code execution** (the target, not the local
  user, becomes root inside the initiator kernel).
- On GENERIC with INVARIANTS ON, slab-grooming for cross-bucket reuse would
  typically panic (chunk poisoning / magic checks in `kern_slaballoc.c`)
  before escalation lands, so the realistic default-kernel ceiling is
  *panic/corruption* — but a same-bucket overwrite (e.g. adjacent CCB →
  adjacent `struct file` in kmalloc-512, no free in between) fires before
  any INVARIANTS check, so a surgical remote RCE on GENERIC is not excluded.

### Honest summary

- The primitive is **real and Critical-class** (write-what-where with both
  offset and bytes attacker-controlled).
- **Local unprivileged → uid=0 on default GENERIC**: **NOT achievable** —
  the code path is gated on a root-only `kldload`.
- **Remote target → ring-0 when an admin has an iSCSI session**:
  **achievable** in principle; the harness proves the offset/byte control,
  and the slab-groom/forge chain is standard for a no-SMEP/no-SMAP/no-KASLR
  target. Full end-to-end demonstration would require setting up an evil
  iSCSI target on the guest, loading the module as root, establishing a
  session, and issuing a SCSI read — beyond what an unprivileged user can
  drive.

Reported impact: **corruption** (the demonstrated primitive), with the
remote-RCE ceiling documented. This is a Critical *remote* bug; it is NOT
a *local unprivileged → root* bug on default GENERIC.

## 4. PHASE 8 — fix validation

### `fix.diff`

A one-hunk, minimal, `git apply`-able unified diff against
`sys/dev/disk/iscsi/initiator/iscsi_subr.c`. The change:

1. Widens the locals `offset`/`len`/`edtlen` to `u_int` (eliminates the
   signed-offset underflow class entirely).
2. Adds a single bounds check **before** the pointer arithmetic:

   ```c
   if (offset > edtlen || len > edtlen - offset) {
        xdebug("bad data-in bo=%u len=%u edtlen=%u", offset, len, edtlen);
        break;
   }
   ```

   `break` exits the `ISCSI_READ_DATA` case without copying, matching the
   function's existing error-handling style.

This supersedes the finding markdown's proposal (which used the same
arithmetic but added a comment-only change); the verdict's `fix.diff` is
functionally identical in intent and additionally tightens the local types
to `u_int`.

### Validation results

| Check                                                | Result                                                |
|------------------------------------------------------|-------------------------------------------------------|
| `git apply --check -p1 fix.diff` (host, on `sys/`)   | **OK applies cleanly**                                |
| `patch -p1 < fix.diff` in guest `/usr/src`           | **Hunk #1 succeeded at 564**                          |
| Build `iscsi_initiator.ko` with fix applied          | **rc=0**, no warnings, no errors (see `fix_build.log`)|
| Fixed `iscsi_subr.o` contains the new xdebug string  | **YES**: `>>> %s: bad data-in bo=%u len=%u edtlen=%u` |
| Unfixed `iscsi_subr.o` (after `patch -R`) contains it| **NO** (clean differential)                           |
| Harness UNFIXED run                                  | **416 bytes written OOB** (exit 1)                    |
| Harness FIXED run                                    | **0 bytes written OOB** (exit 0); legal case unaffected |

Because the bug is in pointer arithmetic (a deterministic computation, not
a stateful race or memory-layout flake), the harness's unfixed-vs-fixed
comparison is a fully deterministic proof of the fix. The end-to-end
module-level proof (load fixed module, run evil target, observe no OOB) is
gated on the same root-only module-load blocker that gates escalation
(§3), so the harness + object-file string differential is the cleanest
deterministic validation available on this guest.

### Before / after contrast

```
baseline (unfixed kernel logic):
  [OOB_pos_bo=0x200_ds=0x100]    -> OOB WRITE  oob_bytes=256
  [OOB_neg_bo=0xFFFFFC00_ds=16]  -> OOB WRITE  oob_bytes=16
  SUMMARY mode=UNFIXED  total_oob_bytes=416  exit=1

patched (offset+len <= edtlen guard):
  [OOB_pos_bo=0x200_ds=0x100]    -> REJECTED by fix
  [OOB_neg_bo=0xFFFFFC00_ds=16]  -> REJECTED by fix
  SUMMARY mode=FIXED  total_oob_bytes=0  exit=0
```

The fix closes the bug.

## 5. PoC changes vs. the finding markdown

The finding markdown's "Proof of concept" section described an evil Python
iSCSI target plus an `iscontrol`-driven SCSI read on the victim — a correct
real-world reproduction recipe, but infeasible to drive from the
unprivileged user on this guest (module + session require root). The
runner substituted a **faithful userspace port** of the exact kernel
arithmetic (`harness.c`) that:

- proves the primitive deterministically (no setup privileged needed),
- runs identically in UNFIXED and FIXED modes from the same source,
- makes the before/after comparison crisp and self-contained.

The harness source, build/run scripts, fix.diff, and full untrimmed logs
are all in this directory.
