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

Unbounded firmware NumSlots heap OOB write in the direct-attached enclosure reservation path (_mapping_get_dev_info/_mapping_get_pcie_dev_info)

Summary

The enclosure-table entry's num_slots comes from the Enclosure Status Change event's NumSlots, a firmware u16 whose only validation in the whole file is !=0. On the DIRECT-ATTACHED topo path start_index is pinned to num_rsvd_entries with no relation to num_slots, and the reservation loop then initializes num_slots consecutive mapping_table entries with no check against max_devices. The expander branch is bounded because find_enc_map_space only reports space within the table; the DA branch bypasses it entirely. A malicious/compromised enclosure (SES data surfaced by IOC firmware), malicious IOC firmware, or NVRAM corruption reports NumSlots up to 65535 vs a mapping_table sized max_devices entries (~8-64KB) -> up to ~2MB linear heap overflow at a 32-byte stride with the fully firmware-controlled 64-bit enclosure SAS address. VERIFIED on the guest: object-level harness with canaries - NumSlots=2048 vs max_devices=264 smashed 57,120 bytes past the 8,448-byte allocation. Fix compile-validated -Werror into a full kernel.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3914 Β· 13 files
FileTypeDescriptionSize
README.md β€” 3.5 KB ↓ raw
VERDICT.md β€” 4.6 KB ↓ raw
harness_df3914.c β€” 7.2 KB view raw
build.sh β€” 118 B view raw
run.sh β€” 113 B view raw
build.log β€” 41 B view raw
run.log β€” 556 B view raw
run.2.log β€” 1.1 KB view raw
env.txt β€” 2.7 KB view raw
fix.diff β€” 2.4 KB view raw
fix_build.log β€” 11.1 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.4 KB view raw

DF-3914 β€” Unbounded firmware NumSlots heap OOB write in the direct-attached enclosure reservation path

File: sys/dev/raid/mpr/mpr_mapping.c (SAS: :1241-1243 + :1292-1299; PCIe: :1392-1394 + :1443-1450) Class: CWE-787 Out-of-bounds Write (heap) Severity: High β€” pass 2 (GLM 5.3) Not a duplicate of DF-1282 (DPM-pg0 DeviceIndex write at :2327) or DF-1283 (slot - start_slot underflow at :1637): different entry path (live topology event, not DPM ingestion) and different missing bound (table size vs NumSlots).

The bug (source-confirmed)

  1. An Enclosure Status Change event creates an enclosure-table entry with et_entry->num_slots = le16toh(event_data->NumSlots) (mpr_mapping.c:2843). The only validation of NumSlots in the entire file is != 0 (:2760-2765). It is a firmware/u16 value (0..65535).
  2. When devices for that enclosure appear, _mapping_get_dev_info() (SAS, :1241) / _mapping_get_pcie_dev_info() (PCIe, :1392) map the enclosure: * expander path (exp_handle != 0): _mapping_find_enc_map_space() only returns a start_idx after finding num_slots contiguous free entries below end_of_table <= max_devices (:979-993) β€” bounded. * direct-attached path (exp_handle == 0 / switch_dev_handle == 0): c map_idx = sc->num_rsvd_entries; /* :1242 */ et_entry->start_index = map_idx; /* :1243 β€” NO num_slots bound */ … and then the reservation loop (:1292-1299, PCIe :1443-1450) initializes et_entry->num_slots consecutive entries starting at num_rsvd_entries with no check that num_rsvd_entries + num_slots <= sc->max_devices.
  3. sc->mapping_table is kmalloc(max_devices * 32, M_MPR) (:2140-2141), max_devices = facts->MaxTargets + MaxVolumes (u16, :2565, typically ~256-2048).

An enclosure whose reported NumSlots exceeds the free table space (lying or compromised enclosure/backplane SES data surfaced by IOC firmware, malicious IOC firmware, or NVRAM corruption) therefore produces a heap OOB write of up to (0xFFFF - max_devices + num_rsvd) * 32 bytes (~2 MB) past the mapping_table allocation, writing at a 32-byte stride: physical_id = enclosure SAS address (fully firmware-controlled 64 bits), device_info = MPR_DEV_RESERVED, phy_bits = firmware u32, missing_count = 0.

Reproduce

