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

All mps_config.c getters free the mps_command while the IOC still owns it after mps_request_polled timeout - late completion UAF / cross-request completion confusion / reply-queue corruption

Summary

mps_request_polled gives up after ~50s and returns ETIMEDOUT WITHOUT cancelling or resetting - the request is still posted. Every getter treats any error as complete and frees the command at its out: label, zeroing the object while the hardware owns it. When the wedged IOC later completes the stale request, the interrupt path writes onto the freed/reallocated object and the completion callback fires with the stale CONFIG reply (misparsed as SCSI I/O), plus foreign/double reply-slot frees. The successor mpr driver fixed exactly this lifecycle (reinit-on-timeout + *cmp=NULL). Lifecycle harness on the guest: cross-request completion, foreign fields, foreign/double reply free all reproduced; the mpr-pattern fixed model holds all invariants. Reinit-on-timeout fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4364 Β· 10 files
FileTypeDescriptionSize
harness.c β€” 14.1 KB view raw
build.sh β€” 88 B view raw
run.sh β€” 90 B view raw
run.log β€” 2.3 KB view raw
env.txt β€” 253 B view raw
fix.diff β€” 1.0 KB view raw
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 4.4 KB view raw

DF-4364 β€” mps_config.c polled-timeout: command freed while the IOC owns it

sys/dev/raid/mps/mps_config.c β€” every getter frees its struct mps_command on the error path even when mps_request_polled() returned ETIMEDOUT, i.e. when the request is still posted to the IOC:

  • mps.c:2140-2161 mps_request_polled() breaks out after ~50 s with ETIMEDOUT β€” without cancelling the request or resetting the chip.
  • every getter's out: label (mps_config.c:167, 311, 637, 762, 884, 1001, 1120, 1241, 1386) runs mps_free_command(sc, cm) (mpsvar.h:514-540), which zeroes the command and returns it to sc->req_list β€” while the IOC still owns that SMID.
  • when the wedged IOC later completes the request, mps_intr_locked() (mps.c:1581-1600) does cm = &sc->commands[SMID], writes cm->cm_reply/cm->cm_reply_data onto the freed or reallocated object, and calls mps_complete_command(cm) β†’ the new owner's completion callback runs with the stale CONFIG reply; later frees return a foreign reply slot to the reply free queue (or leak it β†’ reply-queue exhaustion).

The successor driver fixed exactly this: mpr_wait_command() (sys/dev/raid/mpr/mpr.c:3557-3622) takes struct mpr_command **cmp, on timeout performs mpr_reinit() (diag reset β†’ the stale request can never complete) and clears *cmp when the reinit freed the command.

Reproduce (DragonFly guest, unprivileged)

cc -O2 -Wall -o harness harness.c && ./harness     # exit 1: violations CONFIRMED
cc -O2 -Wall -DFIXED -o harness_fixed harness.c && ./harness_fixed   # exit 0

The guest has no LSI mps HBA, so β€” per the DF-1556/DF-1557 precedent β€” the harness mirrors the kernel object lifecycle field-for-field (free-list semantics of mpsvar.h:514-555, the polled timeout, the late address-reply completion of mps.c:1581-1600) and asserts the kernel invariants. Baseline: 2/2 scenarios violate them; fixed model (mps_request_polled β†’ mps_reinit on timeout, mirroring mpr): 0/2.

Files

  • harness.c β€” lifecycle state-machine harness (baseline + -DFIXED)
  • build.sh / run.sh β€” exact commands
  • run.log β€” FULL guest output: build + decisive baseline (RUN_EXIT=1) + fixed validation (FIXED_EXIT=0)
  • env.txt β€” guest uname / compiler / mps-in-GENERIC / no-HBA proof
  • fix.diff β€” git-apply-able fix vs sys/dev/raid/mps/mps.c
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-4364 VERDICT

