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

Unvalidated IOC-pg8 MaxNumPhysicalMappedIDs used as num_slots: heap OOB write in _mapping_process_dpm_pg0 reservation loop

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 field never validated anywhere. The reservation loop writes num_slots consecutive 32-byte dev_mapping_table entries starting at mapping_table[dev_idx] with no check that dev_idx+num_slots <= max_devices, while the table is kmalloc(max_devices*32). With an IN-BOUNDS dev_idx and MaxNumPhysicalMappedIDs=0xFFFF this is a linear boot-time heap overflow of up to ~2MB past a ~16KB allocation, 32 bytes per entry, each carrying the firmware-chosen 64-bit PhysicalIdentifier. Distinct from DF-1374 (index OOB; here index in bounds, count unbounded) and DF-1375 (event path). Harness-proven on guest (65,281 entries = 2,088,992 bytes past the table, canary at every slot). LEAD: mpr sibling has the same defect at mpr_mapping.c:2285-2290 (unfiled).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4085 Β· 13 files
FileTypeDescriptionSize
README.md β€” 4.6 KB ↓ raw
VERDICT.md β€” 4.4 KB ↓ raw
harness_df4085.c β€” 4.5 KB view raw
build.sh β€” 102 B view raw
run.sh β€” 97 B view raw
build.log β€” 75 B view raw
run.log β€” 597 B view raw
run.2.log β€” 597 B view raw
env.txt β€” 1.4 KB view raw
fix.diff β€” 623 B view raw
fix_build.log β€” 12.0 KB view raw
manifest.json β€” 836 B view raw
verdict.json β€” 3.8 KB view raw

DF-4085 β€” Unvalidated IOC-pg8 MaxNumPhysicalMappedIDs as num_slots β†’ heap OOB write in _mapping_process_dpm_pg0

File: sys/dev/raid/mps/mps_mapping.c (branch :1512-1518, reservation loop :1544-1563) Class: CWE-787 Out-of-bounds Write (heap) / CWE-20 unvalidated firmware count Severity: High β€” pass 2 (GLM 5.3), bucket memcorrupt Not a duplicate of DF-1374 (that finding is the unvalidated DeviceIndex index at the same function): here dev_idx is in bounds and the missing bound is the count β€” num_slots is taken from IOC page 8's MaxNumPhysicalMappedIDs with no check that dev_idx + num_slots <= max_devices. DF-1375's num_slots text covers a different source (live enclosure-event NumSlots in _mapping_get_dev_info:992), not DPM-pg0 ingestion.

