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

mpt_refresh_raid_vol frees a request on ETIMEDOUT that is still queued to the IOC and on request_pending_list β†’ request-pool double-link / cross-command use-after-free

Summary

On progress-indicator timeout the code frees a request the chip still owns (REQ_STATE_QUEUED, linked on request_pending_list). In production kernels (KASSERT INVARIANTS-only) the request returns to the free list while in flight; handed to a new command (double pending-list link), and when the IOC finally completes the RAID action its reply context dispatches the handler on the new owner's request, completing and freeing it - UAF/double-free. Every sibling helper correctly leaves the request for the late-reply free path - the contrast that convicts. Trigger: IOC answering the progress indicator slower than the 2s poll during volume resync. Don't-free-on-timeout fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4211 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.9 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
env.txt β€” 992 B view raw
fix.diff β€” 516 B view raw
manifest.json β€” 816 B view raw
verdict.json β€” 2.0 KB view raw

DF-4211 β€” mpt_refresh_raid_vol frees an in-flight RAID request on ETIMEDOUT

  • File: sys/dev/disk/mpt/mpt_raid.c
  • Lines: 1327–1334
  • Class: use-after-free / request-pool corruption (CWE-415/CWE-672)
  • Bucket: memcorrupt
  • Hardware gate: LSI MPT Fusion RAID controller (mpt(4) RAID personality)

What happens

mpt_refresh_raid_vol() issues MPI_RAID_ACTION_INDICATOR_STRUCT synchronously (mpt_issue_raid_req(..., /*wait*/TRUE) β†’ mpt_wait_req(..., sleep_ok=FALSE, time_ms=2000), sys/dev/disk/mpt/mpt_raid.c:1327-1328, sys/dev/disk/mpt/mpt.c:1316-1363). On timeout the request is still

  1. queued to the IOC (REQ_STATE_QUEUED, frame owned by the chip), and
  2. linked on mpt->request_pending_list (inserted by mpt_send_cmd, mpt.c:1298; nothing removes it on the timeout path).

yet the code immediately does mpt_free_request(mpt, req) (mpt_raid.c:1332).

