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

Unvalidated IOC-pg8 MaxNumPhysicalMappedIDs used as num_slots: heap OOB write in mpr _mapping_process_dpm_pg0 reservation loop (twin of DF-4085)

Summary

In ENCLOSURE_SLOT_MAPPING mode, a DPM pg0 entry whose DeviceIndex is inside the direct-attached range sets num_slots = max_num_phy_ids, the firmware u16 IOC-page-8 MaxNumPhysicalMappedIDs never validated against the mapping-table size anywhere in the driver; the reservation loop writes num_slots consecutive 32-byte dev_mapping_table entries (verified 32B on amd64) with no check that dev_idx+num_slots <= max_devices against the kmalloc(max_devices*32) table. Boot-time heap overflow worst case 65,281 entries / 2,088,992 bytes past a 16,320-byte allocation, gated per entry only by adjacent heap's dpm_entry_num u16 being 0xFFFF. Harness proven on the guest TWICE, stricter than the mps pack (models the goto-fail conflict gate verbatim; scenarios A/B/C incl. 131,072 bytes through 4,096 groomed 0xFFFF blocks). Bound-skip fix compile-validated (-Werror, mpr.ko linked). Distinct from DF-3914/3915.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4092 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 7.2 KB ↓ raw
harness_df4092.c β€” 8.6 KB view raw
build.sh β€” 102 B view raw
run.sh β€” 97 B view raw
build.log β€” 16 B view raw
run.log β€” 928 B view raw
run.2.log β€” 928 B view raw
env.txt β€” 1.8 KB view raw
fix.diff β€” 757 B view raw
fix_build.log β€” 11.4 KB view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 4.8 KB view raw

DF-4092 β€” Unvalidated IOC-pg8 MaxNumPhysicalMappedIDs as num_slots: heap OOB write in mpr _mapping_process_dpm_pg0 (mpr twin of DF-4085)

File: sys/dev/raid/mpr/mpr_mapping.c (:2216, :2284-2290, :2327-2347, kmalloc :2140-2141, facts :2564-2565)

Build

cc -O2 -Wall -o harness_df4092 harness_df4092.c

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

Run

./harness_df4092

Expected output (decisive lines)

  sizeof(dev_mapping_table) : 32 (expect 32 on amd64)
  max_devices               : 510 (kmalloc 16320 bytes, :2140)
  MaxNumPhysicalMappedIDs   : 0xffff (IOC pg8, unvalidated)
  DPM entry 0               : dev_idx=256 (in bounds!), num_slots=65535
  scenario A (cold garbage)  : wrote 254 entries [256,510), conflict gate: TRIPPED at first OOB block (goto fail :2336)
  scenario B (0xFFFF groom)  : wrote 4350 entries, 4096 of them PAST the table
  scenario C (all 0xFFFF)    : 65281 entries past table = 2088992 bytes linear smash
RESULT: OOB WRITE PRIMITIVE PROVEN (canary smash at stride 32; per-entry dpm_entry_num==BAD_IDX gate modeled verbatim)

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

What this proves

Object-level proof of the boot-time heap overflow primitive in _mapping_process_dpm_pg0: with a fully in-bounds DPM DeviceIndex, num_slots = MaxNumPhysicalMappedIDs (firmware u16, never validated) drives the reservation loop past the kmalloc(max_devices*32) mapping table. The harness models the loop verbatim, including the per-entry dpm_entry_num != MPR_DPM_BAD_IDX conflict gate (goto fail) the mps DF-4085 harness did not model: in-bounds entries always pass (cleared to BAD_IDX by _mapping_clear_map_entry), the first OOB block is always read OOB, and every consecutive OOB block holding 0xFFFF at offset 16 is fully overwritten with firmware-controlled data at 32-byte stride.

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-4085 (mps) and DF-3914/DF-3915 (mpr). Threat actor: malicious/buggy IOC firmware or a PCI-passthrough/emulated HBA.

VERDICT.md
↓ download raw

DF-4092 VERDICT

Status: not_reproduced (kernel level) β€” hardware-gated; primitive proven at object level on the audit guest (userland harness, verbatim loop including the conflict gate). Confidence: certain (source arithmetic + object-level proof; only the in-kernel execution is gated by absent hardware).

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

