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

_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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4086 Β· 13 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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:1734 sc->max_devices = sc->facts->MaxTargets + sc->max_volumes; (MaxTargets: U16 IOC facts mpi2_ioc.h:275; MaxVolumes: U8 mpi2_ioc.h:282).
  • :1746-1747 num_rsvd_entries = 1 when pg8 RESERVED_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 run map_idx <= end_idx and touch mapping_table[max_devices], one entry (32 bytes) past the kmalloc(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

  1. _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).
  2. _mapping_get_ir_mt_idx_from_handle :407-411 β€” same, on dev_handle.
  3. _mapping_get_free_ir_mt_idx :456-468 β€” first pass returns the first index whose device_info lacks MPS_MAP_IN_USE; at map_idx == max_devices that is adjacent-heap memory β†’ returns an OOB index. Second pass can equally return it via missing_count (OOB read).
  4. 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 writes physical_id (WWID), channel, id, dev_handle, device_info, init_complete β€” 32-byte OOB write with firmware-controlled fields. _mapping_update_ir_missing_cnt :612-627 then also marks dpm_flush_entry[dpm_idx] (bounded) β€” fine.
  5. mps_mapping_check_devices :1645-1664 β€” for (i = start_idx; i < end_idx + 1; ...) writes init_complete/missing_count at the OOB index; currently dead code (declared mpsvar.h:745, zero callers β€” the same deadness that neutralizes the DF-3915 twin in mps), noted for completeness.
  6. _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 (...) reads device_info at 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.c on the guest (unprivileged user, cc 8.3): verbatim range computation + <= walks + the :2222-2232 event write; guard entry [max_devices] (canary 0x4141...) is overwritten with the fw WWID 0x57119418BADB00B5 β†’ 32-byte OOB write proven; output identical across two runs (run.log, run.2.log).
  • fix.diff (clamp end_idx <= max_devices - 1) applied to the guest /usr/src together with DF-4085's bound and the rest of this batch's mps fixes; module rebuilt with -Werror, mps.ko linked (BUILD_EXIT=0, fix_build.log); /usr/src restored 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_testable
baseline no→ patch + rebuild →patched clean

Applied 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
↓ fix.diffper-fix-DF-4086

Confirmed kernel references

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.