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

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.

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.

  • DF-1556 (sibling): missing MIN() on raid_volume_pg0 bcopy in same file.
  • DF-1491 (sibling, mpr_config.c): latent missing MIN() in the mpr twin.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1557 Β· 15 files
FileTypeDescriptionSize
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
README.md readme how to build/run/interpret the harness
↓ download 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 loop
  • build.sh, run.sh β€” exact reproducible commands
  • build.log, run.log, run.2.log, run.3.log β€” full run logs
  • fix.diff β€” git-apply-able fix (matches the finding proposal)
  • fix_build.log, fix_run.log β€” Phase 8 kernel-build validation
  • env.txt β€” guest environment
  • VERDICT.md β€” full narrative
  • manifest.json β€” machine-readable catalog
VERDICT.md verdict full narrative: mechanism, primitive, harness proof, fix validation
↓ download raw

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)

  1. Trigger. Same as DF-1556: mps_wd_config_pages() is called from mps_sas.c:3320 during mps_attach_sas() and on every SAS topology-change / IR-config-change event on any HBA flagged MPS_FLAGS_WD_AVAILABLE. An attacker who supplies a crafted RAID Volume Page 0 reply controls every byte.
  2. The pre-existing gate at mps_config.c:399-400 validates only VolumeType == RAID0 and NumPhysDisks <= 8. It does not validate PhysDiskMap.
  3. 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 counter index is bounded by NumPhysDisks (≀8), but the write index pRVPD->PhysDiskMap is an unchecked U8 (range 0..255, declared at mpi2_cnfg.h:1317).
  4. The target array sc->DD_column_map[MPS_MAX_DISKS_IN_VOL=10] (mpsvar.h:442) is the final field of struct mps_softc (the struct closes at mpsvar.h:443). Each slot is struct mps_column_map = {uint16_t dev_handle; uint8_t phys_disk_num;} = 4 bytes (with 1 byte padding, mpsvar.h:270-273).
  5. Primitive. DD_column_map[10].phys_disk_num is at byte offset 10*4+2 = 42 from DD_column_map base β€” i.e. 2 bytes past the end of the softc. DD_column_map[255].phys_disk_num is at byte offset 255*4+2 = 1022 β€” i.e. 982 bytes past the end of the softc. The written value (pRVPD->PhysDiskNum, also firmware-controlled U8) 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).

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_testable

compile+harness validated

kernel build rc=0 + harness SIGSEGV proof
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 18 02:40:00 UTC 2026

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.