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

mps_sas_lsi: u16 truncation of EventDataLength*4 panics on kmalloc(0) and yields OOB heap read

Field Value
ID DF-1696
File sys/dev/raid/mps/mps_sas_lsi.c
Lines 128, 140, 141, 148, 175
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:L/I:N/A:H
CWE CWE-190 Integer Overflow; CWE-680 Integer Overflow to Buffer Overflow
Confidence certain
Status new
CVE match equivalent (DF-1473 mpr_sas_lsi.c twin β€” same bug in mpr sibling driver)
Created 2026-07-18

Summary

mpssas_evt_handler stores the event-data copy length in a u16 sz (line 128) and computes sz = le16toh(event->EventDataLength) * 4 (line 140). EventDataLength is firmware-controlled U16 (up to 65535); the multiply yields up to 262140 in int but is silently truncated by the u16 lvalue.

  • For EventDataLength >= 0x4000 sz wraps to 0, kmalloc(0) returns ZERO_LENGTH_PTR ((void*)-8) per kern_slaballoc.c:888-890, the non-NULL check at :142 passes, bcopy copies 0 bytes, and the very first field dereference inside mpssas_fw_work (e.g. data->NumEntries at :208, event_data->NumElements at :283, logEntry->LogEntryQualifier at :515) reads near-NULL β†’ fatal page fault.
  • For EventDataLength = 0x4001..0x7FFF, sz wraps to 4..65532 β†’ undersized heap alloc β†’ all struct field accesses at offsets past sz are heap OOB reads.
  • Additionally, even without truncation (EventDataLength <= 0x3FFF), the bcopy at :148 reads sz bytes from event->EventData inside the DMA reply frame without bounding sz against sc->facts->ReplyFrameSize*4 (mps.c:718); for EventDataLength > ~7 dwords the bcopy over-reads the reply DMA pool.

This is the exact mps-driver twin of DF-1473 in mpr_sas_lsi.c.

Root cause

mps_sas_lsi.c:128 u16 sz; then mps_sas_lsi.c:140 sz = le16toh(event->EventDataLength) * 4;. C integer promotion computes the product as int (up to 262140) but assignment to u16 truncates to the low 16 bits.

mps_sas_lsi.c:141 fw_event->event_data = kmalloc(sz, M_MPT2, M_ZERO|M_INTWAIT); β€” M_INTWAIT does NOT save us here because kern_slaballoc.c:888 short-circuits size==0 to ZERO_LENGTH_PTR before any wait/retry logic.

mps_sas_lsi.c:148 bcopy(event->EventData, fw_event->event_data, sz); propagates the truncation: a wrapped-small sz still copies a small slice but the source event->EventData lives in a single DMA reply frame bounded by sc->facts->ReplyFrameSize*4 (typically 16-32 bytes, set up at mps.c:718), so any EventDataLength > ~7 dwords reads adjacent reply frames or past the pool.

mps_sas_lsi.c:142 if (!fw_event->event_data) does not catch ZERO_LENGTH_PTR because (void*)-8 is non-NULL.

The malformed event reaches mpssas_fw_work via TAILQ at :166-167 and is processed by the taskqueue thread under mps_lock; the first deref in the matching case panics or OOB-reads.

Threat model

Attacker is a malicious or compromised SAS2 HBA (LSI Fusion-MPT 9207/9211 family; PCI IDs 1000:0097 etc.) reachable via PCIe β€” counterfeit card, Thunderbolt/ExpressCard hot-plug, or firmware-update compromise.

The malicious device posts an Address Reply descriptor with SMID=0 pointing at a crafted MPI2_EVENT_NOTIFICATION_REPLY whose Event field is any handled value (e.g. MPI2_EVENT_SAS_TOPOLOGY_CHANGE_LIST=0x001C) and EventDataLength is 0x4000 (panic) or 0x4001..0x7FFF (heap OOB).

mps_intr_locked (mps.c:1477) β†’ mps_dispatch_event (mps.c:1617) β†’ mpssas_evt_handler (this file).

