mps_wd_config_pages writes DD_column_map via firmware-supplied PhysDiskMap with no bounds check β heap OOB write past end of mps_softc
- File:
sys/dev/raid/mps/mps_config.c - Lines: 460, 461, 462, 463, 464
- Severity: High
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:H/I:H/A:H - CWE: CWE-787 Out-of-bounds Write
- Confidence: certain
Summary
In mps_wd_config_pages(), the loop that maps a WarpDrive volume's physical
disks uses the firmware-supplied pRVPD->PhysDiskMap byte as the array index
into sc->DD_column_map[] without any bounds check.
DD_column_map is sized MPS_MAX_DISKS_IN_VOL=10 (mpsvar.h:442) and is the
LAST field of struct mps_softc, so any PhysDiskMap value > 9 (it is a U8
with range 0..255) writes a controlled byte at a controlled offset into kernel
heap immediately past the softc allocation.
Root cause
At mps_config.c:460-465:
pRVPD = (pMpi2RaidVol0PhysDisk_t)&raid_vol_pg0->PhysDisk;
for (index = 0; index < raid_vol_pg0->NumPhysDisks; index++) {
sc->DD_column_map[pRVPD->PhysDiskMap].phys_disk_num = pRVPD->PhysDiskNum;
pRVPD++;
}
The loop counter index is bounded by NumPhysDisks, and the earlier check at
line 400 (raid_vol_pg0->NumPhysDisks > 8) does bound the iteration count to
<=8.
But the WRITE INDEX into DD_column_map is pRVPD->PhysDiskMap
(mpi2_cnfg.h:1317 declares it as U8 PhysDiskMap), which comes directly from
the firmware reply page and is never compared against MPS_MAX_DISKS_IN_VOL.
The earlier check at line 400 only validates VolumeType and NumPhysDisks β it
does NOT validate PhysDiskMap.
struct mps_column_map (mpsvar.h:270-273) is
{uint16_t dev_handle; uint8_t phys_disk_num;} = 4 bytes;
DD_column_map[MPS_MAX_DISKS_IN_VOL=10] (mpsvar.h:442) is the final member of
struct mps_softc (struct closes at mpsvar.h:443).
A PhysDiskMap value of 10 already writes one slot (4 bytes) past the end of the
softc; PhysDiskMap=255 writes phys_disk_num (one controlled byte) at byte
offset 255*4+2 = 1022 past DD_column_map, i.e. ~982 bytes past the end of the
softc allocation.
The written value (pRVPD->PhysDiskNum, also firmware-controlled U8) and the
slot offset are both fully attacker-chosen.
Threat
Same reachability as the sibling finding above: mps_sas.c:3320 calls
mps_wd_config_pages during attach and on every SAS topology change / IR config
change event, on any HBA flagged MPS_FLAGS_WD_AVAILABLE.
The attacker supplies a RAID Volume Page 0 with VolumeType=RAID0,
NumPhysDisks in [1,8] (to pass the line-400 gate), and at least one
PhysDisk entry whose PhysDiskMap field is > 9.
Result is a single-byte, controlled-value, controlled-offset heap write past the
mps_softc object.
Because the offset (PhysDiskMap*4+2, range 0..1022) and the value
(PhysDiskNum, 0..255) are both arbitrary, this is a high-quality
kernel-heap-corruption primitive suitable for function-pointer/vtable overwrite
and full uid=0 escalation after heap grooming.
The mpr(4) sibling driver does not contain this WD code path, so this is a
unique mps(4) defect, not a duplicate of the mpr_config.c DF-1491 family.
Recommended fix
Validate PhysDiskMap against MPS_MAX_DISKS_IN_VOL before using it as an array
index. Drop entries that do not map cleanly, since a conforming WarpDrive config
must enumerate each column exactly once:
--- a/sys/dev/raid/mps/mps_config.c
+++ b/sys/dev/raid/mps/mps_config.c
@@ -459,8 +459,14 @@
*/
pRVPD = (pMpi2RaidVol0PhysDisk_t)&raid_vol_pg0->PhysDisk;
for (index = 0; index < raid_vol_pg0->NumPhysDisks; index++) {
+ if (pRVPD->PhysDiskMap >= MPS_MAX_DISKS_IN_VOL) {
+ mps_dprint(sc, MPS_FAULT,
+ "PhysDiskMap %u out of range in WD volume! "
+ "Direct Drive I/O will not be used.\n",
+ pRVPD->PhysDiskMap);
+ goto out;
+ }
sc->DD_column_map[pRVPD->PhysDiskMap].phys_disk_num =
pRVPD->PhysDiskNum;
pRVPD++;
}
Defense-in-depth: also widen the existing NumPhysDisks check at line 400 to
> MPS_MAX_DISKS_IN_VOL rather than the magic constant 8, so the bound and the
array size cannot drift independently.
Related findings
- DF-1556 (sibling): missing
MIN()onraid_volume_pg0bcopy in same file. - DF-1491 (sibling, mpr_config.c): latent missing
MIN()in the mpr twin.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1557 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace reproduction of the kernel mps_wd_config_pages mapping loop with guard-page fault detection | 7.7 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -Wextra -o harness harness.c | 139 B | view raw |
| run.sh | run-script | ./harness <PhysDiskMap> <PhysDiskNum> <NumPhysDisks> (default 255 65 1) | 280 B | view raw |
| build.log | build-log | final successful build, full output | 183 B | view raw |
| run.log | run-log | decisive run: PhysDiskMap=10 (+2B OOB) -> SIGSEGV | 1.1 KB | view raw |
| run.2.log | run-log | stress run: PhysDiskMap=255 (+982B OOB) -> SIGSEGV | 1.1 KB | view raw |
| run.3.log | run-log | control run: PhysDiskMap=9 (last valid in-bounds slot) | 731 B | view raw |
| fix.diff | suggested-fix | git-apply-able fix: bounds-check PhysDiskMap before indexing DD_column_map (matches finding proposal) | 937 B | view raw |
| fix_build.log | build-log | Phase 8 single-fix kernel build output (mps_config.c compiled clean with -Werror) | 4.1 KB | view raw |
| fix_run.log | run-log | Phase 8 health check: patched #1 kernel boots, fixes present in source | 1.9 KB | view raw |
| env.txt | environment | guest uname, cc version, pci inventory, KASLR=0, no LSI HBA | 1.3 KB | view raw |
| VERDICT.md | verdict | full narrative: mechanism, primitive, harness proof, fix validation | 6.4 KB | β raw |
| README.md | readme | how to build/run/interpret the harness | 3.8 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-1557 β PoC: unchecked PhysDiskMap OOB write in mps_wd_config_pages
- File:
sys/dev/raid/mps/mps_config.c:462 - Class: CWE-787 Out-of-bounds Write (unchecked firmware array index)
- Severity: High
- Status: REPRODUCED (source-trace + userspace harness; latent on this guest β no LSI HBA)
Build
./build.sh # cc -O2 -Wall -Wextra -o harness harness.c
Run
./run.sh # defaults: PhysDiskMap=255, value=0x41, count=1 (max OOB)
./run.sh 10 65 1 # minimal OOB: PhysDiskMap=10 (+2 bytes past softc)
./run.sh 255 65 1 # max offset: PhysDiskMap=255 (+982 bytes past softc)
./run.sh 9 65 1 # control: PhysDiskMap=9 (last valid in-bounds slot)
Args: <PhysDiskMap> <PhysDiskNum> <NumPhysDisks>
Expected output (bug present)
write target: &DD_column_map[10].phys_disk_num = byte offset 42 from DD_column_map base
-> 2 bytes PAST end of DD_column_map (= past end of struct mps_softc ...)
[BUGGY] running loop: sc->DD_column_map[10].phys_disk_num = 0x41 ...
>>> SIGSEGV/11 caught: controlled-byte OOB write past DD_column_map[9] confirmed.
>>> PRIMITIVE CONFIRMED: a malicious HBA returning a RAID Volume Page 0 with
PhysDiskMap=10 writes byte 0x41 at offset +2 past the end of struct mps_softc.
>>> This is the exact code at sys/dev/raid/mps/mps_config.c:462 (no bounds check
on PhysDiskMap).
The control run (./run.sh 9 ...) prints PhysDiskMap=9 <
MPS_MAX_DISKS_IN_VOL=10: in-bounds write, no OOB. and exits cleanly,
confirming the overflow boundary is exactly at PhysDiskMap=10.
How it works
The harness reproduces the exact C logic of the kernel
mps_wd_config_pages() PhysDisk mapping loop:
/* mps_config.c:460 */ pRVPD = (pMpi2RaidVol0PhysDisk_t)&raid_vol_pg0->PhysDisk;
/* mps_config.c:461 */ for (index = 0; index < raid_vol_pg0->NumPhysDisks; index++) {
/* mps_config.c:462 */ sc->DD_column_map[pRVPD->PhysDiskMap].phys_disk_num =
/* mps_config.c:463 */ pRVPD->PhysDiskNum;
/* mps_config.c:464 */ pRVPD++;
/* }
The loop counter index is bounded by NumPhysDisks (β€8 by the line-400
gate), but the write index pRVPD->PhysDiskMap is an unchecked U8 (0..255)
straight from the firmware reply. DD_column_map[10] is the first slot
past the array, and because DD_column_map is the final field of struct
mps_softc (mpsvar.h:442-443), any PhysDiskMap >= 10 writes past the end
of the softc allocation. A PROT_NONE guard page placed immediately after
the DD_column_map tail makes even the minimal OOB write fault crisply.
Why a harness (not a live trigger)?
The audit guest has no LSI SAS HBA, so the mps(4) driver β although
compiled into X86_64_GENERIC β never attaches, and the live
mps_wd_config_pages() code path is never executed at runtime on this
guest. The harness reproduces the exact kernel C logic in userspace to
prove the primitive; the bug itself is confirmed by source trace at
sys/dev/raid/mps/mps_config.c:462.
Fix
See fix.diff β add an explicit PhysDiskMap >= MPS_MAX_DISKS_IN_VOL bounds
check before the indexed write, routing to the same goto out cleanup every
other validation failure in this function uses. Matches the finding
proposal. Validated to apply cleanly and compile into the single-fix #1
kernel with -Werror and zero warnings; see VERDICT.md and fix_run.log.
Files
harness.cβ userspace reproduction of the kernel mapping loopbuild.sh,run.shβ exact reproducible commandsbuild.log,run.log,run.2.log,run.3.logβ full run logsfix.diffβ git-apply-able fix (matches the finding proposal)fix_build.log,fix_run.logβ Phase 8 kernel-build validationenv.txtβ guest environmentVERDICT.mdβ full narrativemanifest.jsonβ machine-readable catalog
DF-1557 β VERDICT
Status: REPRODUCED (primitive confirmed via source trace + userspace harness)
Impact: controlled single-byte OOB write at controlled offset past struct mps_softc
Confidence: certain
Class: CWE-787 Out-of-bounds Write β unchecked firmware-supplied array index
Verdict (one line)
The bug is real and confirmed at sys/dev/raid/mps/mps_config.c:462 β the
WarpDrive PhysDisk mapping loop uses the firmware-supplied U8 PhysDiskMap
byte directly as the array index into sc->DD_column_map[MPS_MAX_DISKS_IN_VOL]
without any bounds check; any PhysDiskMap >= 10 writes a controlled byte at
a controlled offset past the end of the mps_softc allocation.
Why this is a valid (latent) primitive, not a false positive
Same reachability caveat as the sibling DF-1556: the guest has no LSI SAS
HBA, so the in-kernel mps(4) driver β although compiled into
X86_64_GENERIC (sys/config/X86_64_GENERIC:92) β never attaches and the
live code path cannot be exercised at runtime on this guest. This is the
documented Phase 6 valid hard blocker: the path is dead/unreachable at
runtime on this guest and no in-kernel harness can exercise it without the
missing hardware. The primitive is proved at the object/harness level
(DF-0594/0616/0281 latent-bug pattern).
Mechanism (trigger β primitive β effect)
- Trigger. Same as DF-1556:
mps_wd_config_pages()is called frommps_sas.c:3320duringmps_attach_sas()and on every SAS topology-change / IR-config-change event on any HBA flaggedMPS_FLAGS_WD_AVAILABLE. An attacker who supplies a crafted RAID Volume Page 0 reply controls every byte. - The pre-existing gate at
mps_config.c:399-400validates onlyVolumeType == RAID0andNumPhysDisks <= 8. It does not validatePhysDiskMap. - The buggy loop at
mps_config.c:460-465:c pRVPD = (pMpi2RaidVol0PhysDisk_t)&raid_vol_pg0->PhysDisk; for (index = 0; index < raid_vol_pg0->NumPhysDisks; index++) { sc->DD_column_map[pRVPD->PhysDiskMap].phys_disk_num = pRVPD->PhysDiskNum; pRVPD++; }The loop counterindexis bounded byNumPhysDisks(β€8), but the write indexpRVPD->PhysDiskMapis an uncheckedU8(range 0..255, declared atmpi2_cnfg.h:1317). - The target array
sc->DD_column_map[MPS_MAX_DISKS_IN_VOL=10](mpsvar.h:442) is the final field ofstruct mps_softc(the struct closes atmpsvar.h:443). Each slot isstruct mps_column_map={uint16_t dev_handle; uint8_t phys_disk_num;}= 4 bytes (with 1 byte padding,mpsvar.h:270-273). - Primitive.
DD_column_map[10].phys_disk_numis at byte offset10*4+2 = 42fromDD_column_mapbase β i.e. 2 bytes past the end of the softc.DD_column_map[255].phys_disk_numis at byte offset255*4+2 = 1022β i.e. 982 bytes past the end of the softc. The written value (pRVPD->PhysDiskNum, also firmware-controlledU8) and the slot offset are both fully attacker-chosen.
Harness proof (harness.c)
The harness reproduces the exact kernel C logic of the buggy loop against a
buffer laid out like the tail of struct mps_softc (DD_column_map[10] as
the final field), with a PROT_NONE guard page placed immediately after.
Run as the unprivileged maxx user:
$ ./run.sh 10 65 1 # minimal OOB: PhysDiskMap=10, value=0x41 -> 2 bytes PAST end of DD_column_map [BUGGY] running loop: sc->DD_column_map[10].phys_disk_num = 0x41 ... >>> SIGSEGV/11 caught: controlled-byte OOB write past DD_column_map[9] confirmed. >>> PRIMITIVE CONFIRMED: ... writes byte 0x41 at offset +2 past end of softc. $ ./run.sh 255 65 1 # max offset: PhysDiskMap=255 -> 982 bytes PAST end of DD_column_map >>> PRIMITIVE CONFIRMED: ... writes byte 0x41 at offset +982 past end of softc. $ ./run.sh 9 65 1 # control: PhysDiskMap=9 (last valid slot) PhysDiskMap=9 < MPS_MAX_DISKS_IN_VOL=10: in-bounds write, no OOB. wrote in-bounds: DD_column_map[9].phys_disk_num = 0x41
Exploit chain
Not applicable as a default-GENERIC uid=0 chain β blocked by a valid hard
blocker: the vulnerable code path is unreachable at runtime on this guest
(no LSI SAS HBA), so there is no live path from an unprivileged user to the
sink. Realistic ceiling on a host with a malicious/passed-through LSI
WarpDrive HBA: a single-byte, controlled-value, controlled-offset write past
the mps_softc object (offset range 0..982 bytes past the allocation). The
mps_softc is a large kmalloc object; an offset in the ~0..1024-byte range
is a high-quality heap-corruption primitive suitable for vtable / ops-vector
/ function-pointer overwrite after heap grooming. No uid=0 is claimed
here because the live trigger is not exercisable on this guest.
PoC changes
The PoC directory was empty on arrival. I authored:
- harness.c β self-contained userspace reproduction of the buggy mapping
loop, with struct mps_column_map and Mpi2RaidVol0PhysDisk_t reproduced
verbatim from the kernel headers (with citations in comments). The
DD_column_map tail is positioned so its last byte touches the guard page
boundary, making even a 1-byte OOB write fault crisply.
- build.sh, run.sh β exact reproducible commands.
- fix.diff β the verified fix (see below).
Recommended fix
Add an explicit bounds check on PhysDiskMap before indexing
DD_column_map, faulting out of WD setup on any out-of-range value (a
conforming WarpDrive config must enumerate each column exactly once). This
matches the finding markdown's ## Recommended fix proposal (same
logic, same goto out idiom used by every other validation failure in this
function). See fix.diff.
Fix validation (Phase 8)
fix_status: not_testable β the live in-kernel before/after test cannot be
run because the guest has no LSI SAS HBA (mps never attaches). Validated:
(a) fix.diff applies cleanly with patch -p1 --forward (hunk #1 succeeded
at line 459); (b) the patched mps_config.c compiles cleanly into both the
kernel and the mps module with -Werror and zero warnings; (c) the
single-fix kernel (#1, sha256 7a3b1de7...) boots and the guest is fully
responsive; (d) the fix logic is correct by inspection β the >=
MPS_MAX_DISKS_IN_VOL guard catches every offending value and routes to the
same goto out cleanup the rest of the function uses. See fix_build.log
and fix_run.log.
Fix verification
not_testablecompile+harness validated
kernel build rc=0 + harness SIGSEGV proof
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED (harness). mps_wd_config_pages DD_column_map[PhysDiskMap] no bounds check -> controlled byte OOB at chosen offset past mps_softc. mps in GENERIC, no SAS HBA.
No comments yet.