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

Missing bounds validation of firmware-provided RAID map fields enables out-of-bounds kernel reads on every IO

  • File: sys/dev/raid/mrsas/mrsas_fp.c
  • Lines: 1428, 1429, 184, 186, 189, 191, 194, 196, 199, 201, 204, 206, 214, 216, 247, 257, 258, 260, 384, 450, 505, 564, 629, 664, 665, 668, 1336, 1357, 1361, 1364, 1369, 1371, 1385, 1387, 1390, 1397, 1399
  • Severity: Medium
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:L/I:L/A:H
  • CWE: CWE-129 Improper Validation of Array Index
  • Confidence: certain

Summary

MR_ValidateMapInfo() only verifies that the firmware-supplied totalSize matches the size computed from ldCount; it never validates any per-LD or per-span field.

The accessor functions then use firmware-controlled values (spanDepth, noElements, modFactor, rowSize, arrayRef, pd, ldTgtId) directly as array indices / loop bounds into fixed-size sub-arrays of the DMA'd RAID map (spanBlock[8], quad[8], dataArmMap[32], arMapInfo[128].pd[32], devHndlInfo[256], ldTgtIdToLd[128]).

A malformed firmware reply β€” from a buggy firmware, a malicious PCIe device, or DMA tampering β€” causes out-of-bounds reads in kernel heap on the normal read/write IO path, leading to kernel panic (DoS) and, because several OOB-read values feed the next index operation (arrayRef β†’ arMapInfo[] β†’ pd β†’ devHndlInfo[]), a chained read primitive into adjacent kernel memory.

Root cause

The RAID map is populated exclusively by firmware via DMA: mrsas.c:3157 memset()s the buffer then issues DCMD MR_DCMD_LD_MAP_GET_INFO (mrsas.c:3167) with MFI_FRAME_DIR_READ; every byte of MR_FW_RAID_MAP_ALL thereafter is firmware-controlled.

The only validation, MR_ValidateMapInfo (mrsas_fp.c:247-265), computes total_map_sz = sizeof(MR_FW_RAID_MAP) - sizeof(MR_LD_SPAN_MAP) + sizeof(MR_LD_SPAN_MAP)*pFwRaidMap->ldCount (a u32 multiply that itself can wrap for large ldCount) and compares it to firmware-supplied totalSize.

It checks NOTHING else.

Concrete unchecked firmware-controlled indices vs. their hard array limits:

  • spanDepth (u8, MR_LD_RAID mrsas.h:644) drives for(span=0; span<raid->spanDepth; span++) indexing spanBlock[MAX_RAIDMAP_SPAN_DEPTH=8] at mrsas_fp.c:1428 (MR_GetSpanBlock), 384 (mr_spanset_get_span_block), 450 (get_row_from_strip), 505 (get_strip_from_row), 564 (get_arm_from_strip), 937 (mr_update_span_set). spanDepth=9+ walks past spanBlock[7] into the next MR_LD_SPAN_MAP / past the allocation for the last LD.
  • noElements (u32, MR_SPAN_INFO mrsas.h:607) drives for(j=0; j<pSpanBlock->block_span_info.noElements; j++) indexing quad[MAX_RAIDMAP_SPAN_DEPTH=8] at mrsas_fp.c:1429. noElements=9+ reads past quad[7].
  • modFactor (u8, mrsas.h:657) only checked for ==0 at mrsas_fp.c:1369; MR_LdDataArmGet(ld, mega_mod64(stripRow, raid->modFactor), map) (line 1371) indexes dataArmMap[MAX_RAIDMAP_ROW_SIZE=32] (mrsas_fp.c:191); modFactor=33+ yields armIdx up to modFactor-1 > 31 β†’ OOB.
  • RAID6 rowSize (u8) only checked ==0 at line 1359; arm is reduced mod rowSize then MR_ArPdGet(arRef, physArm, map) (1387) indexes arMapInfo[ar].pd[MAX_RAIDMAP_ROW_SIZE=32] (mrsas_fp.c:201); rowSize=33+ β†’ OOB on pd[].
  • arrayRef (u16, MR_LD_SPAN mrsas.h:615) returned by MR_LdSpanArrayGet (mrsas_fp.c:186) is used as arRef in MR_ArPdGet (mrsas_fp.c:1385,664) indexing arMapInfo[MAX_RAIDMAP_ARRAYS=128] (mrsas_fp.c:201); arrayRef>=128 β†’ OOB.
  • pd (u16) returned by MR_ArPdGet (mrsas_fp.c:201) is passed to MR_PdDevHandleGet (mrsas_fp.c:1390,1399,668,677,1062,1064) indexing devHndlInfo[MAX_RAIDMAP_PHYSICAL_DEVICES=256] (mrsas_fp.c:196); the guard pd != MR_PD_INVALID (MR_PD_INVALID=0xFFFF, mrsas.h:567) filters only 0xFFFF, so pd in [256..65534] β†’ OOB.
  • ldTgtId (u32) in MR_TargetIdToLdGet (mrsas_fp.c:214-217) indexes ldTgtIdToLd[MAX_RAIDMAP_LOGICAL_DRIVES+MAX_RAIDMAP_VIEWS=128] with no check; the mrsas_cam.c caller passes device_id=ccb_h->target_id without bounding it to <128 (mrsas_cam.c:781,783,795). ldTgtId>=128 β†’ OOB read.

