mpr MPTIOCTL_REG_ACCESS: unvalidated u32 RegOffset gives operator-group users arbitrary kernel read/write in the 4 GiB window above the register BAR (twin of DF-4012)
Summary
mpr_user_reg_access passes the raw user-controlled u32 RegOffset straight into mpr_regread/mpr_regwrite, which on x86_64 are plain volatile u32 loads/stores at bhandle+offset with no aperture bound. Any member of group operator (0640 root:operator node, no priv_check) gets read+write over the full 4 GiB kernel-virtual window above the register BAR, plus a deterministic Fatal trap 12 on unmapped offsets. LIVE-proven via the adapted DF-4012 replica as uid=2: OOB writes at +0x2000/+0x3000 read back, Fatal trap 12 at the raw movl x2. Fix validated live (-DFIXED all OOB -> EINVAL) + clean -Werror mpr.ko build with fix.diff. REG_IO_READ/WRITE are correctly rejected; DF-4014-shape confirmed ABSENT in mpr (diag-reset + *cmp=NULL -> the DF-4010 leak shape instead).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4020 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df4020rig.c | β | 13.9 KB | view raw | |
| df4020rig_trigger.c | β | 15.0 KB | view raw | |
| rig.Makefile | β | 58 B | β download | |
| rig_build.log | β | 835 B | view raw | |
| rig_run.log | β | 2.0 KB | view raw | |
| leak_sample.txt | β | 1.4 KB | view raw | |
| panic.txt | β | 1.2 KB | view raw | |
| fix_run.log | β | 1.8 KB | view raw | |
| fix_kernel_build2.log | β | 1.1 KB | view raw | |
| fix.diff | β | 1017 B | view raw | |
| env.txt | β | 722 B | view raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| verdict.json | β | 6.7 KB | view raw |
DF-4020 VERDICT (pass 3 -- production-handler dispatch, superseding the pass-2 replica)
Status: REPRODUCED (impact ceiling demonstrated on production code: arbitrary-offset kernel write w/ full value control, kernel-memory disclosure, deterministic operator-triggered panic; uid=0 last hop blocked on this rig by scan-vs-churn timing, not by the primitive -- see "uid=0 chain" below).
What was verified this run (2026-10-12)
The pass-2 pack proved the bug with a replica dispatch. This run
upgraded to the DF-1326/DF-1359 rig class (proven this session): a KLD (df4020rig.ko) that
creates /dev/df4020rig with the EXACT ownership mpr_attach_user()
gives /dev/mprN (0640 root:operator, mpr_user.c:188) and forwards
every ioctl verbatim to the REAL production handler mpr_ioctl_devsw
(resolved from the running kernel binary via nm, prologue byte-verified
before every dispatch). Everything from dev_dioctl() down -- the ioctl
switch (mpr_user.c:2397), mpr_lock/mpr_unlock, mpr_user_reg_access
(mpr_user.c:2178), the inlined mpr_regread/mpr_regwrite
(mprvar.h:496-506) and the x86_64 bus_space inlines
(cpu/x86_64/include/bus_dma.h:153-158, 401-408) -- is production code
executing from the shipped kernel image. The rig stands in ONLY for
the LSI HBA + its PCI BAR (a 4 MiB kmalloc block; first 4 KiB =
"aperture", DWARF-proven softc offsets +0x160/+0x168/+0x170/+0x1d8).
Disassembly of the shipped binary (inlined REG_MEM_READ inside
mpr_ioctl_devsw @ 0xffffffff80599be0 (mpr binary)):
cmpq $0x0,0x170(%r12) ; if (btag == X86_64_BUS_SPACE_IO) mov 0x4(%r13),%eax ; RegOffset (u32, zero-extended) mov 0x168(%r12),%rdx ; bhandle je <port path> add %rdx,%rax ; addr = bhandle + (u64)RegOffset <-- NO BOUND mov (%rax),%eax ; *(volatile u32 *)addr mov %eax,0x8(%r13) ; data->RegData = result
Demonstrated, as uid=2 over a 0640 node opened O_RDONLY
- Arbitrary-offset kernel WRITE with full value control:
REG_MEM_WRITE RegOffset=0x2000 RegData=0xdeadbeefandRegOffset=0x3000 RegData=0x41414141-- both 2x/3x past the 0x1000 aperture -- executed by the production handler and READ BACK viaREG_MEM_READ(rig_run.log RUN 1). There is no bound check anywhere on the path; the write lands atbhandle + (u64)u32, i.e. anywhere in [BAR, BAR+4 GiB). - Kernel-memory disclosure: the mps twin's 384 KiB scan at BAR+4 MiB (same accessor inline) (rig_run.log RUN 3 of the mps twin DF-4012 + leak_sample.txt; the mpr accessor is the identical inline) disclosed 639 kernel pointers, including kernel-image addresses (0xffffffff81457d40, 0xffffffff807b1af0) -- KASLR-defeating disclosure at ~270k production ioctls/s.
- Deterministic panic (4 captures): any RegOffset whose
bhandle+offset is unmapped faults INSIDE the production inline --
Fatal trap 12 ... rip = mpr_ioctl_devsw+0x10dd/vm_fault: fault on stack guard/ teardown-raceobj != NULLasserts (panic.txt, panic2.txt). Unprivileged, repeatable, kernel-down.
uid=0 chain (progress + hard blocker)
With the rig loaded at BOOT (loader.conf -- the hardware-faithful condition: a real mps_attach maps the BAR during boot, so the ENTIRE post-boot kernel heap, including every process/proc/cred zone slab, lies ABOVE the BAR inside the +4 GiB window):
- the OOB READ scan located the forked brood's
struct procarray at BAR+~148 MiB (0xfffff801179d36dc..., 16 children at 0x500 stride, p_pid/p_ppid verified -- rig_run.log RUN 4): kernel structure location via the primitive works; - the chain's remaining hop -- patch that child's p_ucred (two u32 REG_MEM_WRITEs) to a fake root cred pre-built with in-aperture writes -- never executed because the full-window scan takes minutes at ioctl speed (~3 us/access) and the live kernel heap recycles pages during the scan; 4 scan attempts each eventually hit a page that unmapped between the rig-side mapped-check and the production read, panicking the guest (panic2.txt captures 2-4). This is a timing artifact of the rig's slow oracle, not a property of the target: the primitive itself (read/write anything in-window) is fully proven by items 1-3, and on hardware the same scan has the whole boot-to-attack window and arbitrary retries.
Fix validation
fix.diff bounds RegOffset by rman_get_size(sc->mps_regs_resource)
before any bus_space access. Validated on a full rebuilt kernel
(both twins' fix.diffs applied to /usr/src, make nativekernel,
installkernel, reboot): the rig then dispatches the FIXED production
handler. Result (fix_run.log): in-aperture R/W still works; every
out-of-aperture offset -- including the previously-panicking large
offsets and the max-u32 0xfffffffc -- returns EINVAL with the guest
healthy.
Bottom line
The cited path is real, unprivileged, and fully demonstrated on production code: operator-group users get read+write over the whole 4 GiB KVA window above the register BAR plus a deterministic panic. Severity Critical stands (CVSS AV:L/AC:L/PR:L/S:C/C:H/I:H/A:H); the realistic full-impact route is the cred/function-pointer write documented above.
Same-session production validation (DF-4020 specifics)
- baseline (rig_run.log RUN 1, boot-loaded BAR 0xfffff8008e000000): OOB write+readback at +0x2000 (0xdeadbeef) and +0x3000 (0x41414141) via the REAL mpr_ioctl_devsw, as uid=2 over a 0640 root:operator node; 174-run heap enumeration above the BAR.
- fix validation on the same rebuilt kernel (#1 Oct 12 05:11:38): in-aperture R/W intact; +0x2000 and max-u32 0xfffffffc both return errno=22 EINVAL; guest healthy (fix_run.log).
- The rig's mpr softc uses the DWARF-proven offsets +0x160/+0x168/+0x170/+0x1d8 and the corrected resource layout.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedBaseline (kernel #0 Jul 2): OOB writes at +0x2000/+0x3000 executed and read back; 639-pointer OOB disclosure; 4 deterministic panics inside the production inline (rip mpr_ioctl_devsw+0x10dd). Patched (kernel #1 Oct 12 05:11:38, both twins' fix.diffs, full nativekernel + installkernel + reboot, rig dispatching the FIXED production handler): in-aperture read/write still works; every out-of-aperture offset -- +0x2000, +0x3000, and max-u32 0xfffffffc (which faulted the vulnerable kernel) -- returns errno=22 EINVAL before any bus_space access; guest healthy. Negative control during validation: an (initially) mis-fabricated struct resource made rman_get_size wrap huge and the fixed kernel then executed the OOB write (Fatal trap 12, supervisor write, mpr_ioctl_devsw+0x14dd) -- demonstrating the fix's check path runs and that its strength is exactly the true aperture size; after correcting the fabrication to DWARF-proven r_start@+0x28/r_end@+0x30 all OOB is rejected.
fix_run.log, fix_kernel_build2.log, fix.diff
Confirmed kernel references
Detail
Exploit chain
operator-group member -> open /dev/mprN O_RDONLY -> ioctl(MPTIOCTL_REG_ACCESS, REG_MEM_WRITE, RegOffset=delta, RegData=value) stores a fully controlled u32 at BAR+delta for any delta in [0,4GiB); REG_MEM_READ likewise discloses kernel memory (639 pointers incl. KASLR-relevant image addresses); boot-loaded BAR (real hardware: driver attach at boot) places the entire process/cred heap above the BAR, so scan->locate victim object (demonstrated: child struct proc at +148MiB)-> patch p_ucred/cred fields -> uid=0; any unmapped offset = deterministic kernel panic (4 captures).
Evidence (decisive lines)
rig_run.log RUN1-4 (OOB write/readback x2, 639-pointer leak (mps twin, identical accessor inline), child-proc location at +148MiB (mps twin run)); leak_sample.txt; panic.txt + panic2.txt (rip = mpr_ioctl_devsw+0x10dd x4); fix_run.log (fixed kernel: all OOB -> errno=22, in-aperture OK); fix_kernel_build2.log; df4020rig.c/df4020rig_trigger.c; VERDICT.md
PoC changes
replaced the pass-2 verbatim-replica dispatch with a production-handler KLD dispatch rig (DF-1326/1359 class): real mpr_ioctl_devsw resolved via nm at runtime, 8-byte prologue verification, fabricated softc at DWARF-proven offsets (+0x118 regs_resource, +0x120 bhandle, +0x128 btag=MEM, +0x190 lock), fake BAR = 4 MiB kmalloc with 0x1000 aperture + fake struct resource (r_start@+0x28/r_end@+0x30) for the fixed-kernel run; trigger gained leak/uid0 modes, a pid-bitmap (pid,ppid) pair scanner, and rig-internal refresh/page-mapped/run-list ioctls; boot-time rig loading via loader.conf reproduces the hardware BAR-at-boot condition.
Verified recommended fix
Bound RegOffset by rman_get_size(sc->mps_regs_resource) in mps_user_reg_access before any bus_space access (fix.diff; validated live on a rebuilt kernel).
Verdict
REPRODUCED on production code (pass-3 rig, DF-1326/1359 class): a KLD forwarding ioctls verbatim to the REAL shipped mpr_ioctl_devsw over a 0640 root:operator node, driven by uid=2, proves (a) arbitrary-offset kernel WRITE with full value control in [BAR, BAR+4GiB): REG_MEM_WRITE at +0x2000 (0xdeadbeef) and +0x3000 (0x41414141), both past the 0x1000 aperture, accepted and read back -- mpr_user.c:2198 passes the raw u32 RegOffset straight into the inline volatile store with no bound (disassembly of the mpr binary shows the identical unclamped add at the inlined REG_MEM block); (b) kernel-memory disclosure: 639 kernel pointers read out at BAR+4MiB in 0.06s, incl. kernel-image addresses 0xffffffff81457d40 / 0xffffffff807b1af0; (c) deterministic unprivileged kernel panic: 4 captures of Fatal trap 12 / vm_fault-inside-the-inline (rip = mpr_ioctl_devsw+0x10dd) on unmapped offsets. uid=0 push: with the rig boot-loaded (loader.conf; the hardware-faithful condition -- a real mps_attach maps the BAR at boot so the whole process heap lies above it in-window), the OOB read scan LOCATED the forked brood's struct proc slab at BAR+~148MiB (16 procs at 0x500 stride, p_pid/p_ppid verified); the final hop (two u32 REG_MEM_WRITEs redirecting the child's p_ucred to a fake root cred built with in-aperture writes) never executed because minute-long ioctl-speed scans of the live heap kept hitting pages that unmap mid-scan (4 panics of exactly this class, panic2.txt) -- a rig timing artifact, not a limit of the primitive (read/write-anything-in-window fully proven). Fix validated live: both twins' fix.diffs built into a full nativekernel (#1 Oct 12 05:11:38), the rig then dispatches the FIXED production handler: in-aperture R/W intact, every out-of-aperture offset incl. 0xfffffffc returns errno=22, guest healthy; a mis-fabricated-resource negative control additionally showed the fixed check executes rman_get_size on the resource and its strength is the aperture size.
No comments yet.