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)
PoC verification
Evidence pack
findings/poc/DF-1606 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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 viaacpi_wmi). - This QEMU/KVM guest has no HP WMI hardware, so
acpi_hp.kodoes not load, no softc is ever allocated, and/dev/hpcmidoes 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, andhas_cmiwould 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 --checksucceeds against the hostsys/tree.patch -p1 --forward < fix.diffsucceeded in-guest on/usr/src(hunk #1 applied at line 1178).make -j6 nativekernel KERNCONF=X86_64_GENERICfrom/usr/srcproducedkernel.strippedandkernel.debugwith rc=0 and no errors β full build log is infix_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: withmaxInstance=140,cmi_order_sizecaps at 128 and the adjacent-slab canary shows 0 corrupted bytes. Run output is infix_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_testablecompile+harness validated
kernel build rc=0 + harness before/after
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.
No comments yet.