u16 truncation in EventDataLength*4 size computation causes zero-size alloc panic and undersized-buffer OOB read
- File:
sys/dev/raid/mpr/mpr_sas_lsi.c - Lines: 136, 148, 149, 156
- Severity: High
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:H - CWE: CWE-190 Integer Overflow or Wraparound
- Confidence: certain
Summary
mprsas_evt_handler computes the event-data copy size as
u16 sz = le16toh(event->EventDataLength) * 4. EventDataLength is a U16
firmware-controlled field from the DMA reply; multiplying by 4 yields up to
262140, which silently truncates to 16 bits. For
EventDataLength=0x4000/0x8000/0xC000, sz wraps to 0 β kmalloc(0)
returns ZERO_LENGTH_PTR ((void*)-8), and every subsequent structure field
access (data->NumEntries, data->ReasonCode, etc.) dereferences near-NULL β
guaranteed kernel panic. For EventDataLength=0x4001 etc., sz wraps to a tiny
value β undersized heap allocation β OOB heap read when parsing any event data
structure whose fields extend past the allocated region.
Root cause
At mpr_sas_lsi.c:136, sz is declared u16. At line 148:
sz = le16toh(event->EventDataLength) * 4;. The multiplication is performed in
int arithmetic (C integer promotion), producing the correct product up to
262140, but the assignment to u16 sz truncates to the low 16 bits.
For EventDataLength β₯ 0x4000 (16384), the true byte size is β₯ 65536, but sz
becomes (true_size & 0xFFFF).
Manifestation A (panic): EventDataLength=0x4000 β sz=0 β
kmalloc(0, M_MPR, M_ZERO|M_NOWAIT) returns ZERO_LENGTH_PTR per
kern_slaballoc.c:888-890 β non-NULL so the check at line 150 passes β
bcopy copies 0 bytes β fw_event->event_data = (void*)-8. When
mprsas_fw_work processes the event, e.g. line 254
data = (...)fw_event->event_data then line 256 data->ReasonCode reads from
address -8+1 = 0xFFFFFFFFFFFFFFF9 (x86-64 kernel, unmapped) β page fault β
panic.
Manifestation B (OOB read): EventDataLength=0x4001 β sz=4 β
kmalloc(4) β only 4 bytes allocated and copied β accessing
data->NumEntries (offset 0x08 in SAS_TOPOLOGY_CHANGE_LIST, offset 0x00
NumElements in IR_CONFIG_CHANGE_LIST at line 285-286 reading Flags at
offset 0x04) reads past the 4-byte allocation into adjacent kernel heap.
Additionally, even without truncation (EventDataLength β€ 0x3FFF), the
bcopy at line 156 reads sz bytes from event->EventData in the reply DMA
frame without bounding sz against replyframesz (typically ~96 bytes), so
EventDataLength > ~24 causes an OOB read of the reply DMA pool β same class
as DF-1370 in the sibling mps driver.
Threat
Attacker is a malicious or compromised SAS HBA (PCIe device β e.g., a
counterfeit card, Thunderbolt/ExpressCard attachment, or
firmware-update-compromised HBA) that writes a crafted EventDataLength into
the DMA event reply buffer. The event reply is consumed by
mpr_intr_locked β mpr_dispatch_event (mpr.c:2403,2452) β
mprsas_evt_handler (interrupt context).
For manifestation A, a single event with EventDataLength=0x4000 causes an
immediate kernel panic β unambiguous system crash (A:H).
For manifestation B, an undersized allocation leads to OOB heap reads that can leak adjacent kernel heap data (pointers, potentially defeating KASLR) or corrupt internal driver state by acting on garbage values (C:L, A:L).
Physical access to a PCIe slot or prior firmware compromise is required; no userland syscall triggers this path.
Exploit / PoC
Reproduce with a malicious PCIe device (FPGA-based card or custom QEMU device
model for the LSI SAS3 controller PCI ID 1000:0097) that sends an
MPI2_EVENT_NOTIFICATION_REPLY via the reply DMA post queue with
EventDataLength set to 0x4000 (dwords).
- Boot DragonFlyBSD with the
mprdriver loaded and an emulated/malicious SAS3 HBA present. - Have the device post an Address Reply descriptor (SMID=0) pointing to a
reply frame whose
Eventfield isMPI2_EVENT_SAS_TOPOLOGY_CHANGE_LIST (0x0016)andEventDataLengthis0x4000. mpr_intr_lockedreads the reply, callsmpr_dispatch_eventβmprsas_evt_handler.sz = 0x4000*4 = 0x10000, truncated tou16 = 0.kmalloc(0)returnsZERO_LENGTH_PTR.- The event is queued; the taskqueue thread calls
mprsas_fw_work; theSAS_TOPOLOGY_CHANGE_LISTcase castsfw_event->event_datato a struct pointer and dereferences offset 0x00 (EnclosureHandle) at address0xFFFFFFFFFFFFFFF8β fatal pagefault/crash.
Expected result: immediate kernel panic (panic message: virtual page fault in kernel mode).
For the OOB variant, set EventDataLength=0x4001 (sz wraps to 4) and embed
NumEntries=255 in the copied 4 bytes via the first dword β the loop at line
216 reads 255 PHY entries past the 4-byte allocation, leaking ~1KB of adjacent
kernel heap.
Recommended fix
Declare sz as uint32_t and clamp it to the maximum sane event data size
bounded by the reply frame.
--- a/sys/dev/raid/mpr/mpr_sas_lsi.c
+++ b/sys/dev/raid/mpr/mpr_sas_lsi.c
@@ -133,7 +133,7 @@
MPI2_EVENT_NOTIFICATION_REPLY *event)
{
struct mpr_fw_event_work *fw_event;
- u16 sz;
+ uint32_t sz;
mpr_dprint(sc, MPR_TRACE, "%s\n", __func__);
MPR_DPRINT_EVENT(sc, sas, event);
@@ -145,8 +145,14 @@
kprintf("%s: allocate failed for fw_event\n", __func__);
return;
}
- sz = le16toh(event->EventDataLength) * 4;
- fw_event->event_data = kmalloc(sz, M_MPR, M_ZERO|M_NOWAIT);
+ sz = (uint32_t)le16toh(event->EventDataLength) * 4;
+ /*
+ * Clamp to the reply frame size to avoid OOB reads of the DMA
+ * reply pool and reject zero-length / impossibly large values.
+ */
+ if (sz == 0 || sz > sc->replyframesz)
+ sz = sc->replyframesz;
+ fw_event->event_data = kmalloc(sz, M_MPR, M_ZERO|M_NOWAIT);
if (!fw_event->event_data) {
kprintf("%s: allocate failed for event_data\n", __func__);
kfree(fw_event, M_MPR);
Related findings
- DF-1370 (twin, mps driver):
EventDataLengthOOB read of reply DMA pool. - DF-1474 (sibling): unbounded
NumEntriesloop in same file. - DF-1475 (sibling): off-by-one
PhyNumtarget-ID fallback in same file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1473 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | reproduces the u16 truncation of EventDataLength*4 | 2.6 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 65 B | view raw |
| run.sh | run-script | ./harness | 41 B | view raw |
| build.log | build-log | in-guest build, BUILD_EXIT=0 | 13 B | view raw |
| run.log | run-log | decisive run; 4/6 cases truncated | 855 B | view raw |
| env.txt | environment | uname + guest PCI inventory | 543 B | view raw |
| fix.diff | suggested-fix | declare sz as u32 (was u16) | 486 B | view raw |
| fix_build.log | fix-build-log | patched nativekernel, rc=0 | 5.6 MB | β download |
| VERDICT.md | verdict | full narrative | 3.4 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-1473 β mpr_sas_lsi.c u16 truncation in EventDataLength*4
Verdict
REPRODUCED (source-level harness). The bug is real; impact ceiling is a
kernel page-fault panic (EventDataLength=0x4000 β kmalloc(0) β ZERO_LENGTH_PTR
deref) and/or OOB heap read of the M_MPR slab bucket (EventDataLength=0x4001
β kmalloc(4) β under-sized buffer parsed by the event-handler thread). The
path is exercised only by an LSI SAS3 HBA (PCI 1000:0097) sending a crafted
event-reply DMA frame; QEMU does not emulate that HBA so the kernel code path
cannot be triggered on this guest. fix.diff applies cleanly and
nativekernel succeeds (rc=0).
Mechanism (sys/dev/raid/mpr/mpr_sas_lsi.c)
- Line 136:
u16 sz; - Line 148:
sz = le16toh(event->EventDataLength) * 4;βevent->EventDataLengthisU16(sys/dev/raid/mpr/mpi/mpi2_ioc.h:563); the multiplication by 4 promotes to int (32-bit) but the assignment back tou16 sztruncates the high 16 bits. EventDataLength = 0x4000(16384) β 0x4000 * 4 = 0x10000 β truncated to 0.- Line 149:
fw_event->event_data = kmalloc(0, M_MPR, M_ZERO|M_NOWAIT);In DragonFlykmalloc(0)returnsZERO_LENGTH_PTR(-8), which is non-NULL, so the NULL check on line 150 passes. - Line 156:
bcopy(event->EventData, fw_event->event_data, 0)is a no-op. - Later (taskqueue thread,
mprsas_fw_work),fw_event->event_data(== -8) is dereferenced to readdata->ReasonCodeβ page fault β panic. EventDataLength = 0x4001β sz truncated to 4 β kmalloc(4) succeeds β bcopy copies 4 bytes β event-struct parsing reads fields past byte 4 β OOB heap read of the M_MPR slab bucket.
Harness proof (harness.c)
Compares the buggy (u16-truncated) and correct (u32) sizes for six EDL values:
case EDL buggy_sz correct_sz normal EventDataLength=8 8 32 32 EventDataLength=24 (reply sz) 24 96 96 EventDataLength=0x4000 -> sz=0 16384 0 65536 EventDataLength=0x4001 -> sz=4 16385 4 65540 EventDataLength=0x4002 -> sz=8 16386 8 65544 EventDataLength=0xFFFF -> sz=0xFFFC 65535 65532 262140 Buggy: 4/6 cases produce a truncated (wrong) size.
Exploit-chain note
Trigger requires a malicious or compromised SAS3 HBA in a PCIe slot (or a passthrough VFIO of one into a VM β a realistic cloud-tenant-attack scenario). On the audit guest (no SAS3 HW) the path cannot be exercised. The primitive characterization is: a single malicious event-reply DMA frame triggers a panic (DoS) or a controlled OOB heap read; with a sustained stream of crafted events the OOB reads can be used for heap disclosure / grooming. Realistic ceiling is DoS + info-leak.
PoC changes
- Original PoC was README-only.
- Added harness.c, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.
Fix
fix.diff declares sz as u32 so the multiplication result is not
truncated. Matches the finding markdown proposal ("declare sz uint32_t").
Fix-validation
patch -p1 --forward succeeds (hunk #1 at line 133). nativekernel
completes with rc=0 (saved as fix_build.log). No run-time exercise is
possible because no SAS3 HBA exists on the guest β fix_status:
"not_testable". Diff applies and compiles; changed logic closes the cited
truncation.
Fix verification
not_testablenot_testable because no LSI SAS3 HBA exists on the audit guest; validated that fix.diff applies cleanly (hunk #1 at line 133) and the single-fix nativekernel compiles rc=0 (fix_build.log).
baseline (harness): Buggy: 4/6 cases produce a truncated (wrong) size patched kernel build: === NK_DONE rc=0 ===
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- r
- /
- m
- p
- r
- _
- s
- a
- s
- _
- l
- s
- i
- .
- c
- :
- 1
- 3
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- r
- /
- m
- p
- r
- _
- s
- a
- s
- _
- l
- s
- i
- .
- c
- :
- 1
- 4
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- r
- /
- m
- p
- r
- _
- s
- a
- s
- _
- l
- s
- i
- .
- c
- :
- 1
- 4
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- r
- /
- m
- p
- r
- _
- s
- a
- s
- _
- l
- s
- i
- .
- c
- :
- 1
- 5
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- p
- r
- /
- m
- p
- i
- /
- m
- p
- i
- 2
- _
- i
- o
- c
- .
- h
- :
- 5
- 6
- 3
Detail
Exploit chain
none (HW-gated): no LSI SAS3 HBA (PCI 1000:0097) in QEMU. Primitive characterized via harness: a malicious/compromised SAS3 HBA (or VFIO-passthrough of one) can choose EDL to control the truncated size precisely, yielding either panic (DoS) or OOB heap read in M_MPR slab. Realistic ceiling: DoS + info-leak from a malicious PCIe device.
Evidence (decisive lines)
EventDataLength=0x4000 -> sz=0 16384 0 65536 EventDataLength=0x4001 -> sz=4 16385 4 65540 EventDataLength=0xFFFF -> sz=0xFFFC 65535 65532 262140 Buggy: 4/6 cases produce a truncated (wrong) size. CONFIRMED: u16 sz truncation at mpr_sas_lsi.c:148 yields kmalloc(0) -> ZERO_LENGTH_PTR deref panic (EDL=0x4000) and kmalloc(4)-then-OOB-parse (EDL=0x4001).
PoC changes
Original folder was README-only. Added harness.c reproducing the u16 truncation, build/run scripts, env, full logs, fix.diff, VERDICT.md, manifest.json.
Verified recommended fix
fix.diff declares sz as u32 (was u16) so EventDataLength*4 is no longer truncated. Matches finding markdown proposal (declare sz uint32_t). A separate defense-in-depth clamp against replyframesz is suggested but not required for the cited root cause.
Verdict
REPRODUCED at the source-logic level. mpr_sas_lsi.c:136 declares u16 sz; line 148 sz = le16toh(event->EventDataLength)*4 truncates the product to the low 16 bits. EventDataLength=0x4000 -> sz=0 -> kmalloc(0) returns ZERO_LENGTH_PTR(-8), non-NULL so the NULL check passes, later taskqueue thread derefs fw_event->event_data == -8 -> page-fault panic. EventDataLength=0x4001 -> sz=4 -> kmalloc(4), bcopy 4 bytes, subsequent parsing reads past the 4-byte allocation (OOB heap read). Harness compares buggy vs correct sizes for 6 EDL values; 4 produce a truncated/wrong size. No SAS3 HBA on the audit guest so kernel path cannot be triggered; harness proof only.
No comments yet.