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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4868 Β· 6 files
FileTypeDescriptionSize
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:

  1. mps_table.c:462 β€” hexdump(frame, 128, NULL, 0) assumes a β‰₯128-byte request frame. cm->cm_req points into the DMA request pool whose per-frame size is the IOC-facts IOCRequestFrameSize * 4 (mps.c:750, mps.c:812-823) β€” a firmware-reported value, not guaranteed β‰₯128 bytes.
  2. mps_table.c:463-472 β€” while (frame != NULL) walk with sge++ 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 (each seg%d line prints attacker-visible kernel heap content: flags, length, 64-bit addresses).
  3. mps_table.c:473-485 β€” on a LAST_ELEMENT (0x80) flag the walker takes TAILQ_FIRST/TAILQ_NEXT of cm->cm_chain_list (:478-481) and then dereferences chain->chain at :482 without a NULL check. cm_chain_list is 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.
  4. mps_table.c:484 β€” second fixed hexdump(frame, 128, ...) on the chain frame, which is likewise IOCRequestFrameSize * 4 bytes (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).

VERDICT.md
↓ download raw

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: calls mps_print_scsiio_cmd(sc, cm) with no gate (not behind sc->mps_debug, unlike every other printer in this driver).
  • sys/dev/raid/mps/mps_table.c:494-495 β€” mps_print_scsiio_cmd casts cm->cm_req to MPI2_SCSI_IO_REQUEST * and calls mps_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 is sc->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 is flags & (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 β€” on MPI2_SGE_FLAGS_LAST_ELEMENT (0x80, mpi2.h:935): chain = TAILQ_FIRST(&cm->cm_chain_list) (first hit) or TAILQ_NEXT(...); then frame = (char *)chain->chain at :482 β€” no NULL check. cm->cm_chain_list is TAILQ_INIT'd empty at sys/dev/raid/mps/mps.c:859 and only populated by mps.c:1808 when the SGL spills out of the request frame; small I/Os therefore guarantee TAILQ_FIRST(...) == NULL.
  • mps_table.c:484 β€” chain-frame hexdump(frame, 128, ...); chain frames are IOCRequestFrameSize * 4 bytes (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_testable
baseline no→ patch + rebuild →patched clean

mechanical guards (NULL break + iteration caps + bounded hexdump); cannot be boot-validated without mps hardware in the guest

findings/poc/DF-4868/fix.diff
↓ fix.diffper-fix-DF-4868

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.