# DF-1359 — VERDICT

## Verdict

**INCONCLUSIVE (live) — bug CONFIRMED in source; trigger requires LSI SAS HBA
PCI hardware absent from this QEMU guest.** Identical twin of DF-1326 in the
MPS driver. The primitive is a kernel-stack buffer overflow with full
attacker-controlled bytes. Fix authored and built cleanly into a single-fix
kernel; the patched kernel is boot-stable and the cited path is closed in
source. A userspace structural harness proves the overflow primitive
independently of the live device.

## Bug class

Kernel stack buffer overflow via `copyin` of an attacker-controlled size into
a fixed-size stack variable, with the bounds check ordered AFTER the overflow.
Identical class to DF-1326 (different driver — MPS instead of MPR).

## Source confirmation (path:line)

The bug is real and byte-for-byte identical to DF-1326. `mps_user_pass_thru`
declares `tmphdr` as a 12-byte `MPI2_REQUEST_HEADER` on its stack:

```c
/* sys/dev/raid/mps/mps_user.c:747 */
MPI2_REQUEST_HEADER    *hdr, tmphdr;
```

The overflow (the cited `:802`):

```c
/* sys/dev/raid/mps/mps_user.c:802-809 */
err = copyin(PTRIN(data->PtrRequest), &tmphdr, data->RequestSize);
/*                                       ^^^^^^^   ^^^^^^^^^^^^^^^^^
**                                       12 bytes    attacker uint32   */
if (err != 0)
    goto RetFreeUnlocked;

if (data->RequestSize > (int)sc->facts->IOCRequestFrameSize * 4) {
    err = EINVAL;                                /* bounds check runs */
    goto RetFreeUnlocked;                        /* AFTER the overflow */
}
```

`MPI2_REQUEST_HEADER` is the same 12-byte struct as in MPR
(`sys/dev/raid/mps/mpi/mpi2.h:693` — identical typedef). `data->RequestSize`
is a user-controlled `uint32_t` straight from the ioctl arguments. Any
`RequestSize > 12` overflows the stack frame.

Same companion read-overflow at lines 830 and 878:

```c
/* sys/dev/raid/mps/mps_user.c:830 */
bcopy(&tmphdr, task, data->RequestSize);   /* reads past 12-byte tmphdr */
/* sys/dev/raid/mps/mps_user.c:878 */
bcopy(&tmphdr, hdr, data->RequestSize);    /* same */
```

Device-node creation (mode 0640 root:operator):

```c
/* sys/dev/raid/mps/mps_user.c:188 */
sc->mps_cdev = make_dev(&mps_ops, unit, UID_ROOT, GID_OPERATOR, 0640,
                        "mps%d", unit);
```

## Primitive characterization (structural harness)

`harness_overflow.c` (same file as DF-1326 — the primitive is identical)
proves the overflow mathematically:

```
sizeof(MPI2_REQUEST_HEADER) = 12
BUG: copyin 64 bytes into 12-byte tmphdr overflows by 52 bytes
...
Primitive confirmed: 52 of 52 out-of-band bytes are attacker-controlled.
In the kernel these bytes overwrite saved RBP/RIP/locals on the function's
stack frame.
```

## Live reproduction: NOT POSSIBLE on this guest

```
$ ls /dev/mps*
ls: /dev/mps*: No such file or directory

$ pciconf -lv | grep -iE 'mps|LSI|SAS'
(no match)

$ id maxx
uid=1001(maxx) gid=1001(maxx) groups=1001(maxx)

$ grep operator /etc/group
operator:*:5:root                  # maxx is NOT in operator
```

Same situation as DF-1326: no LSI SAS HBA PCI hardware on QEMU, no attach,
no cdev, no `/dev/mpsN`. The mps driver is compiled into the kernel (visible
in `kldstat -v` as `pci/mps`), so on real hardware the bug would auto-attach
and be live-reachable.

This is case **(d)** in the procedure. Live-reachable on real hardware with
an LSI SAS2008-class HBA.

## Phase 6 — escalation analysis (not exercisable live)

Identical to DF-1326 (same overflow class, same byte control). On this guest
(no SMAP/SMEP/KASLR/PTI; INVARIANTS ON in GENERIC), the realistic chain on
real hardware is: overwrite saved RIP in `mps_user_pass_thru`'s frame with a
KASLR-known kernel-text gadget (or userspace shellcode address since SMEP is
OFF) → pivot to `commit_creds(prepare_kernel_cred(0))` → root. Not
demonstrable here only because the trigger device is absent.

## Fix (fix.diff)

Three logical changes, all in `sys/dev/raid/mps/mps_user.c`:

1. **Bounds-check `RequestSize` BEFORE the copyin** — rejects requests
   outside `[sizeof(tmphdr), IOCRequestFrameSize*4]`.
2. **Copy only `sizeof(tmphdr)` bytes** for the routing peek.
3. **Replace both `bcopy(&tmphdr, ..., RequestSize)` sites with a fresh
   `copyin(PtrRequest, ..., RequestSize)`** into the properly-sized HW
   command buffer.

## Phase 8 — fix validation (not_testable)

- `fix.diff` applies cleanly via `patch -p1 --forward` (all 3 hunks succeed).
- `make -j6 nativekernel KERNCONF=X86_64_GENERIC` builds the patched source
  with `cc 8.3 -Werror`, rc=0, no warnings on `mps_user.c`.
- Patched kernel installs and boots cleanly (`kern.version` → `#3`).
- `nm /boot/kernel/kernel` shows `t mps_user_pass_thru` linked in.
- Live PoC cannot be re-run (no `/dev/mpsN` on this guest). `fix_status:
  not_testable` — diff applies, compiles, boots, and source read confirms
  the previously-unchecked copyin is now range-checked and limited to
  `sizeof(tmphdr)`.

## PoC changes

No original PoC shipped for DF-1359; created `poc_mps_stackoverflow.c`
(adapted from the DF-1326 PoC with the mps ioctl/device names) plus the same
`harness_overflow.c` userspace primitive proof.

## Files

| File | Purpose |
|------|---------|
| `poc_mps_stackoverflow.c` | live-trigger PoC for mps (compiles, ENOENT on guest) |
| `harness_overflow.c`      | userspace structural primitive proof (52/52 attacker bytes) |
| `harness_run.log`         | harness output on baseline kernel |
| `harness_run_patched.log` | harness output on patched kernel (unchanged — pure userspace) |
| `fix.diff`                | bounds-check before copyin + sizeof(tmphdr) limit + direct copyin at bcopy sites |
| `build.sh` / `run.sh`     | exact build and run commands |
| `fix_build.log`           | full untrimmed `nativekernel` log for the patched build (rc=0) |
| `env.txt`                 | guest uname / pciconf / dev nodes / kldstat / kernel sha256 |