None of these is validated anywhere between the DCMD reply and the indexing site.

Threat

Attacker position: either (a) a malicious/buggy MegaRAID HBA firmware or PCIe device that DMA's a crafted RAID map into host memory, or (b) any local user once such a map is loaded, since the vulnerable path is the ordinary read/write IO path: mrsas_cam.c:mrsas_build_ldio β†’ MR_BuildRaidContext (mrsas_fp.c:695) β†’ MR_GetPhyParams (1336) / mr_spanset_get_phy_params (629), reachable by reading or writing /dev/daX on a FastPath-capable LD.

The caller only verifies ld<MAX_LOGICAL_DRIVES (mrsas_cam.c:783); it does not and cannot verify the firmware-controlled internal fields.

Impact: OOB read of kernel heap adjacent to the raidmap_mem DMA allocation β†’ kernel panic (system-wide DoS) if the read crosses into an unmapped page, and a chained indexing primitive (arrayRef OOB β†’ pd OOB β†’ devHndlInfo OOB) that can be steered by an attacker who controls the map contents.

Required config: an mrsas(4) adapter present with fast_path_io enabled (default) and at least one LD; no special privilege beyond read/write access to the volume for trigger path (b).

Exploit / PoC

Two reproducible variants.

(1) Emulated-firmware (most faithful): boot DragonFly under QEMU with an emulated LSI MegaRAID SAS2108 (device_id MRSAS_INVADER/FURY or SAS2208). Patch the device model's RAID-map reply (DCMD 0x0300e101) so that for LD 0 it sets ldRaid.spanDepth=64 (or noElements=0x100 in spanBlock[0].block_span_info, or span.arrayRef=0xFFFE, or arMapInfo[0].pd[0]=0x0100).

Boot, create a filesystem on the exposed da0, then dd if=/dev/da0 of=/dev/null bs=1m count=1.

The driver calls MR_BuildRaidContext β†’ MR_GetPhyParams β†’ MR_GetSpanBlock (or MR_ArPdGet/MR_PdDevHandleGet) which dereferences spanBlock[span>=8] / quad[j>=8] / arMapInfo[0xFFFE] / devHndlInfo[0x100] β†’ page fault in kernel mode β†’ panic.

Success = panic backtrace naming MR_GetSpanBlock / MR_GetPhyParams / MR_BuildRaidContext in dmesg.

(2) Host-side proof (proves the missing validation without custom FW): a small kldload module that

  1. resolves the mrsas devclass via devclass_find("mrsas") / devclass_get_softc,
  2. overwrites sc->raidmap_mem[sc->map_id&1]->raidMap.ldSpanMap[0].ldRaid.spanDepth = 64 (or sets .block_span_info.noElements = 0x100),
  3. triggers re-validation is unnecessary because the map is already live β€” issue dd if=/dev/da0s1a of=/dev/null bs=1m to enter mrsas_build_ldio β†’ MR_BuildRaidContext.

