# DF-2383 — PoC verdict

**File:** `sys/dev/acpica/acpi_hp/acpi_hp.c`

**Verdict:** **REPRODUCED** — deterministic use-after-free WRITE through the
real `acpi_hp_notify` running after the real `acpi_hp_detach` + newbus
softc free; also a kernel PANIC variant of the same window. The verified
fix.diff makes both manifestations disappear (A/B validated).

## Line-accurate confirmation

1. `sys/contrib/dev/acpica/source/include/platform/acdragonfly.h:219-220`
   — `AcpiOsWaitEventsComplete()` is defined as an **empty macro**: ACPICA
   notify-handler removal never drains in-flight dispatches on DragonFly.
2. `sys/dev/acpica/acpi_wmi/acpi_wmi.c:638-651` —
   `acpi_wmi_notify_handler` copies `winfo->event_handler` +
   `event_handler_user_data` under the WMI serial lock, **releases the
   lock** (`ACPI_SERIAL_END`, :649), and only then invokes the handler
   (:651). A detach can complete entirely between those two points.
3. `sys/dev/acpica/acpi_hp/acpi_hp.c:810-826` — `acpi_hp_notify` takes
   **no** `hp` serial lock; it dereferences `dev`/`sc`
   (`device_get_softc`, :815), `sc->wmi_dev` (:818) and then calls
   `acpi_hp_evaluate_auto_on_off(sc)` (:822), which reads `sc->wmi_dev`
   (:351), `sc->verbose` (:357), the six `*_if_radio_*` flags
   (:359-398) and **writes** `sc->was_wlan_on_air` (:403),
   `sc->was_bluetooth_on_air` (:416), `sc->was_wwan_on_air` (:431).
4. `sys/dev/acpica/acpi_hp/acpi_hp.c:579-605` — `acpi_hp_detach` removes
   the handler (:591) — **but on the wrong device**: the KBI macro
   dispatches kobj on the passed `device_t` (generated `acpi_wmi_if.h`:
   `KOBJOPLOOKUP(((kobj_t)dev)->ops, ...)`), and the acpi_hp driver does
   not implement `acpi_wmi_if`, so the lookup falls back to
   `kobj_error_method()` (`sys/kern/subr_kobj.c:69-73`, returns ENXIO).
   **The handler is never actually unregistered.** Detach then frees the
   sysctl ctx, and after it returns newbus frees the softc
   (`device_set_driver(dev, NULL)` → `kfree(dev->softc, M_BUS)`,
   `sys/kern/subr_bus.c:1941-1944`) and the device struct
   (`kobj_delete`, subr_bus.c:1307).
5. Net: any WMI event dispatched (or merely copied-but-not-yet-invoked)
   around `kldunload acpi_hp` operates on freed memory — and because of
   (4), any event *after* unload is guaranteed to dispatch with a freed
   device as context.

## Mechanical verification (verbatim-interposition harness)

Kernel-compiled harness (HPHARNESS = stock INVARIANTS
X86_64_GENERIC + `device acpi_hp` + `device hp_sim`):

- `sys/dev/acpica/acpi_hp/acpi_hp.c` compiled **verbatim**
  (sha256-gated: `58852b3530da0c99d3407428a75b5be0441e797a4bc1f21bb009faaf21fd429f`).
- `hp_sim.c` provides the surrounding world: a fake newbus bus driver
  literally named `acpi_wmi`, parented to the **real acpi0** (so the
  verbatim `acpi_hp_attach`:495 resolves its parent softc to the REAL
  `acpi_softc` and creates its sysctls under `hw.acpi.hp.*` exactly as
  on HP hardware), implementing the 7 `acpi_wmi_if` KBI methods with the
  same lock protocol as the real `acpi_wmi.c`. The real verbatim
  `acpi_hp_probe/attach/detach/notify/sysctl` code all runs; teardown is
  the real newbus `device_delete_child` path (detach → kfree(softc) →
  device free) — the kldunload equivalent.
- Event injection mirrors `acpi_wmi_notify_handler` (copy under lock,
  release, then invoke). The DF-2383 stall hook parks the handler
  *inside the real `acpi_hp_notify`* (inside the interposed
  `ACPI_WMI_GET_EVENT_DATA` it calls at :818). The freed 1208-byte M_BUS
  chunk is then deterministically reclaimed by a kmalloc victim
  (per-CPU free list LIFO — `victim==old_sc hit=1` in every run), the
  victim is poisoned, and the handler is released.

Results (baseline kernel, verbatim file):

