β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-1326

Stack buffer overflow in mpr_user_pass_thru: copyin of user-controlled RequestSize into 12-byte MPI2_REQUEST_HEADER before bounds check

Summary

mpr_user_pass_thru at mpr_user.c:800: copyin(PTRIN(data->PtrRequest),&tmphdr,data->RequestSize) where tmphdr is MPI2_REQUEST_HEADER (12 bytes on stack at :742). data->RequestSize is uint32_t user-controlled. Bounds check if(RequestSize>reqframesz) at :804 runs AFTER copyin. RequestSize>12 -> stack frame overflow with fully controlled bytes (saved RBP/RIP). /dev/mprN mode 0640 GID_OPERATOR. Reliable kernel panic or RCE. Fix: check RequestSize BEFORE copyin, copyin only sizeof(tmphdr).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1326 Β· 13 files
FileTypeDescriptionSize
poc_mpr_stackoverflow.c trigger-source minimal MPTIOCTL_PASS_THRU overflow trigger 1.6 KB view raw
harness_overflow.c primitive-proof userspace structural proof of the 12-byte stack overflow 3.6 KB view raw
harness_run.log run-log harness output proving 52/52 attacker-controlled bytes 2.7 KB view raw
harness_run_patched.log run-log harness output on patched kernel (unchanged - pure userspace) 2.7 KB view raw
fix.diff suggested-fix bounds-check before copyin + sizeof(tmphdr) limit + direct copyin at bcopy sites 2.5 KB view raw
build.sh build-script cc -O2 -Wall for both PoC and harness 413 B view raw
run.sh run-script runs trigger (ENOENT) and harness 812 B view raw
fix_build.log build-log full nativekernel build log with DF-1326 fix applied (rc=0, -Werror) 5.6 MB ↓ download
env.txt environment uname, pciconf (no LSI SAS), dev nodes, kldstat, kernel sha256 2.5 KB view raw
VERDICT.md verdict full narrative: source confirmation, primitive char, fix validation 8.8 KB ↓ raw
README.md readme original README shipped with the PoC 746 B ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme original README shipped with the PoC
↓ download raw

DF-1326 PoC: mpr_user_pass_thru stack buffer overflow

Build

cc -O2 -Wall -o poc_mpr_stackoverflow poc_mpr_stackoverflow.c

Run

./poc_mpr_stackoverflow /dev/mpr0

Expected output

  • Kernel panic with stack-protector violation (if compiled with -fstack-protector)
  • OR kernel RCE if stack canary is absent (return address overwritten with 0x41414141...)
  • OR page fault if the overwritten return address is unmapped

Notes

  • Requires /dev/mprN access (mode 0640 root:operator)
  • The copyin at mpr_user.c:800 writes RequestSize bytes into 12-byte stack MPI2_REQUEST_HEADER
  • The bounds check if (data->RequestSize > sc->reqframesz) at line 804 runs AFTER the overflow
  • Any RequestSize > 12 overflows the stack frame
VERDICT.md verdict full narrative: source confirmation, primitive char, fix validation
↓ download raw

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:

/* 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):

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:

/* 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:

/* 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:

/* 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

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable live (HW absent); fix.diff applies cleanly + kernel builds rc=0.

fix.diff applies, kernel build rc=0, symbol present in patched kernel.
↓ fix.diffkernel #1/#2/#3 builds rc=0

Confirmed kernel references

Detail

Exploit chain

Not exercisable (no LSI SAS HBA). On real HW: stack overflow saved RIP -> shellcode -> uid0. No -fstack-protector.

Evidence (decisive lines)

ls /dev/mpr*: ENOENT. Harness: 52/52 attacker-controlled OOB bytes for RequestSize=64. fix.diff applies (3 hunks), kernel #2 builds rc=0.

Verified recommended fix

Reorder: bounds-check RequestSize BEFORE copyin, copyin sizeof(tmphdr) only; replace bcopy sites with fresh copyin (closes both write+read overflow).

Verdict

INCONCLUSIVE live. mpr_user_pass_thru copyin RequestSize into 12-byte stack header BEFORE bounds check. Bug REAL in source. No LSI SAS HBA in QEMU -> /dev/mprN absent.