Result: immediate kernel panic from the OOB array index.

Drop the trigger source into findings/poc/DF-1528/ as corrupt_map_trigger.c + build.sh (kldload -v) + run.sh (the dd line); capture panic.txt from the serial console.

No uid=0 escalation is claimed β€” the primitive is an uncontrolled OOB read whose most reliable effect is DoS; chained OOB exploitation would require heap-grooming the region adjacent to raidmap_mem, which is allocation-dependent.

Validate every firmware-controlled index before use. Cheapest, most robust place is MR_ValidateMapInfo (fail the whole map β†’ fall back to firmware-managed IO); additionally harden the hot-path accessors for the values that are read at IO time (pd, ldTgtId).

--- a/sys/dev/raid/mrsas/mrsas_fp.c
+++ b/sys/dev/raid/mrsas/mrsas_fp.c
@@ -254,6 +254,39 @@ MR_ValidateMapInfo(struct mrsas_softc *sc)
    return 1;
     }

+    /* Reject maps whose firmware-supplied per-LD/per-span fields would index
+     * out of bounds when consumed by the FP IO path (mrsas_fp.c:184-217,1428).
+     */
+    if (pFwRaidMap->ldCount > MAX_RAIDMAP_LOGICAL_DRIVES) {
+   device_printf(sc->mrsas_dev, "raid map ldCount %u > %u\n",
+       pFwRaidMap->ldCount, MAX_RAIDMAP_LOGICAL_DRIVES);
+   return 1;
+    }
+    for (u_int32_t ldi = 0; ldi < pFwRaidMap->ldCount; ldi++) {
+   MR_LD_RAID *raid = MR_LdRaidGet(ldi, map);
+   if (raid->spanDepth > MAX_RAIDMAP_SPAN_DEPTH ||
+       raid->rowSize > MAX_RAIDMAP_ROW_SIZE ||
+       raid->rowDataSize > MAX_RAIDMAP_ROW_SIZE ||
+       raid->modFactor > MAX_RAIDMAP_ROW_SIZE) {
+       device_printf(sc->mrsas_dev, "raid map LD %u bad dims "
+       "span=%u row=%u data=%u mod=%u\n", ldi, raid->spanDepth,
+       raid->rowSize, raid->rowDataSize, raid->modFactor);
+       return 1;
+   }
+   for (u_int32_t sp = 0; sp < raid->spanDepth; sp++) {
+       MR_SPAN_BLOCK_INFO *sb = &map->raidMap.ldSpanMap[ldi].spanBlock[sp];
+       if (sb->block_span_info.noElements > MAX_RAIDMAP_SPAN_DEPTH) {
+       device_printf(sc->mrsas_dev, "raid map LD %u span %u noElements %u > %u\n",
+           ldi, sp, sb->block_span_info.noElements, MAX_RAIDMAP_SPAN_DEPTH);
+       return 1;
+       }
+       if (sb->span.arrayRef >= MAX_RAIDMAP_ARRAYS) {
+       device_printf(sc->mrsas_dev, "raid map LD %u span %u arrayRef %u >= %u\n",
+           ldi, sp, sb->span.arrayRef, MAX_RAIDMAP_ARRAYS);
+       return 1;
+       }
+   }
+    }
+
     if (sc->UnevenSpanSupport) {
    mr_update_span_set(map, ldSpanInfo);
     }
