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

acpi_cpu has no detach teardown: ACPI notify handler (context=sc), both sysctl contexts and the sensordev outlive the softc β†’ use-after-free on driver/module detach; attach error paths leak the same resources and orphan the first child

Summary

The notify handler is installed with the softc as context and never removed; bare bus_generic_detach frees the softc while the handler, sysctl OIDs (children hang handlers on glob ctx) and sensordev keep referencing it - the next 0x80/0x81 notify dispatches over freed memory (UAF call through cpu_cst_notify). Gated: root + module-built acpi (compiled-in on the guest - unload refused, evidence captured). Sibling drivers DO remove handlers. Remove-first detach fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5006 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.8 KB ↓ raw
VERDICT.md β€” 2.6 KB ↓ raw
fix.diff β€” 2.6 KB view raw
env_probe.txt β€” 1.1 KB view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 3.2 KB view raw

DF-5006 β€” acpi_cpu: no detach teardown β€” stale ACPI notify handler, sysctl contexts and sensordev outlive the softc (UAF), attach error paths leak them

sys/dev/acpica/acpi_cpu.c installs three things into global namespaces and never removes any of them:

  • AcpiInstallNotifyHandler(handle, ACPI_DEVICE_NOTIFY, acpi_cpu_notify, sc) at acpi_cpu.c:252 β€” context = sc (the softc);
  • two sysctl contexts with persistent OIDs: glob_sysctl_ctx (hw.acpi.cpu, :171-178, holds the children's cx_lowest etc. handlers added via sc->cst_parent->glob_sysctl_ctx, acpi_cpu_cstate.c:759-774) and pcpu_sysctl_ctx (hw.acpi.cpuN, :180-188);
  • sensordev_install(&sc->cpu_sensdev) at :233.

Yet the driver's detach method is plain bus_generic_detach (acpi_cpu.c:67) β€” it detaches the cpu_cst/cpu_pst children and nothing else. After a driver detach (module unload of an acpi.ko built as a module), the softc is freed by newbus (sys/kern/subr_bus.c:1941-1944) while:

  • the ACPI notify handler still points at it β€” the next Notify(cpu, 0x80/ 0x81) from AML (P-state/C-state changes fire routinely, e.g. on resume or thermal events) dispatches acpi_cpu_notify (:324-342) which reads sc->cpu_cst_notify / sc->cpu_pst_notify and sc->cpu_dev from freed memory β†’ use-after-free (call through a freed pointer β†’ RIP control lands on whatever the slab now holds; realistically a panic);
  • the sysctl OIDs registered against &sc->glob_sysctl_ctx / &sc->pcpu_sysctl_ctx keep resolving hw.acpi.cpu.cx_lowest etc. into handlers whose context pointers reference freed softc memory β†’ UAF read/ write on sysctl access;
  • the sensordev stays in the global sensordev_list with a dangling softc-backed name.

The notify-function-pointer protocol has the same hole without unload only in theory: children set cpu_cst_notify at attach (acpi_cpu_cstate.c:751, acpi_cpu_pstate.c:810) before AcpiInstallNotifyHandler runs at acpi_cpu.c:252 (bus_generic_attach at :250 precedes it), so install-order is safe β€” but nothing ever clears the pointers, so the window opens the moment a child detaches before the parent.

Attach error paths leak the same resources: if the first BUS_ADD_CHILD fails (:236-237) both sysctl contexts and the sensordev stay installed; if the second fails (:243-244) the first child is orphaned on top. (The sysctl_ctx_free(&sc->glob_sysctl_ctx) at :186 on a softc where glob_sysctl_ctx was never initialized for cpu_id != 0 is harmless only because newbus zero-allocates softc β€” sys/kern/subr_bus.c:1952-1954 β€” and sysctl_ctx_free on a zeroed TAILQ head is a no-op.)

Sibling drivers in the same directory do remove their handlers (acpi_thermal.c:384, acpi_video, acpi_cmbat, …), which makes the omission a genuine divergence, not a convention.

Gating (why this is Low and untested on the guest)

  • Requires a privileged operator: the only route to acpi_cpu detach on DragonFly is unloading an acpi.ko built as a module (DRIVER_MODULE(cpu, acpi, …) driver detach during kldunload); there is no unprivileged or remote trigger, and no runtime device-delete path for ACPI cpu devices.
  • On the audit guest acpi is compiled into the kernel (kldstat shows no acpi.ko; kldunload acpi β†’ "can't find file acpi", see env_probe.txt), so the path cannot be exercised there at all.

No PoC is provided (contract: skip for Low, root+build-config gated). The fix.diff adds a real acpi_cpu_detach (remove handler β†’ detach children β†’ free both contexts β†’ deinstall sensordev) and fixes the attach error paths.

Environment evidence (guest)

See env_probe.txt: live cpu_cst0..5 children, hw.acpi.cpu.* C1-only sysctls (matches the corepower audit), acpi compiled-in β†’ unload refused.

VERDICT.md
↓ download raw

DF-5006 VERDICT β€” UNTESTED (gated: privileged unload + module-built acpi); code defect certain

What is certain (code): acpi_cpu.c installs an ACPI notify handler with sc as context (acpi_cpu.c:252), two sysctl contexts (:171, :180) that the children also hang handlers on (acpi_cpu_cstate.c:759-774), and a sensordev (:233), and provides no teardown: device_detach is bare bus_generic_detach (acpi_cpu.c:67). Newbus frees the softc on driver detach (sys/kern/subr_bus.c:1941-1944), after which (a) any Notify(.,0x80/0x81) from AML dispatches acpi_cpu_notify (:324-342) over a freed softc β€” read of sc->cpu_cst_notify/sc->cpu_pst_notify and call through it = use-after-free; (b) hw.acpi.cpu.cx_lowest-style sysctls resolve handlers whose ctx pointers reference freed softc memory; (c) the sensordev stays globally registered. Attach error paths (:236-237, :243-244) leak the same resources without any detach at all.

Why not exercised on the guest: the only route to this driver's detach is unloading an acpi.ko built as a module β€” a privileged, non-default configuration. On the audit guest acpi is compiled into the kernel (env_probe.txt: kldunload acpi β†’ "can't find file acpi"), so the path is unreachable there; no unprivileged or remote trigger exists. Per contract (Low severity, privileged-only), no PoC was attempted β€” status untested, not a refutation.

Reachability detail for maintainers: the notify-pointer protocol is attach-order-safe today (bus_generic_attach at :250 precedes AcpiInstallNotifyHandler at :252, and children set the pointers in their attach β€” acpi_cpu_cstate.c:751, acpi_cpu_pstate.c:810) β€” the UAF needs the driver/module teardown that this driver simply doesn't implement. FreeBSD's acpi_cpu grew a proper detach (AcpiRemoveNotifyHandler + sysctl_ctx_free + sensordev_deinstall) years ago; DragonFly's 2008 import never got it.

Fix: fix.diff β€” acpi_cpu_detach (remove handler first, then detach children, then free both contexts, then deinstall sensordev) + attach error-path cleanup (delete orphaned cpu_cst child, free contexts, deinstall sensordev). Not validated on the guest (path unreachable there); kernel-build of this diff is straightforward (no API changes) but was not performed since the guest cannot reach the code path either way.

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
baseline no→ patch + rebuild →patched clean

fix.diff authored (real detach + error-path cleanup); not validated: the detach path is unreachable on this guest (acpi compiled into the kernel) and building a module-acpi custom kernel was out of scope for a Low finding.

fix.diff
↓ fix.diffper-fix-DF-5006

Confirmed kernel references

Detail

Evidence (decisive lines)

fix.diff (proposed teardown); env_probe.txt (guest: cpu_cst0..5 live, acpi compiled-in, kldunload refused β€” path unreachable on guest)

PoC changes

no seed; no runnable PoC (path gated on module-built acpi + kldunload)

Verified recommended fix

add acpi_cpu_detach: AcpiRemoveNotifyHandler before bus_generic_detach, then sysctl_ctx_free(glob+pcpu) and sensordev_deinstall; fix attach error paths to free the same (fix.diff)

Verdict

UNTESTED by design (contract: Low severity + privileged-only trigger; guest path unreachable). Code defect is certain: acpi_cpu.c installs an ACPI_DEVICE_NOTIFY handler with the softc as context (:252), two sysctl contexts (:171/:180) that children hang handlers on (acpi_cpu_cstate.c:759-774), and a sensordev (:233), but device_detach is bare bus_generic_detach (:67) β€” nothing is ever removed. On driver/module detach newbus frees the softc (subr_bus.c:1941-1944) while the handler, sysctl OIDs and sensordev keep referencing it: the next AML Notify(cpu,0x80/0x81) runs acpi_cpu_notify (:324-342) over freed memory (UAF call through cpu_cst_notify/cpu_pst_notify) and sysctl access hits handlers with dangling ctx. Attach error paths (:236-237, :243-244) leak the same resources without any unload. Gating: only reachable by unloading an acpi.ko built as a module (root-only, non-default config); guest has acpi compiled-in (kldunload acpi -> 'can't find file', env_probe.txt) so the path cannot be exercised there. fix.diff adds a real acpi_cpu_detach + error-path cleanup (modeled on the sibling drivers that DO remove handlers, e.g. acpi_thermal.c:384).