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

Use-after-free of softc in acpi_hp_notify vs. acpi_hp_detach (DF-2381 AcpiOsWaitEventsComplete no-op)

Summary

acpi_hp_notify dispatched by acpi_wmi_notify_handler AFTER releasing acpi_wmi serial lock and does not take hp serial lock. acpi_hp_detach calls ACPI_WMI_REMOVE_EVENT_HANDLER then immediately tears down softc but AcpiOsWaitEventsComplete is empty no-op so AcpiRemoveNotifyHandler does not drain in-flight handler invocations. Notify running on another CPU can dereference freed softc. Same DF-2381 family root cause (acdragonfly.h:219-220 no-op). ACPI_WMI_REMOVE_EVENT_HANDLER only NULLs winfo->event_handler under lock neither waits for nor prevents concurrent dispatch that already snapshotted handler/handler_data. Local attacker with kldunload privilege racing against incoming ACPI WMI hotkey event. [VERIFIED pass2: reproduced deterministically 3 of 3 - stalled notify released after detach and kfree writes was_wlan_on_air into the replacement object at softc+56, late-dispatch variant panics at acpi_hp_notify+0x27; root cause STRENGTHENED - detach passes its own dev to the KBI so kobj_error_method means the handler is never unregistered at all; root kldunload plus HP hardware reach, no unpriv path; fix-validated correct remove target plus WMI dispatch drain]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2383 Β· 12 files
FileTypeDescriptionSize
README.md readme harness design + build/run instructions 2.8 KB ↓ raw
VERDICT.md verdict full narrative: line-accurate analysis, mechanical runs, reach/privesc assessment, fix A/B, independent sbuf bug note 8.2 KB ↓ raw
hp_sim.c source interposition harness (fake acpi_wmi bus on real acpi0 + KBI methods + stall/watchdog + victim reclaim + worker cmd plane) 33.8 KB view raw
build.sh build-script sha256-gated push + nativekernel build/install (baseline | fixed) 126 B view raw
run.sh run-script control/uaf x3/delayed scenario driver 145 B view raw
build.log build-log FULL nativekernel build+install output (rc=0) 5.6 MB ↓ download
run.log run-log baseline + fixed scenario outputs 4.9 KB view raw
serial.baseline_runs.txt serial-log untrimmed serial console record of the decisive baseline runs 3.4 KB view raw
panic.txt panic Fatal trap 12 (fault 0x8) at acpi_hp_notify+0x27 via hp_sim_inject_thread 2.6 KB view raw
fix.diff suggested-fix acpi_hp.c wrong-device remove + acpi_wmi.c dispatch drain (git-apply clean) 2.4 KB view raw
env.txt environment unames, config, sha256 gate, softc offsets, topology 1.7 KB view raw
verdict.json verdict-json machine verdict (persist_poc.py schema) 5.5 KB view raw
README.md readme harness design + build/run instructions
↓ download raw

DF-2383 β€” UAF of softc in acpi_hp_notify vs. acpi_hp_detach

File: sys/dev/acpica/acpi_hp/acpi_hp.c Verdict: REPRODUCED β€” see VERDICT.md + verdict.json

Harness design (verbatim-interposition, kernel-compiled)

  • sys/dev/acpica/acpi_hp/acpi_hp.c is compiled VERBATIM into the custom HPHARNESS kernel (stock INVARIANTS X86_64_GENERIC + device acpi_hp + device hp_sim); build.sh sha256-gates it at 58852b3530da0c99d3407428a75b5be0441e797a4bc1f21bb009faaf21fd429f.
  • hp_sim.c supplies the surrounding world:
  • a fake newbus bus driver literally named acpi_wmi, parented to the real acpi0 through the real BUS_ADD_CHILD (proper ACPI ivars), so the verbatim acpi_hp_attach resolves its parent softc to the real acpi_softc and creates its sysctls under hw.acpi.hp.* exactly like on HP hardware;
  • the 7 acpi_wmi_if KBI methods with the lock protocol of the real acpi_wmi.c (install :408-431, remove :441-460 β€” no drain, get_event_data :466-495 + the DF-2383 stall hook, evaluate_call with controllable replies);
  • event injection mirroring acpi_wmi_notify_handler (:627-653): handler+ctx copied under the WMI lock, lock RELEASED, handler invoked β€” plus an optional dispatch delay;
  • a worker-kthread command plane (hw.hpsim.cmd): a sysctl handler must not create sysctls (kernel_sysctl holds SYSCTL_SLOCK across handlers), so create/destroy/scenario run in the worker and are observed via hw.hpsim.cmd_done / hw.hpsim.log;
  • deterministic victim reclaim of the freed 1208-byte softc chunk (kmalloc(size, M_BUS) β€” the per-CPU free list is LIFO, so victim == old_sc on every run) with field-level diff reporting against a mirror of struct acpi_hp_softc (offsets printed at boot: wmi_dev=+8, verbose=+28, was_wlan_on_air=+56, ...).
  • Teardown is the REAL newbus path (device_delete_child β†’ verbatim acpi_hp_detach β†’ kfree(softc, M_BUS) β†’ device free) β€” the kldunload equivalent.

Build

