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

acpi_button has no device_detach: ACPI notify handler and fixed-event handler (context = softc) plus PRW wake GPE are never removed β€” DF-5006/DF-5074 family teardown gap, guaranteed UAF if a detach path is ever added

Summary

Handlers keep context=sc forever; today pinned resources + blocked kldunload (structurally non-UAF, verified live on guest: kldunload acpi ENOENT, module builds abort at bus_generic_detach). Any future detach path turns a button press into a softc UAF. Mirror-attach detach fix authored (with the AcpiOsExecute-drain caveat noted).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5091 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 4.0 KB ↓ raw
trigger.sh β€” 1.2 KB view raw
build.sh β€” 133 B view raw
run.sh β€” 160 B view raw
run.log β€” 979 B view raw
env.txt β€” 660 B view raw
fix.diff β€” 1.8 KB view raw
manifest.json β€” 948 B view raw
verdict.json β€” 3.7 KB view raw

DF-5091 β€” acpi_button: no device_detach β†’ ACPI notify/fixed-event handlers (context = softc) are never removed

What the finding is (source-level, certain)

sys/dev/acpica/acpi_button.c registers two kinds of ACPICA callbacks whose context is the per-device softc and never removes either:

  • AcpiInstallFixedEventHandler(event, acpi_button_fixed_handler, sc) (acpi_button.c:150-151, fixed PNP0C0C/ACPI_FPB & PNP0C0E/ACPI_FSB path)
  • AcpiInstallNotifyHandler(handle, ACPI_DEVICE_NOTIFY, acpi_button_notify_handler, sc) (acpi_button.c:158-159, AML path)

The methods table (acpi_button.c:74-83) has no device_detach entry β€” probe/attach/suspend/shutdown/resume only. Both handlers defer work via AcpiOsExecute(OSL_NOTIFY_HANDLER, ..., sc) (acpi_button.c:255, 258), and attach also enables a wake GPE that is never disabled (acpi_button.c:169-170). This is the DF-5006 (acpi_cpu) / DF-5074 (acpi_acad) family, mirrored.

Consequences: 1. If a detach path is ever added (bus re-enumeration, driver split into its own module, force-unload), the still-registered ACPICA handler fires into freed softc memory β†’ UAF. The AcpiOsExecute indirection widens the race window past AcpiRemoveNotifyHandler's own quiesce. 2. Today it is structurally non-UAF: acpi_button.c is compiled into acpi.ko (sys/dev/acpica/Makefile:66) or into the base kernel (device acpi in X86_64_GENERIC), and the acpi bus driver's own detach is bus_generic_detach (sys/dev/acpica/acpi.c:182) β€” detaching acpi0 walks the children, hits the missing child detach method, gets ENXIO, and the unload aborts before any softc is freed. Resource pinning + permanently blocked kldunload acpi.

Reproduce

Nothing to build. Run on the host repo root:

findings/poc/DF-5091/run.sh     # pushes trigger.sh to the guest, runs it as root

