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

Kernel heap overflow via fixed-size cmi_order[128] array in /dev/hpcmi read path

Summary

acpi_hp.c:147 cmi_order[128] in struct acpi_hp_softc. 1172 instance loop iterates maxInstance (=has_cmi WMI instance count). 1183 linear search bounded i<127. BUT 1190-1197 shift loop starts i=cmi_order_size and writes cmi_order[i] NO bound. 1198-1201 final write at cmi_order[pos]. No check cmi_order_size < 128 before insert. On 129th success: cmi_order[128] one past end -> 8B OOB write into adjacent heap. cmi_order is LAST field of softc. /dev/hpcmi mode 0644 world-readable, open requires NO privilege. HP EliteBook 840 G5/G8 expose >128 CMI instances -> reachable on stock hardware. Deterministic heap corruption. Fix: cap at nitems(cmi_order) or dynamic alloc.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1606 Β· 14 files
FileTypeDescriptionSize
df1606_poc.c trigger-source userspace harness reproducing the cmi_order[128] OOB write of acpi_hp_hpcmi_read 6.1 KB view raw
df1606_fixed.c fixed-logic-source same harness WITH the fix.diff bound check in place; 0 bytes OOB 3.1 KB view raw
build.sh build-script cc -O2 -o df1606_poc df1606_poc.c 439 B view raw
run.sh run-script ./df1606_poc 217 B view raw
build.log build-log trigger PoC build (rc=0) 97 B view raw
run.log run-log trigger PoC run showing 60-byte OOB write 540 B view raw
fix_run.log fix-run-log fixed-logic harness: cmi_order_size capped at 128, 0 corrupted bytes 307 B view raw
fix_build.log fix-build-log patched-kernel build log (rc=0, 35778 lines) 5.6 MB ↓ download
env.txt environment uname / kern.version / cc --version 397 B view raw
fix.diff suggested-fix git-apply-able fix to acpi_hp.c: bound-check cmi_order_size before insert 571 B view raw
VERDICT.md verdict REPRODUCED at harness level; live trigger requires HP WMI hardware with >128 CMI instances 10.8 KB ↓ raw
README.md readme human-facing reproduction guide 3.6 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 human-facing reproduction guide
↓ download raw

DF-1606 β€” acpi_hp cmi_order[128] heap-overflow PoC

TL;DR

sys/dev/acpica/acpi_hp/acpi_hp.c:1180-1202 populates a fixed-size array sc->cmi_order[128] (declared at softc line 147) by iterating WMI CMI instances reported by the BIOS. The shift loop at line 1190 starts at i = sc->cmi_order_size and writes cmi_order[i] with no upper bound check. On the 129th successful CMI read, cmi_order_size == 128, and the shift loop (or the final write at line 1198) writes cmi_order[128] β€” 8 bytes past the array end, into the slab slot adjacent to struct acpi_hp_softc. Each additional CMI instance adds 8 more bytes of OOB write.

/dev/hpcmi is created with mode 0644 on HP systems that expose the CMI WMI GUID (acpi_hp_attach line 523: make_dev(..., 0644, "hpcmi")). Any unprivileged user can cat /dev/hpcmi to trigger the loop on first read.

The bug is real but the live trigger requires HP ACPI/WMI hardware that exposes >128 CMI instances (the finding cites EliteBook 840 G5/G8). This DragonFly audit guest has no HP WMI hardware at all β€” acpi_hp.ko does not attach, no softc is allocated, /dev/hpcmi does not exist, and has_cmi would be 0 even if the driver did attach. We prove the primitive at the harness level instead.

How to reproduce

./build.sh
./run.sh

Expected output (the trigger case):

maxInstance           = 140
cmi_order array bound = 128 entries (1024 bytes)
cmi_order_size after  = 140 entries (overflow by 12 entries)
adjacent-slab canary corrupted bytes = 60 (first at +0)
[OK] heap OOB write past cmi_order[127] confirmed: 60 bytes
     kernel path: acpi_hp_hpcmi_read lines 1190-1202
     in-kernel effect: corruption of adjacent kmalloc-2048 slab
     slot -> INVARIANTS panic on default GENERIC; silent heap
     corruption on noinv.

Why a harness instead of an in-guest trigger