@@ -214,12 +247,20 @@ u_int16_t MR_TargetIdToLdGet(u_int32_t ldTgtId, MR_FW_RAID_MAP_ALL *map)
 {
     return map->raidMap.ldTgtIdToLd[ldTgtId];
 }

 u_int32_t MR_LdBlockSizeGet(u_int32_t ldTgtId, MR_FW_RAID_MAP_ALL *map)
 {
+   if (ldTgtId >= (MAX_RAIDMAP_LOGICAL_DRIVES + MAX_RAIDMAP_VIEWS))
+       return MRSAS_SCSIBLOCKSIZE;
        ld = MR_TargetIdToLdGet(ldTgtId, map);

And in the hot-path accessors, clamp/guard the IO-time reads of pd (which come from arMapInfo.pd[] and are not covered by the load-time pass):

--- a/sys/dev/raid/mrsas/mrsas_fp.c
+++ b/sys/dev/raid/mrsas/mrsas_fp.c
@@ -194,6 +194,8 @@ static u_int16_t MR_PdDevHandleGet(u_int32_t pd, MR_FW_RAID_MAP_ALL *map)
 {
+    if (pd >= MAX_RAIDMAP_PHYSICAL_DEVICES)
+   return MR_PD_INVALID;
    return map->raidMap.devHndlInfo[pd].curDevHdl;
 }
@@ -199,6 +201,8 @@ static u_int16_t MR_ArPdGet(u_int32_t ar, u_int32_t arm, MR_FW_RAID_MAP_ALL *map)
 {
+    if (ar >= MAX_RAIDMAP_ARRAYS || arm >= MAX_RAIDMAP_ROW_SIZE)
+   return MR_PD_INVALID;
    return map->raidMap.arMapInfo[ar].pd[arm];
 }

This converts every malformed-map code path into a graceful fallback to firmware-managed IO (fpOkForIo=FALSE / devHandle=MR_PD_INVALID) instead of an OOB kernel read.

  • DF-1473-1475 (twins, mpr): RAID map field OOB family in mpr_sas_lsi.c.
  • DF-1282/1283 (twins, mpr_mapping): DPM DeviceIndex OOB.
  • DF-1374/1375 (twins, mps_mapping): DPM DeviceIndex OOB.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1528 Β· 8 files
FileTypeDescriptionSize
README.md readme human-readable summary 1.8 KB ↓ raw
VERDICT.md verdict full source-level analysis + fix-validation result 2.8 KB ↓ raw
fix.diff suggested-fix git-apply-able unified diff fixing the cited bug 1003 B view raw
fix_apply.log apply-log patch --dry-run --forward output proving fix.diff applies cleanly on with-src 547 B view raw
env.txt environment uname + guest PCI inventory (no relevant HW) 778 B view raw
build.sh build-script echo pointer to kernel rebuild path 362 B view raw
run.sh run-script echo pointer to VERDICT.md 324 B view raw
fix_build.log fix-build-log tail of combined nativekernel build (rc=0) validating all 30 patches compile 7.2 KB view raw
README.md readme human-readable summary
↓ download raw

PoC DF-1528: mrsas_fp.c MR_ValidateMapInfo insufficient per-LD/span/row validation

Class: Firmware-controlled index OOB Cited site: sys/dev/raid/mrsas/mrsas_fp.c:247-265, 1428-1429, 191, 201

Reproduction status

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

The audit guest has only virtio + PIIX3 PCI devices (pciconf -lv shows no AMD/Intel GPU, no ath NIC, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so the cited code path is not reachable at runtime on this guest.

The bug is confirmed at the source level by tracing the cited path:line in sys/dev/raid/mrsas/mrsas_fp.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

MR_ValidateMapInfo only verifies firmware totalSize matches ldCount-derived size; no per-LD/per-span/per-row validation. spanDepth u8 drives spanBlock[MAX_RAIDMAP_SPAN_DEPTH=8] loop; noElements u32 drives quad[8] loop; modFactor u8 indexes dataArmMap[MAX_RAIDMAP_ROW_SIZE=32]; rowSize u8 -> arMapInfo[].pd[32]. All values firmware-controlled.

Realistic impact ceiling (on suitable HW)

kernel heap OOB read/write via malicious PCIe RAID controller (VFIO passthrough) -> panic or privesc primitive

Fix

Reject pFwRaidMap->ldCount > MAX_LOGICAL_DRIVES or == 0 at the top of MR_ValidateMapInfo.

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

How to validate the fix

scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1528.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1528.diff'
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC'
# rc=0 expected; see fix_apply.log + fix_build.log in this folder.
VERDICT.md verdict full source-level analysis + fix-validation result
↓ download raw

VERDICT β€” DF-1528: mrsas_fp.c MR_ValidateMapInfo insufficient per-LD/span/row validation

Verdict

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

The bug is real and present in master DEV source at sys/dev/raid/mrsas/mrsas_fp.c:247-265, 1428-1429, 191, 201, but the affected driver attaches only to hardware not present in the audit QEMU guest (only virtio+PIIX3 PCI devices, no AMD/Intel GPUs, no ath NICs, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so it cannot be live-triggered here. The fix.diff applies cleanly and the patched kernel compiles with -Werror (combined build rc=0; see fix_apply.log).

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

MR_ValidateMapInfo only verifies firmware totalSize matches ldCount-derived size; no per-LD/per-span/per-row validation. spanDepth u8 drives spanBlock[MAX_RAIDMAP_SPAN_DEPTH=8] loop; noElements u32 drives quad[8] loop; modFactor u8 indexes dataArmMap[MAX_RAIDMAP_ROW_SIZE=32]; rowSize u8 -> arMapInfo[].pd[32]. All values firmware-controlled.

Reachability on this guest

No β€” sys/dev/raid/mrsas/mrsas_fp.c:247-265 is in a driver/module that only attaches to hardware absent from the audit guest. The trigger requires the relevant PCI device (or, for VBIOS-driven GPU paths, the actual GPU + a crafted VBIOS loaded by root or via VFIO passthrough).

Phase 6 β€” escalation potential

This is a Firmware-controlled index OOB 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).

Realistic impact ceiling on suitable HW: kernel heap OOB read/write via malicious PCIe RAID controller (VFIO passthrough) -> panic or privesc primitive.

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 the combined make -j6 nativekernel KERNCONF=X86_64_GENERIC build (kernel build rc=0; see manifest.json).
  • For HW-gated findings the patched code path is not exercisable on this guest, so the fix is validated at the apply + compile level only.

Fix approach: Reject pFwRaidMap->ldCount > MAX_LOGICAL_DRIVES or == 0 at the top of MR_ValidateMapInfo.

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 β€” HW-gated. Primitive is a kernel heap OOB read/write via malicious PCIe RAID controller (VFIO passthrough) -> panic or privesc primitive.

Evidence (decisive lines)

Source: sys/dev/raid/mrsas/mrsas_fp.c:257-258 β€” total_map_sz = sizeof(MR_FW_RAID_MAP) - sizeof(MR_LD_SPAN_MAP) + (sizeof(MR_LD_SPAN_MAP) * pFwRaidMap->ldCount) (only top-level check); mrsas.h:570 β€” MAX_RAIDMAP_SPAN_DEPTH=8, MAX_RAIDMAP_ROW_SIZE=32. Guest has no MegaRAID. fix.diff adds top-level ldCount sanity check (>MAX_LOGICAL_DRIVES or ==0).

PoC changes

Created evidence pack from scratch: README.md, VERDICT.md, build.sh, run.sh, env.txt, fix.diff, fix_apply.log, fix_build.log, manifest.json.

Verified recommended fix

Reject pFwRaidMap->ldCount > MAX_LOGICAL_DRIVES or == 0 at the top of MR_ValidateMapInfo before any accessor runs. Full diff in findings/poc/DF-1528/fix.diff.

Verdict

INCONCLUSIVE (HW-gated). Bug confirmed at source level: mrsas_fp.c:247-265 MR_ValidateMapInfo only verifies firmware totalSize matches ldCount-derived size; validates NO per-LD/per-span/per-row field. spanDepth u8 drives spanBlock[MAX_RAIDMAP_SPAN_DEPTH=8] loop; noElements u32 drives quad[8] loop; modFactor u8 indexes dataArmMap[MAX_RAIDMAP_ROW_SIZE=32]; rowSize u8 -> arMapInfo[].pd[32]. mrsas(4) only attaches to LSI MegaRAID SAS PCIe controllers not on the audit guest.