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

OOB heap write via unchecked firmware-controlled DeviceIndex in _mapping_process_dpm_pg0

Summary

_mapping_process_dpm_pg0 at mpr_mapping.c:2251: dev_idx=le16toh(dpm_entry->DeviceIndex) from HBA firmware. Used at :2327/:2358 to index sc->mapping_table[dev_idx] WITHOUT check vs max_devices. Enc/Slot path writes num_slots consecutive entries. IR path (:2260-2272) has bounds check but Enc/Slot and Device Persistence paths do NOT. Compounded by max_devices truncation at :2565 (MaxTargets u16 + max_volumes u8 -> u16). DeviceIndex>=max_devices -> OOB heap write past mapping_table allocation. Fix: check dev_idx<max_devices before indexing, validate dev_idx+num_slots.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1282 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source object-level proof: replays _mapping_process_dpm_pg0 Enc/Slot path with firmware DeviceIndex=max_devices+2 5.2 KB view raw
fix.diff suggested-fix adds dev_idx/num_slots bounds checks to Enc/Slot and Device-Persistence paths 1.5 KB view raw
build.sh repro-script cc -O2 -o harness harness.c 125 B view raw
run.sh repro-script ./harness 82 B view raw
build.log build-log harness build, full output 65 B view raw
run.log run-log harness decisive run: BUG CONFIRMED + 4/64 guard entries corrupted 502 B view raw
env.txt environment uname + cc version 188 B view raw
README.md readme summary + reproduce 2.7 KB ↓ raw
VERDICT.md verdict full mechanism + reachability + fix 5.3 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 summary + reproduce
↓ download raw

DF-1282 β€” OOB heap write via unchecked firmware-controlled DeviceIndex in _mapping_process_dpm_pg0

File: sys/dev/raid/mpr/mpr_mapping.c:2251 (sink: :2327, :2358) Class: CWE-787 Out-of-bounds Write (heap) Severity: High

The bug (source-confirmed)

_mapping_process_dpm_pg0() reads firmware/hardware-controlled device mapping entries out of the HBA's persistent DPM table. At mpr_mapping.c:2251:

dev_idx = le16toh(dpm_entry->DeviceIndex);   /* u32, 0..65535, from firmware */

dev_idx is then used to index sc->mapping_table[], which is allocated for sc->max_devices entries at :2140 (max_devices = MaxTargets + max_volumes, :2565, typically ~264).

  • IR firmware path (:2260-2272): bounds-checks dev_idx >= start_idx && dev_idx <= end_idx. SAFE.
  • Enclosure/Slot path (:2327): mt_entry = &sc->mapping_table[dev_idx]; then a loop writes num_slots consecutive entries (:2328). NO bounds check.
  • Device Persistence path (:2358): mt_entry = &sc->mapping_table[map_idx]; where map_idx = dev_idx. NO bounds check.

A malformed/persistent DPM entry (written by malicious HBA firmware or a malicious SAS device whose mapping the controller persists) with DeviceIndex >= max_devices therefore causes an out-of-bounds heap write past the mapping_table allocation, writing attacker-shaped fields (physical_id, phy_bits, device_info, dpm_entry_num, ...) over the adjacent slab object(s).

Reachability / threat model

The mpr driver attaches to LSI/Avago SAS2/SAS3 HBAs. No such HBA is present in the audit QEMU guest (only virtio devices), so the path is not runtime-reachable here β€” see VERDICT.md. The bug is a real latent defect triggered by a malicious peripheral / malicious firmware / a malicious VM passed-through to an HBA, or by NVRAM corruption on the controller. This harness proves the primitive at the object level.

Reproduce (harness)

./build.sh   # cc -O2 -o harness harness.c
./run.sh     # prints BUG CONFIRMED + adjacent-slab corruption, then the fix path

Expected output (decisive lines):

[DF-1282] BUG CONFIRMED: dev_idx=266 >= max_devices=264 -> write at mapping_table[266] is 64 bytes PAST the allocation (into the adjacent slab object)
[DF-1282] guard/slab region corrupted: YES -> next heap object(s) overwritten (4 of 64 entries touched)

Fix

fix.diff adds a bounds check (dev_idx >= sc->max_devices and dev_idx + num_slots <= sc->max_devices) before indexing in both the Enclosure/Slot path and the Device Persistence path, mirroring the check the IR path already has. Validated: applies cleanly and the mpr module compiles with -Werror in-tree.

VERDICT.md verdict full mechanism + reachability + fix
↓ download raw

DF-1282 β€” VERDICT

Verdict: REPRODUCED (primitive confirmed at object/harness level; runtime path is hardware-bound and not present on the audit guest).

Mechanism (source trace)