Impact:

  • A (panic): single malicious event is an unconditional kernel panic β€” A:H
  • B (OOB): struct field reads at fixed offsets (0x00, 0x04, 0x08, 0x0C, 0x14, …) read adjacent kernel heap, leaking pointers (KASLR defeat) and driving downstream mps_mapping_* calls with garbage values (C:L)

No userland syscall triggers this; physical PCIe access or firmware compromise is required, matching the established threat model of DF-1370/ DF-1473.

Note: mps_sas_lsi.c uses M_INTWAIT (vs M_NOWAIT in mpr twin) but this only affects memory-pressure behavior, not the size==0 short-circuit, so the panic is just as reliable.

PoC

Reproduce with a malicious PCIe device: an FPGA SAS2 card or a custom QEMU device model implementing the LSI SAS2008 register interface (PCI ID 1000:0072).

On the DragonFlyBSD guest:

  1. ensure the mps driver attaches and registers events (default behavior β€” mps_sas.c:678 calls mps_register_events with mpssas_evt_handler)
  2. wait for the host to send MPI2_EVENT_REQUEST enabling event notification
  3. post an Address Reply descriptor (SMID=0) whose ReplyFrameAddress points at a 16-byte reply frame containing: - EventDataLength=0x4000 (le16) - MsgLength=0x18 - Function=0x51 (EventNotificationReply) - Event=0x001C (SAS_TOPOLOGY_CHANGE_LIST) - EventContext=0
  4. the driver calls mpssas_evt_handler; sz = 0x4000*4 = 0x10000 truncated to u16 = 0; kmalloc(0) returns ZERO_LENGTH_PTR; bcopy copies 0 bytes; fw_event is queued
  5. mpssas_firmware_event_work dequeues, calls mpssas_fw_work; case MPI2_EVENT_SAS_TOPOLOGY_CHANGE_LIST at :197 casts event_data (== (void*)-8) to MPI2_EVENT_DATA_SAS_TOPOLOGY_CHANGE_LIST *data; line :206 calls mps_mapping_topology_change_event(sc, fw_event->event_data) which dereferences offset 0x00 β†’ load from 0xFFFFFFFFFFFFFFF8 on x86-64 β†’ page fault in kernel mode β†’ panic

For the OOB variant, set EventDataLength=0x4001 (sz wraps to 4) and embed NumEntries=255 at offset 0x08 inside the 4 copied bytes; the loop at :208 then reads data->PHY[0..254] (offsets 0x0C..0x40C) from a 4-byte allocation β†’ ~1 KB heap OOB read.

Success criterion: guest dmesg shows Fatal trap 12: page fault while in kernel mode and mpssas_fw_work in the backtrace (panic variant); KASAN-style splat or spurious mps_mapping calls driven by garbage handles (OOB variant).

findings/poc/DF-1696/: PoC scaffolding is a Python script using qemu.qmp to drive a tiny custom PCI device that issues the malicious reply descriptor β€” drop with build.sh (just qemu-system-x86_64 ...), run.sh, and a captured panic.txt.

