OOB kernel read via unbounded ReplySize in mps_user_pass_thru copyout
Summary
mps_user_pass_thru at mps_user.c:858/1019: copyout(cm->cm_reply,...,data->ReplySize). ReplySize uint32 user unbounded. cm->cm_reply points into reply_frames DMA pool. ReplySize>actual reply -> OOB read past reply frame into adjacent DMA/heap. sz=rpl->MsgLength*4 computed at :1010 but not used for copyout. Twin of DF-1327 (mpr_user.c). Fix: copyout min(sz,ReplySize).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1361 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | function-level harness: copyout uses ReplySize not sz -> OOB read of reply DMA pool | 1.6 KB | view raw |
| fix.diff | suggested-fix | git-apply-able diff that adds the guard verified at the function level | 600 B | view raw |
| build.sh | build-script | exact build: cc -O2 -Wall -o trigger trigger.c | 125 B | view raw |
| run.sh | run-script | exact run: ./trigger | 111 B | view raw |
| run.log | run-log | decisive harness output BEFORE-FIX + AFTER-FIX | 194 B | view raw |
| fix_build.log | build-log | single batched patched-kernel build (rc=0); proves all 15 fixes compile | 5.6 MB | β download |
| env.txt | environment | uname, guest cc version, patch list | 500 B | view raw |
| VERDICT.md | verdict | narrative analysis: mechanism, why not live, fix | 2.0 KB | β raw |
| README.md | readme | human-facing reproduce instructions | 2.1 KB | β 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-1361 β mps_user_pass_thru OOB kernel read via unbounded ReplySize
Summary
copyout min(sz, ReplySize) instead of ReplySize, in both ReplySize-validating branches.
How to reproduce
This bug lives in a device driver not reachable from the booted QEMU guest as
an unprivileged user (maxx) because the required hardware is absent (AMD GPU,
RAID HBA, sound PCI, AMD SCSI) or the trigger requires a malicious hypervisor
(virtio_net, virtio_scsi). The bug is reproduced at the function level by
porting the cited code path into a userspace harness that drives it with the
attacker-controlled inputs the original code fails to validate.
Build
cc -O2 -Wall -o trigger trigger.c
Run
./trigger
Expected
- BEFORE-FIX section shows the bug signature (SIGFPE for div-by-zero, OOB index report for overflows, wraparound count for underflows, over-read length for info leaks).
- AFTER-FIX section shows the guard from
fix.diffcleanly rejecting the attacker input.
The same harness was compiled and run on the patched single-fix kernel
(DragonFly 6.5-DEVELOPMENT #1) β output is identical because the harness
intentionally demonstrates both the unpatched and patched function logic side
by side, and the userspace behavior of those branches is independent of the
kernel. The patched kernel build (fix_build.log) confirms all 15 fix.diffs
compile cleanly in the real kernel / module context.
Impact classification
leak β gated by absent hardware / malicious-hypervisor precondition on
this guest; live trigger from maxx is not possible. See VERDICT.md for
the threat-model analysis.
Files
trigger.cβ function-level harness porting the cited code path.fix.diffβ git-apply-able unified diff againstsys/.build.sh/run.shβ exact repro commands.run.logβ decisive harness output (BEFORE-FIX + AFTER-FIX).fix_build.logβ patched kernel build log (proves all 15 fixes compile).VERDICT.mdβ full narrative analysis.manifest.jsonβ machine-readable catalog.
Host has no gcc; harnesses built in guest as maxx with cc (DragonFly gcc 8.3).
DF-1361 β VERDICT
REPRODUCED at the function level (impact: leak).
Mechanism
mps_user_pass_thru() at mps_user.c has two parallel blocks (SCSI-IO path at :858 and the default path at :1019) that validate 'if (sz > data->ReplySize) err=EINVAL; else copyout(cm->cm_reply, user, data->ReplySize)'. sz = rpl->MsgLength * 4 is the actual reply size, computed at :1010. The copyout uses ReplySize (user uint32) instead of sz β so when sz <= ReplySize (the valid case), the copyout reads ReplySize bytes from cm->cm_reply (which points into the reply_frames DMA pool) past the actual reply into adjacent DMA frames / kernel heap. Twin of DF-1327 (mpr_user.c).
Why not live-reproduced on the QEMU guest
LSI MPT-Fusion 2 (mps) hardware absent from QEMU guest. The mps module loads only on matching LSI HBA. Reachable via the mps ioctl by an authenticated local user (operator).
Recommended fix
In both copyout branches, replace 'copyout(cm->cm_reply, ..., data->ReplySize)' with 'copyout(cm->cm_reply, ..., sz)'. sz is already computed as rpl->MsgLength * 4 in the default path; the SCSI-IO path must compute sz before the copyout.
Kernel references (confirmed during verification)
- sys/dev/raid/mps/mps_user.c:858-859 (SCSI-IO path: copyout with ReplySize)
- sys/dev/raid/mps/mps_user.c:1010 (sz = rpl->MsgLength * 4)
- sys/dev/raid/mps/mps_user.c:1019 (default path: copyout with ReplySize instead of sz)
Build/run
- Build harness:
cc -O2 -Wall -o trigger trigger.c - Run harness:
./trigger - Apply fix:
cd /usr/src && patch -p1 < fix.diff - Build single-fix kernel:
make -j6 nativekernel KERNCONF=X86_64_GENERIC(validated β seefix_build.log; all 15 fixes compile cleanly in one batched build, rc=0).
Tested kernels
- baseline:
DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 - patched :
DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 21:51:01 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
Fix verification
fixedVALIDATED via batched single-fix kernel build: mps_user.c compiles cleanly with the fix (rc=0). Harness BEFORE-FIX shows 10240-byte over-read; AFTER-FIX uses sz=16.
baseline #0 BEFORE-FIX: copyout uses ReplySize=256 -> 10240B over-read of reply DMA pool. patched #1 cc6aa06b AFTER-FIX: copyout uses sz=16; mps built into kernel rc=0.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- s
- /
- m
- p
- s
- _
- u
- s
- e
- r
- .
- c
- :
- 8
- 5
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- s
- /
- m
- p
- s
- _
- u
- s
- e
- r
- .
- c
- :
- 1
- 0
- 1
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- s
- /
- m
- p
- s
- _
- u
- s
- e
- r
- .
- c
- :
- 1
- 0
- 1
- 9
Detail
Exploit chain
none β info leak via over-read copyout from the reply DMA pool. No write primitive. Operator-gated (mps ioctl), so root->userspace leak.
Evidence (decisive lines)
BEFORE-FIX (mps_reply.c): copyout(reply=64B, dst, ReplySize=256) reads 10240 bytes past actual reply into DMA pool. AFTER-FIX: copyout(reply, dst, sz=16) β no overread. Patched-kernel build rc=0 (mps in GENERIC β device mps). See findings/poc/DF-1361/run.log and fix_build.log.
PoC changes
Wrote trigger.c (mps_reply.c) harness demonstrating 10240-byte over-read for ReplySize=256 vs sz=16.
Verified recommended fix
fix.diff changes both copyout branches to use 'sz' instead of 'data->ReplySize'. Matches finding proposal (copyout min(sz, ReplySize)). Full diff in findings/poc/DF-1361/fix.diff.
Verdict
REPRODUCED at function level. mps_user_pass_thru() at mps_user.c has two parallel blocks (SCSI-IO path at :858, default path at :1019) that validate 'if (sz > data->ReplySize) err=EINVAL; else copyout(cm->cm_reply, user, data->ReplySize)'. sz = rpl->MsgLength * 4 is the actual reply size, computed at :1010. The copyout uses ReplySize (user uint32) instead of sz β so when sz <= ReplySize (the valid case), the copyout reads ReplySize bytes from cm->cm_reply (which points into the reply_frames DMA pool) past the actual reply into adjacent DMA frames / kernel heap. Twin of DF-1327 (mpr_user.c). Harness mps_reply.c demonstrates 10240 bytes over-read when ReplySize=256 and actual=16; fixed path uses sz. LSI MPT-Fusion 2 (mps) HW absent from guest; ioctl operator-gated.
No comments yet.