Expected (matches run.log): - kldstat -v | grep button β†’ acpi/acpi_button driver registered - dmesg | grep acpi_button β†’ empty (no acpi_button device on this guest; QEMU's buttons are fixed events handled by acpi.c:1974-1993, whose "acpi0: Power Button (fixed)" line is the only button-ish dmesg output) - kldunload acpi β†’ can't find file acpi: No such file or directory (ACPI is built into the kernel; there is no module to unload) - guest stays up (uptime at the end)

Verdict

not_reproduced / impact none: the UAF half of the gap has no reachable trigger on this system (no acpi_button device instance, no unload path at all); the pinning/blocked-teardown half is confirmed by the run. The structural gap itself is certain from source. Severity: Low (matches the DF-5006/DF-5074 family precedent).

fix.diff adds the missing acpi_button_detach (git-apply --check clean; NOT applied β€” sys/ is a read-only audit target). Not build-validated: fix validation is only mandatory for reproduced memory corruption.

VERDICT.md
↓ download raw

VERDICT β€” DF-5091 (acpi_button no-detach teardown gap)

status: not_reproduced | reproduced: false | impact: none | confidence: certain (structural gap) guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 ... x86_64 (X86_64_GENERIC), up, clean

What was claimed

acpi_button_attach() installs ACPICA callbacks with context = sc (AcpiInstallFixedEventHandler at sys/dev/acpica/acpi_button.c:150-151, AcpiInstallNotifyHandler at :158-159) and the driver has no device_detach (methods table at :74-83) β€” so the handlers, the AcpiOsExecute deferrals they queue (:255, :258) and the wake GPE enabled at :169-170 are never torn down. DF-5006 (acpi_cpu) / DF-5074 (acpi_acad) family mirror.

What was run (see run.log, trigger.sh)

  1. uname -a β€” DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC.
  2. kldstat β€” kernel + ehci.ko + xhci.ko only; no acpi.ko module (ACPI compiled in via device acpi, X86_64_GENERIC:59).
  3. kldstat -v | grep button β€” acpi/acpi_button driver registered in the builtin kernel.
  4. dmesg | grep acpi_button β€” empty: no acpi_button device attached on this guest. The single button-ish dmesg line, acpi0: Power Button (fixed), comes from acpi.c:1974-1993 (acpi_enable_fixed_events), where acpi.c installs its own fixed-event handlers (context = acpi_softc) because QEMU's FADT declares the fixed buttons.
  5. kldunload acpi β†’ can't find file acpi (rc=1); kldunload -a β†’ illegal option. No unload path exists on this system.
  6. Guest alive (uptime), no panic, no dirt.

Why not_reproduced (and why that is the honest classification)

The finding has two halves:

  • Structural gap (confirmed, certain): source shows no detach, no handler removal, no GPE undo. Runtime confirms the pinning consequence: the driver cannot be removed from the running system.
  • UAF (not reachable today): freeing the softc requires a successful device_detach of an acpi_button device. On this guest there is no such device; in a module build, unloading acpi.ko routes through bus_generic_detach (sys/dev/acpica/acpi.c:182) β†’ child DETACH method missing β†’ ENXIO β†’ unload aborts before any softc is freed. Hence "structurally non-UAF today" β€” exactly the DF-5074 conclusion, now also demonstrated live (blocked/absent unload on the running guest).

Exploit chain: none possible on this configuration. An attacker gains nothing: no user-triggerable detach path exists (DragonFly has no devd/devctl "detach" operation, no kldunload -f), and unprivileged users cannot kldunload at all (priv required).

Exploitability ceiling (if a detach path is ever added)

Power/sleep button notify β†’ acpi_button_notify_handler β†’ AcpiOsExecute(..., sc) fires after detach frees the softc β†’ UAF with attacker-influenced timing (press button while unload in flight). That is the reason the fix belongs in-tree now, cheaply.

Fix

fix.diff (generated against the read-only sys/ tree; git apply --check clean; NOT applied): adds acpi_button_detach() mirroring attach β€” removes the fixed-event handler or the notify handler. Residual hardening note in the diff: AcpiRemoveNotifyHandler does not drain AcpiOsExecute work already queued; a generation counter in the softc would close that.

Fix validation (patched-kernel rebuild) not performed: mandatory only when a memory-corruption finding reproduces; here nothing reproduces (baseline clean, no fault to A/B). fix_status: not_testable.

Attempts

1 decisive run (run.log); guest reused from the earlier i915/acpi inspection, no reset needed (no state was modified β€” all probes read-only, kldunload attempts failed benignly).

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 and git-apply --check clean against sys/ but not build-validated: the finding produced no runtime fault on the baseline guest (structurally non-UAF today; no acpi_button device, no unload path), so there is no bad behavior to A/B. Fix validation is mandatory only when a memory-corruption finding reproduces.

findings/poc/DF-5091/fix.diff (proposed, unapplied; adds acpi_button_detach + DEVMETHOD entry; notes the AcpiOsExecute drain requirement)
↓ fix.diffper-fix-DF-5091

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (decisive: 'kldstat -v' shows acpi/acpi_button driver registered; 'dmesg|grep acpi_button' empty; 'kldunload acpi' fails 'can't find file acpi'; guest stays up). VERDICT.md for the full path:line trace; fix.diff for the proposed detach (git apply --check clean, not applied).

PoC changes

None β€” structural finding; trigger is a plain /bin/sh runtime demonstration (driver registration, device absence, blocked/absent unload path), no seed code to repair.

Verified recommended fix

Add acpi_button_detach() mirroring attach (remove fixed-event or notify handler; disable the PRW wake GPE) and register DEVMETHOD(device_detach, ...) β€” see fix.diff.

Verdict

Structural teardown gap confirmed in source and by runtime demonstration, but the UAF half is not reachable on any current configuration, so the security impact today is none. acpi_button_attach() installs AcpiInstallFixedEventHandler/AcpiInstallNotifyHandler with context=sc (acpi_button.c:150-151,158-159) and the driver has no device_detach (acpi_button.c:74-83), mirroring DF-5006 (acpi_cpu) and DF-5074 (acpi_acad). On the guest: the acpi_button driver is registered in the builtin kernel (kldstat -v), no acpi_button device instance is attached (dmesg empty; QEMU fixed buttons are handled by acpi.c:1974-1993), no acpi.ko exists to unload (kldunload acpi -> 'can't find file'), and in a module build unloading acpi.ko aborts at bus_generic_detach (acpi.c:182) -> missing child detach -> ENXIO before any softc is freed. Net effect today: permanently pinned handlers/resources and a guaranteed UAF if a detach path is ever added (including via the AcpiOsExecute deferrals at acpi_button.c:255,258). Low severity, fix is cheap and belongs in-tree.