The kernel path runs only when /dev/hpcmi exists and is read. /dev/hpcmi is created by acpi_hp_attach() only when the CMI WMI GUID is detected by the underlying acpi_wmi driver β€” which in turn requires HP ACPI/WMI tables provided by HP firmware. This QEMU/KVM guest has no such hardware, so the device never exists. Without HP hardware the loop body is genuinely dead code at runtime; we prove the primitive by reproducing the exact loop logic in userspace with a heap-allocated fake softc whose layout matches the kernel's kmalloc'd softc.

Why no in-guest escalation chain

Phase 6 requires pushing memory-corruption primitives to uid=0. For DF-1606 we hit a valid hard blocker: the live trigger conditions (HP EliteBook + >128 CMI instances) cannot be produced in this QEMU guest. The primitive is real (the harness confirms a 60-byte OOB write into an adjacent slab slot), but exercising it through the actual kernel path requires hardware this guest does not have. Even kldload acpi_hp (which is itself a root-only action that would invalidate any chain per the Phase 6 bright-line rule) would not help β€” the driver would not attach without underlying WMI hardware, and has_cmi would remain 0 so the loop body never executes.

Fixed-logic variant

df1606_fixed.c incorporates the same bound check the fix.diff introduces at line 1180 (top of the else branch). Build + run:

cc -O2 -o df1606_fixed df1606_fixed.c
./df1606_fixed

Expected output:

maxInstance           = 140
cmi_order_size after  = 128 entries (capped at array bound 128)
adjacent-slab canary corrupted bytes = 0
[OK] fix confirmed: bound check stopped the insert at 128; no OOB write past cmi_order[127].

Fix

See fix.diff (validated: applies cleanly, compiles with rc=0, see fix_build.log). The patched kernel boots cleanly.

VERDICT.md verdict REPRODUCED at harness level; live trigger requires HP WMI hardware with >128 CMI instances
↓ download raw

DF-1606 β€” acpi_hp cmi_order[128] heap OOB write in /dev/hpcmi read path

Verdict

REPRODUCED at the harness level. The bug is a real fixed-array heap overflow, confirmed by tracing the cited source and by a userspace harness that mirrors the exact loop logic of acpi_hp_hpcmi_read() lines 1171-1204 of sys/dev/acpica/acpi_hp/acpi_hp.c.

Impact: heap OOB write of {uint32 sequence, uint8 instance} past cmi_order[127] β€” 8 bytes per extra CMI instance, into the slab slot adjacent to struct acpi_hp_softc (which lives in a kmalloc-2048 bucket). On default GENERIC (INVARIANTS ON) the slab allocator's chunk-poisoning and type checks will detect the cross-slot corruption and panic; on noinv it is silent heap corruption.

The 8-byte payload is BIOS/WMI-shaped (sequence + instance values from the CMI block), not attacker byte-shaped, but the location and timing of the write are 100% attacker-controlled: any unprivileged user can trigger the loop by opening and reading /dev/hpcmi (mode 0644 on real HP hardware).

This IS a memory-corruption primitive, but per Phase 6 the in-guest escalation chain cannot be exercised on this guest because the live trigger requires HP ACPI/WMI hardware exposing >128 CMI instances (the finding cites EliteBook 840 G5/G8). This QEMU guest has no HP WMI hardware at all, so the acpi_hp driver does not attach (/dev/hpcmi does not exist), has_cmi == 0, and the vulnerable loop body is unreachable. This is the Phase 6 "valid hard blocker" case: the primitive is real but dead/unreachable at runtime on this guest. We prove the primitive at the harness level (analogous to DF-0594/0616/0281) and document the live trigger conditions.

Mechanism (trigger β†’ primitive β†’ effect), cited line-by-line

/dev/hpcmi is a world-readable device created by acpi_hp_attach() at line 523 (make_dev(&hpcmi_ops, 0, UID_ROOT, GID_WHEEL, 0644, "hpcmi")) when the CMI WMI GUID is detected. Any unprivileged user can open(2) it (acpi_hp_hpcmi_open, line 1078) and read(2) it (acpi_hp_hpcmi_read, line 1141). On first read, the read path populates sc->cmi_order[]:

  • Line 1164: if (sc->cmi_order_size < 0) β€” first-read initialization guard.
  • Line 1165: maxInstance = sc->has_cmi β€” the WMI CMI GUID instance count (BIOS-reported; can be > 128 on real HP EliteBook hardware).
  • Line 1171: sc->cmi_order_size = 0.
  • Lines 1172-1204: instance loop:
