# DF-4286 — acpi_thinkpad LED async task not drained on detach → softc use-after-free

## What

`thinkpad_led()` [sys/dev/acpica/acpi_thinkpad/acpi_thinkpad.c:390-404]
queues `thinkpad_led_task` via `AcpiOsExecute(OSL_NOTIFY_HANDLER, ...)`
whenever the led(4) device is written, tracking in-flight state only in
the flag `sc->led_busy` (cleared by the task at acpi_thinkpad.c:415).
`acpi_thinkpad_detach()` [634-663] calls `led_destroy(sc->led_dev)`
[659-660] — which waits **only** for the led(4) cdev references
(sys/dev/misc/led/led.c:319-340) — and then returns; the softc is freed by
the bus code afterwards. A `thinkpad_led_task()` already queued (or
executing) at that moment runs `acpi_thinkpad_sysctl_set(sc, ...)`
[412] on the freed softc: `ACPI_SERIAL_BEGIN(thinkpad)`, AML evaluation
with `sc->handle`/`sc->light_handle` read from freed memory, writes to
`sc->light_val`/`sc->led_busy` — a use-after-free with attacker-unfriendly
but root-racy timing.

## Trigger

Root-only, hardware-gated (real ThinkPad with a CMOS/LGHT thinklight
method): race `echo 1 > /dev/led/thinklight` (or any led(4) toggle)
against `kldunload acpi_thinkpad` / devctl detach. The AML evaluation
inside the task takes milliseconds, making the window practically hittable
with a toggle loop in one thread and unload in another.

## Fix

`fix.diff` — after `led_destroy()` (which prevents new tasks from being
queued), drain the in-flight task by waiting for `sc->led_busy` to clear.

No runtime PoC: the driver attaches only on IBM0068/LEN0068/LEN0268 ACPI
hardware (probe at acpi_thinkpad.c:448-456); the audit guest is not a
ThinkPad. Source-level analysis; module build verified on the guest
(`make` in sys/dev/acpica/acpi_thinkpad compiles the unmodified file
cleanly).