Status: reproduced (harness; guest has no mps HBA β€” DF-1556/1557 method). Impact class: use-after-free / cross-request completion confusion / reply-queue corruption (CWE-416/662). Primitive certain at the lifecycle level.

Root cause (line-accurate)

  1. mps_request_polled() β€” sys/dev/raid/mps/mps.c:2140-2161: polls for ~50 s (timeout++ > 1000 Γ— 50 ms), then returns ETIMEDOUT with the request frame still posted to the IOC. No reinit, no cancel. (Contrast mpr_wait_command, sys/dev/raid/mpr/mpr.c:3557-3622: timeout β†’ mpr_reinit(sc) diag reset + *cmp = NULL when the reinit freed the command β€” the upstream fix for precisely this hazard.)
  2. All ten getters in sys/dev/raid/mps/mps_config.c treat any error from mps_request_polled as "command complete": goto out β†’ mps_free_command(sc, cm) at the out: labels (mps_config.c:167-168, 311-312, 637-638, 761-762, 884-885, 1001-1002, 1120-1121, 1241-1242, 1386-1387).
  3. mps_free_command() β€” mpsvar.h:514-540 β€” zeroes the command (cm_reply, cm_flags, cm_complete, …) and pushes it onto sc->req_list; the SMID the IOC owns is untouched and the request stays outstanding.
  4. Late completion, mps_intr_locked() β€” mps.c:1518-1600: cm = &sc->commands[desc->AddressReply.SMID] (:1581) β†’ the freed or since-reallocated command; cm->cm_reply = reply and cm->cm_reply_data = … (:1582-1584) are writes into a foreign object; mps_complete_command(cm) (:1600, body :1405-1431) then runs the new owner's cm_complete callback with the stale CONFIG reply, wakes the wrong sleeper, and decrements io_cmds_active.
  5. Follow-on corruption when the confused owner later completes: * reallocated-as-SCSI-IO case β€” the SCSI completion misparses the CONFIG reply; mps_free_command returns the stale reply frame address to the reply free queue (mpsvar.h:518-519) β€” a foreign/double slot free (reply FIFO accounting corrupted); * still-FREE case β€” the FREE object now carries live cm_reply fields; the next mps_alloc_command() hands it out; if that request completes via the SCSI_IO_SUCCESS path (cm_reply = NULL, mps.c:1516) the slot is leaked β†’ progressive reply-queue exhaustion β†’ adapter wedge (DoS).

Preconditions: an IOC that stalls a config request for ~50 s and completes it afterwards β€” a hung, buggy, or malicious firmware (or device posing as the HBA). Not unprivileged-user-reachable without such hardware; same device-trust threat model as the known DF-1556/DF-1557, which is why this is Medium rather than High.

Reproduction (guest, unprivileged user maxx)

harness.c mirrors the kernel lifecycle field-for-field (free-list order and field zeroing of mpsvar.h:514-555; polled timeout of mps.c:2150-2158; late address-reply completion of mps.c:1581-1600; mps_complete_command of mps.c:1405-1431; reply-slot ownership epochs standing in for reply-free-queue frames). Two scenarios:

  • A (reallocated): timed-out config cm freed β†’ immediately reused for a SCSI IO (BUSY, callback installed, canary) β†’ late completion lands on the BUSY command: CROSS-REQUEST COMPLETION (SCSI callback invoked with the CONFIG reply frame), FOREIGN FIELDS written onto the live command, REPLY FOREIGN-FREE when the SCSI IO later frees it. CONFIRMED.
  • B (still free): late completion writes cm_reply/cm_reply_data onto the FREE object β†’ reply slot DOUBLE-BOOKED (leak or double-free on the next owner). CONFIRMED.

Baseline RUN_EXIT=1 (2/2 scenarios violated). Full output in run.log.

Fix validation

