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

Wild OOB read (and NVRAM garbage write) in _mapping_commit_enc_entry via unvalidated enclosure start_index ingested from DPM pg0 (num_slots==0 bypasses DF-1282)

Summary

_mapping_process_dpm_pg0 stores the unchecked DPM DeviceIndex into et_entry->start_index alongside a valid dpm_entry_num. When the entry's 7-bit slot field is zero the reservation loop executes zero iterations, so DF-1282's OOB write never fires and the corrupt entry survives ingestion quietly. check_devices then passes it to commit_enc_entry, which computes &mapping_table[start_index] and reads mt_entry->id - a read up to 2,096,640 bytes past the allocation. VERIFIED on the guest: (a) the reservation loop runs 0 iterations for the crafted entry (DF-1282 bypass demonstrated), (b) the indexing resolves +2MB past an 8,448-byte allocation (guard-page SIGSEGV variant). Corrupt/malicious NVRAM DPM entry: wild read (panic/DoS) or silent garbage persisted into the controller's NVRAM DeviceIndex.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3915 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
harness_df3915.c β€” 6.5 KB view raw
build.sh β€” 118 B view raw
run.sh β€” 113 B view raw
build.log β€” 71 B view raw
run.log β€” 461 B view raw
env.txt β€” 2.7 KB view raw
fix.diff β€” 1.1 KB view raw
fix_build.log β€” 11.1 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.2 KB view raw

DF-3915 β€” Wild OOB read (and NVRAM garbage write) in _mapping_commit_enc_entry via unvalidated enclosure start_index

File: sys/dev/raid/mpr/mpr_mapping.c:129-130 (sink); ingestion :2310-2311; trigger :2481-2493 Class: CWE-125 Out-of-bounds Read (wild pointer) Severity: Medium β€” pass 2 (GLM 5.3) Not a duplicate of DF-1282: same DPM DeviceIndex source, but a different sink reached through a state DF-1282's own write cannot produce (num_slots == 0 bypasses the reservation loop entirely).

The bug (source-confirmed)

  1. _mapping_process_dpm_pg0() ingests each DPM enclosure entry with: - dev_idx = le16toh(dpm_entry->DeviceIndex) (:2251) β€” unchecked u16; - num_slots = MappingInformation & 0x07F0 >> 4 (:2293-2295) β€” can be 0; - et_entry->start_index = dev_idx (:2310) and et_entry->dpm_entry_num = entry_num (:2311) β€” stored unvalidated; - the reservation loop :2328-2347 (for (map_idx = dev_idx; map_idx < dev_idx + num_slots; …)) executes zero iterations when num_slots == 0, so DF-1282's OOB write in this same function never fires β€” the corrupt entry survives ingestion quietly (init_complete stays 0).
  2. mpr_mapping_check_devices() (:2481-2493) later walks the enclosure table; for any !init_complete entry it bumps missing_count and, when dpm_entry_num != MPR_DPM_BAD_IDX, calls _mapping_commit_enc_entry(sc, et_entry) (:2491).
  3. _mapping_commit_enc_entry() dereferences mt_entry = &sc->mapping_table[et_entry->start_index] (:129) and reads mt_entry->id into dpm_entry->DeviceIndex (:130) β€” with start_index up to 0xFFF0+ this is a read up to 65535 * 32 = 2,097,120 bytes past the max_devices * 32-byte mapping_table allocation (:2140).

