# DF-1327 — mpr_user_pass_thru kernel-heap info leak

## Verdict
**INCONCLUSIVE (real bug, needs hardware absent from guest).** The
vulnerability is **confirmed real** by a complete source trace; it cannot
be *executed* on this QEMU audit guest because the `mpr` driver has no
LSI SAS3+ HBA to attach to, so `/dev/mpr0` does not exist and the PoC
fails at `open()` with `ENOENT`.  The fix was validated to **apply and
compile** in a full `nativekernel` build.

## Mechanism (trigger → primitive → effect)

`mpr_user_pass_thru()` issues a passthrough command to the IOC firmware
and then copies the reply back to userspace.  The reply copy is:

```
sys/dev/raid/mpr/mpr_user.c:857   if ((cm != NULL) && (cm->cm_reply != NULL)) {
sys/dev/raid/mpr/mpr_user.c:858       rpl = (MPI2_DEFAULT_REPLY *)cm->cm_reply;
sys/dev/raid/mpr/mpr_user.c:859       sz = rpl->MsgLength * 4;            // real reply size (~96 B)
sys/dev/raid/mpr/mpr_user.c:861       if (sz > data->ReplySize) {         // WARN only, no clamp
sys/dev/raid/mpr/mpr_user.c:862           mpr_printf(... "user reply buffer (%d) "
sys/dev/raid/mpr/mpr_user.c:864               "smaller than returned buffer (%d)\n", ...);
sys/dev/raid/mpr/mpr_user.c:865       }
sys/dev/raid/mpr/mpr_user.c:866       mpr_unlock(sc);
sys/dev/raid/mpr/mpr_user.c:867       copyout(cm->cm_reply, PTRIN(data->PtrReply),
sys/dev/raid/mpr/mpr_user.c:868           data->ReplySize);               // *** LEAK: user-controlled len ***
sys/dev/raid/mpr/mpr_user.c:869       mpr_lock(sc);
```

A second, identical site exists in the main pass-thru return path:

```
sys/dev/raid/mpr/mpr_user.c:1085      sz = rpl->MsgLength * 4;
sys/dev/raid/mpr/mpr_user.c:1087      if (sz > data->ReplySize) { mpr_printf(...); }
sys/dev/raid/mpr/mpr_user.c:1093      copyout(cm->cm_reply, PTRIN(data->PtrReply), data->ReplySize);
```

`data->ReplySize` is a **fully user-controlled `uint32_t`** in the
`mpr_pass_thru_t` ioctl argument (`sys/dev/raid/mpr/mpr_ioctl.h:194`).
The check at 861/1087 only *warns* when the user buffer is *smaller* than
the reply — it never clamps the **copyout length**.  Setting `ReplySize`
much larger than the actual reply (`sz`, ~96 bytes) makes `copyout()`
read `ReplySize` bytes from `cm->cm_reply`, which sits inside the
**reply_frames DMA pool**.  Every byte past the real reply is **kernel
heap**: function pointers, DMA bus addresses, command metadata, and the
contents of adjacent reply frames.  This is a deterministic, repeatable
kernel-heap information leak → KASLR-bypass / heap-layout disclosure.

## Reachability on this guest

- `mpr` **is** compiled into `X86_64_GENERIC` (`kldstat -v` → `pci/mpr`).
- **But** there is **no LSI/Avogo SAS3+ HBA** on the QEMU guest (only
  Intel PIIX3 IDE + Virtio block).  `mpr_attach_user()` therefore never
  runs, so `make_dev()` is never called and **no `/dev/mpr0` exists**:
  ```
  $ ls /dev/mpr*    ->  No such file or directory
  ```
- The PoC (`poc.c`) opens `/dev/mpr0` and issues `MPTIOCTL_PASS_THRU`.
  Run output:
  ```
  poc: open /dev/mpr0: No such file or directory   (RUN_EXIT=1)
  ```
- **Privilege model (for a hardware-equipped host):** the device node is
  created `UID_ROOT, GID_OPERATOR, 0640`
  (`mpr_user.c:205`); `mpr_open()` (`mpr_user.c:225`) returns 0
  unconditionally; `mpr_ioctl()` (`mpr_user.c:2259`) does **no**
  `priv_check()`/`suser()`.  So the bug is reachable by **root or any
  `operator`-group member** — not a fully-unprivileged user — on a host
  that has an mpr HBA.  The finding's "operator group" framing is correct.

This is the Phase-4(d) case: *genuinely not reachable on this kernel
guest* because the required hardware is absent.  It is **not** a
false-positive, **not** already-fixed, and **not** a missing-setup we can
supply (the setup is physical hardware + operator group, both absent).

## Exploit chain

None developed — the primitive is an **information leak** (read-only OOB),
not a memory-corruption write, so there is no `uid=0` chain to pursue
per Phase 6.  The realistic impact ceiling is a deterministic
kernel-heap disclosure: leak `N` bytes of the reply DMA pool per call,
including kernel text/data pointers and DMA bus addresses → defeats KASLR
and reveals heap layout for a follow-on corruption exploit.  `N` is
bounded only by the user's `ReplySize` (up to ~4 GiB before `copyout`
faults on an unmapped user page).

## PoC changes

The PoC folder was empty (no seeded scaffolding).  I authored:
- `poc.c` — opens `/dev/mpr0`, issues `MPTIOCTL_PASS_THRU` with
  `ReplySize = 1 MiB`, hexdumps the leaked reply-pool bytes.  The on-wire
  `mpr_pass_thru_t` and `MPTIOCTL_PASS_THRU` are reproduced inside the PoC
  because `<dev/raid/mpr/mpr_ioctl.h>` is not installed under
  `/usr/include`.
- `build.sh` / `run.sh` — exact build/run commands.

## Fix

`fix.diff` clamps both copyout sites to the actual reply length `sz`
(after clamping `sz` down to `ReplySize` when the user buffer is smaller),
so the copyout never reads past `cm->cm_reply`.  The diff applies cleanly
(`git apply --check`) and was **built** in a full
`make -j6 nativekernel KERNCONF=X86_64_GENERIC` on the audit guest
(`mpr_user.o` recompiled, `rc=0`, no errors) — see `fix_build.log`.

## Fix validation

`fix_status: not_testable`.  The PoC cannot run on this guest (no
`/dev/mpr0`), so a runtime before/after comparison is impossible.  The
fix was validated to (a) apply to the audit source, (b) compile in a full
nativekernel build, and (c) close the code path by inspection — the
copyout length is now `sz` (real reply size), not the user-controlled
`ReplySize`.