fix.diff (against the read-only sys/ tree; never applied there) mirrors the proven mpr pattern: on timeout, mps_request_polled() calls mps_reinit(sc) (diag reset β€” the IOC discards outstanding requests, so a late completion is impossible) before returning ETIMEDOUT, making the callers' frees safe. Modeled with -DFIXED: both scenarios hold all invariants, FIXED_EXIT=0. (An alternative minimal backport β€” leak the command on ETIMEDOUT in the config getters instead of freeing β€” has the same safety property; the reinit variant additionally recovers the adapter and matches upstream evolution.) fix_status: fixed at harness level; a kernel-build validation is impossible on this guest (no HBA).

Guest left clean (userland harness only, /tmp artifacts).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Harness-level fix validation: baseline violates all invariants (RUN_EXIT=1); with fix.diff modeled (-DFIXED: mps_reinit on timeout quiesces the IOC before the free) the identical scenarios hold all invariants (FIXED_EXIT=0). Kernel-build validation not possible on this guest (no LSI HBA); the change mirrors the already-proven mpr_wait_command pattern.

findings/poc/DF-4364/run.log (baseline vs FIXED sections); fix.diff
↓ fix.diffper-fix-DF-4364

Confirmed kernel references

Detail

Exploit chain

hung/malicious IOC stalls a config request ~50s then completes it late -> freed mps_command is reused for unrelated SCSI IO -> late completion invokes the SCSI completion callback with a CONFIG reply (misparsed as MPI2_SCSI_IO_REPLY) and stamps foreign cm_reply/cm_reply_data -> subsequent free corrupts the reply free queue (foreign/double slot free) or leaks the slot (reply exhaustion -> adapter wedge); alternatively the late completion lands on a still-FREE command, arming the same corruption for the next owner. Device-driven (firmware must stall then complete); realistic ceiling is kernel panic / I/O corruption / adapter DoS, not direct code execution

Evidence (decisive lines)

findings/poc/DF-4364/run.log: baseline Scenario A prints CROSS-REQUEST COMPLETION + FOREIGN FIELDS ON LIVE SCSI IO COMMAND + REPLY FOREIGN-FREE (RUN_EXIT=1), Scenario B prints STALE FIELDS ON FREE OBJECT + DOUBLE-BOOKED reply slot; fixed run prints invariants hold 2x, FIXED_EXIT=0

PoC changes

wrote harness from scratch (no seed): field-for-field lifecycle model of mpsvar.h:514-555 free-list semantics, mps.c:2140-2161 polled timeout, mps.c:1581-1600 late completion, reply-slot ownership epochs; first draft had missing BUSY transition + weak confusion check, fixed during verification

Verified recommended fix

Mirror the mpr fix: on polled timeout, mps_request_polled() must quiesce the chip (mps_reinit) before returning so the caller's mps_free_command cannot race a late IOC completion (fix.diff); minimal backport alternative: leak the cm on ETIMEDOUT in the mps_config.c getters.

Verdict

REPRODUCED (harness; guest has no mps HBA - DF-1556/1557 method). mps_request_polled (mps.c:2140-2161) returns ETIMEDOUT with the request still posted to the IOC; every getter in mps_config.c then frees the command at its out: label (mps_config.c:167,311,637,762,884,1001,1120,1241,1386). When the wedged IOC later completes the stale SMID, mps_intr_locked (mps.c:1581-1600) writes cm_reply/cm_reply_data into the freed/reallocated object and runs mps_complete_command on it: the new owner's completion callback fires with the stale CONFIG reply (cross-request type confusion), and the follow-on free returns a foreign reply frame to the reply free queue (or leaks it -> queue exhaustion). The mpr driver fixed exactly this lifecycle (mpr_wait_command takes mpr_command*, reinit-on-timeout, cmp=NULL). Harness mirrors the kernel lifecycle field-for-field: 2/2 scenarios violate the invariants (exit 1); the fix.diff model (mps_request_polled calls mps_reinit on timeout) holds all invariants (exit 0).