mpr diag-buffer registration: shared DMA trio + stale per-type size yields OOB kernel disclosure, cross-type free, and NULL-deref/UAF
Summary
mpr_diag_register overwrites the single shared sc->fw_diag_{dmat,buffer,map} trio without checking it is already owned by another buffer type, while fw_diag_buffer_list[type].size keeps the stale larger size; mpr_diag_read_buffer bounds-checks against the stale size but copyouts from the smaller live allocation (up to ~1MB kernel disclosure); unregister of the first type frees the second type's resources; the next read dereferences NULL/freed dmat at bus_dmamap_sync. Serialized by MPR_FLAGS_BUSY - fully deterministic, no race. Operator-group member with an mpr controller. REGISTER(TRACE,1MB)->REGISTER(SNAPSHOT,4KB)->READ->UNREGISTER->READ sequence replicated verbatim on guest (1,044,480-byte disclosure figure). Per-type-resource fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4008 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| diag_statemachine.c | replication-source | verbatim port of the three diag state functions (bus_dma stubbed) | 7.7 KB | view raw |
| replication_run.log | run-log | five-step sequence showing all three failure shapes | 812 B | view raw |
| build.log | build-log | guest cc output + uname | 150 B | view raw |
| README.md | readme | finding summary + trigger sequence | 3.3 KB | β raw |
| VERDICT.md | verdict | full narrative with path:line trace | 4.5 KB | β raw |
| verdict.json | verdict | machine verdict | 3.1 KB | view raw |
DF-4008 β mpr diag-buffer cross-type registration state confusion
What
MPTIOCTL_DIAG_ACTION (REGISTER/UNREGISTER/READ_BUFFER) manages three
logical buffer types (fw_diag_buffer_list[TRACE|SNAPSHOT|EXTENDED],
mprvar.h:440-441) but stores the live DMA resources in one shared trio
of softc fields β sc->fw_diag_buffer, sc->fw_diag_busaddr,
sc->fw_diag_dmat, sc->fw_diag_map (mprvar.h:410-413). mpr_diag_register()
(mpr_user.c:1442-1638) never checks that the shared trio is free before
overwriting it, and per-type state (pBuffer->size, unique_id) stays stale
afterwards. With two or more buffer types enabled by the IOC
(mpr.c:588-599 sets enabled=TRUE for each advertised capability β a
SAS3008-class FW advertises TRACE+SNAPSHOT+EXTENDED), an ioctl-capable user
can drive three distinct memory-safety failures:
- Stale-size OOB read / kernel memory disclosure: register type A with
RequestedBufferSize = 1 MB, then type B with 4 KB.sc->fw_diag_buffernow points at B's 4 KB allocation, butlist[A].sizeis still 1 MB.mpr_diag_read_buffer()with A'sUniqueIdbounds-checks against the stale 1 MB (off + bytes > pBuffer->size, mpr_user.c:1798) and thencopyouts from the 4 KB allocation (mpr_user.c:1812-1814) β up to ~1 MB of adjacent kernel memory disclosed to userspace. - Cross-type free / tag leak:
mpr_diag_unregister()of A freessc->fw_diag_buffer/dmat(mpr_user.c:1681-1694) β which are B's resources. B's registration state survives with freed backing. - NULL-deref panic / UAF read: any subsequent
READ_BUFFERfor B hitsbus_dmamap_sync(sc->fw_diag_dmat=NULL, ...)(mpr_user.c:1805) β NULL pointer dereference panic; if the pointers were not reset, it is acopyoutfrom freed DMA memory (UAF read).
Trigger (privileged surface)
/dev/mprN is 0640 root:operator (mpr_user.c:205) β same boundary as
DF-1326. Sequence: REGISTER(TRACE,1MB) β REGISTER(SNAPSHOT,4KB) β
READ_BUFFER(uidA, 0, 1MB) β UNREGISTER(uidA) β READ_BUFFER(uidB,...).
mpr_user_diag_action serializes via MPR_FLAGS_BUSY (mpr_user.c:2000-2005),
so the confusion is fully deterministic β no race needed.
Hardware gate: the first REGISTER must survive
mpr_post_fw_diag_buffer() (needs real FW accepting DIAG_BUFFER_POST) and
the IOC must advertise β₯2 diag capabilities. Source-level mechanism is
otherwise unconditional.
Replication (guest, userspace logic-level)
The state machine is pure kernel bookkeeping around bus_dma; diag_statemachine.c
replicates mpr_diag_register / mpr_diag_unregister / mpr_diag_read_buffer
verbatim modulo bus_dma stubs and runs the exact sequence above.
replication_run.log (guest, 6.5-DEVELOPMENT):
2) ... live_alloc now 4096 bytes, but TRACE.size is still 1048576 3) READ uid=0xA -> copyout OOB READ: bound check used stale size=1048576 but live allocation is 4096 bytes -> 1044480 bytes past the end disclosed to user 4) UNREGISTER uid=0xA -> shared trio freed ... this was SNAPSHOT's allocation 5) READ uid=0xB -> bus_dmamap_sync(NULL) => KERNEL NULL-DEREF PANIC here
Fix sketch
Track the live allocation per buffer type (store dmat/map/buffer/busaddr
inside mpr_fw_diagnostic_buffer_t), or refuse REGISTER while any type holds
the shared trio; recompute pBuffer->size from the live allocation at
READ_BUFFER time.
DF-4008 β VERDICT
Verdict
INCONCLUSIVE (live) β mechanism certain at source level; live trigger requires an LSI SAS controller (IOC advertising β₯2 FW-diag buffer capabilities and accepting DIAG_BUFFER_POST), absent from this QEMU guest. A verbatim userspace replication of the three state functions on the guest demonstrates all three failure transitions (stale-size 1,044,480- byte OOB disclosure, cross-type free, NULL-dmat panic point). The bug is in pure kernel bookkeeping β no race, fully deterministic, serialized by MPR_FLAGS_BUSY.
Root cause (path:line)
struct mpr_softckeeps ONE shared diag resource trio (sys/dev/raid/mpr/mprvar.h:410-413) for THREE logical buffer types (fw_diag_buffer_list[MPI2_DIAG_BUF_TYPE_COUNT=3], mprvar.h:440-441).mpr_diag_register()(mpr_user.c:1504-1531) unconditionally runsbus_dma_tag_create(..., &sc->fw_diag_dmat)andbus_dmamem_alloc(..., &sc->fw_diag_buffer, ..., &sc->fw_diag_map)with no check that the trio is already in use by another type; the previous tag/buffer pointers are simply overwritten (leak). Per-typepBuffer->size(mpr_user.c:1596) retains the old, larger size.mpr_diag_read_buffer()bounds-checksStartingOffset + BytesToReadagainst the stale per-type size (mpr_user.c:1798-1802) but reads from the shared current allocationsc->fw_diag_buffer + StartingOffset(mpr_user.c:1812-1814) β OOBcopyoutto userspace.mpr_diag_unregister()(mpr_user.c:1681-1694) frees whatever the shared trio currently references (the other type's memory) while resetting only the unregistered type'sunique_id.- A subsequent READ_BUFFER of the surviving type dereferences the freed/NULL
sc->fw_diag_dmatatbus_dmamap_sync(mpr_user.c:1805) β NULL-deref panic, or reads freed memory (UAF) at :1812.
Enabling condition: mpr.c:588-599 sets enabled = TRUE for every diag
capability bit the IOC reports β SAS3008-class FW reports TRACE, SNAPSHOT
and EXTENDED, so β₯2 simultaneously enabled types is the common case on
real hardware.
Reachability
MPTIOCTL_DIAG_ACTION (mpr_user.c:2365-2373) β mpr_user_diag_action()
(:1992) β mpr_do_diag_action() (:1881) β register/unregister/read. Device
0640 root:operator (mpr_user.c:205); ioctls not gated on open mode (group
perm r-- suffices, proven for the same node mode in DF-1326's replica run).
No HW-independent prefix reaches the bug: REGISTER requires a successful
mpr_post_fw_diag_buffer() (mpr_user.c:1612, real FW round-trip) before the
state that matters is installed.
Impact assessment
- OOB read is a disclosure primitive: attacker-sized (first registration size is user-chosen, bounded by bus_dma_tag maxsize = that same value), repeated, serialized, arbitrary-offset within the stale size β kernel heap disclosure to an operator-group user.
- NULL-deref is a reliable panic/DoS.
- Cross-type free is a leak + logical corruption enabler. No write primitive found in this path (the walk only reads; post/release write only FW-controlled request frames). Ceiling: kernel info leak + DoS for group-operator; not by itself a privesc write primitive.
Verification posture
- Guest: no mpr device (
ls /dev/mpr*β ENOENT;pciconf -lβ no LSI/SAS). diag_statemachine.c(this pack) replays the exact register/register/ read/unregister/read sequence with the three functions ported verbatim (bus_dma stubbed); run on the guest (seereplication_run.log): step 3 prints the 1,044,480-byte stale-size OOB disclosure, step 4 the cross-type free, step 5 the NULL-dmat panic point.attempts: 1 (logic replication deterministic).
Recommended fix
--- a/sys/dev/raid/mpr/mprvar.h
+++ b/sys/dev/raid/mpr/mprvar.h
@@ typedef struct mpr_fw_diagnostic_buffer {
uint32_t unique_id;
+ bus_dma_tag_t dmat; /* per-type, not shared */
+ bus_dmamap_t map;
+ uint8_t *buffer;
+ bus_addr_t busaddr;
boolean_t enabled;
(and move the trio operations in mpr_user.c:1508-1633 / 1681-1694 /
1805-1814 onto pBuffer->dmat/map/buffer/busaddr; alternatively reject
REGISTER while sc->fw_diag_buffer != NULL.)
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.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testablefix.diff not yet authored for this finding (see VERDICT.md sketch: per-type dmat/map/buffer/busaddr fields)
Confirmed kernel references
- sys/dev/raid/mpr/mpr_user.c:1504
- sys/dev/raid/mpr/mpr_user.c:1508
- sys/dev/raid/mpr/mpr_user.c:1523
- sys/dev/raid/mpr/mpr_user.c:1596
- sys/dev/raid/mpr/mpr_user.c:1612
- sys/dev/raid/mpr/mpr_user.c:1681
- sys/dev/raid/mpr/mpr_user.c:1798
- sys/dev/raid/mpr/mpr_user.c:1805
- sys/dev/raid/mpr/mpr_user.c:1812
- sys/dev/raid/mpr/mprvar.h:410
- sys/dev/raid/mpr/mprvar.h:440
- sys/dev/raid/mpr/mpr.c:588
Detail
Exploit chain
operator-group ioctl user: REGISTER(TRACE,1MB) -> REGISTER(SNAPSHOT,4KB) -> READ_BUFFER(uidTRACE,0,1MB) discloses ~1MB past a 4KB DMA allocation to userspace; UNREGISTER(uidTRACE) frees SNAPSHOT's resources; READ_BUFFER(uidSNAPSHOT) panics (bus_dmamap_sync(NULL), mpr_user.c:1805) or reads freed memory.
Evidence (decisive lines)
replication_run.log (all five steps with the three failure shapes printed), diag_statemachine.c (verbatim port of mpr_diag_register/unregister/read_buffer modulo bus_dma stubs), VERDICT.md root-cause path:line trace
PoC changes
n/a (new finding; replication authored during pass 2)
Verified recommended fix
Make the diag DMA trio per-type inside mpr_fw_diagnostic_buffer_t (or refuse REGISTER while sc->fw_diag_buffer is live); bound READ_BUFFER against the live allocation size.
Verdict
Mechanism certain at source level, hardware-gated live: mpr_diag_register() overwrites the single shared fw_diag_{dmat,buffer,map,busaddr} trio without checking it is free, while per-type fw_diag_buffer_list[].size keeps the stale (larger) size; with two diag types enabled by the IOC (mpr.c:588-599 enables every advertised capability) an ioctl user gets (1) a stale-size OOB copyout (kernel disclosure up to first-registration size minus live size), (2) a cross-type free of the other type's DMA resources, (3) a NULL-dmat bus_dmamap_sync panic or UAF read on the next READ_BUFFER. Deterministic, no race (serialized by MPR_FLAGS_BUSY). No LSI SAS controller on the audit guest, so not live-reproduced; verbatim userspace replication on the guest shows all three transitions (1,044,480-byte OOB disclosure figure, cross-free, NULL-deref point).
No comments yet.