_mapping_get_ir_maprange off-by-one: end_idx == max_devices -> OOB reads and conditional 32-byte OOB write of mapping_table[] on IR events
Summary
end_idx = start_idx + max_volumes - 1 unclamped. With IOC facts MaxTargets==0 (max_devices == max_volumes) plus pg8 RESERVED_TARGETID_0 and low volume mapping - all firmware-controlled - end_idx == max_devices, one past the last valid index. Every IR walk uses inclusive <=: reads mapping_table[max_devices] (OOB); _mapping_get_free_ir_mt_idx can RETURN the OOB index; mps_mapping_ir_config_change_event then writes a full 32-byte entry there (physical_id = firmware volume WWID, dev_handle, channel, device_info). Harness-proven on guest (OOB read + free-idx 255 + guard-entry overwrite with fw WWID). Not in the mpr twin set (the same defect exists unfixed in mpr_mapping.c:241-256).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4086 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.0 KB | β raw | |
| VERDICT.md | β | 4.2 KB | β raw | |
| harness_df4086.c | β | 4.9 KB | view raw | |
| build.sh | β | 102 B | view raw | |
| run.sh | β | 97 B | view raw | |
| build.log | β | 75 B | view raw | |
| run.log | β | 612 B | view raw | |
| run.2.log | β | 612 B | view raw | |
| env.txt | β | 1.4 KB | view raw | |
| fix.diff | β | 556 B | view raw | |
| fix_build.log | β | 12.0 KB | view raw | |
| manifest.json | β | 875 B | view raw | |
| verdict.json | β | 3.8 KB | view raw |
DF-4086 β _mapping_get_ir_maprange off-by-one: end_idx == max_devices β OOB read + conditional 32-byte OOB write of mapping_table[]
File: sys/dev/raid/mps/mps_mapping.c (range calc :221-235; OOB sinks
:366, :409, :457/:462, write :2222-2232, secondary :1647, :2126-2129)
Class: CWE-191 integer underflow/off-by-one β CWE-125 read + CWE-787 write
Severity: High β pass 2 (GLM 5.3), bucket memcorrupt
Fresh pass-2 finding (not DF-1374: no DPM DeviceIndex involved; not
DF-1375: no slot arithmetic; the mpr sibling checklist DF-3914..3919 does not
cover it either β verified against the findings DB).
The bug (source-confirmed)
/* mps_mapping.c:221-235 */
if (volume_mapping_flags == MPI2_IOCPAGE8_IRFLAGS_LOW_VOLUME_MAPPING) {
*start_idx = 0;
if (ioc_pg8_flags & MPI2_IOCPAGE8_FLAGS_RESERVED_TARGETID_0)
*start_idx = 1;
} else
*start_idx = sc->max_devices - sc->max_volumes;
*end_idx = *start_idx + sc->max_volumes - 1; /* <-- unclamped */
max_devices = facts->MaxTargets + facts->MaxVolumes (:1734). If the IOC
facts report MaxTargets == 0 while IOC pg8 sets
RESERVED_TARGETID_0 and low volume mapping (all firmware-controlled),
then start_idx = 1, max_devices = max_volumes, and
end_idx = 1 + max_volumes - 1 == max_devices /* one past last index */
Every IR walk in the file uses inclusive <= against end_idx:
| sink | effect at map_idx == max_devices |
|---|---|
_mapping_get_ir_mt_idx_from_wwid :366 |
OOB read of physical_id |
_mapping_get_ir_mt_idx_from_handle :409 |
OOB read of dev_handle |
_mapping_get_free_ir_mt_idx :457/:462 |
OOB reads; returns max_devices when the OOB memory lacks MPS_MAP_IN_USE |
mps_mapping_check_devices :1647 |
OOB r/w of init_complete/missing_count (function currently dead β no callers β noted) |
_mapping_check_update_ir_mt_idx :2126-2129 |
OOB read of device_info |
mps_mapping_ir_config_change_event :2222-2232 |
full 32-byte struct OOB WRITE: physical_id = firmware volume WWID, dev_handle, channel=MPS_RAID_CHANNEL, id, device_info = MPS_DEV_RESERVED|MPS_MAP_IN_USE, init_complete=0 |
The write is reached whenever _mapping_get_free_ir_mt_idx returns the OOB
index (table entries all in use, adjacent heap garbage without the IN_USE
bit) β then a RAID-volume IR config-change event (firmware-sourced) writes
one dev_mapping_table entry immediately past the
kmalloc(max_devices*32) allocation. Heap-adjacent corruption with mostly
controlled contents; on the intentionally permissive audit guest this is
classic slab-neighbor overwrite material.
The high-volume-mapping branch (start = max_devices - max_volumes,
end = max_devices - 1) is always in bounds; the bug needs the low-mapping +
reserved + MaxTargets == 0 combination, all in one firmware image.
Reproduction (object-level, hardware-gated)
No mps HBA in the audit guest (env.txt). harness_df4086.c replicates the
range function, the <= walks and the event write verbatim:
cc -O2 -Wall -o harness_df4086 harness_df4086.c && ./harness_df4086
Decisive output (run.log, stable in run.2.log):
facts: MaxTargets=0 MaxVolumes=255 -> max_devices=255
IR maprange: start=1 end=255 (end == max_devices => OFF-BY-ONE: `<=` walks hit index 255)
wwid walk :366 read physical_id at mapping_table[255] -> OOB READ
free-idx walk :457 returned 255 (== max_devices, OUT OF BOUNDS)
event write: mapping_table[255] = {wwid 0x57119418badb00b5, handle 0x0b1d, info 0x30000000}
guard[0].physical_id == fw WWID: 32-byte heap OOB WRITE at index max_devices
RESULT: OFF-BY-ONE OOB READ + CONDITIONAL OOB WRITE PROVEN
Fix
fix.diff β clamp *end_idx to max_devices - 1 in
_mapping_get_ir_maprange() so all <= walks stay in bounds regardless of
firmware facts. Compile-validated with the rest of this batch's mps fixes
(-Werror, mps.ko linked β fix_build.log).
DF-4086 VERDICT
Status: not_reproduced (kernel level) β hardware-gated; off-by-one read + conditional OOB write proven at object level on the audit guest. Confidence: certain (arithmetic + sinks traced line-by-line).
Why end_idx can equal max_devices
mps_mapping.c:1734sc->max_devices = sc->facts->MaxTargets + sc->max_volumes;(MaxTargets: U16 IOC factsmpi2_ioc.h:275; MaxVolumes: U8mpi2_ioc.h:282).:1746-1747num_rsvd_entries = 1when pg8RESERVED_TARGETID_0.:221-235_mapping_get_ir_maprange(): low-volume-mapping βstart_idx = 1(reserved),end_idx = start_idx + max_volumes - 1.- With
MaxTargets == 0:max_devices == max_volumesβend_idx == max_devicesβ the loops below runmap_idx <= end_idxand touchmapping_table[max_devices], one entry (32 bytes) past thekmalloc(max_devices * 32)(:1393-1394) allocation.
All four inputs (MaxTargets, IR capability, volume-mapping mode, reserved-id flag) come from the IOC firmware/pci-attached device; nothing clamps the range.
Sinks verified in source
_mapping_get_ir_mt_idx_from_wwid:364-368βfor (map_idx = start_idx; map_idx <= end_idx; map_idx++, mt_entry++) if (mt_entry->physical_id == wwid)β OOB read; if adjacent-heap garbage equals the WWID (fw can retry WWIDs), the returned index flows to:2174-2179(mt_entry->device_info |= MPS_MAP_IN_USEβ OOB read-modify-write)._mapping_get_ir_mt_idx_from_handle:407-411β same, ondev_handle._mapping_get_free_ir_mt_idx:456-468β first pass returns the first index whosedevice_infolacksMPS_MAP_IN_USE; atmap_idx == max_devicesthat is adjacent-heap memory β returns an OOB index. Second pass can equally return it viamissing_count(OOB read).mps_mapping_ir_config_change_event:2206-2232β on volume ADDED / VOLUME_CREATED with no matching WWID:map_idx = _mapping_get_free_ir_mt_idx(sc); ... mt_entry = &sc->mapping_table[map_idx];then writesphysical_id(WWID),channel,id,dev_handle,device_info,init_completeβ 32-byte OOB write with firmware-controlled fields._mapping_update_ir_missing_cnt:612-627then also marksdpm_flush_entry[dpm_idx](bounded) β fine.mps_mapping_check_devices:1645-1664βfor (i = start_idx; i < end_idx + 1; ...)writesinit_complete/missing_countat the OOB index; currently dead code (declaredmpsvar.h:745, zero callers β the same deadness that neutralizes the DF-3915 twin in mps), noted for completeness._mapping_check_update_ir_mt_idx:2124-2129βdo { mt_idx++; if (mt_idx > end_idx) return 1; mt_entry = &sc->mapping_table[mt_idx]; } while (...)readsdevice_infoat the OOB index.
Verification performed
- Guest
pciconf -l(env.txt): no SAS HBA β no in-kernel run possible (same hardware gate as DF-1374/DF-1375/DF-3914..3919). harness_df4086.con the guest (unprivileged user, cc 8.3): verbatim range computation +<=walks + the:2222-2232event write; guard entry[max_devices](canary0x4141...) is overwritten with the fw WWID0x57119418BADB00B5β 32-byte OOB write proven; output identical across two runs (run.log,run.2.log).fix.diff(clampend_idx <= max_devices - 1) applied to the guest/usr/srctogether with DF-4085's bound and the rest of this batch's mps fixes; module rebuilt with-Werror,mps.kolinked (BUILD_EXIT=0,fix_build.log);/usr/srcrestored pristine afterwards; guest reset with clean-source snapshot.
Exploitability ceiling
Same threat model as the other mps mapping findings (malicious/buggy IOC
firmware or emulated HBA via PCI passthrough): IR config-change event β
32-byte struct write just past mapping_table with fw-chosen WWID/handle β
adjacent slab-object corruption β potential kernel code execution. Gated
here only by absent hardware.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testableApplied to the guest /usr/src together with this batch's other mps fixes (DF-4085..DF-4090); make in sys/dev/raid/mps compiled all sources with -Werror and linked mps.ko (BUILD_EXIT=0). Runtime fix validation not testable: the path requires an mps(4) HBA delivering firmware data, absent on this guest.
findings/poc/DF-4086/fix.diff, findings/poc/DF-4086/fix_build.log
Confirmed kernel references
- sys/dev/raid/mps/mps_mapping.c:221
- sys/dev/raid/mps/mps_mapping.c:233
- sys/dev/raid/mps/mps_mapping.c:234
- sys/dev/raid/mps/mps_mapping.c:366
- sys/dev/raid/mps/mps_mapping.c:409
- sys/dev/raid/mps/mps_mapping.c:457
- sys/dev/raid/mps/mps_mapping.c:462
- sys/dev/raid/mps/mps_mapping.c:1647
- sys/dev/raid/mps/mps_mapping.c:2126
- sys/dev/raid/mps/mps_mapping.c:2222
- sys/dev/raid/mps/mps_mapping.c:2232
- sys/dev/raid/mps/mps_mapping.c:1734
Detail
Exploit chain
Hardware-gated: malicious/buggy IOC facts (MaxTargets=0 + IR + reserved-targetid-0 + low volume mapping) -> IR config change event on volume add -> _mapping_get_free_ir_mt_idx returns max_devices (OOB) when in-range slots are busy -> 32-byte struct write one entry past mapping_table with fw WWID/handle -> adjacent slab-object corruption -> potential kernel code execution. Stopped at primitive proof: no HBA in guest.
Evidence (decisive lines)
run.log/run.2.log (guest harness: end==max_devices off-by-one, OOB read at :366, free-idx walk returns 255, guard entry overwritten with fw WWID), harness_df4086.c (verbatim range fn + walks + :2222-2232 write), env.txt, VERDICT.md, fix.diff + fix_build.log (-Werror module build)
PoC changes
No seed PoC (pass-2 finding). Harness replicates the range function, both <= walks and the event write verbatim; built/run on guest as unprivileged user.
Verified recommended fix
Clamp end_idx to sc->max_devices - 1 in _mapping_get_ir_maprange so the inclusive IR walks cannot index past mapping_table.
Verdict
Source-certain off-by-one -> heap OOB read + conditional 32-byte heap OOB write, pass 2, fresh finding (not DF-1374/1375; not in the mpr twin set). _mapping_get_ir_maprange (mps_mapping.c:221-235) computes end_idx = start_idx + max_volumes - 1 unclamped; with firmware facts MaxTargets==0 plus pg8 RESERVED_TARGETID_0 and low volume mapping, end_idx == max_devices while max_devices = MaxTargets+MaxVolumes (:1734). Every IR walk uses inclusive <= (:366,:409,:457,:1647,:2126) and reads mapping_table[max_devices], one 32-byte entry past the kmalloc; _mapping_get_free_ir_mt_idx :457 can RETURN the OOB index, and mps_mapping_ir_config_change_event :2222-2232 then writes a full firmware-controlled dev_mapping_table entry there (physical_id = volume WWID, dev_handle, device_info). Harness proves range end==max_devices, the OOB read, the returned OOB index and the 32-byte OOB write with the fw WWID (run.log). Not reproduced at kernel level: no SAS HBA on the guest (env.txt). mps_mapping_check_devices :1647 sink is dead code (no callers) - noted.
No comments yet.