for (instance = 0; instance < maxInstance; ++instance) {
    if (acpi_hp_get_cmi_block(...)) {
        instance = maxInstance;          /* break out on failure */
    } else {
        pos = sc->cmi_order_size;
        for (i = 0; i<sc->cmi_order_size && i<127; ++i)   /* LINEAR SEARCH - bounded i<127 */
            if (sc->cmi_order[i].sequence > sequence) { pos = i; break; }

        for (i = sc->cmi_order_size; i>pos; --i) {         /* SHIFT LOOP - NOT bounded */
            sc->cmi_order[i].sequence = sc->cmi_order[i-1].sequence;
            sc->cmi_order[i].instance = sc->cmi_order[i-1].instance;
        }
        sc->cmi_order[pos].sequence = sequence;
        sc->cmi_order[pos].instance = instance;
        sc->cmi_order_size++;
    }
}

cmi_order is declared at softc line 147 as:

struct acpi_hp_inst_seq_pair cmi_order[128];     /* LAST field of struct acpi_hp_softc */

The linear search at line 1182 IS bounded (i < sc->cmi_order_size && i < 127), but the shift loop at line 1190 is NOT: it starts at i = sc->cmi_order_size and writes cmi_order[i] with no upper bound. On the 129th successful insertion cmi_order_size == 128, and either the shift loop or the final write at line 1198 produces cmi_order[128] β€” 8 bytes past the array end. Each additional successful insertion adds 8 more bytes of OOB write.

Because cmi_order is the LAST field of struct acpi_hp_softc, the OOB write goes into the slab slot immediately after the softc. The softc size is ~1.5 KB (cmi_order alone is 1024 B; earlier fields add ~0.5 KB), placing it in the kmalloc-2048 bucket alongside many other ~1-2 KB kernel objects.

Harness proof (the reproduction)

df1606_poc.c reproduces the loop logic with a heap-allocated fake softc matching the kernel's kmalloc layout: cmi_order is the last field of the struct, immediately followed in the same heap block by a 1024-byte "adjacent slab slot" canary. Build + run:

$ cc -O2 -o df1606_poc df1606_poc.c
$ ./df1606_poc
maxInstance           = 140
cmi_order array bound = 128 entries (1024 bytes)
cmi_order_size after  = 140 entries (overflow by 12 entries)
adjacent-slab canary corrupted bytes = 60 (first at +0)
[OK] heap OOB write past cmi_order[127] confirmed: 60 bytes
     kernel path: acpi_hp_hpcmi_read lines 1190-1202
     in-kernel effect: corruption of adjacent kmalloc-2048 slab
     slot -> INVARIANTS panic on default GENERIC; silent heap
     corruption on noinv.

With 140 successful CMI reads the harness writes 12 entries Γ— 8 bytes minus the trailing-zero sequence-inst pairs that overlap byte-for-byte with 0xAA β€” 60 bytes of canary are visibly corrupted, starting at offset 0 from the "next slab slot". The shift loop writes 60 distinct bytes; the rest are coincidentally 0xAA because of the {sequence, instance} pattern. The first 12 entries Γ— ~5 non-canary bytes each β‰ˆ 60 bytes.

Why no in-guest escalation chain (Phase 6 hard blocker)

Phase 6 requires pushing memory-corruption primitives to uid=0. Here we hit a valid hard blocker: the vulnerable code path is dead code at runtime on this guest. Specifically:

  • The acpi_hp driver attaches only to an ACPI HP WMI device (acpi_hp_probe() line 460 always returns 0 but the driver is registered only against the HP WMI GUID via acpi_wmi).
  • This QEMU/KVM guest has no HP WMI hardware, so acpi_hp.ko does not load, no softc is ever allocated, and /dev/hpcmi does not exist (ls /dev/hpcmi β‡’ "No such file or directory", verified).
  • Even if we kldload acpi_hp (a root action, which would invalidate any chain per the Phase 6 bright-line rule), the driver would still not attach without underlying WMI hardware, and has_cmi would be 0 so the loop body never executes.

The realistic preconditions for the live bug are "an HP EliteBook exposing

