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

acpi_asus LED state changes run in an undrained AcpiOsExecute task: detach/led_destroy free the softc under a pending acpi_asus_led_task (UAF window)

Summary

Every LED change is deferred asynchronously via AcpiOsExecute after a lock-free busy check/set; detach never drains that queue (the tree's OSL exposes no drain primitive), so a task queued just before detach executes over the freed softc (led->sc, model->bled_set, AML eval). Secondary: the default arm leaves busy set - permanently wedging that LED. Track+drain+clear-busy fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4410 Β· 5 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 2.7 KB ↓ raw
env.txt β€” 1.9 KB view raw
fix.diff β€” 2.4 KB view raw
verdict.json β€” 2.6 KB view raw

DF-4410 β€” acpi_asus LED state changes run in an undrained async task: detach/led_destroy frees the softc under a pending acpi_asus_led_task (use-after-free)

  • File: sys/dev/acpica/acpi_asus/acpi_asus.c (+ sys/dev/misc/led/led.c interplay)
  • Class: CWE-416 (use after free) / CWE-362 (race window)
  • Severity: Low β€” the write side is root-only (/dev/led/* are created 0600 root:wheel, led.c:305-306), and the free side requires root (device detach / module unload). No unprivileged path.

Defect (path:line)

LED write path (root writes /dev/led/bled etc., or led_create_state at attach calls sc->func(...) directly, led.c:312):

acpi_asus_led()            acpi_asus.c:922-935
        if (led->busy)             /* racy check, no lock */
                return;
        led->busy = 1;
        led->state = state;
        AcpiOsExecute(OSL_NOTIFY_HANDLER, acpi_asus_led_task, led);  /* async */

The task (acpi_asus.c:877-920) later dereferences led->sc, sc->model->bled_set, sc->handle and calls acpi_SetInteger, then led->busy = 0.

Nothing drains that pending task at detach:

acpi_asus_detach()   acpi_asus.c:832-874
        led_destroy(sc->s_bled.cdev);   /* frees the ledsc immediately;
                                           led.c:318-341 has no OSL-queue
                                           flush and does not know about the
                                           acpi_asus_led_task */
        ...
        sysctl_ctx_free(&sc->sysctl_ctx); /* softc itself freed by newbus
                                             right after detach returns */

The tree's ACPICA OSL has no AcpiOsWaitEvents()-style drain (sys/dev/acpica/Osd/OsdSchedule.c only implements AcpiOsExecute), so a task queued microseconds before led_destroy runs after both the ledsc and the acpi_asus_softc are freed β†’ led->sc / sc->model->bled_set / acpi_SetInteger(sc->handle, ...) all operate on freed memory β†’ UAF read + AML engine call with attacker-uncontrolled-but-recycled pointers.

Secondary: led->busy check/set (acpi_asus.c:928-931) is unlocked, so two concurrent LED writes can both queue a task (benign duplication, but shows the glue has no serialization at all); and the default: arm of acpi_asus_led_task (acpi_asus.c:912-915) returns without clearing led->busy, permanently wedging that LED.

Build / run

Not runnable on the audit guest: no /dev/led consumers exist (no ASUS nodes β€” env.txt), and both sides of the race are root-only. On matching hardware: echo 1 > /dev/led/bled & kldunload acpi_asus in a loop β†’ panic in acpi_asus_led_task on freed softc.

Track outstanding LED tasks in the softc and drain them in detach before led_destroy (dying flag + counter + wait), see fix.diff.

VERDICT.md
↓ download raw

DF-4410 VERDICT β€” untested (source-verified UAF window; root-gated, firmware-gated)

Reproduced?

No. Status untested: needs an attached acpi_asus (guest has zero ATK0100/ASUS010 nodes β€” env.txt) plus root on both sides of the race (/dev/led/* is 0600 root:wheel per led.c:305-306; softc freeing requires detach/kldunload). No unprivileged trigger.

Defect (certain from source)

  • acpi_asus_led (acpi_asus.c:922-935) defers every LED state change into AcpiOsExecute(OSL_NOTIFY_HANDLER, acpi_asus_led_task, led) β€” an asynchronous OSL work-queue β€” after a lock-free check/set of led->busy.
  • acpi_asus_led_task (acpi_asus.c:877-920) later dereferences led->sc (i.e. the enclosing acpi_asus_softc), reads sc->model->bled_set/…->gled_set/… and executes acpi_SetInteger(sc->handle, method, state).
  • acpi_asus_detach (acpi_asus.c:832-874) calls led_destroy() for each LED (acpi_asus.c:841-857). led_destroy (led.c:318-341) immediately unlinks and kfrees the ledsc; it has no knowledge of the queued acpi_asus_led_task. The softc itself is freed by newbus when detach returns. The tree's OSL provides no drain primitive (sys/dev/acpica/Osd/OsdSchedule.c β€” only AcpiOsExecute), and detach never synchronizes with the OSL queue at all.
  • Result: any LED write (root) landing within the OSL-queue latency before detach produces a task that runs on freed memory: UAF reads (led->type, led->sc, sc->model->…) and a call into the AML interpreter with recycled pointer values (sc->handle). Window is the notify-handler queue depth β€” real, but small and root-gated.
  • Same glue also: (a) default: arm of the task returns without led->busy = 0 (acpi_asus.c:912-915) β€” permanent wedge of that LED; (b) the busy flag itself is racy (acpi_asus.c:928-931), so duplicate tasks can be queued.

Impact ceiling

Root-gated kernel panic / corruption of freed slab memory. Not an escalation primitive (the freed slot is not attacker-shaped, and reaching the race already requires root).

Fix

fix.diff: per-softc led_lock + led_dying flag + outstanding-task counter; acpi_asus_led only queues while alive; the task drops the count and wakes detach; detach waits for quiescence before destroying the LED devices. Not validated on hardware (fix_status: not_testable).

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 from source analysis (drain-before-destroy); cannot be built/validated on the audit guest - acpi_asus never attaches here and no /dev/led exists.

findings/poc/DF-4410/fix.diff; env.txt
↓ fix.diffper-fix-DF-4410

Confirmed kernel references

Detail

Exploit chain

root: echo 1 > /dev/led/bled immediately followed by kldunload acpi_asus -> pending task executes over freed softc (UAF). No unprivileged chain.

Evidence (decisive lines)

findings/poc/DF-4410/VERDICT.md, README.md, env.txt, fix.diff (drain design)

Verified recommended fix

Add per-softc led_lock/led_dying/led_pending; gate AcpiOsExecute on !dying, count outstanding tasks, and have detach wait for quiescence before led_destroy; clear led->busy on the task default path; see fix.diff.

Verdict

Source-verified use-after-free window: every acpi_asus LED state change is deferred into AcpiOsExecute(OSL_NOTIFY_HANDLER, acpi_asus_led_task, led) (acpi_asus.c:922-935), but acpi_asus_detach never drains that queue - led_destroy (acpi_asus.c:841-857 -> led.c:318-341) frees the ledsc immediately and newbus frees the softc when detach returns, so a task queued just before detach runs acpi_asus_led_task (acpi_asus.c:877-920) over freed memory (led->sc, sc->model->bled_set, acpi_SetInteger(sc->handle,...)). The OSL has no drain primitive (OsdSchedule.c implements only AcpiOsExecute). Secondary: default-arm of the task returns without clearing led->busy (permanent LED wedge), and the busy check/set is racy. Untested at runtime: guest has no ATK0100/ASUS010 nodes and no /dev/led consumers (env.txt); both sides of the race are root-only (0600 led nodes per led.c:305-306; detach/kldunload needs root).