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

mps_user_pass_thru frees the in-flight command and its DMA buffer the instant mps_wait_command() times out (use-after-free with device DMA writeback)

Summary

mps_wait_command returns ETIMEDOUT without aborting the command - the chip still owns the request/reply frames and the SGL over cm->cm_data. mps_user_pass_thru immediately kfrees cm->cm_data and returns the slot to req_list: the SMID stays outstanding while both the slot and DMA target are reusable - late completion mutates a possibly-reused command and residual device DMA lands in freed memory. TM branch same defect. Opposite (more dangerous) direction than the DF-4010 mpr twin. Abandon-policy fix compiled+booted; live before/after needs the HBA.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4014 Β· 5 files
FileTypeDescriptionSize
README.md file 451 B ↓ raw
VERDICT.md file 4.3 KB ↓ raw
fix.diff file 1.3 KB view raw
fix_kernel_build.log file 767 B view raw
verdict.json file 3.7 KB view raw
README.md file
↓ download raw

DF-4014 β€” mps_user_pass_thru frees the in-flight command on wait timeout (UAF)

Source-certain (no LSI HBA on the audit guest, so no live run). See VERDICT.md for the line-complete trace: mps.c:2118-2132 (no abort on ETIMEDOUT) -> mps_user.c:977-982 -> :1039-1046 free-while-in-flight; mpsvar.h:514-540 shows the slot returning to the free list. fix.diff abandons the command instead (the driver's own config-page policy, mps_user.c:240-245).

VERDICT.md file
↓ download raw

DF-4014 β€” VERDICT

Verdict

Source-certain, HW-gated (not live-reproducible on this guest: no LSI SAS HBA, /dev/mps* absent). mps_user_pass_thru() frees its command β€” and the user-sized cm->cm_data buffer the chip's SGL still points at β€” the moment mps_wait_command() returns ETIMEDOUT. mps_wait_command() (sys/dev/raid/mps/mps.c:2118-2132) performs no abort: it lksleep()s on the command and returns ETIMEDOUT while the request is still enqueued to the hardware with MPS_CM_FLAGS_WAKEUP set. The immediate free hands the command slot back to the allocation free list and kfree()s the DMA target while the chip retains the SMID and the SGL β€” a classic free-while-in-flight (use-after-free) with device-driven writeback into freed memory.

Bug class

Use-after-free / DMA-after-free triggered by command timeout. The mpr sibling driver has the opposite defect (DF-4010: it leaks the buffer across its reinit path because mpr_wait_command() takes &cm and the reinit frees the command itself) β€” DragonFly's mps kept the old single-pointer wait API and the immediate free, which is the memory-unsafe direction.

Source trace (path:line)

  1. sys/dev/raid/mps/mps_user.c:975 β€” err = mps_wait_command(sc, cm, 30); under mps_lock.
  2. sys/dev/raid/mps/mps.c:2126-2130 β€” on lksleep() timeout the function returns ETIMEDOUT; nothing dequeues or aborts cm (the chip still owns the request frame, reply frame and the SGL built over cm->cm_data).
  3. sys/dev/raid/mps/mps_user.c:977-982 β€” if (err) { ...; mps_unlock(sc); goto RetFreeUnlocked; }.
  4. sys/dev/raid/mps/mps_user.c:1039-1046 β€” RetFreeUnlocked: takes the lock, kfree(cm->cm_data, M_MPSUSER) and mps_free_command(sc, cm).
  5. sys/dev/raid/mps/mpsvar.h:514-540 β€” mps_free_command() resets the command to MPS_CM_STATE_FREE and TAILQ_INSERT_TAIL(&sc->req_list, ...); the slot is instantly allocatable by any other thread (CAM I/O, another ioctl) while the outstanding SMID still refers to it.
  6. Late completion: the reply descriptor's SMID resolves back to this cm; cm_reply is written by hardware/DMA and the completion path mutates a command that may already be reused (cross-command corruption), and the chip's residual DMA lands in the freed cm->cm_data chunk.

The task-management branch has the same defect: sys/dev/raid/mps/mps_user.c:838-844 (wait, EIO) β†’ :863 mpssas_free_tm() frees the in-flight TM command.

Contrast with the driver's own policy for config pages (sys/dev/raid/mps/mps_user.c:240-245): "Leave the request. Without resetting the chip, it's still owned by it and we'll just get into trouble freeing it now." β€” the pass-thru paths violate exactly this.

Reachability

Any operator-group user (0640 /dev/mpsN, ioctls not gated on open mode β€” see DF-1359/DF-1326 boundary analysis) can issue MPTIOCTL_PASS_THRU with a request the firmware takes β‰₯30 s on (or never completes); the timeout then arms the UAF. Exploitability on real hardware: medium-high (requires an HBA and control over command latency, but the free-then-DMA window is wide and repeatable).

Why not live-reproduced

No LSI Fusion-MPT PCI device exists on the audit QEMU guest; the shipped driver never attaches (/dev/mps* absent), so the wait/timeout path cannot be entered. A KLD replica of a chip-async timeout was judged out of scope for fidelity: the essence of the bug is the device's late DMA into freed memory, which a stubbed replica can only assert structurally, not demonstrate. The source trace above is line-complete.

Fix

fix.diff β€” abandon the command on wait-timeout (both the TM branch and the normal branch route to Ret without freeing), matching the driver's own documented abandon policy for timed-out config requests. The abandoned command is reclaimed on the next diag reset. Note: this trades the UAF for a bounded leak until a proper mps_abort_command() exists; the companion mps_user_command() path (:707-711) already leaks by design (DF-4015).

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

fix.diff dry-run applies cleanly; combined nativekernel build (DF-4012+4013+4014 diffs together) rc=0 under -Werror, installed and booted (kern.version 'DragonFly 6.5-DEVELOPMENT #1: Thu Sep 17 14:24:13 UTC 2026'). Live before/after comparison is impossible on this guest (no mps HBA to drive the timeout path), hence not_testable rather than fixed.

fix.diff; fix_kernel_build.log
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Sep 17 14:24:13 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (combined-fix kernel)

Confirmed kernel references

Detail

Exploit chain

operator-group user -> MPTIOCTL_PASS_THRU with a request the firmware holds >30 s -> ETIMEDOUT -> immediate kfree(cm->cm_data) + mps_free_command(cm) while the chip still DMAs per the SGL and the SMID stays outstanding -> freed chunk reused by slab grooming -> device DMA / late-completion writes corrupt the replacement object -> on real hardware: kernel heap corruption toward uid=0 (characterization needs the HBA).

Evidence (decisive lines)

VERDICT.md (line-complete trace), fix.diff (abandon-on-timeout routing both branches to Ret, mirroring the driver's own config-page policy)

PoC changes

no kernel replica built: the essence of the bug is the device's late DMA into freed memory, which a stubbed replica cannot demonstrate beyond the source trace

Verified recommended fix

On wait-timeout, abandon the command instead of freeing it (fix.diff), matching the driver's documented config-page policy; long term add a real mps abort/reinit path.

Verdict

Source-certain use-after-free, not live-reproducible on this guest (no LSI Fusion-MPT HBA, /dev/mps* absent, so the wait/timeout path cannot be entered). mps_wait_command() (mps.c:2118-2132) returns ETIMEDOUT WITHOUT aborting the command -- the chip still owns the request/reply frames and the SGL over cm->cm_data. mps_user_pass_thru() then immediately kfrees cm->cm_data and mps_free_command()'s the slot back to sc->req_list (mps_user.c:977-982 -> :1039-1046; mpsvar.h:514-540), handing both to new users while the outstanding SMID still refers to them: a late completion mutates a possibly-reused command (cross-command corruption) and the chip's residual DMA lands in freed memory. The task-management branch has the same defect (:838-844 -> mpssas_free_tm at :863). This is the DF-4010 (mpr) site with the opposite and more dangerous defect: mpr LEAKS the buffer across its reinit; mps FREES while in flight. The driver itself documents the correct policy for config pages (mps_user.c:240-245: 'Leave the request... we'll just get into trouble freeing it now').