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)
PoC verification
Evidence pack
findings/poc/DF-2383 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.cis compiled VERBATIM into the customHPHARNESSkernel (stock INVARIANTSX86_64_GENERIC+device acpi_hp+device hp_sim);build.shsha256-gates it at58852b3530da0c99d3407428a75b5be0441e797a4bc1f21bb009faaf21fd429f.hp_sim.csupplies the surrounding world:- a fake newbus bus driver literally named
acpi_wmi, parented to the real acpi0 through the realBUS_ADD_CHILD(proper ACPI ivars), so the verbatimacpi_hp_attachresolves its parent softc to the realacpi_softcand creates its sysctls underhw.acpi.hp.*exactly like on HP hardware; - the 7
acpi_wmi_ifKBI methods with the lock protocol of the realacpi_wmi.c(install:408-431,remove:441-460 β no drain,get_event_data:466-495 + the DF-2383 stall hook,evaluate_callwith 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_sysctlholdsSYSCTL_SLOCKacross handlers), so create/destroy/scenario run in the worker and are observed viahw.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, sovictim == old_scon every run) with field-level diff reporting against a mirror ofstruct 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β verbatimacpi_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.
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
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.sys/dev/acpica/acpi_wmi/acpi_wmi.c:638-651βacpi_wmi_notify_handlercopieswinfo->event_handler+event_handler_user_dataunder 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.sys/dev/acpica/acpi_hp/acpi_hp.c:810-826βacpi_hp_notifytakes nohpserial lock; it dereferencesdev/sc(device_get_softc, :815),sc->wmi_dev(:818) and then callsacpi_hp_evaluate_auto_on_off(sc)(:822), which readssc->wmi_dev(:351),sc->verbose(:357), the six*_if_radio_*flags (:359-398) and writessc->was_wlan_on_air(:403),sc->was_bluetooth_on_air(:416),sc->was_wwan_on_air(:431).sys/dev/acpica/acpi_hp/acpi_hp.c:579-605βacpi_hp_detachremoves the handler (:591) β but on the wrong device: the KBI macro dispatches kobj on the passeddevice_t(generatedacpi_wmi_if.h:KOBJOPLOOKUP(((kobj_t)dev)->ops, ...)), and the acpi_hp driver does not implementacpi_wmi_if, so the lookup falls back tokobj_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).- Net: any WMI event dispatched (or merely copied-but-not-yet-invoked)
around
kldunload acpi_hpoperates 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.ccompiled verbatim (sha256-gated:58852b3530da0c99d3407428a75b5be0441e797a4bc1f21bb009faaf21fd429f).hp_sim.cprovides the surrounding world: a fake newbus bus driver literally namedacpi_wmi, parented to the real acpi0 (so the verbatimacpi_hp_attach:495 resolves its parent softc to the REALacpi_softcand creates its sysctls underhw.acpi.hp.*exactly as on HP hardware), implementing the 7acpi_wmi_ifKBI methods with the same lock protocol as the realacpi_wmi.c. The real verbatimacpi_hp_probe/attach/detach/notify/sysctlcode all runs; teardown is the real newbusdevice_delete_childpath (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 realacpi_hp_notify(inside the interposedACPI_WMI_GET_EVENT_DATAit 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=1in 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
_WEDACPI 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_BUSchunk. 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):
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).acpi_wmi.cβ in-flight dispatch refcount (sc->wmi_dispatching) incremented inacpi_wmi_notify_handlerwhile the WMI lock is held, andacpi_wmi_remove_event_handler_methoddrains (lksleep(sc, &acpi_wmi_serial, ...)) before returning. This is the DF-2381 family fix shape (drain protocol standing in for the no-opAcpiOsWaitEventsComplete).
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=03/3 - delayed: teardown drains the 2 s-late dispatch; no panic (baseline panicked here); guest stays up
Classification
status: reproducedreproduced: 1impact: 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: certainfix_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_testableConfirmed kernel references
- sys/dev/acpica/acpi_hp/acpi_hp.c:810-826
- sys/dev/acpica/acpi_hp/acpi_hp.c:815
- sys/dev/acpica/acpi_hp/acpi_hp.c:822
- sys/dev/acpica/acpi_hp/acpi_hp.c:351-431
- sys/dev/acpica/acpi_hp/acpi_hp.c:579-605
- sys/dev/acpica/acpi_hp/acpi_hp.c:591-592
- sys/dev/acpica/acpi_wmi/acpi_wmi.c:627-653
- sys/dev/acpica/acpi_wmi/acpi_wmi.c:649-651
- sys/dev/acpica/acpi_wmi/acpi_wmi.c:441-460
- sys/contrib/dev/acpica/source/include/platform/acdragonfly.h:219-220
- sys/kern/subr_bus.c:1941-1944
- sys/kern/subr_kobj.c:69-73
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.
No comments yet.