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

mpr_print_sgl walks the request-frame SGL unbounded, dereferences a possibly-NULL chain, and hexdumps a fixed 128 bytes β€” unconditional console path on firmware MPI2_IOCSTATUS_INVALID_SGL

Summary

Fired unconditionally (no debug gating) from scsiio_complete on INVALID_SGL - the status that declares the SGL malformed. The walkers advance with no per-frame bound (OOB heap read to console), the chain transition derefs chain->chain unchecked (NULL+0x10 panic), and the hexdump is fixed 128 bytes. Same trust boundary as pass-1 DF-3816/3817. Cap+NULL-check fix. MPT3 hw-gated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4789 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 3.1 KB ↓ raw
env.txt β€” 920 B view raw
fix.diff β€” 1.7 KB view raw
manifest.json β€” 533 B view raw
verdict.json β€” 2.3 KB view raw

DF-4789 β€” mpr_print_sgl: unbounded SGE walk, NULL chain deref, unconditional 128-byte hexdump on firmware INVALID_SGL

What

sys/dev/raid/mpr/mpr_table.c:530-584 (mpr_print_sgl), reached unconditionally (not gated on sc->mpr_debug) from mpr_sas.c:2814 whenever the controller returns MPI2_IOCSTATUS_INVALID_SGL for a completed SCSI IO:

  1. mpr_table.c:547 β€” hexdump(frame, 128, NULL, 0) assumes a β‰₯128-byte request frame; cm->cm_req points into the DMA request pool sized from IOC-facts IOCRequestFrameSize (firmware-reported). A smaller reported frame size makes the hexdump read past the frame into adjacent DMA heap and print it to console.
  2. mpr_table.c:548-572 (IEEE walk) β€” ieee_sge++ with no per-frame bound: the walk continues until an SGE happens to carry END_OF_LIST. If the (driver-built) SGL is malformed β€” exactly the condition INVALID_SGL announces β€” the walk runs off the end of the request frame, reading and printing heap past it.
  3. mpr_table.c:557-568 β€” on a CHAIN_ELEMENT flag the walker takes TAILQ_FIRST/TAILQ_NEXT(&cm->cm_chain_list) and then dereferences chain->chain at :568 without checking chain != NULL. If the frame claims more chain elements than the command actually has (or the command has none), this is a NULL pointer dereference β†’ panic.
  4. mpr_table.c:573-583 (simple walk) β€” same unbounded sge++ pattern.

Sibling context: pass-1 already filed the adjacent debug-path issues DF-3816 (dump_reqs sysctl unlocked walk) and DF-3817 (unbounded hexdump of event data) as Low; this is the same class in the INVALID_SGL printer, which pass 1 did not cover.

Trigger / threat

Triggered by the controller firmware returning MPI2_IOCSTATUS_INVALID_SGL on any CAM SCSI IO completion (an unprivileged user generating disk I/O reaches this completion path; the IOC status itself is firmware-produced). With a malicious/buggy IOC (or corrupted DMA), this is an OOB heap read printed to console (info disclosure to anyone who can read the console/syslog) and/or a NULL-deref panic (local DoS). No write primitive. The walk data comes from the request the driver built, so on a healthy controller+driver pair this never fires β€” firmware-gated, hence Low.

Reproduce (requires LSI Fusion-MPT3 hardware β€” NOT available in the audit QEMU guest)

Not runnable on the audit guest (virtio-only; no vendor 0x1000 MPT3 device β€” see env.txt). On hardware, a fault-injected IOC reply with IOCStatus=INVALID_SGL on a command whose frame SGL has a chain flag but an empty cm_chain_list panics at mpr_table.c:568.

Fix

fix.diff β€” break out when the chain list is exhausted, and cap the per-frame SGE walk so it can never run past a request frame.

VERDICT.md
↓ download raw

DF-4789 VERDICT

  • Status: untested (hardware-gated)
  • Confidence: likely
  • Severity filed: Low (CWE-125 OOB read / CWE-476 NULL deref, firmware-gated)

