mps_config_get_raid_volume_pg0 missing MIN() guard on final bcopy β heap overflow from firmware-supplied PageLength
- File:
sys/dev/raid/mps/mps_config.c - Lines: 1084, 1089, 1117
- 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
mps_config_get_raid_volume_pg0() is the only config-page getter in this file
that does NOT bound its final bcopy by sizeof(struct): it copies
cm->cm_length bytes (firmware-controlled PageLength * 4) into the caller's
destination buffer.
The sole in-tree caller (mps_wd_config_pages) supplies an 84-byte buffer, but a
malicious/compromised HBA can return PageLength up to 65535, yielding a heap
overflow of up to ~64 KB past the destination.
This is the direct mps sibling of the mpr_config.c DF-1491 latent-MIN() bug.
Root cause
At mps_config.c:1084 cm->cm_length is computed as
le16toh(mpi_reply->Header.PageLength) * 4 straight from the firmware reply (no
clamp against the destination type).
At line 1089 the scratch page is kmalloc'd with that same length (so the
source side is fine).
At line 1117 the result is copied out with
bcopy(page, config_page, cm->cm_length) β no MIN().
Every other getter in the file uses MIN(): line 163
MIN(cm->cm_length, sizeof(Mpi2IOCPage8_t)), line 634
MIN(cm->cm_length, sz), line 880
MIN(cm->cm_length, sizeof(Mpi2SasDevicePage0_t)), line 998
MIN(cm->cm_length, sizeof(Mpi2BiosPage3_t)), line 1237
MIN(cm->cm_length, sizeof(Mpi2RaidVolPage1_t)), line 1382
MIN(cm->cm_length, sizeof(Mpi2RaidPhysDiskPage0_t)).
Only line 1117 is unbounded.
The caller's buffer at mps_config.c:378-380 is
sizeof(Mpi2RaidVolPage0_t) + sizeof(Mpi2RaidVol0PhysDisk_t) * MPS_MAX_DISKS_IN_VOL
= 44 + 4*10 = 84 bytes (MPS_MAX_DISKS_IN_VOL=10 per mpsvar.h:106; struct
sizes from mpi2_cnfg.h:1362-1383).
The NumPhysDisks>8 sanity check at line 400 happens AFTER this bcopy runs, so
it does not protect the copy.
A page reply claiming PageLength=22 (cm_length=88) already overflows by 4
bytes; PageLength=255 overflows by 936 bytes.
Threat
Reachable from any local context that causes the mps(4) driver to enumerate
RAID config pages on a WarpDrive-class HBA
(sc->mps_flags & MPS_FLAGS_WD_AVAILABLE, set per mpsvar.h:292).
The trigger path is mps_sas.c:3320 mps_wd_config_pages() β
mps_config_get_raid_volume_pg0() at mps_config.c:386 (first volume) and again
at line 472 (second volume).
It runs during normal attach/discovery and on every topology-change/reinit event, so an attacker who can supply a crafted reply (malicious PCIe card, compromised HBA firmware, or a passed-through SAS controller in a VM) controls both the length and the contents of the overflow.
Impact is kernel heap corruption adjacent to the 84-byte raid_vol_pg0
allocation β a primitive sufficient for kernel-code-execution / privilege
escalation via heap grooming.
Recommended fix
Bound the final bcopy by sizeof(Mpi2RaidVolPage0_t), matching every sibling
getter in this file.
--- a/sys/dev/raid/mps/mps_config.c
+++ b/sys/dev/raid/mps/mps_config.c
@@ -1114,7 +1114,8 @@
goto out;
}
- bcopy(page, config_page, cm->cm_length);
+ bcopy(page, config_page,
+ MIN(cm->cm_length, sizeof(Mpi2RaidVolPage0_t)));
out:
kfree(page, M_MPT2);
If the variable-length PhysDisk tail must be preserved, add an explicit
out-capacity argument to mps_config_get_raid_volume_pg0() and clamp against it;
do not trust firmware PageLength.
Related findings
- DF-1491 (sibling, mpr_config.c): latent missing
MIN()in the mpr driver's twin function (currently dead code / 0 callers in-tree). - DF-1557 (sibling):
PhysDiskMapOOB write in same file'smps_wd_config_pages.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1556 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace reproduction of the kernel mps_config_get_raid_volume_pg0 bcopy logic with guard-page fault detection | 7.4 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -Wextra -o harness harness.c | 139 B | view raw |
| run.sh | run-script | ./harness <PageLength> (default 22) | 214 B | view raw |
| build.log | build-log | final successful build, full output | 183 B | view raw |
| run.log | run-log | decisive run: PageLength=22 (+4B overflow) -> SIGSEGV | 934 B | view raw |
| run.2.log | run-log | stress run: PageLength=255 (+936B overflow) -> SIGSEGV | 944 B | view raw |
| run.3.log | run-log | control run: PageLength=21 (exact fit, no overflow) | 667 B | view raw |
| fix.diff | suggested-fix | git-apply-able fix: bound bcopy by caller buffer capacity (supersedes finding proposal) | 896 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 | 7.2 KB | β raw |
| README.md | readme | how to build/run/interpret the harness | 3.4 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-1556 β PoC: missing-MIN() heap overflow in mps_config_get_raid_volume_pg0
- File:
sys/dev/raid/mps/mps_config.c:1117 - Class: CWE-787 Out-of-bounds Write (missing length guard)
- 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 # default: PageLength=22 (minimal +4 byte overflow)
./run.sh 22 # explicit minimal overflow
./run.sh 255 # max overflow with U8 PageLength (+936 bytes)
./run.sh 21 # control: exact-fit, no overflow
Expected output (bug present)
[BUGGY] bcopy(page, caller_buf, cm_length=88) into 84-byte buffer...
overflow by 4 bytes -> write will land in guard page (PROT_NONE) -> SIGSEGV
>>> SIGSEGV/11 caught: OOB write past 84-byte caller buffer confirmed.
>>> PRIMITIVE CONFIRMED: a malicious HBA returning PageLength=22 (cm_length=88)
overflows the mps_wd_config_pages RAID-volume buffer by 4 bytes.
>>> This is the exact code at sys/dev/raid/mps/mps_config.c:1117 (no MIN()).
The control run (./run.sh 21) prints cm_length=84 <= buf=84: no overflow
this run. and exits cleanly, confirming the overflow boundary is exactly at
PageLength=22.
How it works
The harness reproduces the exact C logic of the kernel
mps_config_get_raid_volume_pg0() final bcopy:
/* mps_config.c:1084 */ cm->cm_length = le16toh(mpi_reply->Header.PageLength) * 4;
/* mps_config.c:1117 */ bcopy(page, config_page, cm->cm_length); /* no MIN() */
against a destination buffer sized identically to the sole in-tree caller's
allocation (mps_config.c:378-380):
raid_vol_pg0 = kmalloc(sizeof(Mpi2RaidVolPage0_t) +
(sizeof(Mpi2RaidVol0PhysDisk_t) * MPS_MAX_DISKS_IN_VOL), /* = 44 + 4*10 = 84 */
M_MPT2, M_ZERO | M_INTWAIT);
A PROT_NONE guard page is placed immediately after the 84-byte caller
buffer so any overflow faults with SIGSEGV, proving the primitive. Every
sibling config-page getter in mps_config.c (lines 163, 634, 880, 998, 1237,
1382) uses MIN(cm->cm_length, sizeof(...)); only line 1117 omits it.
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() β mps_config_get_raid_volume_pg0() 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:1117.
Fix
See fix.diff β bound the final bcopy by the caller's actual buffer
capacity (sizeof(Mpi2RaidVolPage0_t) + sizeof(Mpi2RaidVol0PhysDisk_t) *
MPS_MAX_DISKS_IN_VOL), preserving the legitimate PhysDisk tail. 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 bcopy logicbuild.sh,run.shβ exact reproducible commandsbuild.log,run.log,run.2.log,run.3.logβ full run logsfix.diffβ git-apply-able fix (supersedes 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-1556 β VERDICT
Status: REPRODUCED (primitive confirmed via source trace + userspace harness)
Impact: heap-overflow (OOB write), bounded only by attacker-controlled firmware PageLength
Confidence: certain
Class: CWE-787 Out-of-bounds Write β missing MIN() length guard on bcopy
Verdict (one line)
The bug is real and confirmed at sys/dev/raid/mps/mps_config.c:1117 β the
final bcopy(page, config_page, cm->cm_length) is the only config-page
getter in this file that omits the MIN(cm->cm_length, sizeof(...)) guard
every sibling uses; a malicious/compromised LSI WarpDrive HBA returning
PageLength >= 22 overflows the caller's 84-byte buffer.
Why this is a valid (latent) primitive, not a false positive
The guest has no LSI SAS HBA (pciconf -l shows only virtio devices), 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.
Per the procedure, the primitive is instead proved at the object/harness
level (the DF-0594/0616/0281 latent-bug pattern), with the live trigger
conditions documented below.
Mechanism (trigger β primitive β effect)
- Trigger. Any local context that causes
mps(4)to enumerate RAID config pages on a WarpDrive-class HBA setssc->mps_flags & MPS_FLAGS_WD_AVAILABLE(mpsvar.h:292) and reachesmps_wd_config_pages()(mps_sas.c:3320). This happens during normalmps_attach_sas()and on every SAS topology-change / IR-config-change event. An attacker who supplies a crafted reply (a malicious PCIe card, a compromised HBA firmware, or a passed-through SAS controller in a VM) controls the page contents. - Caller allocation at
mps_config.c:378-380:c raid_vol_pg0 = kmalloc(sizeof(Mpi2RaidVolPage0_t) + (sizeof(Mpi2RaidVol0PhysDisk_t) * MPS_MAX_DISKS_IN_VOL), M_MPT2, M_ZERO | M_INTWAIT);=44 + 4*10 = 84bytes (MPS_MAX_DISKS_IN_VOL=10,mpsvar.h:106;Mpi2RaidVolPage0_t=44 permpi2_cnfg.h:1362-1383). - Length computed from firmware at
mps_config.c:1084:c cm->cm_length = le16toh(mpi_reply->Header.PageLength) * 4;β taken straight from the firmware reply, no clamp against the destination type.PageLengthis aU16(range 0..65535), socm_lengthcan be up to 262140. - Sink β the unbounded bcopy at
mps_config.c:1117:c bcopy(page, config_page, cm->cm_length);Every sibling getter bounds this copy:MIN(cm->cm_length, sizeof(Mpi2IOCPage8_t))(line 163),MIN(cm->cm_length, sz)(634),MIN(cm->cm_length, sizeof(Mpi2SasDevicePage0_t))(880),MIN(cm->cm_length, sizeof(Mpi2BiosPage3_t))(998),MIN(cm->cm_length, sizeof(Mpi2RaidVolPage1_t))(1237),MIN(cm->cm_length, sizeof(Mpi2RaidPhysDiskPage0_t))(1382). Only line 1117 omits it. - Primitive. A page claiming
PageLength=22(cm_length=88) already overflows by 4 bytes;PageLength=255overflows by 936 bytes; the theoretical max (PageLength=65535) overflows by ~256 KB. The overflow contents are attacker-controlled (the firmware-supplied page bytes) and land in kernel heap adjacent to the 84-byteraid_vol_pg0allocation. - The
NumPhysDisks > 8sanity check at line 400 happens AFTER the bcopy at line 1117 has already run, so it does not protect the copy. This is the key fact that makes the bug exploitable on the very first call.
Harness proof (harness.c)
The harness reproduces the exact kernel C logic of the buggy bcopy against an
84-byte buffer (sized identically to the caller's allocation at
mps_config.c:378-380) with a PROT_NONE guard page placed immediately
after, so any overflow faults with SIGSEGV. Compiled and run as the
unprivileged maxx user:
$ ./run.sh 22 # minimal overflow
overflow by 4 bytes -> write will land in guard page (PROT_NONE) -> SIGSEGV
>>> SIGSEGV/11 caught: OOB write past 84-byte caller buffer confirmed.
>>> PRIMITIVE CONFIRMED: a malicious HBA returning PageLength=22 (cm_length=88)
overflows the mps_wd_config_pages RAID-volume buffer by 4 bytes.
$ ./run.sh 255 # max overflow with U8 PageLength
overflow by 936 bytes -> ... -> SIGSEGV
>>> PRIMITIVE CONFIRMED: ... overflows ... by 936 bytes.
$ ./run.sh 21 # control: exact-fit, no overflow
cm_length=84 <= buf=84: no overflow this run.
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. Documenting the realistic ceiling: on a host where a malicious or
passed-through LSI WarpDrive HBA is present, the primitive is a
fully-attacker-controlled heap overflow of up to ~64 KB past an 84-byte
kmalloc in M_MPT2, sufficient for kernel-heap grooming (corrupt an
adjacent victim object β function-pointer ops vector, ucred *, refcount β
then convert to code-exec / privilege escalation). 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 kernel bcopy
logic with guard-page fault detection. All struct layouts are reproduced
verbatim from the kernel headers (with citations in comments); the
CALLER_BUF_SZ is hard-coded to the kernel's actual value 84 with a
_Static_assert so the overflow math is unambiguous.
- build.sh, run.sh β exact reproducible commands.
- fix.diff β the verified fix (see below).
Recommended fix
Bound the final bcopy by the caller's buffer capacity, not by
sizeof(Mpi2RaidVolPage0_t) (the finding's original proposal). The naive
fix would truncate the legitimate variable-length PhysDisk tail (the loop at
mps_config.c:461 reads up to 8 PhysDisk entries past the 44-byte header).
The correct capacity is exactly what the sole in-tree caller allocates:
bcopy(page, config_page,
MIN(cm->cm_length, sizeof(Mpi2RaidVolPage0_t) +
sizeof(Mpi2RaidVol0PhysDisk_t) * MPS_MAX_DISKS_IN_VOL));
This supersedes the finding markdown's ## Recommended fix (which would
drop the PhysDisk tail). 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 1114); (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 β MIN() against the caller's actual 84-byte capacity
preserves the legitimate PhysDisk tail while clamping overflow. 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_config_get_raid_volume_pg0 bcopy missing MIN() guard -> 4B OOB past 84B caller buffer. mps in GENERIC, no SAS HBA.
No comments yet.