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

mps table layer latent defects: live-but-gated unvalidated NumEntries walk in mps_print_evt_sas, three dead config-page printers, sentinel+1 fallback in mps_describe_table

Summary

Twin of DF-4790 with one delta: mps_print_evt_sas IS live (every SAS event) and its topology walk iterates fw-supplied NumEntries over the reply frame with no EventDataLength bound (up to ~1KB OOB read in the reply DMA arena) - gated on the default-off root debug flag, hence latent. Three dead config-page printers inherit the unvalidated-fw-page pattern if resurrected; describe_table returns sentinel+1 by convention. Bound-the-walk + literal-fallback + delete-dead fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4869 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
env.txt β€” 1.1 KB view raw
fix.diff β€” 2.3 KB view raw
manifest.json β€” 572 B view raw
verdict.json β€” 3.0 KB view raw

DF-4869 β€” mps debug/table layer: live-but-gated unvalidated NumEntries walk, dead config-page printers, sentinel+1 convention in mps_describe_table

What

Three related latent defects in sys/dev/raid/mps/mps_table.c (twin of DF-4790 on the mpr side):

  1. Unvalidated firmware-count walk in mps_print_evt_sas (mps_table.c:332-345): for MPI2_EVENT_SAS_TOPOLOGY_CHANGE_LIST it walks for (i = 0; i < data->NumEntries; i++) phy = &data->PHY[i]; where NumEntries is a firmware-supplied u8 (up to 255) and PHY[] entries are 4 bytes each (mpi2_ioc.h:825-844). The event buffer at this point is the reply frame itself (sc->reply_frames + offset, sys/dev/raid/mps/mps.c:1530-1543), sized ReplyFrameSize * 4 bytes per frame (mps.c:718) β€” so a 255-entry walk reads up to ~1 KB past a ~96-128-byte frame: a firmware-controlled OOB read inside the reply DMA arena. The function is live β€” mpssas_evt_handler calls it on every SAS event (sys/dev/raid/mps/mps_sas_lsi.c:131) β€” but every field read sits inside mps_dprint_field(MPS_EVENT, ...), gated on sc->mps_debug & MPS_EVENT (mpsvar.h:609, :628-632; default 0, root-set sysctl). Latent unless debugging is enabled.

  2. Dead printers: mps_print_sasdev0 (mps_table.c:259-285), mps_print_expander1 (:383-421) and mps_print_sasphy0 (:423-447) have zero callers tree-wide (only declared in mps_table.h:47-50). They parse config-page buffers whose backing allocation size is never validated against the page struct; any future caller inherits the unvalidated-fw-count/unbounded-read pattern (the same pattern that produced the live findings DF-4559/DF-4560 on this driver's sibling code).

  3. Sentinel+1 convention in mps_describe_table (mps_table.c:63-73): when no code matches it returns table[i+1].string β€” one entry past the {NULL, 0} sentinel. In-bounds today only because all nine tables in this file ship an extra "Unknown …" entry after the sentinel (:96-97, :106-107, :119-120, :127-128, :140-141, :150-151, :157-158, :166-167, :182-183); the two external users (mps_sas_lsi.c:671-673) also conform. Any new table that ends at the sentinel turns the fallback into a one-entry OOB read of static memory.

Minor: case MPI2_EVENT_SAS_DEVICE_STATUS_CHANGE at mps_table.c:363-377 is missing its break; β€” falls through into default: break;, benign today.

Trigger / threat

(1) fires only with hw.mps.N.debug (root-settable) at the MPS_EVENT bit and a malicious/buggy controller sending a topology event with NumEntries larger than the actual event data; (2) has no trigger path at all; (3) has no non-conforming table today. Pure hardening / latent-pattern filing (Info), so the patterns are not resurrected as-is β€” same rationale as DF-4790.

Reproduce

No trigger path exists on the audit guest (no MPT2 hardware β€” see env.txt; and for (2)/(3) no trigger path exists on any machine). Dead/latent code, verified by tree-wide grep for callers (see VERDICT.md).

Fix

fix.diff β€” bound the NumEntries walk by EventDataLength, return a literal fallback in mps_describe_table, and add the missing break;. Preferred alternative for (2): delete the three dead printers.

VERDICT.md
↓ download raw

DF-4869 β€” VERDICT

Status: untested (no trigger path). Reproduced: 0. Impact: none (latent). Confidence: certain.

Why untested

  • The audit guest has no LSI (vendor 0x1000) MPT2 controller; the mps driver never attaches (env.txt) β€” so the SAS event path itself cannot run.
  • mps_print_sasdev0 / mps_print_expander1 / mps_print_sasphy0 have zero callers tree-wide; there is no trigger path on any machine.
  • The mps_describe_table sentinel+1 fallback is in-bounds for every table that exists today; the OOB read only materializes for a future non-conforming table.

Liveness / bounds evidence (grep, tree-wide)

  • Callers of mps_print_evt_sas: exactly one β€” mpssas_evt_handler (sys/dev/raid/mps/mps_sas_lsi.c:131), the registered mps event callback; runs on every async SAS event. All field reads inside the PHY walk are arguments to mps_dprint_field(sc, MPS_EVENT, ...), which compiles to if (sc->mps_debug & MPS_EVENT) kprintf(...) (sys/dev/raid/mps/mpsvar.h:609, :628-632) β€” args evaluated only when the debug bit is set (default 0; sc->mps_debug is settable only via the per-device sysctl, root-only).
  • Callers of mps_print_sasdev0 / mps_print_expander1 / mps_print_sasphy0: none (declarations only, sys/dev/raid/mps/mps_table.h:47-50). Compare: mps_print_iocfacts (live, mps.c:1046), mps_print_portfacts (live, mps.c:1158), mps_print_event (live, mps.c:1264/:1648/:1724), mps_print_scsiio_cmd (live, mps_sas.c:2063 β€” see DF-4868).
  • Event buffer at the walk: the reply frame inside sc->reply_frames (mps.c:1530-1543), per-frame size sc->facts->ReplyFrameSize * 4 (mps.c:718, firmware-reported); EventDataLength (dwords) is never used to bound the walk at mps_table.c:332.
  • mps_describe_table fallback: return(table[i+1].string) at sys/dev/raid/mps/mps_table.c:72 β€” reads the entry after the {NULL,0} sentinel that terminated the loop at :68. All nine in-file tables carry a trailing "Unknown" entry; external callers (mps_sas_lsi.c:673) pass conforming tables.

Exploit chain

None β€” no reachable path in the shipped tree. Ceiling (if debugging is enabled by root AND a malicious controller supplies the event): console disclosure of reply-pool DMA memory past the event frame. Filed as Info hardening, mirroring DF-4790.

Fix validation

fix_status: not_testable β€” dead/latent code; the diff is a defensive bound plus cleanup, nothing to exercise on 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

dead/latent code β€” nothing to validate on the guest; the diff is a defensive bound plus cleanup only

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

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md (liveness + line cites), VERDICT.md (grep evidence of dead callers + gating analysis), env.txt (guest has no vendor 0x1000 device), fix.diff (NumEntries bound + literal fallback + missing break)

PoC changes

no PoC produced β€” dead/latent code, no trigger path exists (and no MPT2 hardware on the guest)

Verified recommended fix

Delete the dead mps_print_* debug printers, bound NumEntries by EventDataLength, and use a literal fallback in mps_describe_table

Verdict

Latent, mostly-dead defects in mps_table.c (twin of DF-4790): (1) mps_print_evt_sas walks data->PHY[i] for firmware-supplied NumEntries (u8, up to 255 x 4-byte entries, mps_table.c:332-345) with no EventDataLength bound β€” the buffer is the reply frame itself (mps.c:1530-1543, ReplyFrameSize*4 bytes per frame, mps.c:718), so the walk can read ~1KB past a ~96-128-byte frame; unlike the mpr twin this function IS live (mpssas_evt_handler, mps_sas_lsi.c:131, every SAS event) but every read is inside mps_dprint_field gated on sc->mps_debug & MPS_EVENT (mpsvar.h:609/:628-632, default 0, root-set sysctl) β€” same live-but-gated class as pass-1 DF-4560; (2) mps_print_sasdev0 (mps_table.c:259), mps_print_expander1 (:383) and mps_print_sasphy0 (:423) have zero callers tree-wide β€” dead config-page printers that would parse unvalidated fw-sized pages if resurrected; (3) mps_describe_table returns table[i+1].string, one entry past the {NULL,0} sentinel (mps_table.c:72) β€” in-bounds today only because all nine in-file tables ship an extra 'Unknown' entry. No reachable path, nothing to execute on the audit guest (no MPT2 hardware, env.txt); filed as Info so the patterns are not resurrected as-is.