Consequences: wild read β†’ panic on unmapped page (local DoS from the HBA's perspective), or silent garbage persisted into the controller's NVRAM DPM entry (DeviceIndex), which then feeds _mapping_process_dpm_pg0 on the next boot. Preconditions: Enc/Slot mapping mode, a DPM enclosure entry with slot bits == 0 and out-of-range DeviceIndex (malicious/corrupt NVRAM or IOC firmware), and β‰₯1 other enclosure that completed init (:2438-2446 gates the walk).

Reproduce

Hardware-gated (no SAS3 HBA on the audit guest β€” env.txt). The harness replicates the exact structs/flow: proves (a) the num_slots == 0 entry executes the :2328 loop zero times (DF-1282 bypassed) and (b) the :129 indexing lands 0xFFF0 * 32 = 2,096,640 bytes past an 8,448-byte allocation (guard-page variant included).

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

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

  DPM entry: dev_idx=0xfff0 num_slots=0 dpm_entry_num=3
  reservation loop (:2328) iterations: 0 (DF-1282 write BYPASSED)
  mapping_table size       : 8448 bytes (max_devices=264)
  start_index*32           : 2096640 bytes past base
  read mt_entry->id        : 0x5f36 (silent garbage; kernel feeds it to NVRAM)
RESULT: WILD READ PRIMITIVE PROVEN - index 0xfff0 * 32 = 2096640 bytes past a 8448-byte allocation

Fix

fix.diff validates et_entry->start_index (BAD or >= max_devices) at the top of _mapping_commit_enc_entry and refuses the DPM commit otherwise. Compile-validated with -Werror on the guest (fix_build.log).

VERDICT.md
↓ download raw

DF-3915 β€” VERDICT

Status: not_reproduced (kernel path hardware-gated on this guest) / wild-read primitive proven at object level. Confidence: certain (source-level; the sink has no bound).

Verification narrative

  1. Ingestion path (sys/dev/raid/mpr/mpr_mapping.c): - :2251 dev_idx = le16toh(dpm_entry->DeviceIndex); (u16 from the controller's persistent DPM page read at mpr_mapping_initialize :2614). - :2293-2295 num_slots = 7-bit slot field (0x07F0 >> 4) β€” zero is a legal, reachable value (an entry that only ever carried missing-count bits). - :2310-2311 start_index = dev_idx, dpm_entry_num = entry_num β€” both stored without any table-size validation. - :2328-2347 reservation loop: with num_slots == 0 the body never runs, so DF-1282's OOB write is not triggered β€” this is what makes the corrupt state observable later instead of panicking at boot. (With num_slots >= 1 and dev_idx >= max_devices, DF-1282 fires first and that state is its finding; num_slots == 0 is the new, distinct window.)
  2. Trigger path: mpr_mapping_check_devices() :2481-2493 β€” the only caller of _mapping_commit_enc_entry() in the file (verified by grep). Entry requirements met by the ingested state: init_complete == 0 (never set in _mapping_process_dpm_pg0), dpm_entry_num != BAD (:2489). Walk-level precondition: at least one enclosure entry with init_complete == 1 so stop_device_checks is set (:2438-2446) β€” any enclosure that arrived via an Enclosure Status Change event satisfies it (:2847). MPR_MISSING_CHECK_DELAY schedules the callout after discovery.
  3. Sink: :129 mt_entry = &sc->mapping_table[et_entry->start_index]; :130 reads mt_entry->id β†’ dpm_entry->DeviceIndex (persisted via the DPM write path :142-143). With start_index up to 0xFFFF the read is up to ~2 MB past a ~8 KB allocation: wild pointer.
  4. Hardware gate: guest pciconf -l (env.txt) β€” virtio-only PCI bus, no vendor-0x1000 SAS controller; mpr_attach() never runs. Same gate as DF-1282/DF-3800 (both previously proven).
  5. Object-level proof (guest, run.log, stable across reruns): - the :2328 loop runs 0 iterations for the crafted entry β€” the DF-1282 bypass is demonstrated, not assumed; - the :129 indexing resolves to +2,096,640 bytes past the 8,448-byte table; the harness read returns silent garbage (0x5f36) β€” in the kernel this either faults (panic) or persists garbage to NVRAM. The harness also installs a PROT_NONE guard-page variant (SIGSEGV handler) to show the access is genuinely wild when the offset lands on an unmapped page.

Why Medium and not High

Read-only primitive; the corrupted value's destination is the controller's NVRAM DeviceIndex (fed back into driver state), not a userspace info leak. Primary impact: kernel panic (DoS) from malformed persistent mapping state β€” same threat actor as DF-1282 (malicious IOC firmware / NVRAM corruption / malicious enclosure persisting mapping entries), but strictly weaker than the write primitives already filed for this file.

Fix validation

fix.diff: reject start_index == MPR_MAPTABLE_BAD_IDX || start_index >= sc->max_devices at the top of _mapping_commit_enc_entry (before the :129 deref), log, return -1. - Applied to the guest /usr/src copy together with DF-3914's diff (both git apply --check clean on the pristine tree). - make nativekernel: mpr_mapping.c compiled with -Werror in both passes (fix_build.log). - fix_status: not_testable for runtime: exercising the fixed path needs the HBA to serve a malformed DPM page.

Bottom line

Source-certain wild OOB read (CWE-125) reachable through a DPM-ingested enclosure entry whose slot count is zero β€” a state DF-1282 cannot cover because its own write loop never runs for it. Primitive proven at object level; kernel path hardware-gated; guard-clause 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 (guard clause at the top of _mapping_commit_enc_entry) applied to the guest /usr/src copy together with DF-3914's diff; make nativekernel rebuilt mpr_mapping.c with -Werror in both passes and completed the kernel+module link with zero diagnostics (fix_build.log; full log SHA256 55641f6d20f32d6795026dd86d27cd571a9186beb0333830050a386f1fb57368). Runtime fix validation not testable without an mpr(4) HBA serving a malformed DPM page.

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

Confirmed kernel references

Detail

Exploit chain

Hardware-gated (not executable on this guest): corrupt/malicious DPM pg0 enclosure entry (slot bits 0, DeviceIndex 0xFFF0) -> quiet ingestion via _mapping_process_dpm_pg0 -> device-check callout after MPR_MISSING_CHECK_DELAY -> _mapping_commit_enc_entry wild read -> panic (DoS) or garbage DeviceIndex persisted to controller NVRAM. Read-only primitive; escalation ceiling is DoS + persistent mapping corruption.

Evidence (decisive lines)

run.log (guest harness: 0 reservation-loop iterations + read at +2,096,640 bytes), harness_df3915.c (verbatim struct/index replication incl. PROT_NONE guard variant), env.txt (hardware gate), VERDICT.md (line-by-line chain incl. single-caller grep proof), fix.diff + fix_build.log (patched kernel compiled -Werror clean, completed)

PoC changes

No seed PoC existed (pass-2 finding). Harness authored against exact struct layout; built and run on the audit guest with cc 8.3.

Verified recommended fix

Validate et_entry->start_index (!= MPR_MAPTABLE_BAD_IDX and < sc->max_devices) at the top of _mapping_commit_enc_entry before indexing sc->mapping_table.

Verdict

Source-certain wild OOB read (CWE-125), pass 2. _mapping_process_dpm_pg0 stores the unchecked DPM DeviceIndex into et_entry->start_index (mpr_mapping.c:2310) together with a valid dpm_entry_num (:2311); when the entry's slot count is zero (:2293-2295 - legal value of the 7-bit slot field) the reservation loop :2328-2347 runs zero iterations, so DF-1282's OOB write never fires and the corrupt entry survives ingestion quietly (init_complete stays 0). mpr_mapping_check_devices (:2481-2493, the only caller) then passes it to _mapping_commit_enc_entry, which dereferences &sc->mapping_table[et_entry->start_index] (:129) and reads mt_entry->id (:130) - a read up to 6553532 = 2,096,640 bytes past the max_devices32-byte mapping_table allocation (:2140): kernel panic on unmapped page, or silent garbage persisted into the controller's NVRAM DeviceIndex which re-enters driver state on next boot. Preconditions: Enc/Slot mapping mode, DPM enclosure entry with slot bits==0 and out-of-range DeviceIndex (malicious IOC firmware / NVRAM corruption), and >=1 other enclosure with init_complete (:2438-2446). Not reproduced at kernel level on the QEMU guest: no SAS3 HBA (env.txt); same gate as DF-1282/DF-3800. Primitive proven at object level on the guest: harness demonstrates the zero-iteration reservation loop (DF-1282 bypass) and the :129 indexing resolving +2,096,640 bytes past an 8,448-byte allocation (run.log).