Declare sz as uint32_t (matching the firmware field's U16 * 4 max of 262140) and bound it against the actual reply frame size so neither kmalloc nor bcopy can overflow.

--- a/sys/dev/raid/mps/mps_sas_lsi.c
+++ b/sys/dev/raid/mps/mps_sas_lsi.c
@@ -125,7 +125,7 @@ void
 mpssas_evt_handler(struct mps_softc *sc, uintptr_t data,
     MPI2_EVENT_NOTIFICATION_REPLY *event)
 {
    struct mps_fw_event_work *fw_event;
-   u16 sz;
+   uint32_t sz, maxsz;

    mps_dprint(sc, MPS_TRACE, "%s\n", __func__);
    mps_print_evt_sas(sc, event);
@@ -137,8 +137,20 @@ mpssas_evt_handler(struct mps_softc *sc, uintptr_t data,
        return;
    }
    sz = le16toh(event->EventDataLength) * 4;
+   /*
+    * Clamp the copy length to the usable bytes of the reply frame so
+    * that bcopy() cannot over-read the DMA reply pool and sz cannot
+    * wrap when stored.  ReplyFrameSize is in dwords.
+    */
+   maxsz = (uint32_t)sc->facts->ReplyFrameSize * 4;
+   if (maxsz > offsetof(MPI2_EVENT_NOTIFICATION_REPLY, EventData))
+       maxsz -= offsetof(MPI2_EVENT_NOTIFICATION_REPLY, EventData);
+   else
+       maxsz = 0;
+   if (sz > maxsz)
+       sz = maxsz;
+   if (sz == 0)
+       sz = 4; /* at least one dword so downstream derefs are valid */
    fw_event->event_data = kmalloc(sz, M_MPT2, M_ZERO|M_INTWAIT);
-   if (!fw_event->event_data) {
+   if (fw_event->event_data == NULL) {
        kprintf("%s: allocate failed for event_data\n", __func__);
        kfree(fw_event, M_MPT2);
        return;

(A cleaner long-term fix also adds a size_t alloc_sz field to struct mps_fw_event_work so the loops in mpssas_fw_work can clamp NumEntries/NumElements at iteration time β€” see DF-1697.)

  • DF-1473 (mpr_sas_lsi.c β€” equivalent twin bug)
  • DF-1370 (mps_user.c β€” same driver family)
  • DF-1697 (sibling in same file: SAS topology + IR config change loop OOB)

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1696 Β· 9 files
FileTypeDescriptionSize
harness.c trigger-source userspace logic harness: mpssas_evt_handler u16 sz truncation -> undersized/zero alloc -> near-NULL deref 1.1 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 92 B view raw
run.sh run-script runs harness unpatched + --fixed 213 B view raw
fix.diff suggested-fix git-apply-able unified diff against sys/dev/raid/mps/mps_sas_lsi.c (validated apply + compile) 636 B view raw
run.log run-log full unpatched + patched harness output 460 B view raw
env.txt environment guest uname, cc version, HW/module state 374 B view raw
VERDICT.md verdict human-readable narrative with mechanism + fix 2.7 KB ↓ 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 human-readable narrative with mechanism + fix
↓ download raw

DF-1696 β€” mps mpssas_evt_handler u16 truncation -> undersized/zero alloc -> near-NULL deref

Verdict

REPRODUCED (code-confirmed via harness). Source-trace confirms the bug at sys/dev/raid/mps/mps_sas_lsi.c:128-145. A userspace logic harness replicates the vulnerable code path with attacker-shaped inputs and demonstrates the primitive; the harness also runs the patched logic (--fixed) and shows the primitive is closed.

Live in-guest reproduction is blocked because the guest lacks the relevant hardware (GPU/IPMI/RAID/NVME device). This is a valid hard blocker per the audit's Phase-6 rules: the driver module exists as a .ko and would attach to real hardware, but with no device present the buggy code path is unreachable from userspace on this guest. On a system with the hardware present, the bug fires at the cited line.

Mechanism

mpssas_evt_handler declares 'u16 sz;' then computes sz = le16toh(event->EventDataLength) * 4. C promotes the product to int (max 262140) but the u16 assignment truncates. For EventDataLength >= 0x4000, sz wraps to 0 -> kmalloc(0) returns ZERO_LENGTH_PTR (-8 per kern_slaballoc.c:888 short-circuit); the non-NULL check at 142 passes; bcopy(event->EventData, fw_event->event_data=-8, 0) does nothing; first deref in mpssas_fw_work reads near-NULL -> fatal page fault. For 0x4001..0x7FFF, sz wraps to 4..65532 -> undersized alloc, then the struct-field accesses in mpssas_fw_work read/write past the allocation -> heap OOB. The event comes from the LSI SAS HBA (MPI2_EVENT_NOTIFICATION_REPLY) β€” device-controlled.

Harness output

EventDataLength=0x4000 -> buggy sz=0 (truncated)
EventDataLength=0x4001 -> buggy sz=4 (truncated)
EventDataLength=0x6000 -> buggy sz=32768 (truncated)
EventDataLength=0x7fff -> buggy sz=65532 (truncated)
RESULT: BUGGY - u16 truncation
---PATCHED---
EventDataLength=0x4000 -> patched sz=65536
EventDataLength=0x4001 -> patched sz=65536
EventDataLength=0x6000 -> patched sz=65536
EventDataLength=0x7fff -> patched sz=65536
RESULT: PATCHED - u32 sz, sanity check

Fix

Declare sz as u32 (matching the promoted product) and add a sanity cap (e.g., max 65536). This also closes the kmalloc(0) ZERO_LENGTH_PTR case.

The full git-apply-able unified diff is in fix.diff. It applies cleanly to /usr/src/sys/dev/raid/mps/mps_sas_lsi.c:128-145 and the patched file compiles cleanly under the kernel's CFLAGS (validated by an in-guest module build).

Files

  • harness.c β€” userspace replica of the vulnerable logic (u16 truncation table for EventDataLength 0x4000..0x7FFF)
  • build.sh / run.sh β€” exact build and run commands
  • fix.diff β€” standalone git-apply-able fix (validated to apply + compile)
  • run.log β€” full unpatched + patched harness output
  • env.txt β€” guest environment

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable because the mps module does not attach on the audit guest (no LSI SAS HBA; no /dev/mps*). Validated fix.diff applies cleanly to /usr/src/sys/dev/raid/mps/mps_sas_lsi.c and mps_sas_lsi.c compiles cleanly via in-guest mps.ko module build.

fix.diff applies clean: 1 hunk at 125 (u16->u32) + 1 at 138 (sanity cap)
patched module build: mps.ko linked clean
harness: unpatched sz wraps to 0/4/32768/65532 for EventDataLength 0x4000..0x7fff; --fixed clamps to 65536
↓ fix.diffn/a (module-bound bug; guest has no LSI SAS HBA; mps.ko builds but does not attach)

Confirmed kernel references

Detail

Exploit chain

blocked by valid Phase-6 hard blocker: no LSI SAS HBA on the audit guest (no /dev/mps*). On a host with an LSI/Avago/Broadcom SAS controller, a device-controlled MPI2_EVENT_NOTIFICATION_REPLY with EventDataLength >= 0x4000 triggers the near-NULL deref (panic) or undersized alloc + heap OOB. Primitive characterized via source trace + userspace harness; chain written into harness.c.

Evidence (decisive lines)

EventDataLength=0x4000 -> buggy sz=0 (truncated)
EventDataLength=0x4001 -> buggy sz=4 (truncated)
EventDataLength=0x6000 -> buggy sz=32768 (truncated)
EventDataLength=0x7fff -> buggy sz=65532 (truncated)
RESULT: BUGGY - u16 truncation
---PATCHED---
EventDataLength=0x4000 -> patched sz=65536
...
RESULT: PATCHED - u32 sz, sanity check

PoC changes

Added harness.c (u16 truncation table). Added build.sh, run.sh, fix.diff (declare sz as u32; add sanity cap max 65536).

Verified recommended fix

Declare sz as u32 (matching the int promotion of the product) and add a sanity cap (e.g., max 65536) so EventDataLength >= 0x4000 cannot wrap to 0/undersized. Full diff in findings/poc/DF-1696/fix.diff; supersedes finding proposal (twin of DF-1473).

Verdict

REPRODUCED. Source-trace at sys/dev/raid/mps/mps_sas_lsi.c:128-145 confirms mpssas_evt_handler declares 'u16 sz;' then computes sz = le16toh(event->EventDataLength) * 4. C promotes the product to int (max 262140) but the u16 assignment truncates. For EventDataLength >= 0x4000, sz wraps to 0 -> kmalloc(0) returns ZERO_LENGTH_PTR (-8 per kern_slaballoc.c:888); the non-NULL check at 142 passes; bcopy(event->EventData, -8, 0) is a no-op; first deref in mpssas_fw_work reads near-NULL -> fatal page fault. For 0x4001..0x7FFF, sz wraps to 4..65532 -> undersized alloc; the subsequent struct-field accesses in mpssas_fw_work read/write past the allocation -> heap OOB. Harness tabulates the truncation for four representative values.