acpi_thinkpad LED async task (AcpiOsExecute) not drained on detach β use-after-free of the softc
Summary
thinkpad_led queues thinkpad_led_task via AcpiOsExecute with no lifetime tie to the driver; detach relies on led_destroy which waits only for the led cdev references. A queued/in-flight task then executes sysctl_set on the freed softc - ACPI_SERIAL, AML evaluation with handles read from freed memory, writes into the softc. Root-racy on ThinkPad hardware with a thinklight setter; the sensors-side task IS properly serialized - the LED side is not. Drain-after-led_destroy fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4286 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.7 KB | β raw | |
| VERDICT.md | β | 3.0 KB | β raw | |
| fix.diff | β | 546 B | view raw | |
| manifest.json | β | 885 B | view raw | |
| verdict.json | β | 2.6 KB | view raw |
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).
VERDICT β DF-4286
Status: inconclusive (source-certain lifetime defect; driver cannot attach on this guest) Impact: root-racy use-after-free of acpi_thinkpad softc (read/write + AML evaluation with freed handles) Confidence: likely
Proof chain
thinkpad_led()[acpi_thinkpad.c:390-404]: on every led(4) toggle,if (sc->led_busy) return; sc->led_busy = 1; ...; AcpiOsExecute(OSL_NOTIFY_HANDLER, thinkpad_led_task, sc);β fire-and-forget; nothing holds a reference on the driver/softc for the queued callback.thinkpad_led_task()[406-416]:ACPI_SERIAL_BEGIN(thinkpad); acpi_thinkpad_sysctl_set(sc, THINKLIGHT, sc->led_state); ACPI_SERIAL_END(thinkpad); sc->led_busy = 0;β dereferences sc extensively (includingAcpiEvaluateObjectwithsc->light_handleloaded from freed memory).acpi_thinkpad_detach()[634-663]:led_destroy(sc->led_dev)[659-660] waits only for the led(4) cdev refs (sys/dev/misc/led/led.c:319-340 β destroy_dev + kfree of ledsc); the driver softc is freed by the normal device detach machinery right after detach returns. A task queued before led_destroy, or mid-flight, still executes afterwards β UAF.
Contrast: the sensors(9) side of this driver is safe β
sensor_task_unregister [sys/kern/kern_sensors.c:238-250] blocks on the
sensor-thread lock which the thread holds across st->func, so
acpi_thinkpad_refresh cannot run post-detach. The LED path lacks the
equivalent.
Trigger requirements
- Hardware: ThinkPad ACPI device (probe requires IBM0068/LEN0068/LEN0268
HID, acpi_thinkpad.c:375, 448-456) with a thinklight setter
(
light_set_supported, 1006-1037). - Privilege: root on both sides (led cdev is 0600 root:wheel [led.c:304-305]; unload needs root).
- Race: toggle loop on /dev/led/thinklight vs kldunload acpi_thinkpad / devctl detach; AML evaluation gives a wide window.
Why not runtime-verified
The audit guest has no IBM0068/LEN0068/LEN0268 ACPI node (not a ThinkPad;
verified the module builds on the guest: make in
/usr/src/sys/dev/acpica/acpi_thinkpad compiles the unmodified source
cleanly, acpi_thinkpad.ko links). Attach never happens β the race cannot
be staged on this machine. Classified inconclusive/hardware-gated per
audit house style (cf. DF-0281).
Fix
fix.diff: drain in-flight thinkpad_led_task in detach by waiting on
sc->led_busy after led_destroy() (which stops new queueings). Task
clears the flag at acpi_thinkpad.c:415.
Bottom line
Genuine unsynchronized async-callback lifetime bug (UAF class) on the ThinkPad LED path; root-only, hardware-gated, race-window practical. Low severity; worth fixing alongside the other acpi_* detach hygiene.
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_testableNot testable: race requires ThinkPad hardware absent from this guest. fix.diff drains the async task before softc teardown; matches the sensors-side serialization pattern already used by this driver.
source-level: fix.diff vs acpi_thinkpad.c:659-660 and 415
Confirmed kernel references
Detail
Exploit chain
root: thread A loops writes to /dev/led/thinklight (queues thinkpad_led_task, sets led_busy); thread B kldunload acpi_thinkpad -> detach -> led_destroy (waits cdev only) -> softc freed -> task executes ACPI_SERIAL_BEGIN + sysctl_set on freed sc -> UAF read/write + AML eval with freed handles -> panic/corruption (root-triggered)
Evidence (decisive lines)
VERDICT.md proof chain with acpi_thinkpad.c:390-416 vs 659-660 and led.c:319-340; contrast kern_sensors.c:238-250 (safe pattern)
PoC changes
no seed; not runtime-testable on this guest (no ThinkPad ACPI node)
Verified recommended fix
drain in-flight thinkpad_led_task in detach: after led_destroy, tsleep on sc->led_busy until cleared (fix.diff)
Verdict
thinkpad_led() queues thinkpad_led_task via AcpiOsExecute with no lifetime tie to the driver; acpi_thinkpad_detach() waits only inside led_destroy() for the led(4) cdev refs, then returns and the softc is freed while the queued task can still run acpi_thinkpad_sysctl_set(sc,...) on it. Sensors-side unregister is properly serialized (sensor thread holds its lock across func), the LED side is not. Cannot be staged on this guest (driver probes only on IBM0068/LEN0068/LEN0268 ACPI hardware); module build on guest verified clean.
No comments yet.