Source chain (line-by-line)

  1. sys/dev/raid/mpr/mpr_pci.c attach β†’ mpr_startup β†’ mpr_mapping_initialize(sc) (mpr_mapping.c:2545ff).
  2. mpr_mapping.c:2564-2565 max_volumes = facts->MaxVolumes (u8), max_devices = facts->MaxTargets + facts->MaxVolumes (u16 each, sum ≀ 511 by MPI2 field widths). :2569-2570 max_dpm_entries = ioc_pg8.MaxPersistentEntries; is_dpm_enable = !!max_dpm_entries.
  3. mpr_mapping.c:2140-2141 mapping_table = kmalloc(sizeof(struct dev_mapping_table) * max_devices, M_MPR, M_ZERO|M_NOWAIT) β€” 32 bytes/entry, verified: harness prints sizeof == 32 on amd64 (struct mprvar.h:113-125: u64+u32+u32+4u16+4u8 = 8+4+4+8+4 = 28 β†’ padded to 32; same 32-byte stride as the mps twin despite the reserved1/channel layout difference).
  4. mpr_mapping.c:2593-2594 every entry cleared by _mapping_clear_map_entry (:65-75) β†’ dpm_entry_num = MPR_DPM_BAD_IDX (0xFFFF).
  5. mpr_mapping.c:2614-2628 DPM pg0 page read into sc->dpm_pg0 and _mapping_process_dpm_pg0() called (:2627) whenever is_dpm_enable.
  6. mpr_mapping.c:2216 max_num_phy_ids = le16toh(sc->ioc_pg8. MaxNumPhysicalMappedIDs) β€” firmware u16 from IOC page 8; grep confirms no validation against max_devices anywhere in the driver (all other uses are search-skip hints :955/:979/:1045/:1718/:1975).
  7. mpr_mapping.c:2273-2296 ENCLOSURE_SLOT_MAPPING branch: dev_idx < num_rsvd_entries + max_num_phy_ids β‡’ num_slots = max_num_phy_ids (:2290, up to 65535). Else-branch num_slots ≀ 0xF (4-bit slot field) β€” bounded.
  8. mpr_mapping.c:2327-2347 mt_entry = &sc->mapping_table[dev_idx]; for (map_idx = dev_idx; map_idx < (dev_idx + num_slots); map_idx++, mt_entry++) β€” per entry: conflict gate dpm_entry_num != MPR_DPM_BAD_IDX β†’ goto fail (:2330-2336), else writes physical_id (fw 64-bit PhysicalIdentifier, :2340), phy_bits (:2342), id (:2343), dpm_entry_num (:2344), missing_count (:2345), device_info = MPR_DEV_RESERVED (:2346). No check that dev_idx + num_slots <= max_devices.

Overflow condition: dev_idx + max_num_phy_ids > max_devices with dev_idx in bounds (e.g. MaxTargets=508, MaxVolumes=2 β‡’ max_devices=510; MaxNumPhysicalMappedIDs=0xFFFF; dev_idx=0x100): - entries [dev_idx, max_devices) are always overwritten (conflict gate passes on the BAD_IDX-cleared table); - the walk then crosses the allocation end: 2-byte OOB read (u16 dpm_entry_num at offset 16 of each 32-byte block) decides per block, and every consecutive block whose garbage u16 == 0xFFFF takes a full 32-byte fw-controlled write. Worst case (all-BAD_IDX region, e.g. a stale mapping table from a previous attach/reset cycle adjacent in the heap β€” _mapping_clear_map_entry fills whole tables with 0xFFFF sentinels): 65,281 entries Γ— 32 B = 2,088,992 bytes past the 16,320-byte kmalloc, same magnitude as the mps twin.

