# DF-1326 — VERDICT

## Verdict

**INCONCLUSIVE (live) — bug CONFIRMED in source; trigger requires LSI SAS HBA
PCI hardware absent from this QEMU guest.** 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.

## Source confirmation (path:line)

The bug is real. `mpr_user_pass_thru` declares `tmphdr` as a 12-byte
`MPI2_REQUEST_HEADER` on its stack:

```c
/* sys/dev/raid/mpr/mpr_user.c:742 */
MPI2_REQUEST_HEADER    *hdr, tmphdr;
```

`MPI2_REQUEST_HEADER` is exactly **12 bytes** (verified in
`sys/dev/raid/mpr/mpi/mpi2.h:855`):

```c
typedef struct _MPI2_REQUEST_HEADER {     /* 12 bytes total */
    U16  FunctionDependent1;              /* 0x00 */
    U8   ChainOffset;                     /* 0x02 */
    U8   Function;                        /* 0x03 */
    U16  FunctionDependent2;              /* 0x04 */
    U8   FunctionDependent3;              /* 0x06 */
    U8   MsgFlags;                        /* 0x07 */
    U8   VP_ID;                           /* 0x08 */
    U8   VF_ID;                           /* 0x09 */
    U16  Reserved1;                       /* 0x0A */
} MPI2_REQUEST_HEADER;
```

The overflow:

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

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

`data->RequestSize` is a `uint32_t` taken directly from the user ioctl
arguments (the `mpr_pass_thru_t` struct is `copyin`'d earlier by the ioctl
switch and is fully attacker-controlled). Any `RequestSize > 12` writes
attacker bytes past `tmphdr`, smashing the function's stack frame (saved RBP,
saved RIP, other locals) before the bounds check ever runs.

A second, related read-overflow exists at lines 828 and 886:

```c
/* sys/dev/raid/mpr/mpr_user.c:828 */
bcopy(&tmphdr, task, data->RequestSize);
/*     ^^^^^^^            ^^^^^^^^^^^^^^^^^
**     12 bytes            attacker uint32   */
```

This `bcopy` reads `RequestSize` bytes starting at `&tmphdr` — i.e. reads
`RequestSize - 12` bytes off the kernel stack past `tmphdr` — into the
properly-sized HW command buffer. (Information leak of kernel stack into the
request buffer.)

The ioctl is reachable through the device node created at attach time:

```c
/* sys/dev/raid/mpr/mpr_user.c:205 */
sc->mpr_cdev = make_dev(&mpr_ops, unit, UID_ROOT, GID_OPERATOR, 0640,
                        "mpr%d", unit);
```

Operator group is the traditional "administration" group; many real
deployments put trusted operators there. The device only exists when an LSI
SAS HBA PCI device is present and `mpr_attach` runs.

## Primitive characterization (structural harness)

`harness_overflow.c` reproduces the overflow mathematically in userspace
(since the live device is absent). It allocates a 12-byte `MPI2_REQUEST_HEADER`
plus a stack "frame", simulates the unchecked copyin with `RequestSize = 64`,
and reports how many out-of-band bytes are attacker-controlled:

```
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.
```

All 52 bytes that should not be writable are written to attacker-chosen
values. The same arithmetic in-kernel smashes `mpr_user_pass_thru`'s stack
frame.

## Live reproduction: NOT POSSIBLE on this guest

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

$ pciconf -lv | grep -iE 'mpr|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
```

The QEMU guest exposes only Intel 440FX/PIIX3/PIIX4 + virtio devices. **No
LSI SAS HBA** is emulated, so the `mpr` driver never probes/attaches, no cdev
is created, and `/dev/mprN` does not exist. The mpr driver is compiled into
the kernel (`module_register: module pci/mpr already exists!` on first boot
confirms it lives in kernel #1), so the moment matching PCI hardware is
present the driver would attach and the bug would be live.

Even if the device existed, `maxx` is not in the `operator` group, so the
mode-0640 cdev would not be openable as maxx — but on a real deployment
the attacker would be in operator (or the operator group would include more
users). The bug claim is about the ioctl's reachability for any operator
member, not specifically for the audit guest's `maxx`.

This is case **(d)** in the procedure: *genuinely not reachable at runtime on
this guest*. Live-reachable on real hardware with an LSI SAS2008-class HBA.

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

If `/dev/mpr0` were reachable, the primitive would be a **kernel stack
overflow with fully attacker-controlled bytes**, classic stack-smash class.
On this guest (no SMAP/SMEP/KASLR; NX on; INVARIANTS ON in GENERIC), the
realistic chain on real hardware would be:

1. Open `/dev/mpr0` (operator group).
2. Issue `MPTIOCTL_PASS_THRU` with `RequestSize = N` and a payload whose
   bytes 12..N-1 are shaped to overwrite saved RIP with the address of
   `commit_creds(prepare_kernel_cred(0))` (KASLR OFF → known address), or
   with the address of userspace shellcode (SMEP OFF → executable).
3. When `mpr_user_pass_thru` returns, control transfers to the overwritten
   saved RIP.

INVARIANTS-ON would catch some stack canary / panic-before-return paths; the
deterministic chain would be a single-overwrite of the return address into a
KASLR-known kernel-text gadget (since no SMEP, even a userspace trampoline
works). This is a textbook kernel-stack-smash → root chain. Not demonstrable
here only because the trigger device is absent.

## Fix (fix.diff)

Three logical changes, all in `sys/dev/raid/mpr/mpr_user.c`:

1. **Bounds-check `RequestSize` BEFORE the copyin** — rejects requests
   outside `[sizeof(tmphdr), reqframesz]`.
2. **Copy only `sizeof(tmphdr)` bytes** for the routing peek (closes the
   write overflow at the source).
3. **Replace both `bcopy(&tmphdr, ..., RequestSize)` sites with a fresh
   `copyin(PtrRequest, ..., RequestSize)`** into the properly-sized HW
   command buffer (closes the read overflow; preserves semantics — the user
   buffer is the authoritative source of the request bytes).

## 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 `mpr_user.c`.
- Patched kernel installs via `make installkernel` and boots cleanly
  (`kern.version` → `6.5-DEVELOPMENT #2/#3`).
- `nm /boot/kernel/kernel` shows `t mpr_user_pass_thru` linked in (the
  patched function is in the live kernel).
- The live PoC cannot be re-run on the patched kernel to confirm "behavior
  gone" because the trigger device (`/dev/mpr0`) does not exist on this
  guest. `fix_status: not_testable` — diff applies, compiles, boots, and a
  source read confirms the previously-unchecked copyin is now preceded by a
  range check and limited to `sizeof(tmphdr)`.

## PoC changes

`poc_mpr_stackoverflow.c` (authored pre-verification) compiles cleanly on
the guest (only an irrelevant `perror` implicit-declaration warning from
missing `<stdio.h>`). It is left functionally unchanged. New file
`harness_overflow.c` added as the structural primitive proof (since the
live device is absent).

## Files

| File | Purpose |
|------|---------|
| `poc_mpr_stackoverflow.c` | original live-trigger PoC (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 |
| `README.md`               | original README shipped with the PoC |
