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

mpr _mapping_get_ir_maprange off-by-one: end_idx == max_devices -> OOB reads and two 32-byte OOB write sinks on IR paths (twin of DF-4086)

Summary

end_idx = start_idx + max_volumes - 1 unclamped; with firmware facts MaxTargets==0 + RESERVED_TARGETID_0 + low volume mapping, end_idx == max_devices while the inclusive IR walks read mapping_table[max_devices]. _mapping_get_free_ir_mt_idx can return the OOB index and mpr_mapping_ir_config_change_event writes a full firmware-controlled entry there; independently the _mapping_process_dpm_pg0 volume branch accepts DeviceIndex == end_idx and writes the OOB entry at attach time (a sink the mps harness didn't cover). Harness-proven on guest (facts MaxTargets=0/MaxVolumes=255: OOB read + free-idx 255 + event write fw WWID + DPM volume-branch second write). Clamp fix compile-validated. Not covered by DF-3919.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4093 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 5.5 KB ↓ raw
harness_df4093.c β€” 6.4 KB view raw
build.sh β€” 102 B view raw
run.sh β€” 97 B view raw
build.log β€” 16 B view raw
run.log β€” 787 B view raw
run.2.log β€” 787 B view raw
env.txt β€” 1.8 KB view raw
fix.diff β€” 666 B view raw
fix_build.log β€” 11.4 KB view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 4.7 KB view raw

DF-4093 β€” mpr _mapping_get_ir_maprange off-by-one: end_idx == max_devices β†’ OOB reads + two 32-byte OOB write sinks (mpr twin of DF-4086)

File: sys/dev/raid/mpr/mpr_mapping.c (:241-255 range; <= walks :387, :430, :484, :2261, :2708; event write :3082-3088; kmalloc :2140-2141; facts :2564-2565)

Build

cc -O2 -Wall -o harness_df4093 harness_df4093.c

(DragonFly base cc 8.3; any unprivileged user; runs entirely in userland)

Run

./harness_df4093

Expected output (decisive lines)

  sizeof(dev_mapping_table) : 32 (expect 32 on amd64)
  facts: MaxTargets=0 MaxVolumes=255 -> max_devices=255 (:2565)
  valid mapping_table indices: 0..254 (kmalloc 8160 bytes :2140)
  IR maprange: start=1 end=255  (end == max_devices => OFF-BY-ONE: `<=` walks hit index 255)
  wwid walk :387 read physical_id at mapping_table[255] -> OOB READ
  free-idx walk :484 returned 255 (== max_devices, OUT OF BOUNDS)
  event write: mapping_table[255] = {wwid 0x57119418badb00b5, handle 0x0b1d, info 0x30000000}
  guard[255].physical_id == fw WWID: 32-byte heap OOB WRITE at index max_devices
  DPM volume branch :2261 accepted DeviceIndex 255 == end_idx -> second 32-byte OOB WRITE
RESULT: OFF-BY-ONE OOB READS + TWO 32-BYTE OOB WRITES PROVEN

Exit code 0 = primitive proven. run.log / run.2.log hold two identical guest runs.

What this proves

Object-level proof of the off-by-one and both write sinks: with firmware facts MaxTargets==0 + pg8 RESERVED_TARGETID_0 + low volume mapping, end_idx = start_idx + max_volumes - 1 == max_devices; every inclusive IR walk then touches mapping_table[max_devices] (OOB read), _mapping_get_free_ir_mt_idx can return the OOB index, and (a) mpr_mapping_ir_config_change_event writes a full firmware-controlled entry there on a volume-add event, and (b) the DPM-pg0 volume branch writes one there for a DeviceIndex == end_idx persistent entry.

Kernel-level status

Not reproducible on the audit guest: no SAS HBA on the PCI bus (env.txt), so mpr_attach never runs. Same hardware gate as DF-4086 (mps) and DF-3914/DF-3915 (mpr). Threat actor: malicious/buggy IOC firmware or a PCI-passthrough/emulated HBA.

VERDICT.md
↓ download raw

DF-4093 VERDICT

Status: not_reproduced (kernel level) β€” hardware-gated; primitive proven at object level on the audit guest (userland harness, verbatim range function, walks and both write sinks). Confidence: certain.

mpr twin of DF-4086 (mps). Filed separately because the mps pass-2 audit deliberately did not double-file sibling findings.

Source chain (line-by-line)

  1. mpr_mapping.c:2564-2565 max_volumes = facts->MaxVolumes (u8), max_devices = facts->MaxTargets + facts->MaxVolumes.
  2. mpr_mapping.c:2140-2141 mapping_table = kmalloc(32 * max_devices) (sizeof verified 32 on amd64, mprvar.h:113-125).
  3. mpr_mapping.c:241-255 _mapping_get_ir_maprange: with IRVolumeMappingFlags == LOW_VOLUME_MAPPING (:246-248) start_idx = 0, bumped to 1 when MPI2_IOCPAGE8_FLAGS_RESERVED_TARGETID_0 (:250-251); *end_idx = *start_idx + sc->max_volumes - 1 (:254) β€” unclamped. With firmware facts MaxTargets == 0 (β‡’ max_devices == max_volumes), RESERVED_TARGETID_0 and low volume mapping β€” all three firmware- controlled β€” start_idx = 1 and end_idx = max_volumes = max_devices: one entry PAST the last valid index.
  4. Every IR consumer walks map_idx <= end_idx and therefore touches mapping_table[max_devices]: - _mapping_get_ir_mt_idx_from_wwid :387 OOB read (physical_id) - _mapping_get_ir_mt_idx_from_handle :430 OOB read (dev_handle) - _mapping_get_free_ir_mt_idx :484 OOB read (device_info/ missing_count) and :486 returns the OOB index when the adjacent memory lacks MPR_MAP_IN_USE - _mapping_process_dpm_pg0 volume branch :2260-2271 accepts dev_idx == end_idx (DPM pg0 DeviceIndex, u16, fw-controlled) and writes a full entry at mapping_table[dev_idx] β€” unconditional 32-byte OOB write with fw PhysicalIdentifier - mpr_mapping_get_raid_tid :2708 OOB read (CAM-facing volume lookup)
  5. Write sink A (event): mpr_mapping_ir_config_change_event :3045-3088 β€” on MPI2_EVENT_IR_CHANGE_RC_ADDED/VOLUME_CREATED for a volume whose WWID is not yet mapped, map_idx = _mapping_get_free_ir_mt_idx(sc) (:3071) returns max_devices when the in-range slots are busy; :3082-3088 writes physical_id = wwid_table[i] (fw volume WWID), id, dev_handle, device_info β€” full 32-byte fw-controlled entry one past the allocation. Write sink B (boot): the :2260-2271 DPM volume-branch write above β€” same index, reached at attach time from persistent mapping entries.

Note: _mapping_get_high_missing_mt_idx (:341-370) computes &mapping_table[end_idx_ir + 1] == &mapping_table[max_devices + 1] in the same configuration (:358) but its < loop then never dereferences it β€” benign pointer arithmetic, noted for completeness.

Distinct from the existing mpr findings

None of DF-3914..DF-3919 touches _mapping_get_ir_maprange or the IR walks; DF-3919 covers event element-count walks (frame-length mismatch), not the IR index range.

Verification performed

  • Guest pciconf -l (env.txt): no SAS HBA β‡’ mpr_attach never runs; kernel-level reproduction impossible (same gate as DF-3914/DF-3915/ DF-4086).
  • harness_df4093.c built and run twice as the unprivileged user on the guest (cc 8.3, zero warnings): verbatim range function (:241-255 low+reserved configuration), verbatim <= walks, verbatim event write (:3082-3088) and verbatim DPM volume branch (:2260-2271). Facts MaxTargets=0, MaxVolumes=255 β‡’ max_devices=255, start=1, end=255 == max_devices. Decisive results (run.log, stable):
  • wwid walk reads physical_id at mapping_table[255] β€” OOB read;
  • free-idx walk returns 255 (== max_devices) β€” the OOB index escapes;
  • event write lands the fw WWID 0x57119418BADB00B5 at guard[255] β€” 32-byte heap OOB write, verified programmatically;
  • DPM volume branch accepts DeviceIndex 255 == end_idx β€” a second, unconditional 32-byte OOB write.
  • In-kernel equivalent: one 32-byte struct write just past an 8,160-byte kmalloc, contents firmware-chosen (volume WWID at offset 0, dev_handle, device_info). On the no-hardening audit guest this is classic adjacent- slab-object corruption once an HBA is present.

Fix validation

  • fix.diff (this pack): clamp *end_idx to sc->max_devices - 1 at the end of _mapping_get_ir_maprange β€” same shape as the validated DF-4086 fix; fixes every <= consumer at once (:387, :430, :484, :2261, :2708).
  • Applied together with DF-4092's bound-skip to the guest /usr/src; make in sys/dev/raid/mpr compiled all sources with -Werror and linked mpr.ko (BUILD_EXIT=0) β€” fix_build.log.
  • Runtime fix validation not testable (no mpr HBA on the guest).
  • /usr/src restored pristine after the build (md5 match, reverse-patch verified, no leftover artifacts).

Bottom line

Source-certain CWE-125/ CWE-787 off-by-one β†’ OOB reads + two distinct 32-byte OOB write sinks, primitive proven at object level on the audit guest; kernel-level reproduction hardware-gated. Fix compiles clean with -Werror; recommended for upstream together with the mps twin DF-4086.

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 DF-4092's bound-skip; make in sys/dev/raid/mpr compiled all sources with -Werror and linked mpr.ko (BUILD_EXIT=0). Runtime fix validation not testable: the path requires an mpr(4) HBA delivering firmware pages, absent on this guest.

findings/poc/DF-4093/fix.diff, findings/poc/DF-4093/fix_build.log
↓ fix.diffper-fix-DF-4093

Confirmed kernel references

Detail

Exploit chain

Hardware-gated: malicious/buggy IOC facts (MaxTargets=0 + IR + reserved-targetid-0 + low volume mapping) -> volume-add IR config change event -> _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; alternatively the same OOB entry is written at boot via a DPM pg0 entry with DeviceIndex == end_idx. 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 :387, free-idx walk returns 255, event write lands fw WWID at mapping_table[255], DPM volume branch accepts DeviceIndex 255), harness_df4093.c (verbatim range fn + both <= walks + both write sinks), env.txt, VERDICT.md, fix.diff + fix_build.log (-Werror module build)