The bug (source-confirmed)

  1. mps_mapping_initialize() (:1719) enables DPM when ioc_pg8.MaxPersistentEntries != 0 (:1739-1740) and reads the DPM page (:1780), then calls _mapping_process_dpm_pg0() (:1793).
  2. For every non-empty DPM entry in ENCLOSURE_SLOT_MAPPING mode, if dev_idx < num_rsvd_entries + max_num_phy_ids (the direct-attached virtual enclosure range) the code sets c num_slots = max_num_phy_ids; /* :1518 */ max_num_phy_ids = le16toh(sc->ioc_pg8.MaxNumPhysicalMappedIDs) (:1469) β€” a firmware-supplied u16 that is never validated anywhere against max_devices (= facts->MaxTargets + facts->MaxVolumes, :1734).
  3. The reservation loop c mt_entry = &sc->mapping_table[dev_idx]; /* :1544 */ for (map_idx = dev_idx; map_idx < (dev_idx + num_slots); map_idx++, mt_entry++) { ... writes ... } /* :1545-1563 */ writes num_slots consecutive struct dev_mapping_table entries (physical_id = the DPM entry's 64-bit PhysicalIdentifier, phy_bits, channel, id, dpm_entry_num, missing_count, device_info) with no end-of-table check. mapping_table is kmalloc(max_devices * 32, M_MPT2) (mps_mapping.c:1393-1394).

With MaxNumPhysicalMappedIDs = 0xFFFF (or any value pushing dev_idx + max_num_phy_ids past max_devices) and an ordinary in-bounds dev_idx, boot-time DPM ingestion performs a linear heap overflow of up to ~2 MB past a ~16 KB allocation, 32 bytes per entry, each entry carrying the firmware-chosen 64-bit physical_id at offset 0. The stored et_entry->num_slots = num_slots (:1539) additionally re-arms the already-known DF-1375 slot-reservation overflow for every later topology event.

The (else) branch is safe (num_slots = 4-bit slot field of MappingInformation, max 0xF); only the direct-attach branch is unbounded. The enclosure_table growth itself is bounds-checked (:1525-1531), and dpm_entry_used[]/dpm_flush_entry[] walks are bounded by max_dpm_entries β€” the count is the sole missing bound.

Threat model

Same as DF-1374/DF-1375/DF-3914: a malicious or buggy LSI/Avago IOC firmware image, NVRAM corruption of the persistent map, or PCI passthrough of an emulated mps(4) HBA. Everything on this path (IOC facts, IOC pg8, DPM pg0 content) is firmware-controlled DMA'd data; nothing is validated against the allocation sizes derived from the same firmware.

Reproduction (object-level, hardware-gated)

No mps HBA exists in the audit QEMU guest (see env.txt), so the kernel path cannot be exercised. harness_df4085.c replicates the exact struct (mpsvar.h:135, sizeof == 32 on amd64) and the verbatim :1544-1563 loop and drives it with a fully in-bounds dev_idx:

cc -O2 -Wall -o harness harness.c && ./harness

Expected decisive output (see run.log, stable across reruns in run.2.log):

max_devices               : 510 (table = 16320 bytes)
MaxNumPhysicalMappedIDs   : 0xffff (IOC pg8, unvalidated)
DPM entry 0               : dev_idx=256 (in bounds!), num_slots=65535
reservation loop wrote     : [256, 65791)
entries past table end     : 65281
kernel equivalent          : 2088992-byte linear heap write of fw-controlled data
  past a 16320-byte kmalloc, attacker qword 0x5141524153534141 at stride 32
RESULT: OOB WRITE PRIMITIVE PROVEN (canary smash, stride 32)

Fix

fix.diff β€” validate, before storing/looping, that the entry fits the table: dev_idx < max_devices && num_slots <= max_devices - dev_idx, else skip the DPM entry (consistent with the existing conflict handling). Compile-validated: applied (together with DF-4086's clamp and the other mps fixes) to the guest /usr/src; make in sys/dev/raid/mps rebuilt the whole module with -Werror and linked mps.ko (BUILD_EXIT=0, fix_build.log).

VERDICT.md
↓ download raw

DF-4085 VERDICT

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

Source chain (line-by-line)

  1. sys/dev/raid/mps/mps.c:1228 mps_startup() β†’ mps_mapping_initialize(sc) (return ignored β€” moot here: with M_INTWAIT allocations it cannot fail).
  2. mps_mapping.c:1739-1740 max_dpm_entries = ioc_pg8.MaxPersistentEntries; is_dpm_enable = !!max_dpm_entries β€” DPM active on any non-zero value.
  3. mps_mapping.c:1774-1793 DPM pg0 read into sc->dpm_pg0 (kmalloc(8 + 20*max_dpm_entries), :1418-1421) and _mapping_process_dpm_pg0() called.
  4. mps_mapping.c:1469 max_num_phy_ids = le16toh(ioc_pg8.MaxNumPhysicalMappedIDs) β€” u16, firmware field, grep confirms no validation against max_devices anywhere in the driver.
  5. mps_mapping.c:1512-1518 ENCLOSURE_SLOT_MAPPING branch: dev_idx < num_rsvd_entries + max_num_phy_ids β‡’ num_slots = max_num_phy_ids (u16, up to 65535). Else-branch num_slots ≀ 0xF (4-bit slot field) β€” bounded.
  6. mps_mapping.c:1544-1563 mt_entry = &sc->mapping_table[dev_idx]; for (map_idx = dev_idx; map_idx < dev_idx + num_slots; map_idx++, mt_entry++) β€” writes physical_id (fw 64-bit), phy_bits, channel, id, dpm_entry_num, missing_count, device_info per entry. No check that dev_idx + num_slots <= max_devices.
  7. mps_mapping.c:1393-1394 mapping_table = kmalloc(sizeof(struct dev_mapping_table) * max_devices) = 32 bytes/entry (verified: harness prints sizeof == 32 on amd64).

Overflow condition: dev_idx + max_num_phy_ids > max_devices with dev_idx itself in bounds (e.g. MaxTargets=508, MaxVolumes=2 β‡’ max_devices=510; MaxNumPhysicalMappedIDs=0xFFFF; dev_idx=0x100). Result: 65,281 entries Γ— 32 B = 2,088,992 bytes written past a 16,320-byte heap allocation at attach time, fully deterministic, contents firmware-chosen.

Distinct from the known findings: - DF-1374 = unvalidated DeviceIndex (index OOB) at the same function β€” fixed by bounding dev_idx; the count stays unbounded, and with num_slots == 0 (slot-field 0) DF-1374's loop never even writes. This finding is the count (MaxNumPhysicalMappedIDs), reachable with a fully in-bounds index. - DF-1375 covers (a) slot - start_slot underflow in _mapping_add_new_device:1165 and (b) the event-NumSlots reservation loop _mapping_get_dev_info:992-998. Neither is DPM-pg0 ingestion.

Verification performed

  • Guest pciconf -l (env.txt): virtio-only PCI bus, no SAS HBA β‡’ mps_attach never runs; kernel-level reproduction impossible on this target (same gate as DF-1374/DF-1375/DF-3914; the guest is otherwise intentionally exploitation-permissive β€” no SMAP/SMEP/KASLR).
  • harness_df4085.c built and run as the unprivileged user on the guest (cc 8.3), verbatim struct + loop; decisive output in run.log; rerun in run.2.log (identical). Canary 0x5141524153534141 lands at offset 0 of every 32-byte slot past the table end β€” in-kernel equivalent: fw-controlled qword at 32-byte stride over adjacent kmalloc zones (slab metadata, neighboring objects).
  • fix.diff applied to the guest /usr/src (with DF-4086's clamp and the other mps fixes of this batch); make in /usr/src/sys/dev/raid/mps compiled mps_mapping.c with -Werror and linked mps.ko (BUILD_EXIT=0) β€” fix_build.log. Guest /usr/src then restored pristine (reverse-patch verified) and the guest reset with the clean-source snapshot.

Exploitability ceiling (hardware-gated chain)

malicious/compromised IOC firmware or emulated HBA (PCI passthrough) β†’ DPM pg0 entry + IOC pg8 MaxNumPhysicalMappedIDs β†’ boot-time _mapping_process_dpm_pg0 β†’ linear 2 MB heap overwrite with fw-chosen qwords at stride 32 β†’ adjacent slab-object corruption (e.g. function-pointer -bearing structures) β†’ kernel code execution. Blocked here only by absent hardware, not by guest hardening.

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-4085/fix.diff, findings/poc/DF-4085/fix_build.log
↓ fix.diffper-fix-DF-4085

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 -> mps_mapping_initialize -> _mapping_process_dpm_pg0 reservation loop -> ~2MB linear heap overwrite 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: 2,088,992-byte canary smash at stride 32, in-bounds dev_idx), harness_df4085.c (verbatim struct mpsvar.h:135 + loop :1544-1563), env.txt (hardware gate), VERDICT.md (line-by-line chain), fix.diff + fix_build.log (-Werror module build with the fix, mps.ko linked)

PoC changes

No seed PoC (pass-2 finding). Harness authored against the exact struct layout with the verbatim :1544-1563 loop; built and run on the audit guest with cc 8.3 as an unprivileged user.

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), pass 2. In _mapping_process_dpm_pg0 (sys/dev/raid/mps/mps_mapping.c:1512-1518), DPM entries whose DeviceIndex falls in the direct-attached range take num_slots = max_num_phy_ids, the firmware u16 ioc_pg8.MaxNumPhysicalMappedIDs, never validated against the mapping-table size; the reservation loop :1544-1563 then writes num_slots consecutive 32-byte struct dev_mapping_table entries with no check that dev_idx+num_slots <= max_devices (kmalloc(max_devices*32) at :1393-1394). With an IN-BOUNDS dev_idx (distinct from DF-1374's unvalidated index; distinct from DF-1375's event-NumSlots path) a malicious/buggy IOC or emulated HBA gets a linear boot-time heap overflow: harness shows 65,281 entries (2,088,992 bytes) past a 16,320-byte allocation, firmware-controlled qword at stride 32. Not reproduced at kernel level: no SAS HBA on the QEMU guest (env.txt), mps never attaches - same hardware gate as DF-1374/1375/3914. Primitive proven at object level on the guest (harness replicates verbatim struct+loop; run.log, stable across reruns).