Every sibling helper in the same file handles this correctly by leaving the request for the late-completion path (mpt_verify_mwce 977–981, mpt_verify_resync_rate 1030–1034 and 1066–1070 all return without freeing; the late reply is freed by mpt_raid_reply_handler via free_req=TRUE, mpt_raid.c:543-547, exactly like mpt_config_reply_handler's documented "late completion" path, mpt.c:520-525).

Corruption chain (production kernel, no INVARIANTS)

  1. mpt_free_request inserts the request on request_free_list while it is still on request_pending_list (the KASSERT(mpt_req_on_pending_list == 0) guard, mpt.c:1213, only exists in INVARIANTS builds β€” sys/sys/systm.h:95-119).
  2. mpt_get_request hands the same request to a new owner (e.g. a CAM SCSI I/O from mpt_cam.c); mpt_send_cmd inserts it into request_pending_list a second time β†’ the TAILQ now contains the same node twice.
  3. The IOC eventually completes the original RAID action (slow controller during resync β€” the exact condition that produced the timeout). The reply's MsgContext is req->index | raid_handler_id (mpt_raid.c:616), so mpt_intr dispatches mpt_raid_reply_handler on the request that now belongs to the new owner: it TAILQ_REMOVEs it, marks it DONE and frees it β†’ the new owner completes/frees a request it no longer owns β†’ double-free / cross-command use-after-free with attacker-influenced (frame) contents.

Trigger is firmware/hardware timing (IOC > 2 s to answer a RAID action while a volume resync is in progress β€” this code path only runs when MPI_RAIDVOL0_STATUS_FLAG_RESYNC_IN_PROGRESS is set). Same trust boundary as the known fw-fed findings DF-1449/DF-2484/DF-2485 (excluded here β€” this is a different bug class: request lifecycle, not index validation).

Verification status

untested β€” hardware-gated. The QEMU audit guest has no LSI MPT Fusion HBA (pciconf -l shows only virtio/bochs devices, see env.txt); mpt.ko never attaches, so the RAID personality (and this path) cannot execute. Source-level certainty: the timeout/free/late-reply protocol is fully traceable in-tree (see VERDICT.md). No uid=0 chain is developed because the primitive cannot be exercised without the controller.

Reproduce (on hardware)

  1. System with mpt(4) RAID volume, start a resync (mpt_raid.c:1317 requires RESYNC_IN_PROGRESS for the indicator fetch).
  2. Saturate/wedge the IOC so the MPI_RAID_ACTION_INDICATOR_STRUCT reply takes > 2 s (or drop it and let it complete late).
  3. Observe mpt_refresh_raid_vol: Progress Indicator fetch timeout on console, then either the INVARIANTS panic (mpt_free_request: req %p:%u func %x on pending list) or, on a production kernel, later list-corruption panics from request_pending_list/request_free_list double-linking.

Fix

fix.diff β€” drop the mpt_free_request() on the ETIMEDOUT path so the request is reclaimed by the late reply, matching every sibling helper.

VERDICT.md
↓ download raw

VERDICT β€” DF-4211

status: untested (hardware-gated) Β· impact: memory corruption (panic-class, uid0 theoretical) Β· confidence: certain (source)

Reproduced?

No. The single QEMU guest available to this run exposes no LSI MPT Fusion controller (env.txt: pciconf -l lists only hostb/isab/atapci/virtio/bochs VGA), so mpt.ko never attaches and mpt_raid_attach/mpt_refresh_raid_vol cannot run. This is a device-present code path: the entire RAID personality only loads when mpt_raid_probe sees ioc_page2->MaxPhysDisks != 0 (sys/dev/disk/mpt/mpt_raid.c:264-272), which requires a real IOC. Per the honest-status table this is "guest reachable but the target device/feature is absent" β€” recorded as untested with the device-absence evidence in env.txt.

Why the bug is certain anyway (line-level trace)

  1. mpt_issue_raid_req(..., wait=TRUE) β†’ mpt_wait_req(mpt, req, REQ_STATE_DONE, REQ_STATE_DONE, sleep_ok=FALSE, 2000) (mpt_raid.c:1327-1328, mpt.c:621-623).
  2. Timeout path in mpt_wait_req (mpt.c:1344-1361): busy-polls DELAY(500)+mpt_intr() until --timeout == 0 β†’ breaks, sets REQ_STATE_TIMEDOUT, returns ETIMEDOUT. The request is never removed from request_pending_list and stays REQ_STATE_QUEUED β€” the chip still owns the frame.
  3. mpt_refresh_raid_vol then calls mpt_free_request(mpt, req) (mpt_raid.c:1332).
  4. mpt_free_request (mpt.c:1194-1251) puts the request on request_free_list. Its pending-list guard is KASSERT (mpt.c:1213-1215) β€” compiled out without INVARIANTS (sys/sys/systm.h:95-119 shows KASSERT is a no-op unless INVARIANTS).
  5. From here either - the request is re-allocated (mpt_get_request, mpt.c:1255-1279) and re-queued (mpt_send_cmd TAILQ_INSERT_HEAD, mpt.c:1298) while still linked once on the pending list β†’ double-link corruption; or - the late RAID-action reply arrives: context is req->index | raid_handler_id (mpt_raid.c:616), so mpt_intr (mpt.c:809-821) dispatches mpt_raid_reply_handler (mpt_raid.c:521-550) on a request now owned by an unrelated user β€” it completes and frees the new owner's request (use-after-free / double-free).
  6. Sibling helpers prove intent: mpt_verify_mwce (mpt_raid.c:977-981) and mpt_verify_resync_rate (mpt_raid.c:1030-1034, 1066-1070) all return on ETIMEDOUT without freeing; the late reply frees via free_req=TRUE (mpt_raid.c:545-547), exactly the protocol documented in mpt_config_reply_handler ("Whew- we can free this request (late completion)", mpt.c:520-525). mpt_refresh_raid_vol is the one deviant.

Exploit chain

Not developed: no device. With hardware: primitive = one request_t (double-linked on two TAILQs / completed-cross-command). The request frame is IOC-DMA memory; control over contents is indirect. Realistic ceiling on this hardware-fault trigger is a reliable panic; a uid0 chain would require the attacker to control the IOC (malicious firmware / PCIe device), in which case they already own the bus. Severity Medium is the right call.

Fix

Remove the free on the timeout path (fix.diff, mirrors the sibling helpers; late replies are reclaimed by mpt_raid_reply_handler). Fix not runtime-validated for the same hardware reason β€” fix_status: not_testable.

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.diffper-fix-DF-4211

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-4211/VERDICT.md (line-level trace), findings/poc/DF-4211/env.txt (guest pciconf: no MPT HBA), findings/poc/DF-4211/fix.diff

PoC changes

no runnable PoC: path requires LSI MPT Fusion RAID hardware absent from the audit guest (env.txt)

Verified recommended fix

do not free the request on ETIMEDOUT; let the late reply reclaim it via mpt_raid_reply_handler

Verdict

mpt_refresh_raid_vol frees a request on ETIMEDOUT that is still queued to the IOC and linked on request_pending_list (mpt_raid.c:1332). Every sibling helper leaves it for the late-reply path; here the request returns to the free list while in flight, so it is handed to a new command and the late RAID-action reply (context = index|raid_handler_id) completes/frees the new owner's request -> double-link corruption / UAF. Hardware-gated: guest has no MPT HBA (env.txt), so untested at runtime; the protocol violation itself is line-certain.