Hardware-gated: the audit QEMU guest has no LSI/Avago SAS3 HBA (pciconf -l shows virtio-only β€” env.txt), so mpr_attach never runs there. The pack contains an object-level harness that replicates the exact structs and the exact unguarded loops and proves the write primitive (canary region after the allocation receives the attacker value at the 32-byte stride):

build.sh   # cc -O2 -Wall -o harness_df3914 harness_df3914.c
run.sh     # ./harness_df3914

Expected (observed on the guest, run.log):

  enclosure NumSlots (fw)  : 2048
  reservation loop wrote   : [1, 2049)
  table end                : 264  -> overflow of 1785 entries (57120 bytes)
  guard[+0..] physical_id == 0x5141524153534141 at 1785/4096 entries (stride 32)
RESULT: OOB WRITE PRIMITIVE PROVEN (canary smash with attacker value, 32-byte stride)

Fix

fix.diff bounds num_slots on both direct-attached paths and reuses the existing "no space found" handling (mark the event's add entries processed and stop). Compile-validated: applied to the guest /usr/src copy and compiled with -Werror in both the static-kernel and KLD_MODULE passes of make nativekernel (fix_build.log).

VERDICT.md
↓ download raw

DF-3914 β€” VERDICT

Status: not_reproduced (kernel path hardware-gated on this guest) / OOB-write primitive proven at object level. Confidence: certain (source-level; the missing bound is unconditional).

What was verified and how

  1. Root cause, line by line (sys/dev/raid/mpr/mpr_mapping.c): - :2843 et_entry->num_slots = le16toh(event_data->NumSlots); β€” sole validation of the firmware-supplied slot count is != 0 at :2760. - :1241-1243 (SAS) and :1392-1394 (PCIe): direct-attached branch sets et_entry->start_index = sc->num_rsvd_entries with no relation established between num_slots and the table size. - :1292-1299 (SAS) / :1443-1450 (PCIe): reservation loop writes num_slots entries from map_idx β€” the unguarded sink. - :2140-2141 allocation is max_devices * sizeof(dev_mapping_table) (32 B/entry); :2565 max_devices = MaxTargets + MaxVolumes (u16). - Bounded contrast: the expander branch goes through _mapping_find_enc_map_space() :979-993, which only reports space found within end_of_table <= max_devices; the DA branch bypasses it. - grep -n num_slots over the whole file confirms no clamp between :2843 and the two loops.

  2. Distinctness from the known findings (pass-2 mandate): - DF-1282 = _mapping_process_dpm_pg0 mapping_table[dev_idx] write from DPM page0 ingestion (:2327/:2358), including the dev_idx+num_slots loop at :2328. DF-3914 is the event-driven reservation path β€” the overflow source is NumSlots from an Enclosure Status Change event, not a DPM DeviceIndex, and it fires on a live topo event with a clean DPM. - DF-1283 = start_index + slot - start_slot unsigned underflow in _mapping_add_new_device/_mapping_add_new_pcie_device (:1637/:1894). DF-3914 is in _mapping_get_dev_info/_mapping_get_pcie_dev_info, no slot subtraction involved.

  3. Hardware gate (why the kernel path is not runtime-reachable here): guest pciconf -l (env.txt) lists only i440FX/PIIX/virtio-vga/virtio-net/ virtio-blk devices β€” no vendor 0x1000 SAS controller, so mpr_attach() never executes. Same gate already proven for DF-1282/DF-3800.

  4. Object-level proof (guest, run.log + run.2.log): harness replicates struct dev_mapping_table (32 B, mprvar.h:113), allocates max_devices=264 entries plus a 4096-entry canary guard, then runs the verbatim :1292-1299 loop with NumSlots=2048, num_rsvd=1: 1785 entries (57,120 bytes) past the 8,448-byte allocation receive physical_id = 0x5141524153534141 at stride 32 β€” i.e. a firmware-controlled 8-byte value per 32-byte slot, exactly what the kernel loop writes past its slab object. Second run reproduced identically.

Exploit chain (why this matters beyond a panic)

The written physical_id is the enclosure's 64-bit SAS address β€” fully chosen by the (malicious) firmware/enclosure. A ~2 MB linear overflow with a controlled qword every 32 bytes across adjacent kmalloc zones is a classic slab-corruption primitive: overflow into a neighboring object with function pointer / list pointers (e.g. neighboring driver structures) β†’ control flow. Not developed further: hardware-gated (no HBA to carry the malicious event), same trust boundary as DF-1282/DF-1283.

Fix validation

fix.diff adds the missing bound on both DA paths, mirroring the existing "no space" handling (mark add entries processed; stop mapping this event). - Applied cleanly (patch -p1) to the guest /usr/src copy together with DF-3915's diff (git apply --check passes on the pristine tree too). - make nativekernel KERNCONF=X86_64_GENERIC: mpr_mapping.c recompiled with -Werror in both the static-kernel and KLD_MODULE passes β€” no diagnostics (fix_build.log). - fix_status: not_testable for runtime validation: the fixed path needs an mpr(4) controller emitting the malicious enclosure event; no such hardware on the guest.

Bottom line

Real, source-certain, firmware-fed heap OOB write (CWE-787) with a controlled 8-byte-in-32 stride, distinct from DF-1282/DF-1283; primitive proven at object level on the audit guest; kernel path hardware-gated; fix compiles clean.

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

fix.diff applied to the guest /usr/src copy (together with DF-3915's diff, git apply --check clean on the pristine tree); make nativekernel KERNCONF=X86_64_GENERIC rebuilt mpr_mapping.c with -Werror in both static-kernel and KLD_MODULE passes and completed the kernel+module link with zero diagnostics (fix_build.log; full log SHA256 55641f6d20f32d6795026dd86d27cd571a9186beb0333830050a386f1fb57368). Runtime fix validation not testable: exercising the path requires an mpr(4) HBA delivering the malicious enclosure event, absent on this guest.

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

Confirmed kernel references

Detail

Exploit chain

Hardware-gated chain (not executable on this guest): malicious/compromised enclosure SES data or IOC firmware -> Enclosure Status Change event with NumSlots > free space -> SAS/PCIe Topology Change event (direct-attached) -> _mapping_get_dev_info/_mapping_get_pcie_dev_info reservation loop -> linear heap overflow past mapping_table with fw-controlled qword per 32 bytes into adjacent kmalloc zones -> slab metadata / neighboring object corruption -> kernel code execution. Stopped at primitive proof: no HBA.

Evidence (decisive lines)

run.log/run.2.log (guest harness output: 57,120-byte canary smash with 0x5141524153534141 at stride 32), harness_df3914.c (verbatim loop replication), env.txt (hardware gate), VERDICT.md (line-by-line source chain), fix.diff + fix_build.log (patched kernel compiled -Werror clean, completed)

PoC changes

No seed PoC existed (pass-2 finding). Harness authored against the exact struct layout (mprvar.h:113) with the verbatim :1292-1299 loop; built and run on the audit guest with cc 8.3.

Verified recommended fix

Bound et_entry->num_slots by (sc->max_devices - num_rsvd_entries) on both direct-attached paths in _mapping_get_dev_info/_mapping_get_pcie_dev_info and reject the enclosure using the existing no-space handling.

Verdict

Source-certain heap OOB write (CWE-787), pass 2. The firmware-supplied enclosure NumSlots (mpr_mapping.c:2843, only !=0-checked at :2760) is never bounded against the mapping-table size on the DIRECT-ATTACHED reservation path of _mapping_get_dev_info (:1241-1243 -> unbounded loop :1292-1299) and _mapping_get_pcie_dev_info (:1392-1394 -> :1443-1450); the expander path is bounded via _mapping_find_enc_map_space (:979-993), the DA path bypasses it. mapping_table is kmalloc(max_devices*32) (:2140), so an enclosure reporting more slots than free table space produces a linear overflow of up to ~2MB writing the fully firmware-controlled 64-bit enclosure SAS address at a 32-byte stride. Distinct from DF-1282 (DPM-pg0 DeviceIndex write) and DF-1283 (slot-start_slot underflow). Not reproduced at kernel level on the QEMU guest: no LSI/Avago SAS3 HBA present (pciconf in env.txt - virtio-only PCI bus), so mpr_attach never runs; same hardware gate as DF-1282/DF-3800. Primitive proven at object level on the guest: harness replicates the exact structs and verbatim loop; with NumSlots=2048 vs max_devices=264 it smashes 1,785 guard entries (57,120 bytes) past the 8,448-byte allocation with the attacker value at stride 32 (run.log, stable across reruns).