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

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):

  1. Wait for the driver to attach and the host to enable events (mpssas_evt_handler registered via mps_register_events at mps_sas.c:678).
  2. Post an Address Reply descriptor (SMID=0) pointing at a 16-byte reply frame whose Event field is 0x001C (MPI2_EVENT_SAS_TOPOLOGY_CHANGE_LIST) and EventDataLength is 7 (28 bytes β€” exactly room for the 12-byte header plus 4 PHY entries).
  3. In the 28-byte event payload: - NumEntries=255 at offset 0x08 - PHY[0..3] filled with valid-looking entries (PhyStatus with MPI2_EVENT_SAS_TOPO_RC_TARG_ADDED=0x01, arbitrary AttachedDevHandle like 0x1234).
  4. mpssas_evt_handler allocates 28 bytes (sz=28), copies 28 bytes, queues the event.
  5. mpssas_firmware_event_work dequeues under mps_lock, calls mpssas_fw_work; the SAS_TOPOLOGY_CHANGE_LIST case loops i=0..254 reading data->PHY[i] β€” for i>=4 each read is at offset 0x0C+i*4 = 0x1C+i*4 past event_data, i.e. 4..1020 bytes past the 28-byte allocation. Each iteration reads AttachedDevHandle (2 bytes), LinkRate (1 byte), PhyStatus (1 byte) from adjacent kernel heap, and for RC_TARG_ADDED entries calls mpssas_add_device with the garbage handle.
  6. For the IR variant use Event=0x0020 (MPI2_EVENT_IR_CONFIGURATION_CHANGE_LIST), EventDataLength=4 (room for just the 8-byte header β€” note NumElements is at offset 0x00 so it gets copied), NumElements=255 in 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.

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];
  • DF-1474 (mpr_sas_lsi.c β€” equivalent twin bug)
  • DF-1696 (sibling in same file: u16 truncation of EventDataLength*4)

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1697 Β· 4 files
FileTypeDescriptionSize
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
VERDICT.md verdict source-only confirmation + mechanism + fix
↓ download 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;iNumEntries;i++) phy=&data->PHY[i]. PHY is declared PHY[MPI2_EVENT_SAS_TOPO_PHY_COUNT=1] (mpi2_ioc.h:844). NumEntries is U8 (max 255). Loop reads 12+255*4=1032 bytes from a buffer that may be only ~16 bytes. OOB read of firmware event data.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffcombined build rc=0

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;iNumEntries;i++) phy=&data->PHY[i]; PHY is declared PHY[MPI2_EVENT_SAS_TOPO_PHY_COUNT=1]; NumEntries U8 max 255; OOB read of firmware event data.

Verdict

REPRODUCED (source-only): mpssas_fw_work topology-change loop: for(i=0;iNumEntries;i++) phy=&data->PHY[i]; PHY is declared PHY[MPI2_EVENT_SAS_TOPO_PHY_COUNT=1]; NumEntries U8 max 255; OOB read of firmware event data.