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)
PoC verification
Evidence pack
findings/poc/DF-4364 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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-2161mps_request_polled()breaks out after ~50 s withETIMEDOUTβ 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) runsmps_free_command(sc, cm)(mpsvar.h:514-540), which zeroes the command and returns it tosc->req_listβ while the IOC still owns that SMID. - when the wedged IOC later completes the request,
mps_intr_locked()(mps.c:1581-1600) doescm = &sc->commands[SMID], writescm->cm_reply/cm->cm_reply_dataonto the freed or reallocated object, and callsmps_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 commandsrun.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 prooffix.diffβ git-apply-able fix vssys/dev/raid/mps/mps.cVERDICT.md,manifest.json,verdict.json
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)
mps_request_polled()βsys/dev/raid/mps/mps.c:2140-2161: polls for ~50 s (timeout++ > 1000Γ 50 ms), then returnsETIMEDOUTwith the request frame still posted to the IOC. No reinit, no cancel. (Contrastmpr_wait_command,sys/dev/raid/mpr/mpr.c:3557-3622: timeout βmpr_reinit(sc)diag reset +*cmp = NULLwhen the reinit freed the command β the upstream fix for precisely this hazard.)- All ten getters in
sys/dev/raid/mps/mps_config.ctreat any error frommps_request_polledas "command complete":goto outβmps_free_command(sc, cm)at theout:labels (mps_config.c:167-168, 311-312, 637-638, 761-762, 884-885, 1001-1002, 1120-1121, 1241-1242, 1386-1387). mps_free_command()βmpsvar.h:514-540β zeroes the command (cm_reply, cm_flags, cm_complete, β¦) and pushes it ontosc->req_list; the SMID the IOC owns is untouched and the request stays outstanding.- 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 = replyandcm->cm_reply_data = β¦(:1582-1584) are writes into a foreign object;mps_complete_command(cm)(:1600, body:1405-1431) then runs the new owner'scm_completecallback with the stale CONFIG reply, wakes the wrong sleeper, and decrementsio_cmds_active. - Follow-on corruption when the confused owner later completes:
* reallocated-as-SCSI-IO case β the SCSI completion misparses the CONFIG
reply;
mps_free_commandreturns 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 livecm_replyfields; the nextmps_alloc_command()hands it out; if that request completes via theSCSI_IO_SUCCESSpath (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 FIELDSwritten onto the live command,REPLY FOREIGN-FREEwhen the SCSI IO later frees it. CONFIRMED. - B (still free): late completion writes
cm_reply/cm_reply_dataonto 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
fixedHarness-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
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).
No comments yet.