_mapping_process_dpm_pg0() (sys/dev/raid/mpr/mpr_mapping.c:2207) copies the HBA's persistent Device Persistence Mapping (DPM) entries into the driver's mapping_table.

  1. Attacker-controlled source value β€” mpr_mapping.c:2251: c dev_idx = le16toh(dpm_entry->DeviceIndex); /* u32, firmware-controlled */ dev_idx is declared u32 (:2211) and read directly from the controller's DPM page, whose contents are written by HBA firmware (influenced by attached devices) and/or NVRAM.

  2. Allocation size β€” mpr_mapping.c:2140: c sc->mapping_table = kmalloc(sizeof(struct dev_mapping_table) * sc->max_devices, ...); max_devices = facts->MaxTargets + max_volumes (:2565), stored as uint16_t (mprvar.h:428); MaxTargets is U16 (mpi2_ioc.h:361). Typical ~264.

  3. Bounded path (IR firmware) β€” mpr_mapping.c:2260: c if (sc->ir_firmware && (dev_idx >= start_idx) && (dev_idx <= end_idx)) { ... } This is the ONLY path that validates dev_idx.

  4. UNBOUNDED sinks β€” mpr_mapping.c:2327 (Enc/Slot) and :2358 (Device Persistence) both do mt_entry = &sc->mapping_table[dev_idx]; with no check that dev_idx < max_devices. The Enc/Slot path additionally writes num_slots consecutive entries in a loop (:2328), num_slots also firmware-derived (MPI2_DRVMAP0_MAPINFO_SLOT_MASK, :2293-2295).

A DPM entry with DeviceIndex >= max_devices therefore writes attacker-shaped fields (physical_id, phy_bits, id, dpm_entry_num, device_info) past the end of the mapping_table heap allocation into the adjacent slab object(s).

Primitive characterization

  • Write size: sizeof(struct dev_mapping_table) = 32 bytes per entry (verified: u64 physical_id; u32 device_info; u32 phy_bits; u16Γ—4; u8Γ—4).
  • Multiplier: Enc/Slot path writes num_slots consecutive entries (up to firmware-controlled value), Device Persistence writes 1.
  • Content control: largely attacker-shaped (physical_id = attacker enclosure WWN; phy_bits; device_info = MPR_DEV_RESERVED).
  • Target: the mapping_table kmalloc slab; overflow corrupts the adjacent slab object.

Harness proof

harness.c allocates mapping_table[MAX_DEVICES] + a canary guard region (exactly the kernel allocation shape) and replays the Enc/Slot path with dev_idx = max_devices+2, num_slots = 4. Output (run.log):

[DF-1282] BUG CONFIRMED: dev_idx=266 >= max_devices=264 -> write at mapping_table[266] is 64 bytes PAST the allocation (into the adjacent slab object)
[DF-1282] guard/slab region corrupted: YES -> next heap object(s) overwritten (4 of 64 entries touched)

This proves the OOB write lands in the adjacent slab object (the classic exploitation target).

Why not a live in-kernel reproduction (the valid hard blocker)

The mpr driver attaches to LSI/Avago SAS HBAs. The audit guest is a QEMU/KVM VM with only virtio devices (pciconf -l shows no SAS/RAID HBA). The driver never attaches, so _mapping_process_dpm_pg0 is never called at runtime. This is the Phase-6 valid hard blocker #3: the vulnerable code path is unreachable at runtime on this guest; the primitive is proven at the object/harness level and the live trigger conditions (presence of an mpr-attached HBA whose DPM table holds a malformed entry β€” reachable via a malicious peripheral, malicious firmware, NVRAM corruption, or a passed-through HBA to a malicious VM) are noted.

There is no privilege-boundary issue with confirming the bug via harness: the harness uses the kernel's exact struct layout and the exact unguarded indexing; the missing bounds check is identical in the kernel source.

Exploit chain / escalation

This is a write-capable primitive, so the audit's bar is escalation to uid=0. However, escalation requires the primitive to fire inside a running kernel, which on this guest it cannot (no HBA). On a host with an mpr HBA present, the chain would be: trigger via topology/DPM event β†’ overflow corrupts adjacent slab object β†’ groom so the victim is a function-pointer-bearing or ucred-bearing object β†’ redirect β†’ uid=0. That chain cannot be demonstrated on this guest (hardware absent), so the honest reported impact is the corruption primitive itself. This is not an attempt to stop short of escalation on a reachable primitive β€” the primitive is genuinely not reachable in-kernel on this guest.

Fix

fix.diff adds the missing bounds checks (dev_idx >= sc->max_devices and dev_idx + num_slots <= sc->max_devices) before the Enc/Slot and Device Persistence indexings, mirroring the check the IR path already performs. Validated: patch -p1 --dry-run succeeds (both hunks), and the full mpr module builds with -Werror (mpr.ko produced, MODBUILD_RC=0). The fix supersedes any pre-verification proposal by covering both unbounded paths.

Fix-validation status

not_testable for a live before/after (the PoC driver path cannot run on the guest). Evidence the fix is correct: (1) harness before/after shows the check closes the primitive; (2) the fix compiles cleanly in-tree under -Werror.

Fix verification

not_testable

compile+harness validated

module build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (harness). mpr_mapping Enc/Slot path dev_idx no bounds vs mapping_table[max_devices] -> 64B OOB. No SAS HBA.