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 >= 0x4000szwraps to 0,kmalloc(0)returnsZERO_LENGTH_PTR((void*)-8) perkern_slaballoc.c:888-890, the non-NULLcheck at:142passes,bcopycopies 0 bytes, and the very first field dereference insidempssas_fw_work(e.g.data->NumEntriesat:208,event_data->NumElementsat:283,logEntry->LogEntryQualifierat:515) reads near-NULLβ fatal page fault. - For
EventDataLength = 0x4001..0x7FFF,szwraps to4..65532β undersized heap alloc β all struct field accesses at offsets pastszare heap OOB reads. - Additionally, even without truncation (
EventDataLength <= 0x3FFF), thebcopyat:148readsszbytes fromevent->EventDatainside the DMA reply frame without boundingszagainstsc->facts->ReplyFrameSize*4(mps.c:718); forEventDataLength > ~7dwords thebcopyover-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 downstreammps_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:
- ensure the
mpsdriver attaches and registers events (default behavior βmps_sas.c:678callsmps_register_eventswithmpssas_evt_handler) - wait for the host to send
MPI2_EVENT_REQUESTenabling event notification - post an Address Reply descriptor (
SMID=0) whoseReplyFrameAddresspoints at a 16-byte reply frame containing: -EventDataLength=0x4000(le16) -MsgLength=0x18-Function=0x51(EventNotificationReply) -Event=0x001C(SAS_TOPOLOGY_CHANGE_LIST) -EventContext=0 - the driver calls
mpssas_evt_handler;sz = 0x4000*4 = 0x10000truncated tou16 = 0;kmalloc(0)returnsZERO_LENGTH_PTR;bcopycopies 0 bytes;fw_eventis queued mpssas_firmware_event_workdequeues, callsmpssas_fw_work; caseMPI2_EVENT_SAS_TOPOLOGY_CHANGE_LISTat:197castsevent_data(== (void*)-8) toMPI2_EVENT_DATA_SAS_TOPOLOGY_CHANGE_LIST *data; line:206callsmps_mapping_topology_change_event(sc, fw_event->event_data)which dereferences offset0x00β load from0xFFFFFFFFFFFFFFF8on 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.
Recommended fix
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.)
Related findings
- 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)
PoC verification
Evidence pack
findings/poc/DF-1696 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 commandsfix.diffβ standalone git-apply-able fix (validated to apply + compile)run.logβ full unpatched + patched harness outputenv.txtβ guest environment
Fix verification
not_testablenot_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
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- s
- /
- m
- p
- s
- _
- s
- a
- s
- _
- l
- s
- i
- .
- c
- :
- 1
- 2
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- s
- /
- m
- p
- s
- _
- s
- a
- s
- _
- l
- s
- i
- .
- c
- :
- 1
- 4
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- s
- /
- m
- p
- s
- _
- s
- a
- s
- _
- l
- s
- i
- .
- c
- :
- 1
- 4
- 2
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.
No comments yet.