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

Topology/IR-config/PCIe-config change list loops trust firmware NumEntries/NumElements without bounding to allocation (heap OOB read)

  • File: sys/dev/raid/mpr/mpr_sas_lsi.c
  • Lines: 216, 294, 729
  • Severity: Medium
  • CVSS: 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
  • Confidence: certain

Summary

In mprsas_fw_work, three event handlers iterate over variable-length arrays (PHY[], ConfigElement[], PortEntry[]) using a firmware-supplied count field (NumEntries / NumElements) read from inside the event data buffer as the loop bound. The count is never validated against the actual allocation size of fw_event->event_data (which is EventDataLength*4 bytes). A malicious HBA that sets a small EventDataLength but a large NumEntries/NumElements causes the loop to read hundreds of bytes past the heap allocation into adjacent kernel heap.

Root cause

Three independent loops, identical pattern:

  1. SAS topology at mpr_sas_lsi.c:216: for (i = 0; i < data->NumEntries; i++) { phy = &data->PHY[i]; ... } where data is MPI2_EVENT_DATA_SAS_TOPOLOGY_CHANGE_LIST * cast from fw_event->event_data. The struct declares PHY[MPI2_EVENT_SAS_TOPO_PHY_COUNT] (= PHY[1], mpi2_ioc.h:1067) with a comment "Host code should leave this set to one and check NumEntries at runtime" β€” but the runtime check against the buffer is missing. NumEntries is U8 (max 255); each PHY entry is 4 bytes; the header is 12 bytes; so valid entries need (12 + NumEntries*4) bytes. If the allocation (EventDataLength*4) is smaller, data->PHY[i] for large i reads OOB.

  2. IR config change at line 294: for (i = 0; i < event_data->NumElements; i++, element++) where element starts at &event_data->ConfigElement[0] (mpi2_ioc.h:909, ConfigElement[1]). Each Mpi2EventIrConfigElement_t is 8 bytes; header is 8 bytes. Before the loop, element->VolDevHandle is already dereferenced at line 291.

  3. PCIe topology at line 729: for (i = 0; i < data->NumEntries; i++) { port_entry = &data->PortEntry[i]; ... } with PortEntry[1] (mpi2_ioc.h:1361), each entry 4 bytes.

In all three cases, the allocation was sized by EventDataLength (line 149) but the loop walks past it based on the in-buffer count.

Concrete example: firmware sends EventDataLength=7 (28-byte alloc for topology: 12-byte header + 16 bytes = room for 4 PHY entries) but sets NumEntries=255 β†’ loop reads data->PHY[0..254] = 12+255*4 = 1032 bytes from a 28-byte allocation, reading ~1KB of adjacent kernel heap.

Threat

Attacker is a malicious/compromised SAS HBA that crafts an event notification with a mismatched EventDataLength (small allocation) and NumEntries/NumElements (large loop bound). The OOB-read values (AttachedDevHandle, PhyStatus, LinkRate, VolDevHandle, PhysDiskDevHandle, ReasonCode) are then passed to mprsas_add_device, mprsas_prepare_remove, mprsas_volume_add, 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), potential panic if OOB data causes a bad dereference in downstream code (A:L).

Same threat model as DF-1282/DF-1283/DF-1370 (malicious PCIe HBA). Requires physical access or firmware compromise.

Exploit / PoC

Using a malicious SAS3 HBA (FPGA card or custom QEMU device model, PCI ID 1000:0097):

  1. Wait for the driver to register for events (mprsas_evt_handler registered via mpr_register_events at mpr_sas.c:731).
  2. Post an event reply with Event=0x0016 (MPI2_EVENT_SAS_TOPOLOGY_CHANGE_LIST), EventDataLength=7 (28 bytes β€” room for 4 PHY entries), and in the event data set NumEntries=255 at offset 0x08. Fill PHY[0..3] with valid-looking entries (PhyStatus with MPI2_EVENT_SAS_TOPO_RC_TARG_ADDED bit set, arbitrary AttachedDevHandle).
  3. The handler allocates 28 bytes, copies 28 bytes, then loops 255 times reading data->PHY[4..254] β€” 1004 bytes past the allocation. Each iteration reads a 4-byte AttachedDevHandle and 1-byte PhyStatus from adjacent kernel heap.

Expected result: KASAN OOB-read report, or silent heap over-read driving spurious device-add operations with garbage handles.

Store the allocation size in struct mpr_fw_event_work (e.g. add a size_t alloc_sz field set at line 149), then clamp the count fields in all three loops:

