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

dev.mpr.N.dump_reqs sysctl: unlocked walk of live command/chain lists (UAF read) plus fixed 128-byte reads past device-sized frames

Summary

mpr_dump_reqs() reads cm->cm_state and walks cm->cm_chain_list with no lock ('Best effort, no locking') while every other consumer mutates those structures under sc->mpr_lock - a concurrent completion (or the realloc path that kfrees sc->chains) yields use-after-free reads/garbage into the sysctl buffer or a panic. It also copies fixed 128 bytes from request/chain frames whose sizes are device-reported (can be < 128) - OOB read past the last frame of the req_frames/chain_frames DMA allocations. Privileged-only racing the storage completion path: freed/adjacent DMA-heap contents into a sysctl reply; panic under race. Lock + clamp fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3816 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 2.2 KB ↓ raw
probe.log β€” 479 B view raw
fix.diff β€” 1.1 KB view raw
manifest.json β€” 876 B view raw
verdict.json β€” 1.7 KB view raw

DF-3816 β€” mpr_dump_reqs sysctl: unlocked walk of live command/chain lists (UAF read) and fixed 128-byte reads past frame allocations

What

mpr_dump_reqs() (sys/dev/raid/mpr/mpr.c:1875-1927) is the dev.mpr.N.dump_reqs sysctl (CTLFLAG_SKIP, so hidden but callable), gated by caps_priv_check_self(SYSCAP_NODRIVER) β€” privileged-only.

/* Best effort, no locking */                                    /* mpr.c:1904 */
for (i = smid; i < numreqs; i++) {
        cm = &sc->commands[i];
        if (cm->cm_state != state)
                continue;
        ...
        TAILQ_FOREACH_MUTABLE(chain, &cm->cm_chain_list, chain_link, chain1)
                hdr.numframes++;
        sbuf_bcat(sb, cm->cm_req, 128);                          /* mpr.c:1918 */
        TAILQ_FOREACH_MUTABLE(chain, &cm->cm_chain_list, chain_link, chain1)
                sbuf_bcat(sb, chain->chain, 128);                /* mpr.c:1921 */
}

Two defects:

  1. Unlocked concurrent access: the command state machine, cm_chain_list mutation (chains are attached/detached under sc->mpr_lock by the completion path, mprvar.h:592-595) and the command completion are all lock-protected everywhere else; this handler reads them with no lock. A command completing concurrently on the interrupt path frees its chains into sc->chain_list while we walk cm->cm_chain_list β†’ TAILQ traversal over freed elements (use-after-free reads) or over a command that flips state mid-dump β†’ stale/garbage hdr and leaked heap contents into the sysctl return buffer (privileged info leak; contents are attacker-shaped only insofar as freed slab data is). Panic possible via corrupted TAILQ pointers.
  2. Fixed 128-byte copies: sbuf_bcat(sb, cm->cm_req, 128) assumes request frames are β‰₯128 bytes, but frame size is sc->facts->IOCRequestFrameSize * 4 (mpr.c:387) β€” device-reported. For the last frame (i == num_reqs-1) a frame size < 128 reads past the end of the req_frames DMA allocation; same for chain->chain/sc->chain_frame_size (= IOCMaxChainSegmentSize*16, can legitimately be 32/48/…).

Threat

Privileged-only trigger (root-equivalent syscap) racing the storage completion path β†’ Low. Value: hardening + correctness for the debugging facility.

Not runnable here

No SAS3 controller on the audit guest β†’ dev.mpr.N tree doesn't even instantiate (probe.log).

Fix

Take mpr_lock around the walk and clamp each copy to min(reqframesz,128) / min(chain_frame_size,128) β€” see fix.diff.

VERDICT.md
↓ download raw

DF-3816 β€” VERDICT

status: inconclusive (runtime) β€” sysctl node requires an instantiated controller (dev.mpr.N does not exist on this guest, probe.log); source defects are certain.

Source trace

  • sysctl registration: mpr.c:1846-1848 (CTLTYPE_OPAQUE|CTLFLAG_RD|CTLFLAG_SKIP, handler mpr_dump_reqs, arg1 = sc).
  • privilege gate: mpr.c:1888 (caps_priv_check_self(SYSCAP_NODRIVER)).
  • read-only guard: mpr.c:1895 (req->newptr != NULL β†’ EINVAL).
  • unlocked walk: mpr.c:1904 comment "Best effort, no locking"; loop mpr.c:1905-1922 reads cm->cm_state, cm->cm_desc, walks cm->cm_chain_list twice. Every other consumer of these fields holds sc->mpr_lock (intr path mpr.c:2269-2271, completion/freepath mprvar.h:568-602); chains are inserted at mpr.c:3182 under the lock and freed to sc->chain_list at mprvar.h:592-595 β€” a concurrent completion mutates the list mid-TAILQ_FOREACH_MUTABLE β†’ traversal can follow a chain_link that was just kfreed... (chains aren't kfreed individually β€” they return to the pool β€” but the element is simultaneously re-armed onto another command's list, so the dump can follow into another command's chain or loop; on the realloc path mpr.c:787-788 the entire sc->chains array IS kfreed β†’ true UAF).
  • fixed-size copies: mpr.c:1918 sbuf_bcat(sb, cm->cm_req, 128) with cm->cm_req = req_frames + i*reqframesz (mpr.c:1530) β€” last frame reads 128 - reqframesz bytes past the allocation when the device reports IOCRequestFrameSize < 32 dwords; mpr.c:1921 same for chain frames of sc->chain_frame_size bytes (mpr.c:397-405, can be 32/48/64…).

Impact ceiling

Privileged debug interface β†’ info-leak of freed/adjacent DMA-coherent heap into a sysctl buffer + panic under race. Low severity, defense-in-depth fix.

Fix

fix.diff: hold mpr_lock across the loop; clamp copies with min().

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

not_testable
baseline no→ patch + rebuild →patched clean

No controller on guest; the sysctl node cannot be exercised in either kernel.

fix.diff; probe.log.
↓ fix.diffper-fix-DF-3816

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md trace; fix.diff (lock + min() clamps).

PoC changes

No runnable PoC without hardware.

Verified recommended fix

Hold mpr_lock across the dump walk and clamp each sbuf_bcat to the device-reported frame size.

Verdict

Source-certain defects in the privileged dev.mpr.N.dump_reqs handler: it walks cm->cm_chain_list and reads cm state with no lock (mpr.c:1904-1922) while every other consumer mutates those lists under sc->mpr_lock (real UAF on the realloc path where sc->chains is kfreed, mpr.c:787-788), and copies fixed 128 bytes from request/chain frames whose sizes are device-reported and can be smaller (mpr.c:1918/1921 vs mpr.c:387/397-405) β€” OOB read past the req_frames/chain_frames DMA allocations. Runtime inconclusive: no controller on the guest, sysctl tree dev.mpr.* never instantiates (probe.log).