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

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

  1. Boot DragonFlyBSD with the mpr driver loaded and an emulated/malicious SAS3 HBA present.
  2. Have the device post an Address Reply descriptor (SMID=0) pointing to a reply frame whose Event field is MPI2_EVENT_SAS_TOPOLOGY_CHANGE_LIST (0x0016) and EventDataLength is 0x4000.
  3. mpr_intr_locked reads the reply, calls mpr_dispatch_event β†’ mprsas_evt_handler. sz = 0x4000*4 = 0x10000, truncated to u16 = 0. kmalloc(0) returns ZERO_LENGTH_PTR.
  4. The event is queued; the taskqueue thread calls mprsas_fw_work; the SAS_TOPOLOGY_CHANGE_LIST case casts fw_event->event_data to a struct pointer and dereferences offset 0x00 (EnclosureHandle) at address 0xFFFFFFFFFFFFFFF8 β†’ 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.

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);
  • DF-1370 (twin, mps driver): EventDataLength OOB read of reply DMA pool.
  • DF-1474 (sibling): unbounded NumEntries loop in same file.
  • DF-1475 (sibling): off-by-one PhyNum target-ID fallback in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1473 Β· 11 files
FileTypeDescriptionSize
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
VERDICT.md verdict full narrative
↓ download 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)

  1. Line 136: u16 sz;
  2. Line 148: sz = le16toh(event->EventDataLength) * 4; β€” event->EventDataLength is U16 (sys/dev/raid/mpr/mpi/mpi2_ioc.h:563); the multiplication by 4 promotes to int (32-bit) but the assignment back to u16 sz truncates the high 16 bits.
  3. EventDataLength = 0x4000 (16384) β†’ 0x4000 * 4 = 0x10000 β†’ truncated to 0.
  4. Line 149: fw_event->event_data = kmalloc(0, M_MPR, M_ZERO|M_NOWAIT); In DragonFly kmalloc(0) returns ZERO_LENGTH_PTR (-8), which is non-NULL, so the NULL check on line 150 passes.
  5. Line 156: bcopy(event->EventData, fw_event->event_data, 0) is a no-op.
  6. Later (taskqueue thread, mprsas_fw_work), fw_event->event_data (== -8) is dereferenced to read data->ReasonCode β†’ page fault β†’ panic.
  7. 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_testable
baseline reproduced→ patch + rebuild →patched clean

not_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 ===
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master+df1473-fix (single-fix kernel built, rc=0)

Confirmed kernel references

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.