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)
PoC verification
Evidence pack
findings/poc/DF-1326 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
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:
- Open
/dev/mpr0(operator group). - Issue
MPTIOCTL_PASS_THRUwithRequestSize = Nand a payload whose bytes 12..N-1 are shaped to overwrite saved RIP with the address ofcommit_creds(prepare_kernel_cred(0))(KASLR OFF β known address), or with the address of userspace shellcode (SMEP OFF β executable). - When
mpr_user_pass_thrureturns, 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:
- Bounds-check
RequestSizeBEFORE the copyin β rejects requests outside[sizeof(tmphdr), reqframesz]. - Copy only
sizeof(tmphdr)bytes for the routing peek (closes the write overflow at the source). - Replace both
bcopy(&tmphdr, ..., RequestSize)sites with a freshcopyin(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.diffapplies cleanly viapatch -p1 --forward(all 3 hunks succeed).make -j6 nativekernel KERNCONF=X86_64_GENERICbuilds the patched source withcc 8.3 -Werror, rc=0, no warnings onmpr_user.c.- Patched kernel installs via
make installkerneland boots cleanly (kern.versionβ6.5-DEVELOPMENT #2/#3). nm /boot/kernel/kernelshowst mpr_user_pass_thrulinked 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 tosizeof(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_testablenot_testable live (HW absent); fix.diff applies cleanly + kernel builds rc=0.
fix.diff applies, kernel build rc=0, symbol present in patched kernel.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- r
- /
- m
- p
- r
- _
- u
- s
- e
- r
- .
- c
- :
- 7
- 4
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- r
- /
- m
- p
- r
- _
- u
- s
- e
- r
- .
- c
- :
- 8
- 0
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- r
- /
- m
- p
- r
- _
- u
- s
- e
- r
- .
- c
- :
- 8
- 0
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- r
- /
- m
- p
- r
- _
- u
- s
- e
- r
- .
- c
- :
- 8
- 2
- 8
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.
No comments yet.