mps_print_sgl walks the request-frame SGL unbounded, dereferences a possibly-NULL chain, and hexdumps fixed 128 bytes β unconditional console path on firmware MPI2_IOCSTATUS_INVALID_SGL (chain deref sits in the only walk, unlike the mpr twin)
Summary
Every SCSI IO completed with INVALID_SGL calls this printer ungated (no mps_debug gate) - and the SGL is by definition malformed there: the single walk has no per-frame cap, terminates only on flag bits, and on a stale 0x80 LAST_ELEMENT bit derefs chain->chain without NULL check (cm_chain_list is empty for chainless commands = instant panic); both hexdumps are fixed 128 bytes regardless of IOCRequestFrameSize. Same class as mpr twin DF-4789 and DF-3816/3817. MPT2 hw-gated. Cap+NULL-check+frame-sized-hexdump fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4868 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.6 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| env.txt | β | 1.1 KB | view raw | |
| fix.diff | β | 1.8 KB | view raw | |
| manifest.json | β | 641 B | view raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-4868 β mps_print_sgl: unbounded SGL walk, NULL chain deref, fixed 128-byte hexdumps β unconditional on firmware MPI2_IOCSTATUS_INVALID_SGL
What
sys/dev/raid/mps/mps_table.c:449-487 (mps_print_sgl), entered via
mps_print_scsiio_cmd (mps_table.c:489-496, which passes the fw/driver-written
req->SGLOffset0 as the byte offset), reached unconditionally and ungated (raw
kprintf/hexdump; no sc->mps_debug check anywhere in the function) from
sys/dev/raid/mps/mps_sas.c:2063 whenever the controller returns
MPI2_IOCSTATUS_INVALID_SGL for a completed SCSI IO:
mps_table.c:462βhexdump(frame, 128, NULL, 0)assumes a β₯128-byte request frame.cm->cm_reqpoints into the DMA request pool whose per-frame size is the IOC-factsIOCRequestFrameSize * 4(mps.c:750,mps.c:812-823) β a firmware-reported value, not guaranteed β₯128 bytes.mps_table.c:463-472βwhile (frame != NULL)walk withsge++and no per-frame bound: the loop terminates only when an SGE happens to carry END_OF_LIST (0x01) or END_OF_BUFFER (0x40). On the INVALID_SGL path the SGL is by definition malformed β the walk runs off the end of the request frame, reading and printing adjacent request-frame heap to the console (eachseg%dline prints attacker-visible kernel heap content: flags, length, 64-bit addresses).mps_table.c:473-485β on a LAST_ELEMENT (0x80) flag the walker takesTAILQ_FIRST/TAILQ_NEXTofcm->cm_chain_list(:478-481) and then dereferenceschain->chainat:482without a NULL check.cm_chain_listis initialized empty per command (mps.c:859) and only gains entries when the SGL spills into a chain (mps.c:1808); any command whose I/O fit in the request frame has an empty list, so one stale/garbage LAST_ELEMENT flag bit in the walked memory is an instant NULL-deref panic.mps_table.c:484β second fixedhexdump(frame, 128, ...)on the chain frame, which is likewiseIOCRequestFrameSize * 4bytes (mps.c:772,mps.c:821-823).
Unlike its mpr sibling (DF-4789), where the chain deref exists only in the IEEE-SGE walk, the mps driver has a single walk β so every INVALID_SGL completion reaches the chain-deref shape; a simple-SGE command with an empty chain list is the common case.
Trigger / threat
Triggered by controller firmware returning MPI2_IOCSTATUS_INVALID_SGL on any CAM
SCSI IO completion (an unprivileged local user generating disk I/O rides this
completion path; the IOC status itself is firmware-produced β by a buggy/malicious
IOC or by DMA corruption of the driver-built request). Consequences: OOB heap read
printed to console (info disclosure to anyone who can read console/syslog), or a
NULL-pointer panic (local DoS). No write primitive; the walked SGL is driver-built,
so on a healthy controller+driver pair this never fires β firmware-gated, hence Low
(severity aligned with the mpr twin DF-4789 and pass-1 DF-3816/DF-3817 class).
Reproduce (requires LSI Fusion-MPT2 hardware β NOT available in the audit QEMU guest)
Not runnable on the audit guest (virtio-only; no vendor 0x1000 MPT2 device, driver
never attached β see env.txt). On hardware, a fault-injected IOC reply with
IOCStatus=INVALID_SGL for a small (single-frame, chainless) command whose frame
contains a stale 0x80 flag byte in the flags dword of walked memory panics at
mps_table.c:482.
Fix
fix.diff β bound the hexdumps to the actual IOC-facts frame size, cap SGEs walked
per frame, and break out when the chain list is exhausted (NULL guard before
chain->chain).
DF-4868 β VERDICT
Status: untested (hardware-gated). Reproduced: 0. Impact if exercised: panic-class NULL deref / OOB console leak (Low, firmware-gated). Confidence: certain (code-level), likely (real-world triggerability).
Why untested
The audit QEMU/KVM guest is virtio-only: pciconf -l (env.txt) shows no LSI vendor
0x1000 device, there are no hw.mps.* sysctls, and "mps" never appears in
dmesg.boot β the mps(4) driver never attaches, so no MPI2 IOC status can ever be
produced. The sink is only reachable in the completion path of an attached
Fusion-MPT2 controller. Classification follows the DF-4789 protocol (the identical
finding on the mpr twin, filed and persisted the same way).
Line-accurate trace (guest-independent proof of the code path)
sys/dev/raid/mps/mps_sas.c:2062-2065βcase MPI2_IOCSTATUS_INVALID_SGL:callsmps_print_scsiio_cmd(sc, cm)with no gate (not behindsc->mps_debug, unlike every other printer in this driver).sys/dev/raid/mps/mps_table.c:494-495βmps_print_scsiio_cmdcastscm->cm_reqtoMPI2_SCSI_IO_REQUEST *and callsmps_print_sgl(sc, cm, req->SGLOffset0);offset * 4(:459) indexes the request frame with an unvalidated u8.mps_table.c:462βhexdump(frame, 128, NULL, 0); frame pool per-frame size issc->facts->IOCRequestFrameSize * 4(sys/dev/raid/mps/mps.c:750,:812-823) β firmware-reported, never checked β₯128.mps_table.c:463-472β the only loop bound isflags & (MPI2_SGE_FLAGS_END_OF_LIST | MPI2_SGE_FLAGS_END_OF_BUFFER)(mpi2.h:936/941).sge++each iteration; 16-byte SGEs; nothing caps iterations to the frame.mps_table.c:473-482β onMPI2_SGE_FLAGS_LAST_ELEMENT(0x80, mpi2.h:935):chain = TAILQ_FIRST(&cm->cm_chain_list)(first hit) orTAILQ_NEXT(...); thenframe = (char *)chain->chainat:482β no NULL check.cm->cm_chain_listisTAILQ_INIT'd empty atsys/dev/raid/mps/mps.c:859and only populated bymps.c:1808when the SGL spills out of the request frame; small I/Os therefore guaranteeTAILQ_FIRST(...) == NULL.mps_table.c:484β chain-framehexdump(frame, 128, ...); chain frames areIOCRequestFrameSize * 4bytes (mps.c:772,:821-823).
Reachability delta vs the mpr twin (DF-4789): mpr's simple-SGE walk has no chain handling (chain deref only in the IEEE walk); mps has exactly one walk and it contains the chain deref β so the NULL-deref shape is on the default path here.
Exploit chain
None: read-only walk + NULL deref. Ceiling = local DoS (panic) plus console
disclosure of request-pool heap contents (kernel pointers, DMA addresses) in the
seg%d lines and hexdumps. The walked data is the driver-built request, so the
trigger requires firmware misbehavior or DMA corruption; severity stays Low.
Fix validation
fix_status: not_testable β the guards in fix.diff are mechanical (NULL break,
per-frame iteration cap, hexdump bounded to IOCRequestFrameSize*4) but cannot be
boot-validated without MPT2 hardware in the guest.
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_testablemechanical guards (NULL break + iteration caps + bounded hexdump); cannot be boot-validated without mps hardware in the guest
findings/poc/DF-4868/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
README.md (trigger analysis), VERDICT.md (full trace incl. chain-list lifecycle), env.txt (guest has no vendor 0x1000 device, no hw.mps sysctls), fix.diff (chain NULL guard + per-frame walk cap + bounded hexdumps)
PoC changes
no runnable PoC produced: requires Fusion-MPT2 hardware returning INVALID_SGL; audit guest has none (env.txt)
Verified recommended fix
Break the walk when the chain list is exhausted, cap SGEs walked per IOC-facts frame size, and bound the hexdumps (see fix.diff)
Verdict
mps_print_sgl(), reached unconditionally and ungated (raw kprintf, no sc->mps_debug check) from mps_scsiio_complete on any MPI2_IOCSTATUS_INVALID_SGL completion (mps_sas.c:2063 -> mps_table.c:495), walks the request-frame SGL with no per-frame bound (sge++, mps_table.c:471), dereferences chain->chain without a NULL check (mps_table.c:482) β and unlike the mpr twin the chain deref sits in the only walk, so a plain chainless command with an empty cm_chain_list (mps.c:859) panics on the first stale LAST_ELEMENT (0x80) flag β and hexdumps a fixed 128 bytes for both the request frame (mps_table.c:462) and chain frames (mps_table.c:484) regardless of the IOC-facts IOCRequestFrameSize*4 frame size (mps.c:750/:821). With a buggy or malicious controller this is an OOB heap read printed to console plus a NULL-deref panic (local DoS); no write primitive. Not exercised on the audit guest: it is a virtio-only KVM guest with no LSI MPT2 device, the mps driver never attaches (env.txt), and the IOC status cannot be produced. Verified by line-accurate code tracing.
No comments yet.