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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4008 Β· 6 files
FileTypeDescriptionSize
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
README.md readme finding summary + trigger sequence
↓ download 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:

  1. Stale-size OOB read / kernel memory disclosure: register type A with RequestedBufferSize = 1 MB, then type B with 4 KB. sc->fw_diag_buffer now points at B's 4 KB allocation, but list[A].size is still 1 MB. mpr_diag_read_buffer() with A's UniqueId bounds-checks against the stale 1 MB (off + bytes > pBuffer->size, mpr_user.c:1798) and then copyouts from the 4 KB allocation (mpr_user.c:1812-1814) β†’ up to ~1 MB of adjacent kernel memory disclosed to userspace.
  2. Cross-type free / tag leak: mpr_diag_unregister() of A frees sc->fw_diag_buffer/dmat (mpr_user.c:1681-1694) β€” which are B's resources. B's registration state survives with freed backing.
  3. NULL-deref panic / UAF read: any subsequent READ_BUFFER for B hits bus_dmamap_sync(sc->fw_diag_dmat=NULL, ...) (mpr_user.c:1805) β†’ NULL pointer dereference panic; if the pointers were not reset, it is a copyout from 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.

VERDICT.md verdict full narrative with path:line trace
↓ download raw

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_softc keeps 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 runs bus_dma_tag_create(..., &sc->fw_diag_dmat) and bus_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-type pBuffer->size (mpr_user.c:1596) retains the old, larger size.
  • mpr_diag_read_buffer() bounds-checks StartingOffset + BytesToRead against the stale per-type size (mpr_user.c:1798-1802) but reads from the shared current allocation sc->fw_diag_buffer + StartingOffset (mpr_user.c:1812-1814) β†’ OOB copyout to 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's unique_id.
  • A subsequent READ_BUFFER of the surviving type dereferences the freed/NULL sc->fw_diag_dmat at bus_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 (see replication_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).
--- 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_testable
baseline no→ patch + rebuild →patched clean

fix.diff not yet authored for this finding (see VERDICT.md sketch: per-type dmat/map/buffer/busaddr fields)

per-fix-DF-4008

Confirmed kernel references

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).