| scenario | result |
|---|---|
| control (no in-flight notify) | victim diff **0 bytes** — teardown/reclaim itself is clean |
| uaf ×3 | **`uaf victim WRITE at softc+56 (field was_wlan_on_air): 0x00 -> 0x01`** — `RESULT: UAF WRITE CONFIRMED` 3/3, deterministic |
| delayed (handler copied under the lock, invoked 2 s after teardown) | **kernel panic**: `Fatal trap 12, fault va=0x8` at `acpi_hp_notify+0x27` (`sc->wmi_dev` read through the freed device) — panic.txt |
| live-path sanity | `acpi_wmi39: Wireless status is 100` (verbose printf through the real `acpi_hp_evaluate_auto_on_off`) |

The `0x00 -> 0x01` write at softc+56 is exactly
`sc->was_wlan_on_air = sc->was_wlan_on_air?0:1` (acpi_hp.c:403) landing
in the replacement object.

## Reachability / privesc assessment (honest)

- Entry: **root** (`kldunload acpi_hp` / driver teardown) racing WMI
  hotkey events on HP WMI hardware (acpi_hp attaches only when the HP
  BIOS GUIDs exist; module load is typically automatic on such laptops).
- The window is not merely theoretical: the handler is invoked after the
  WMI lock is released and its body includes a full `_WED` ACPI control
  method evaluation (milliseconds of AML on real firmware), so hotkey
  spam during unload hits it easily; plus bug (4) guarantees
  post-free dispatch of the *next* event with no race at all.
- Attacker-controlled shaping: the write is 1 byte (a 0/1 toggle) at a
  fixed offset (+56/+60/+64) into a freed 1208-byte `M_BUS` chunk.
  Reallocation with attacker-shaped content requires an M_BUS consumer
  the attacker can groom — plausible for root (trivially: root can spray
  M_BUS objects of this size class and craft content), **but root is
  already the prerequisite**, so this is *not* an unprivileged privesc.
  No unprivileged trigger path exists (module unload and driver teardown
  are root-only).
- Classification: genuine UAF write primitive + UAF-read-driven panic;
  severity Medium upheld (kernel memory corruption, root/kldunload-gated
  + HP firmware/hardware-gated). NOT closeable as privesc.

## Independent bug discovered during verification (not filed here)

`acpi_hp_attach` never initializes `sc->hpcmi_bufptr` when the CMI GUID
is absent (`has_cmi=0`); the zeroed softc then has
`hpcmi_bufptr == 0 != -1` and `acpi_hp_detach` (:595-598) calls
`sbuf_delete()` on the zeroed `hpcmi_sbuf` — observed live as
`panic: sbuf_delete called with uninitialized or corrupt sbuf`
(INVARIANTS) during a clean teardown. The harness claims the CMI GUID to
avoid it; a maintainer fix should initialize `hpcmi_bufptr = -1`
unconditionally at attach.

## Fix validation (A/B)

`fix.diff` (git-apply-able, verified against the repo tree):

1. `acpi_hp.c` — `ACPI_WMI_REMOVE_EVENT_HANDLER(sc->wmi_dev, ...)` in
   detach (correct device; without this the drain in (2) is unreachable
   from this driver).
2. `acpi_wmi.c` — in-flight dispatch refcount (`sc->wmi_dispatching`)
   incremented in `acpi_wmi_notify_handler` while the WMI lock is held,
   and `acpi_wmi_remove_event_handler_method` drains
   (`lksleep(sc, &acpi_wmi_serial, ...)`) before returning. This is the
   DF-2381 family fix shape (drain protocol standing in for the no-op
   `AcpiOsWaitEventsComplete`).

The harness mirrors (2) via `HP_SIM_FIX_DRAIN=1` in the interposed WMI
layer (the harness kernel does not compile the real acpi_wmi.c; the
sys/ hunk itself is part of fix.diff). Fixed kernel results:

- control: 0 writes (unchanged)
- uaf ×3: `remove_event_handler: removed (drain=1)` — detach **blocked**
  until the stalled handler finished on the LIVE softc; victim diff
  **0 bytes**; `uaf_writes=0` 3/3
- delayed: teardown drains the 2 s-late dispatch; **no panic** (baseline
  panicked here); guest stays up

## Classification

- `status`: reproduced
- `reproduced`: 1
- `impact`: panic (the demonstrable runtime impact: deterministic
  write-into-freed-chunk primitive captured, plus the panic variant of
  the same window; no unprivileged trigger path exists — root/kldunload
  gated, so uid0_privesc does not apply)
- `confidence`: certain
- `fix_status`: fixed (A/B validated: baseline UAF write 3/3 + panic vs
  fixed 0 writes + no panic, conformant behavior unchanged)

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