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)
PoC verification
Evidence pack
findings/poc/DF-4410 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.cinterplay) - 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.
Recommended fix
Track outstanding LED tasks in the softc and drain them in detach before
led_destroy (dying flag + counter + wait), see fix.diff.
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 intoAcpiOsExecute(OSL_NOTIFY_HANDLER, acpi_asus_led_task, led)β an asynchronous OSL work-queue β after a lock-free check/set ofled->busy.acpi_asus_led_task(acpi_asus.c:877-920) later dereferencesled->sc(i.e. the enclosingacpi_asus_softc), readssc->model->bled_set/β¦->gled_set/β¦ and executesacpi_SetInteger(sc->handle, method, state).acpi_asus_detach(acpi_asus.c:832-874) callsled_destroy()for each LED (acpi_asus.c:841-857).led_destroy(led.c:318-341) immediately unlinks andkfrees theledsc; it has no knowledge of the queuedacpi_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β onlyAcpiOsExecute), 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 withoutled->busy = 0(acpi_asus.c:912-915) β permanent wedge of that LED; (b) thebusyflag 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_testablefix.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
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).
No comments yet.