sh build.sh          # baseline kernel
sh build.sh fixed    # fix.diff (acpi_hp.c remove-target + acpi_wmi.c
                     # dispatch drain, mirrored by HP_SIM_FIX_DRAIN=1)

Run

sh run.sh            # cmd 5 control, cmd 4 uaf x3, cmd 6 delayed

Baseline success criterion: RESULT: UAF WRITE CONFIRMED (deterministic write at softc+56 into the reclaimed victim) and the delayed variant's acpi_hp_notify+0x27 panic; fixed kernel: uaf_writes=0 everywhere, no panic.

Fix

fix.diff (git-apply-able, A/B validated): correct KBI device in acpi_hp_detach (sc->wmi_dev, not dev β€” the old call resolves to kobj_error_method ENXIO and never unregisters) + in-flight dispatch refcount/drain in acpi_wmi.c β€” the DF-2381 family fix shape.

VERDICT.md verdict full narrative: line-accurate analysis, mechanical runs, reach/privesc assessment, fix A/B, independent sbuf bug note
↓ download raw

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.

Fix verification

not_testable
↓ fix.diffper-fix-DF-2383

Confirmed kernel references

Detail

Exploit chain

root on HP WMI hardware: kldunload acpi_hp while WMI hotkey events arrive -> acpi_wmi_notify_handler copies handler+ctx under the WMI lock, releases it, then calls acpi_hp_notify -> concurrently acpi_hp_detach returns (its REMOVE call is a kobj ENXIO no-op on the wrong device) and newbus kfrees the 1208-byte softc and the device struct -> the still-running notify continues on the freed softc: reads sc->wmi_dev/sc->verbose/flags and writes sc->was_*_on_air (byte 0/1 toggle at +56/+60/+64) -> demonstrated deterministically as a write into a replacement M_BUS object, and as a page-fault panic (acpi_hp_notify+0x27) when the dispatch lands after the free. Grooming the replacement chunk is trivial for the root attacker that is already required, so the write primitive is real but adds nothing beyond root: no unprivileged privesc chain exists.

Evidence (decisive lines)

run.log (baseline: CONTROL 0 writes vs UAF WRITE at softc+56 3/3, RESULT: UAF WRITE CONFIRMED; fixed: drain=1, 0 writes 3/3; delayed: baseline panic vs fixed clean); panic.txt (Fatal trap 12 fault 0x8 at acpi_hp_notify+0x27 from hp_sim_inject_thread); serial.baseline_runs.txt (full untrimmed serial of the decisive baseline scenarios); run.fixed.hpsimlog.txt (full kernel-side log of the fixed runs); hp_sim.c (interposition harness); fix.diff (validated); build.log (nativekernel build+install, rc=0); env.txt (kernel/config/sha256 gate/offsets)

PoC changes

No seed PoC existed; built the verbatim-interposition kernel harness from scratch: fake acpi_wmi bus driver parented to the REAL acpi0 (so acpi_device_get_parent_softc resolves to the real acpi_softc and hw.acpi.hp.* lands in the real tree), all 7 acpi_wmi_if KBI methods mirroring the real lock protocol, worker-kthread command plane (a sysctl handler may not create sysctls: kernel_sysctl holds SYSCTL_SLOCK across handlers), stall hook inside the interposed ACPI_WMI_GET_EVENT_DATA, deterministic M_BUS victim reclaim of the freed softc, and HP_SIM_FIX_DRAIN mirroring the acpi_wmi.c drain hunk for A/B. Harness-only discoveries: nexus parenting dies in acpi_child_pnpinfo (needs real ACPI ivars via BUS_ADD_CHILD on acpi0) and the has_cmi=0 detach path panics in sbuf_delete (independent bug, noted in VERDICT.md).

Verified recommended fix

acpi_hp_detach: call ACPI_WMI_REMOVE_EVENT_HANDLER(sc->wmi_dev,...) (not dev); acpi_wmi: refcount in-flight dispatches under the WMI lock in acpi_wmi_notify_handler and drain them (lksleep) in acpi_wmi_remove_event_handler_method

Verdict

UAF write through the real acpi_hp_notify confirmed deterministically (3/3) with the verbatim acpi_hp.c compiled into a custom INVARIANTS kernel and the WMI world interposed: a notify stalled mid-handler while the real detach+newbus kfree(softc,M_BUS) ran underneath, the freed 1208-byte chunk was reclaimed by a kmalloc victim (victim==old_sc every run), and the released handler wrote sc->was_wlan_on_air (softc+56, acpi_hp.c:403) into the victim; the copied-but-late-dispatch variant panicked the kernel at acpi_hp_notify+0x27. Root cause confirmed line-accurate: AcpiOsWaitEventsComplete is an empty macro (acdragonfly.h:219-220), acpi_wmi_notify_handler dispatches after releasing the WMI lock (acpi_wmi.c:649-651), acpi_hp_notify takes no hp lock, and acpi_hp_detach removes the handler on the WRONG device (kobj_error_method ENXIO, so the handler is never unregistered at all). Reach is root/kldunload racing hardware WMI events on HP laptops: a genuine kernel UAF write plus panic, but no unprivileged trigger path, so not a privesc. fix.diff (correct remove target + in-flight dispatch drain in acpi_wmi) A/B validated: 0 victim writes and no panic with identical inputs.