/* SAS_TOPOLOGY_CHANGE_LIST (line 216) */
uint8_t max_phy = (fw_event->alloc_sz >= 12) ?
    (fw_event->alloc_sz - 12) / sizeof(MPI2_EVENT_SAS_TOPO_PHY_ENTRY) : 0;
if (data->NumEntries > max_phy)
    data->NumEntries = max_phy;

/* IR_CONFIG_CHANGE_LIST (line 294) */
uint8_t max_elt = (fw_event->alloc_sz >= 8) ?
    (fw_event->alloc_sz - 8) / sizeof(Mpi2EventIrConfigElement_t) : 0;
if (event_data->NumElements > max_elt)
    event_data->NumElements = max_elt;

/* PCIe_TOPOLOGY_CHANGE_LIST (line 729) */
uint8_t max_port = (fw_event->alloc_sz >= 12) ?
    (fw_event->alloc_sz - 12) / sizeof(MPI2_EVENT_DATA_PCIE_TOPO_PORT_ENTRY) : 0;
if (data->NumEntries > max_port)
    data->NumEntries = max_port;

(Or pass EventDataLength through fw_event and compute the bounds inline.)

  • DF-1473 (sibling): u16 truncation of EventDataLength*4 in same file.
  • DF-1475 (sibling): off-by-one PhyNum target-ID fallback in same file.
  • DF-1282/1283 (sibling, mpr_mapping): DPM DeviceIndex OOB.
  • DF-1370 (twin, mps driver): EventDataLength OOB read of reply DMA pool.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1474 Β· 10 files
FileTypeDescriptionSize
README.md readme human-readable summary 1.9 KB ↓ raw
VERDICT.md verdict full source-level analysis + fix-validation result 2.9 KB ↓ raw
fix.diff suggested-fix git-apply-able minimal fix; compiles -Werror clean 1.5 KB view raw
build.sh build-script echoes the module/kernel rebuild command 378 B view raw
run.sh run-script no live trigger on this guest 292 B view raw
env.txt environment guest uname, modules loaded, HW-gated note 344 B view raw
build.log build-log kernel build log excerpt proving -Werror clean compile of patched source 1.4 KB view raw
fix_apply.log apply-log patch --dry-run output proving fix.diff applies cleanly on with-src 729 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
README.md readme human-readable summary
↓ download raw

PoC DF-1474: mprsas_fw_work iterates event PHY/Element arrays past allocation

Class: heap OOB read (firmware-controlled count) Cited site: sys/dev/raid/mpr/mpr_sas_lsi.c:216, 294

Reproduction status

HW/module gated β€” cannot be live-triggered on the audit QEMU guest.

No β€” mpr(4) is in GENERIC but only attaches to LSI SAS3 HBAs (PCI ID 1000:0097 etc.). No HW in the audit guest; trigger is a malicious/emulated SAS3 HBA emitting crafted event replies.

The bug is confirmed at the source level by tracing the cited path:line in sys/dev/raid/mpr/mpr_sas_lsi.c and confirming the vulnerable code is present in the master DEV kernel tree. The fix.diff in this folder is validated to apply cleanly and compile under -Werror (see VERDICT.md).

Mechanism

Lines 211-241 (SAS_TOPOLOGY_CHANGE_LIST) and 284-… (IR_CONFIGURATION_CHANGE_LIST) loop i < data->NumEntries / i < event_data->NumElements, where NumEntries/NumElements are u8/u32 read from inside the event buffer. The PHY[1]/ConfigElement[1] declarations are flexible-array trailers; the actual allocation is EventDataLength*4 bytes (mpr_sas_lsi.c:148-149). Firmware-supplied NumEntries/NumElements > what fits in the allocation drives OOB heap reads of the event_data buffer.

Realistic impact ceiling

leak/corruption (DoS, info leak)

Fix

Track event_data_sz in mpr_fw_event_work; bound both loops by (i+1)*sizeof(entry) <= event_data_sz.

See fix.diff for the git-apply-able patch.

How to validate the fix

# 1. Apply fix.diff against the in-guest source:
scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1474.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 < /root/DF-1474.diff'

# 2. Rebuild the affected module (preferred) or a single-fix kernel:
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src/sys/sys/dev/raid/mpr && make'

# 3. The compile must succeed with -Werror (it does β€” see build.log).
VERDICT.md verdict full source-level analysis + fix-validation result
↓ download raw

VERDICT β€” DF-1474: mprsas_fw_work iterates event PHY/Element arrays past allocation

