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) drivesfor(span=0; span<raid->spanDepth; span++)indexingspanBlock[MAX_RAIDMAP_SPAN_DEPTH=8]atmrsas_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 pastspanBlock[7]into the nextMR_LD_SPAN_MAP/ past the allocation for the last LD.noElements(u32,MR_SPAN_INFO mrsas.h:607) drivesfor(j=0; j<pSpanBlock->block_span_info.noElements; j++)indexingquad[MAX_RAIDMAP_SPAN_DEPTH=8]atmrsas_fp.c:1429.noElements=9+reads pastquad[7].modFactor(u8,mrsas.h:657) only checked for==0atmrsas_fp.c:1369;MR_LdDataArmGet(ld, mega_mod64(stripRow, raid->modFactor), map)(line 1371) indexesdataArmMap[MAX_RAIDMAP_ROW_SIZE=32](mrsas_fp.c:191);modFactor=33+yieldsarmIdxup tomodFactor-1 > 31β OOB.- RAID6
rowSize(u8) only checked==0at line 1359;armis reduced modrowSizethenMR_ArPdGet(arRef, physArm, map)(1387) indexesarMapInfo[ar].pd[MAX_RAIDMAP_ROW_SIZE=32](mrsas_fp.c:201);rowSize=33+β OOB onpd[]. arrayRef(u16,MR_LD_SPAN mrsas.h:615) returned byMR_LdSpanArrayGet(mrsas_fp.c:186) is used asarRefinMR_ArPdGet(mrsas_fp.c:1385,664) indexingarMapInfo[MAX_RAIDMAP_ARRAYS=128](mrsas_fp.c:201);arrayRef>=128β OOB.pd(u16) returned byMR_ArPdGet(mrsas_fp.c:201) is passed toMR_PdDevHandleGet(mrsas_fp.c:1390,1399,668,677,1062,1064) indexingdevHndlInfo[MAX_RAIDMAP_PHYSICAL_DEVICES=256](mrsas_fp.c:196); the guardpd != MR_PD_INVALID(MR_PD_INVALID=0xFFFF,mrsas.h:567) filters only0xFFFF, sopdin[256..65534]β OOB.ldTgtId(u32) inMR_TargetIdToLdGet(mrsas_fp.c:214-217) indexesldTgtIdToLd[MAX_RAIDMAP_LOGICAL_DRIVES+MAX_RAIDMAP_VIEWS=128]with no check; themrsas_cam.ccaller passesdevice_id=ccb_h->target_idwithout 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
- resolves the
mrsasdevclass viadevclass_find("mrsas")/devclass_get_softc, - overwrites
sc->raidmap_mem[sc->map_id&1]->raidMap.ldSpanMap[0].ldRaid.spanDepth = 64(or sets.block_span_info.noElements = 0x100), - triggers re-validation is unnecessary because the map is already live β issue
dd if=/dev/da0s1a of=/dev/null bs=1mto entermrsas_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.
Recommended fix
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.
Related findings
- 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)
PoC verification
Evidence pack
findings/poc/DF-1528 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 β 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 infix_apply.log). - Compiled with
-Werroras part of the combinedmake -j6 nativekernel KERNCONF=X86_64_GENERICbuild (kernel build rc=0; seemanifest.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
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- r
- s
- a
- s
- /
- m
- r
- s
- a
- s
- _
- f
- p
- .
- c
- :
- 2
- 4
- 7
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- r
- s
- a
- s
- /
- m
- r
- s
- a
- s
- _
- f
- p
- .
- c
- :
- 2
- 5
- 7
- s
- y
- s
- /
- d
- e
- v
- /
- r
- a
- i
- d
- /
- m
- r
- s
- a
- s
- /
- m
- r
- s
- a
- s
- .
- h
- :
- 5
- 7
- 0
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.
No comments yet.