Distinct from the existing mpr findings

  • DF-3914 = event NumSlots reservation path (_mapping_get_dev_info / _mapping_get_pcie_dev_info), direct-attached boot default. This finding is the DPM-pg0 ingestion path with num_slots sourced from IOC pg8 MaxNumPhysicalMappedIDs β€” different data source, different loop, and (unlike DF-3914's boot-default path) requires only DPM to be enabled.
  • DF-3915 = _mapping_commit_enc_entry wild start_index read/NVRAM write when slot field is 0 β€” different function and sink.
  • Both DF-3914/3915 fixes (bound dev_idx, bound event num_slots) do not touch the :2290 num_slots = max_num_phy_ids assignment; the count here is a distinct unvalidated firmware field with an in-bounds index.

Verification performed

  • Guest pciconf -l (env.txt): virtio-only PCI bus, no SAS2208/2308/3000 HBA β‡’ mpr_attach never runs; kernel-level reproduction impossible on this target (same gate as DF-3914/DF-3915/DF-4085; guest is otherwise the intentionally permissive no-SMAP/SMEP/KASLR target β€” absent hardware is the only blocker).
  • harness_df4092.c built and run twice as the unprivileged user on the guest (cc 8.3, zero warnings): verbatim mpr struct + verbatim :2327-2347 loop including the goto fail conflict gate. Three adjacent- heap scenarios:
  • A (cold garbage): 254 in-bounds entries written [256,510), gate trips on the first OOB block β€” proves the unconditional in-bounds overwrite and the OOB read that gates the write;
  • B (4096 groomed OOB blocks): 4,096 Γ— 32 B = 131,072 bytes of fw-controlled canary past the table until the first non-0xFFFF block;
  • C (all-0xFFFF region): 65,281 Γ— 32 B = 2,088,992 bytes linear smash β€” the unbounded worst case, identical magnitude to the mps twin DF-4085. Canary 0x5141524153534141 lands at offset 0 of every 32-byte slot past the table end (verified programmatically; exit 0). run.log/run.2.log identical across runs.
  • This harness is stricter than the mps DF-4085 harness: it models the per-entry dpm_entry_num == MPR_DPM_BAD_IDX conflict gate that both the mps and mpr kernels actually execute (the mps harness omitted it), so the OOB write is characterized precisely as "linear through consecutive 0xFFFF-sentinel blocks" rather than unconditional.

Fix validation

  • fix.diff (this pack): bound-skip β€” reject any DPM entry with dev_idx >= sc->max_devices or num_slots > sc->max_devices - dev_idx before the enclosure-table store and the reservation loop (continue, matching the loop structure; mpr_dprint(MPR_ERROR|MPR_MAPPING) matching file style). Same shape as the validated DF-4085 fix.
  • Applied together with DF-4093's clamp to the guest /usr/src; cd /usr/src/sys/dev/raid/mpr && make compiled all module sources with -Werror (flags include -Wall -Wredundant-decls -Wnested-externs -Wstrict-prototypes -Wmissing-prototypes ...) and linked mpr.ko (BUILD_EXIT=0) β€” fix_build.log.
  • Runtime fix validation not testable: the path requires an mpr(4) HBA delivering firmware pages, absent on this guest.
  • /usr/src restored pristine after the build (md5 match with repo, reverse-patch verified, no leftover artifacts).

Bottom line

Source-certain CWE-787 heap OOB write at driver-attach time, primitive proven at object level on the audit guest with the exact mpr struct/loop; kernel-level reproduction hardware-gated (no mpr HBA in the QEMU guest). Fix compiles clean with -Werror; recommended for upstream together with the mps twin DF-4085.

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-4093's clamp; 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-4092/fix.diff, findings/poc/DF-4092/fix_build.log
↓ fix.diffper-fix-DF-4092

Confirmed kernel references

Detail

Exploit chain

Hardware-gated chain (not executable on this guest): malicious/compromised IOC firmware or PCI-passthrough emulated HBA -> IOC pg8 MaxNumPhysicalMappedIDs=0xFFFF + DPM pg0 entry with in-bounds DeviceIndex -> mpr_mapping_initialize -> _mapping_process_dpm_pg0 reservation loop -> linear heap overwrite (up to ~2MB) with fw-controlled qwords at 32-byte stride past mapping_table into adjacent kmalloc zones -> slab metadata / neighboring object corruption -> kernel code execution. Stopped at primitive proof: no HBA in guest.

Evidence (decisive lines)

run.log/run.2.log (guest harness: conflict-gated canary smash, 131,072-byte groomed run and 2,088,992-byte worst case at stride 32, in-bounds dev_idx), harness_df4092.c (verbatim struct mprvar.h:113 + loop :2327-2347 incl. goto-fail gate), env.txt (hardware gate), VERDICT.md (line-by-line chain), fix.diff + fix_build.log (-Werror module build with both mpr fixes, mpr.ko linked)

PoC changes

No seed PoC (fresh twin filing). Harness authored against the exact mpr struct layout (32 bytes on amd64, layout differs from mps: u16 reserved1 instead of u8 reserved1+u8 channel) with the verbatim :2327-2347 loop INCLUDING the dpm_entry_num!=MPR_DPM_BAD_IDX goto-fail gate that the mps DF-4085 harness omitted; three adjacent-heap scenarios; built and run twice on the audit guest with cc 8.3 as an unprivileged user (first iteration fixed a uint16 truncation in the scenario-C guard count).

Verified recommended fix

In _mapping_process_dpm_pg0, skip any DPM entry with dev_idx >= max_devices or num_slots > max_devices - dev_idx before storing into enclosure_table or running the reservation loop.

Verdict

Source-certain heap OOB write (CWE-787), mpr twin of DF-4085. In _mapping_process_dpm_pg0 (sys/dev/raid/mpr/mpr_mapping.c:2273-2347), DPM entries whose DeviceIndex falls in the direct-attached range take num_slots = max_num_phy_ids (:2290), the firmware u16 ioc_pg8.MaxNumPhysicalMappedIDs (:2216), never validated against the mapping-table size; the reservation loop :2327-2347 then writes num_slots consecutive 32-byte struct dev_mapping_table entries (mprvar.h:113, verified 32 bytes on amd64) with no check that dev_idx+num_slots <= max_devices (kmalloc(max_devices*32) at :2140-2141, max_devices = MaxTargets+MaxVolumes at :2565). With an IN-BOUNDS dev_idx (distinct from DF-3914's event-NumSlots path and DF-3915's commit_enc_entry path) a malicious/buggy IOC or emulated HBA gets a boot-time heap overflow: harness proves 65,281 entries (2,088,992 bytes) past a 16,320-byte allocation in the worst case, firmware-controlled qword at stride 32 - while modeling the per-entry dpm_entry_num==MPR_DPM_BAD_IDX conflict gate verbatim (in-bounds entries always pass; each OOB block is OOB-read and written iff its garbage u16 at offset 16 is 0xFFFF, e.g. a stale adjacent mapping table from a previous attach cycle). Not reproduced at kernel level: no SAS HBA on the QEMU guest (env.txt), mpr never attaches - same hardware gate as DF-3914/DF-3915 and mps twin DF-4085. Primitive proven at object level on the guest (run.log/run.2.log, identical across runs).