Line-accurate trace

  1. Entry: mpr_sas.c:2813-2816 c case MPI2_IOCSTATUS_INVALID_SGL: mpr_print_scsiio_cmd(sc, cm); /* unconditional β€” no mpr_debug gating */ mprsas_set_ccbstatus(ccb, CAM_UNREC_HBA_ERROR); Any CAM SCSI IO completion path (mprsas_scsiio_complete) funnels here; the IOC status byte is produced by the controller. Unprivileged users issue disk IO through /dev/da constantly, so the caller context is unprivileged β€” the status* is firmware-controlled.
  2. mpr_table.c:587-593 mpr_print_scsiio_cmd β†’ mpr_print_sgl(sc, cm, req->SGLOffset0).
  3. mpr_table.c:541-547: frame = cm->cm_req (request-frame pool, DMA-coherent, per-frame size sc->req_framesz from IOC facts); hexdump(frame, 128, ...) fixed 128.
  4. IEEE walk :548-572: reads ieee_sge->Flags/Length/Address, prints them; the ONLY exits are END_OF_LIST flag or the flags & CHAIN_ELEMENT transition. No counter bounds ieee_sge to the frame β‡’ malformed SGL (precisely what INVALID_SGL denotes) walks past the frame and prints adjacent DMA-pool memory to the console.
  5. Chain transition :557-568: first chain via TAILQ_FIRST, later via TAILQ_NEXT; then frame = (char *)chain->chain; at :568 β€” if the TAILQ is empty/exhausted, chain == NULL β‡’ read of NULL->chain at offset 0x10 (struct mpr_chain: chain_link TAILQ_ENTRY = 16 bytes, then void *chain, mprvar.h:191-195) β‡’ clean NULL-page panic, not an exploitable primitive).
  6. Simple walk :573-583: identical unbounded sge++ (no chain transition at all).

Impact ceiling

  • Console/syslog disclosure of request-pool heap contents (OOB read) β€” modest.
  • NULL-deref panic β€” local DoS attributed to disk IO storms.
  • No attacker-controlled write; the printer only reads.

Why Low and not higher

Every value the walker trusts (SGE flags, chain offsets) lives in the request frame the driver itself built; on a correctly operating controller the walk is well-formed. The bug fires only when the firmware flags the SGL invalid (malicious/buggy IOC, corrupted DMA, or a driver bug elsewhere) β€” same trust boundary as pass-1's DF-3816/DF-3817 (Low).

Why untested on this run

Audit guest is KVM/virtio-only: no LSI (vendor 0x1000) storage controller exists (env.txt), so mpr never attaches and no INVALID_SGL completion can be synthesized from inside the guest. Code-read verification only.

Guard the chain-list exhaustion (if (chain == NULL) break; before chain->chain) and cap the per-frame SGE walk (fix.diff).

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); cannot be boot-validated without mpr hardware in the guest

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

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md (trigger analysis), VERDICT.md (full trace), env.txt (guest has no vendor 0x1000 device), fix.diff (chain NULL guard + walk caps)

PoC changes

no runnable PoC produced: requires Fusion-MPT3 hardware returning INVALID_SGL; audit guest has none

Verified recommended fix

Break the walk when the chain list is exhausted and cap SGEs walked per frame (see fix.diff)

Verdict

mpr_print_sgl(), reached unconditionally from mprsas_scsiio_complete on any MPI2_IOCSTATUS_INVALID_SGL completion (mpr_sas.c:2814), walks the request-frame SGL with no per-frame bound (ieee_sge++/sge++, mpr_table.c:555/581), dereferences chain->chain without a NULL check when the chain TAILQ is exhausted (mpr_table.c:568), and hexdumps a fixed 128 bytes regardless of the IOC-facts request-frame size (mpr_table.c:547). 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 MPT3 controller (env.txt), so the mpr driver never attaches and the IOC status cannot be produced. Verified by line-accurate code tracing.