PoC changes

No seed PoC (fresh twin filing). Harness replicates the mpr range function, the <= walks, the event write :3082-3088 (mpr writes no channel field - struct differs from mps) AND the DPM volume-branch write :2260-2271 (second sink, absent from the mps harness); built/run twice on the guest as an 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 reads + two 32-byte heap OOB write sinks (CWE-125/CWE-787), mpr twin of DF-4086, not covered by any existing mpr finding (DF-3919 is event element-count walks, not the IR index range). _mapping_get_ir_maprange (mpr_mapping.c:241-255) computes end_idx = start_idx + max_volumes - 1 (:254) unclamped; with firmware facts MaxTargets==0 (max_devices == max_volumes, :2565) plus pg8 RESERVED_TARGETID_0 (:250) and low volume mapping (:248) - all firmware-controlled - end_idx == max_devices. Every IR walk uses inclusive <= and reads mapping_table[max_devices], one 32-byte entry past the kmalloc(max_devices*32) (:2140): :387 (wwid), :430 (handle), :484 (free-idx), :2708 (get_raid_tid, CAM-facing). _mapping_get_free_ir_mt_idx :486 can RETURN the OOB index, and mpr_mapping_ir_config_change_event :3082-3088 then writes a full firmware-controlled entry there (volume WWID, dev_handle, device_info); independently, the _mapping_process_dpm_pg0 volume branch :2260-2271 accepts a DPM DeviceIndex == end_idx and writes the OOB entry at attach time. Harness proves the range math, the OOB reads, the returned OOB index and both 32-byte OOB writes with the fw WWID (run.log). Not reproduced at kernel level: no SAS HBA on the QEMU guest (env.txt), mpr never attaches - same hardware gate as mps twin DF-4086.