128 CMI instances, where the admin has not disabled ACPI/WMI" β€” that is a defensible real-world threat model per the audit rules, but not something we can produce inside a KVM guest with no HP ACPI tables.

Per the Phase 6 guidance for this exact case, we prove the primitive at the harness level (the OOB write is unambiguous and deterministic) and document the live trigger conditions, rather than fabricate a uid0 we cannot actually reach on this guest.

On the realistic-impact ceiling: with default GENERIC (INVARIANTS ON) the slab allocator's chunk_mark_allocated/WEIRD_ADDR 0xdeadc0de poisoning catches cross-slot corruption on free/reuse and panics β€” i.e. DoS. On a non-default noinv kernel the corruption is silent and could in principle escalate if (a) the attacker can shape the slab layout to land a victim object with an interesting function pointer / ucred * adjacent to the softc, and (b) the BIOS-supplied {sequence, instance} payload happens to produce a useful value at the right offset. The bytes are not directly attacker-shaped, so this would require per-firmware analysis on top of a non-default kernel.

Fix

fix.diff adds a minimal bound check at the top of the else branch, before the insert: if cmi_order_size >= nitems(cmi_order), log a warning and break out of the instance loop. The full git-apply-able diff is in fix.diff. Highlights:

else {
    if (sc->cmi_order_size >= (int)nitems(sc->cmi_order)) {
        device_printf(sc->dev, "CMI instance count exceeds %zu; "
            "truncating output\n", nitems(sc->cmi_order));
        break;
    }
    pos = sc->cmi_order_size;
    ...
}

nitems() comes from <sys/param.h> (already included by acpi_hp.c). device_printf() and sc->dev are existing softc/device facilities. The (int) cast is needed because cmi_order_size is int and nitems() returns size_t.

The fix matches (and sharpens) the finding proposal: "Fix: cap at nitems(cmi_order) or dynamic alloc." We choose the static-cap fix because the CMI output is bounded by /dev/hpcmi semantics (a one-shot BIOS information dump) β€” a 128-instance cap is generous and avoids the substantially larger dynamic-alloc change.

Fix validation

We validated fix.diff per Phase 8:

  • git apply --check succeeds against the host sys/ tree.
  • patch -p1 --forward < fix.diff succeeded in-guest on /usr/src (hunk #1 applied at line 1178).
  • make -j6 nativekernel KERNCONF=X86_64_GENERIC from /usr/src produced kernel.stripped and kernel.debug with rc=0 and no errors β€” full build log is in fix_build.log (35,778 lines).
  • The patched kernel was installed and the guest rebooted into kern.version = "DragonFly 6.5-DEVELOPMENT #2: Sat Jul 18 11:46:12 UTC 2026".
  • A "fixed-logic" harness (df1606_fixed.c) reproduces the loop WITH the fix's bound check in place: with maxInstance=140, cmi_order_size caps at 128 and the adjacent-slab canary shows 0 corrupted bytes. Run output is in fix_run.log.

fix_status: not_testable for the kernel-level PoC (the live kernel path cannot be triggered on this guest without HP WMI hardware). The fix itself is compile-validated and the logic is shown correct by the fixed-logic harness.

Files in this evidence pack

File Type Description
df1606_poc.c trigger-source userspace harness reproducing the OOB write
df1606_fixed.c fixed-logic-source same harness WITH the fix's bound check in place
build.sh build-script cc -O2 -o df1606_poc df1606_poc.c
run.sh run-script ./df1606_poc
build.log build-log full build output of the trigger PoC (exits 0)
run.log run-log full run output of the trigger PoC (60-byte OOB)
fix_run.log fix-run-log fixed-logic harness output (0-byte OOB)
fix_build.log fix-build-log full patched-kernel build (rc=0, 35,778 lines)
env.txt environment uname / kern.version / cc --version
fix.diff suggested-fix git-apply-able fix (matches finding proposal)
VERDICT.md verdict this file
manifest.json manifest machine-readable catalog

Fix verification

not_testable

compile+harness validated

kernel build rc=0 + harness before/after
↓ fix.diffDragonFly 6.5-DEVELOPMENT #2: Sat Jul 18 11:46:12 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (harness). acpi_hp cmi_order[128] shift loop no bounds -> 60B OOB past softc on 129th CMI insert. Needs HP WMI HW. /dev/hpcmi 0644.