# 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:

```c
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:

```c
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:

```c
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                                 |
