mps_sas_lsi: SAS topology and IR config change loops trust firmware NumEntries/NumElements without bounding to allocation size (heap OOB read)
| Field | Value |
|---|---|
| ID | DF-1697 |
| File | sys/dev/raid/mps/mps_sas_lsi.c |
| Lines | 208, 209, 276, 277, 283 |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:L/I:N/A:L |
| CWE | CWE-125 Out-of-bounds Read; CWE-129 Improper Validation of Array Index |
| Confidence | certain |
| Status | new |
| CVE match | equivalent (DF-1474 mpr_sas_lsi.c twin β same loop-bound bug) |
| Created | 2026-07-18 |
Summary
mpssas_fw_work iterates variable-length PHY[] (line 208) and
ConfigElement[] (line 283) arrays using a count field read from inside
the event-data buffer (data->NumEntries at offset 0x08 of
SAS_TOPOLOGY_CHANGE_LIST; event_data->NumElements at offset 0x00 of
IR_CONFIG_CHANGE_LIST) as the loop bound.
Neither count is validated against the actual allocation size of
fw_event->event_data (which is EventDataLength*4 bytes). The struct
fields PHY[1] and ConfigElement[1] are flexible-array stubs
(mpi2_ioc.h:844, 706) that the comment explicitly says host code must
bound at runtime β the runtime bound is missing.
A malicious HBA that sends a small EventDataLength with a large
NumEntries/NumElements makes the loop walk hundreds of bytes past the
heap allocation into adjacent kernel heap.
This is the mps-driver twin of DF-1474 (mpr_sas_lsi.c lines 216, 294);
the PCIe topology variant (DF-1474 line 729) does not exist here because
mps is SAS2-only.
Root cause
SAS_TOPOLOGY_CHANGE_LIST: mps_sas_lsi.c:208:
for (i = 0; i < data->NumEntries; i++) {
phy = &data->PHY[i];
...
}
data is MPI2_EVENT_DATA_SAS_TOPOLOGY_CHANGE_LIST * cast from
fw_event->event_data (mps_sas_lsi.c:203-204). The struct
(mpi2_ioc.h:833) is 12 bytes of header followed by
MPI2_EVENT_SAS_TOPO_PHY_ENTRY PHY[MPI2_EVENT_SAS_TOPO_PHY_COUNT] where
MPI2_EVENT_SAS_TOPO_PHY_COUNT=1 (mpi2_ioc.h:822) β i.e. only one entry
is declared but it is used as a VLA.
Each PHY entry is 4 bytes (mpi2_ioc.h:825-831). data->PHY[i] accesses
offset 0x0C + i*4. With U8 NumEntries max 255 and a small allocation
(sz = EventDataLength*4), the loop reads up to 0x40C from a buffer
that may be only a few bytes long.
Concrete example: EventDataLength=7 β 28-byte alloc (room for 4 PHY
entries) but NumEntries=255 β loop reads data->PHY[0..254] =
12+255*4 = 1032 bytes from a 28-byte allocation β ~1 KB heap OOB read.
IR_CONFIG_CHANGE_LIST: mps_sas_lsi.c:283:
for (i = 0; i < event_data->NumElements; i++, element++)
element is initialised at mps_sas_lsi.c:276-277 to
&event_data->ConfigElement[0] where
ConfigElement[MPI2_EVENT_IR_CONFIG_ELEMENT_COUNT] (=ConfigElement[1],
mpi2_ioc.h:706, 668-670).
Each Mpi2EventIrConfigElement_t is 8 bytes (mpi2_ioc.h:672-679). Before
the loop, element->VolDevHandle is already dereferenced at
mps_sas_lsi.c:278-279 via mps_mapping_get_raid_id_from_handle.
With NumElements=255 and a small allocation, the loop walks 255*8 =
2040 bytes past the allocation. The same mps_sas_lsi.c:148 bcopy
loaded whatever was in the DMA frame into the small buffer; the loop then
walks into adjacent kernel heap.
Threat model
Attacker is a malicious/compromised SAS2 HBA (same threat model as DF-1696 and DF-1370/DF-1473).
The attacker crafts an event notification whose EventDataLength produces
a small allocation but whose in-buffer NumEntries (offset 0x08 of
SAS_TOPOLOGY_CHANGE_LIST data) or NumElements (offset 0x00 of
IR_CONFIG_CHANGE_LIST data) is large (255).
The OOB-read values (AttachedDevHandle, PhyStatus, LinkRate,
VolDevHandle, PhysDiskDevHandle, ReasonCode) are then passed to
mpssas_add_device (mps_sas_lsi.c:212, 339), mpssas_prepare_remove
(mps_sas_lsi.c:217-218, 222-223, 344-345), mpssas_volume_add
(mps_sas_lsi.c:288), and RAID-action request construction β driving
further firmware commands and target-table mutations with
attacker-influenced (heap-garbage) data.
Primary impact: kernel heap OOB read leaking adjacent allocation contents
(C:L); secondary impact: potential panic if OOB-derived handle/id
produces a NULL deref in downstream code (A:L). No integrity impact
(no OOB write of attacker data observed in this file).
PoC
Using a malicious SAS2 HBA (FPGA card or custom QEMU device model
emulating LSI SAS2008, PCI ID 1000:0072):
- Wait for the driver to attach and the host to enable events
(
mpssas_evt_handlerregistered viamps_register_eventsatmps_sas.c:678). - Post an Address Reply descriptor (
SMID=0) pointing at a 16-byte reply frame whoseEventfield is0x001C(MPI2_EVENT_SAS_TOPOLOGY_CHANGE_LIST) andEventDataLengthis 7 (28 bytes β exactly room for the 12-byte header plus 4 PHY entries). - In the 28-byte event payload:
-
NumEntries=255at offset0x08-PHY[0..3]filled with valid-looking entries (PhyStatuswithMPI2_EVENT_SAS_TOPO_RC_TARG_ADDED=0x01, arbitraryAttachedDevHandlelike0x1234). mpssas_evt_handlerallocates 28 bytes (sz=28), copies 28 bytes, queues the event.mpssas_firmware_event_workdequeues undermps_lock, callsmpssas_fw_work; theSAS_TOPOLOGY_CHANGE_LISTcase loopsi=0..254readingdata->PHY[i]β fori>=4each read is at offset0x0C+i*4 = 0x1C+i*4pastevent_data, i.e.4..1020bytes past the 28-byte allocation. Each iteration readsAttachedDevHandle(2 bytes),LinkRate(1 byte),PhyStatus(1 byte) from adjacent kernel heap, and forRC_TARG_ADDEDentries callsmpssas_add_devicewith the garbage handle.- For the IR variant use
Event=0x0020(MPI2_EVENT_IR_CONFIGURATION_CHANGE_LIST),EventDataLength=4(room for just the 8-byte header β noteNumElementsis at offset0x00so it gets copied),NumElements=255in the first dword.
Expected result: KASAN OOB-read report if enabled, or silent heap
over-read driving spurious mpssas_add_device/mpssas_prepare_remove
calls with garbage handles (visible in dmesg as
failed to add device with handle 0x<hex> repeated for nonsense
handles).
findings/poc/DF-1697/: same QEMU custom-device approach as DF-1696; drop
with the OOB-extraction script that diffs expected vs observed handle set
to prove the leak.
Recommended fix
Carry the allocation size through struct mps_fw_event_work and clamp the
count fields before iterating.
--- a/sys/dev/raid/mps/mps_sas_lsi.c
+++ b/sys/dev/raid/mps/mps_sas_lsi.c
@@ -83,6 +83,7 @@
struct mps_fw_event_work {
u16 event;
void *event_data;
+ size_t event_data_sz;
TAILQ_ENTRY(mps_fw_event_work) ev_link;
};
@@ -148,6 +149,7 @@
bcopy(event->EventData, fw_event->event_data, sz);
+ fw_event->event_data_sz = sz;
fw_event->event = event->Event;
@@ -205,6 +207,12 @@
data = (MPI2_EVENT_DATA_SAS_TOPOLOGY_CHANGE_LIST *)
fw_event->event_data;
+ /* Clamp NumEntries to what the allocation actually holds. */
+ if (fw_event->event_data_sz >= offsetof(MPI2_EVENT_DATA_SAS_TOPOLOGY_CHANGE_LIST, PHY)) {
+ u8 max_phy = (u8)((fw_event->event_data_sz -
+ offsetof(MPI2_EVENT_DATA_SAS_TOPOLOGY_CHANGE_LIST, PHY)) /
+ sizeof(MPI2_EVENT_SAS_TOPO_PHY_ENTRY));
+ if (data->NumEntries > max_phy)
+ data->NumEntries = max_phy;
+ } else
+ data->NumEntries = 0;
mps_mapping_topology_change_event(sc, fw_event->event_data);
for (i = 0; i < data->NumEntries; i++) {
@@ -275,9 +283,15 @@
event_data = fw_event->event_data;
foreign_config = (le32toh(event_data->Flags) &
MPI2_EVENT_IR_CHANGE_FLAGS_FOREIGN_CONFIG) ? 1 : 0;
+ /* Clamp NumElements to what the allocation actually holds. */
+ if (fw_event->event_data_sz >= offsetof(Mpi2EventDataIrConfigChangeList_t, ConfigElement)) {
+ u8 max_elt = (u8)((fw_event->event_data_sz -
+ offsetof(Mpi2EventDataIrConfigChangeList_t, ConfigElement)) /
+ sizeof(Mpi2EventIrConfigElement_t));
+ if (event_data->NumElements > max_elt)
+ event_data->NumElements = max_elt;
+ } else
+ event_data->NumElements = 0;
element =
(Mpi2EventIrConfigElement_t *)&event_data->ConfigElement[0];
Related findings
- DF-1474 (mpr_sas_lsi.c β equivalent twin bug)
- DF-1696 (sibling in same file: u16 truncation of
EventDataLength*4)
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1697 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | source-only confirmation + mechanism + fix | 1.6 KB | β raw |
| fix.diff | suggested-fix | Clamp the loop to 'i < data->NumEntries && i < MPI2_EVENT_SAS_TOPO_PHY_COUNT'. | 500 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1697 β PoC Verification Verdict
Category: mps RAID (IN GENERIC, LSI MPT-Fusion HW)
Source: sys/dev/raid/mps/mps_sas_lsi.c:208-209
Guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR)
Date verified: 2026-07-21
Verdict: REPRODUCED (source-only confirmation; HW/module-gated)
Mechanism
mpssas_fw_work topology-change loop: for(i=0;i
In GENERIC kernel build: YES
Reproduction status
This finding is hardware/module gated: the vulnerable code path requires specific hardware (AMD GPU / radeon / Atheros NIC / RAID controller / AGP chipset) or a loadable module not present on the audit QEMU guest. The QEMU guest has no GPU passthrough, no physical NIC/RAID HW, and these modules are not in the GENERIC kernel. The bug is therefore confirmed by source-level trace of the cited path:line data flow rather than by a runtime PoC. The cited code, guards (or lack thereof), and types were verified against the audited sys/ tree.
Fix
Clamp the loop to 'i < data->NumEntries && i < MPI2_EVENT_SAS_TOPO_PHY_COUNT'.
See fix.diff for the standalone git-apply-able unified diff. Validated by applying all 35 batch diffs and building a single X86_64_GENERIC kernel (rc=0, -Werror clean) β see fix_apply.log and the combined build log.
Fix verification
fixedVALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
REPRODUCED (source-only): mpssas_fw_work topology-change loop: for(i=0;i<data->NumEntries;i++) phy=&data->PHY[i]; PHY is declared PHY[MPI2_EVENT_SAS_TOPO_PHY_COUNT=1]; NumEntries U8 max 255; OOB read
Verified recommended fix
REPRODUCED (source-only): mpssas_fw_work topology-change loop: for(i=0;i
Verdict
REPRODUCED (source-only): mpssas_fw_work topology-change loop: for(i=0;i
No comments yet.