# DF-1917 — Verification Verdict

## Verdict: REPRODUCED (source-confirmed + sgl_off-OOB-harness) — Phase-6 hard blocker (no HBA)

The unbounded-`sgl_off` OOB SGE write is confirmed at
`sys/dev/raid/mrsas/mrsas_ioctl.c:219-255`.  The harness reproduces the
verbatim buggy arithmetic against a 1024-byte model of `cmd->frame` and
shows that for every attacker-supplied `sgl_off` that would push
`sgl_off + 8*sge_count` past `MRSAS_MFI_FRAME_SIZE`, the kernel writes
attacker-supplied SGE bytes (`phys_addr`, `length`) past the 1024-byte
DMA allocation into adjacent kernel heap.

## Mechanism

```c
// mrsas_ioctl.h:80-92  -- attacker-controlled u32 fields, no validation
struct mrsas_iocpacket {
    u_int16_t host_no;
    u_int16_t __pad1;
    u_int32_t sgl_off;     // <-- attacker u32
    u_int32_t sge_count;
    ...
};

// mrsas_ioctl.c:219-220  -- u32 offset used verbatim
kern_sge32 = (struct mrsas_sge32 *)
    ((unsigned long)cmd->frame + user_ioc->sgl_off);

// mrsas_ioctl.c:225-255  -- up to 16 SGEs written at frame+sgl_off+8*i
for (i = 0; i < user_ioc->sge_count; i++) {
    ...
    kern_sge32[i].phys_addr = (u_int32_t)ioctl_data_phys_addr[i];
    kern_sge32[i].length    = user_ioc->sgl[i].iov_len;
}
```

`cmd->frame` is `bus_dmamem_alloc`'d at exactly `MRSAS_MFI_FRAME_SIZE`
(=1024 bytes; `mrsas.h:876`, `mrsas.c:451-462`).  `mrsas_sge32` is
8 bytes (`mrsas.h:1883-1886`, `#pragma pack(1)`).  `MAX_IOCTL_SGE`=16
(`mrsas_ioctl.h:66`).  So a single ioctl can land up to 16×8 = 128 bytes
of attacker-offset writes; with `sgl_off=0x3FC` and `sge_count=16`, 124
of those bytes go past byte 1024.

`sge_count` IS bounds-checked at `mrsas_ioctl.c:186` (≤ 16), but
`sgl_off` is never validated anywhere.

## Harness evidence (run.log)

```
DF-1917: mrsas_passthru unbounded sgl_off (mrsas_ioctl.c:219-255)
  sgl_off=0x00000080 sge_count= 1 -> 0 OOB bytes (in-bounds)
  sgl_off=0x000003f8 sge_count= 1 -> 0 OOB bytes (in-bounds)
  sgl_off=0x000003fc sge_count= 1 -> 4 OOB bytes (OOB WRITE past 1024-byte frame)
  sgl_off=0x00000400 sge_count= 1 -> 8 OOB bytes (OOB WRITE past 1024-byte frame)
  sgl_off=0x000003fc sge_count=16 -> 124 OOB bytes (OOB WRITE past 1024-byte frame)
  sgl_off=0xffffffff sge_count= 1 -> 8 OOB bytes (OOB WRITE past 1024-byte frame)
```

## Why no live trigger / Phase-6 hard blocker

The mrsas cdev is created only by `mrsas_attach`
(`sys/dev/raid/mrsas/mrsas.c:790-792`):

```c
sc->mrsas_cdev = make_dev(&mrsas_ops, device_get_unit(dev), UID_ROOT,
    GID_OPERATOR, (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP), "mrsas%u",
    device_get_unit(dev));
```

— and `mrsas_attach` runs only when the PCI probe finds an LSI MegaRAID
SAS HBA (`sys/dev/raid/mrsas/mrsas.c:3668-3669`:
`DRIVER_MODULE(mrsas, pci, ...)`).  The QEMU audit guest emulates **no**
such HBA (verified: `pciconf -lv` lists none; `/dev/mrsas*` absent), so
`mrsas_passthru` is unreachable at runtime on this guest.  Additionally
the node would be mode `0660 root:operator`, and `maxx` (uid 1001) is
**not** in `operator` (verified: `pw groupshow operator` ⇒
`operator:*:5:root`), so the unprivileged ceiling is "operator-group
member".  Valid Phase-6 hard blocker (per AGENT.md: *"...dead/unreachable
at runtime on this guest...prove the primitive at the object/harness
level..."*).

## Exploit chain

Not applicable on this guest (no HBA → no `/dev/mrsas*` → no
`mrsas_passthru` call from maxx).  On real hardware with the HBA, an
operator-group member would have an arbitrary-offset (limited to
low-entropy phys_addr/length content) kernel heap write relative to a
1024-byte DMA allocation; on `noinv` this would be silently corrupting,
on default GENERIC (`INVARIANTS` ON) the slab poison/magic checks in
`kern_slaballoc.c` would likely catch cross-zone reuse and panic (DoS).
A full uid=0 chain would require (a) operator-group membership, (b)
info-leak of the kernel-heap layout adjacent to `cmd->frame`, (c) a
victim object with a function pointer / `ucred *` / refcount landing in
the spill zone — feasible in principle on real hardware but not
demonstrable on this guest.

## PoC changes

- Added `harness.c`: verbatim buggy arithmetic against a 1024-byte
  frame model; 6 test vectors covering in-bounds, edge, OOB, max-spill,
  pointer-wrap.
- Added `fixcheck.c`: models the patched predicate from `fix.diff`
  (`mrsas_ioctl.c:242-246`) and shows it rejects the 4 OOB vectors.
- Added `fix.diff`: validates `sgl_off + sge_count*sizeof(mrsas_sge32)
  <= MRSAS_MFI_FRAME_SIZE` before computing `kern_sge32`, returning
  `EINVAL` otherwise.

## Fix validation (Phase 8)

`fix.diff` was applied as part of a combined patch (with DF-1918 and
DF-1919, same file) to `/usr/src` on the `with-src` snapshot.  Kernel
rebuilt cleanly (`make -j6 nativekernel KERNCONF=X86_64_GENERIC`,
`NK_DONE rc=0`; `mrsas_ioctl.o` recompiled without warnings).  The
single-fix kernel installed as
`/boot/kernel/kernel` (sha256
`c8c9a25c98bc8e06c300820f141d8d1a3e89dcda21c7d2585b36bf8ddb72f064`)
and **booted successfully** as
`DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 20:05:20 UTC 2026`.
The harness re-ran on the patched kernel (system healthy).

Because `/dev/mrsas0` does not exist on this guest, the kernel-side fix
could not be exercised via the live syscall path.  `fixcheck.c` models
the patched predicate directly: it rejects 4/6 vectors (the 4 that
caused OOB), accepts the 2 in-bounds vectors — proving the patched
predicate closes the OOB inputs.

- baseline (#0 `5dc83dac…`): harness shows 4 vectors with OOB write.
- patched (#1 `c8c9a25c…`): harness unchanged (it is a userspace
  model); fixcheck shows the same 4 vectors now `REJECTED (EINVAL)`.

`fix_status: fixed` — the patched predicate provably rejects every
input vector that previously caused an OOB write, and the patched
kernel is bootable and stable.
