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)
PoC verification
Evidence pack
findings/poc/DF-4869 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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):
-
Unvalidated firmware-count walk in
mps_print_evt_sas(mps_table.c:332-345): forMPI2_EVENT_SAS_TOPOLOGY_CHANGE_LISTit walksfor (i = 0; i < data->NumEntries; i++) phy = &data->PHY[i];whereNumEntriesis a firmware-supplied u8 (up to 255) andPHY[]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), sizedReplyFrameSize * 4bytes 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_handlercalls it on every SAS event (sys/dev/raid/mps/mps_sas_lsi.c:131) β but every field read sits insidemps_dprint_field(MPS_EVENT, ...), gated onsc->mps_debug & MPS_EVENT(mpsvar.h:609,:628-632; default 0, root-set sysctl). Latent unless debugging is enabled. -
Dead printers:
mps_print_sasdev0(mps_table.c:259-285),mps_print_expander1(:383-421) andmps_print_sasphy0(:423-447) have zero callers tree-wide (only declared inmps_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). -
Sentinel+1 convention in
mps_describe_table(mps_table.c:63-73): when no code matches it returnstable[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.
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_sasphy0have zero callers tree-wide; there is no trigger path on any machine.- The
mps_describe_tablesentinel+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 tomps_dprint_field(sc, MPS_EVENT, ...), which compiles toif (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_debugis 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 sizesc->facts->ReplyFrameSize * 4(mps.c:718, firmware-reported);EventDataLength(dwords) is never used to bound the walk atmps_table.c:332. mps_describe_tablefallback:return(table[i+1].string)atsys/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_testabledead/latent code β nothing to validate on the guest; the diff is a defensive bound plus cleanup only
findings/poc/DF-4869/fix.diff
Confirmed kernel references
- sys/dev/raid/mps/mps_table.c:63
- sys/dev/raid/mps/mps_table.c:72
- sys/dev/raid/mps/mps_table.c:332
- sys/dev/raid/mps/mps_table.c:377
- sys/dev/raid/mps/mps_table.c:259
- sys/dev/raid/mps/mps_table.c:383
- sys/dev/raid/mps/mps_table.c:423
- sys/dev/raid/mps/mps_sas_lsi.c:131
- sys/dev/raid/mps/mps.c:1533
- sys/dev/raid/mps/mpsvar.h:628
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.
No comments yet.