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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4286 Β· 5 files
FileTypeDescriptionSize
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.md
↓ download raw

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

  1. 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.
  2. 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 (including AcpiEvaluateObject with sc->light_handle loaded from freed memory).
  3. 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_testable
baseline no→ patch + rebuild →patched clean

Not 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
↓ fix.diffper-fix-DF-4286

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.