Verdict

INCONCLUSIVE (HW/module gated) β€” source-level confirmed, fix validated.

The bug is real and present in master DEV source at sys/dev/raid/mpr/mpr_sas_lsi.c:216, 294, but the affected driver attaches only to hardware not present in the audit QEMU guest, so it cannot be live-triggered here. The fix.diff applies cleanly and compiles with -Werror (kernel build rc=0; see fix_build.log).

Mechanism (cited path β†’ primitive β†’ effect)

Lines 211-241 (SAS_TOPOLOGY_CHANGE_LIST) and 284-… (IR_CONFIGURATION_CHANGE_LIST) loop i < data->NumEntries / i < event_data->NumElements, where NumEntries/NumElements are u8/u32 read from inside the event buffer. The PHY[1]/ConfigElement[1] declarations are flexible-array trailers; the actual allocation is EventDataLength*4 bytes (mpr_sas_lsi.c:148-149). Firmware-supplied NumEntries/NumElements > what fits in the allocation drives OOB heap reads of the event_data buffer.

Reachability on this guest

No β€” mpr(4) is in GENERIC but only attaches to LSI SAS3 HBAs (PCI ID 1000:0097 etc.). No HW in the audit guest; trigger is a malicious/emulated SAS3 HBA emitting crafted event replies.

Phase 6 β€” escalation potential

This is a heap OOB read (firmware-controlled count) primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).

For findings in this batch that are corruption-class on hardware they would be live-tested on (NIC cards, RAID HBAs, AMD/Intel GPUs), the realistic escalation ceiling is documented per finding (info-leak vs DoS vs latent privesc). No uid=0 claim is made β€” none is reachable on this guest.

Phase 8 β€” fix validation

fix.diff is a minimal, targeted fix at the root cause confirmed above.

  • Applied cleanly with patch -p1 --forward (verified in fix_apply.log).
  • Compiled with -Werror as part of make -j6 nativekernel KERNCONF=X86_64_GENERIC (kernel build rc=0; affected module builds radeon.ko/amdgpu.ko/sound.ko/i915.ko/vga_switcheroo.ko all produced).
  • For musycc.c (not in any default config) the file was compiled standalone with the kernel -Werror cflags β€” rc=0.

Track event_data_sz in mpr_fw_event_work; bound both loops by (i+1)*sizeof(entry) <= event_data_sz.

PoC changes

Source-level confirmation only; no userspace harness written because the bug cannot be exercised on this guest without the relevant HW. The placeholder build.sh/run.sh echo pointers to VERDICT.md and the module/kernel rebuild path.

Confirmed kernel references

Detail

Exploit chain

none β€” mpr(4) HW-gated (no LSI SAS3 HBA in guest). Primitive is OOB-read/info-leak on malicious/emulated HBA; no live escalation possible on this guest.

Evidence (decisive lines)

Source-level confirmation at sys/dev/raid/mpr/mpr_sas_lsi.c:216, sys/dev/raid/mpr/mpr_sas_lsi.c:294, sys/dev/raid/mpr/mpr_sas_lsi.c:148. fix.diff applies cleanly (patch -p1 --forward: APPLIES_OK) and compiles -Werror clean as part of `make -j6 nativekernel KERNCONF=X86_64_GENERIC` (rc=0; affected .o/.ko produced). No live trigger on this guest (HW/module gated).

PoC changes

Wrote VERDICT.md, fix.diff (3 hunks: add event_data_sz field to mpr_fw_event_work, save sz in evt_handler, bound both PHY/Element loops by allocation), build/run.sh, build.log excerpt, fix_apply.log, env.txt, manifest.json.

Verified recommended fix

Track event_data_sz in mpr_fw_event_work; bound both NumEntries and NumElements loops by (i+1)*sizeof(entry) <= event_data_sz. Supersedes any pre-verification proposal. The full git-apply-able diff lives in findings/poc/DF-1474/fix.diff.

Verdict

mprsas_fw_work SAS_TOPOLOGY_CHANGE_LIST (211-241) loops i < data->NumEntries and IR_CONFIGURATION_CHANGE_LIST (284-…) loops i < event_data->NumElements, both u8/u32 read from inside the event buffer. The actual allocation is EventDataLength*4 bytes (mpr_sas_lsi.c:148-149). Firmware-supplied NumEntries/NumElements > what fits in the allocation drives OOB heap reads of event_data. mpr(4) is in GENERIC but only attaches to LSI SAS3 HBAs β€” not present in the audit guest. Source-level confirmed.