tasklet_kill() UAF: premature return on TASKLET_STATE_RUN leaves dangling tasklet_entry; runner later calls t->func from freed memory
Summary
tasklet_kill() (linux_tasklet.c:170-175) waits only for TASKLET_STATE_RUN to clear via tasklet_unlock_wait (190-196) which spins only on test_bit(TASKLET_STATE_RUN &t->state). RUN is set exclusively by tasklet_trylock() at line 88 of PROCESS_TASKLET_LIST i.e. only AFTER runner has dequeued-and-cleared-SCHED and is about to dispatch t->func. For any tasklet that is SCHED-pending but not yet dispatched RUN is clear and tasklet_kill returns immediately WITHOUT acquiring tasklet_lock and WITHOUT waiting for runner to drain entry. tasklet_entry allocated by TASKLET_SCHEDULE_COMMON (line 146 te->ts=t) remains on tlist/tlist_hi with live pointer to tasklet_struct after tasklet_kill returns. Caller (standard DRM teardown pattern intel_engine_cs.c:1066 intel_lrc.c:2306) then frees containing struct. When single tasklet_runner thread eventually reaches that te line 69 struct tasklet_struct *t=te->ts yields dangling pointer; every subsequent access is UAF: test_bit(DYING) (75) atomic_read(count) (81) test_and_clear_bit(SCHED) (85) tasklet_trylock (88) and critically t->func(t->data) (93) = function-pointer call from freed/reused slab memory = RIP control under heap grooming. Compare Linux upstream tasklet_kill which spins on SCHED until provably dequeued. Attacker: any local unprivileged user with DRM device access triggering schedule-then-teardown (GPU context destroy/engine park/reset/mode-set/driver unload). Race window widened by flooding tlist with dummy schedules. Impact: UAF with function-pointer call controlled t->func and t->data from freed slab = arbitrary kernel code execution CPL0 -> LPE uid 0. Reliable on SMP single-threaded runner makes timing deterministic under load.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2145 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | source-level analysis with path:line citations | 2.2 KB | β raw |
| reachability.txt | environment | guest PCI/device survey proving no required HW | 1.6 KB | view raw |
| fix.diff | suggested-fix | git-apply-able fix (validated: applies clean) | 842 B | view raw |
| build.sh | build-log | documents HW requirement | 550 B | view raw |
| run.sh | run-log | documents HW requirement | 272 B | view raw |
| env.txt | environment | guest uname and environment | 491 B | view raw |
DF-2145: tasklet_kill() UAF β premature return leaves dangling tasklet_entry
Verdict: NOT REPRODUCED (HW-gated) β source-confirmed real bug
Reachability
NOT reachable on this QEMU guest. tasklet_kill() is in sys/dev/drm/linux_tasklet.c,
part of drm.k. Tasklets are used by DRM GPU drivers (radeon, amdgpu, i915) for deferred
interrupt-bottom-half processing. Without GPU hardware, no tasklets are scheduled, and
tasklet_kill() is never called from userspace-reachable paths.
drm.ko loads but the tasklet runner (tasklet_runner kthread) has an empty queue β
no tasklet_entry structs exist to exploit.
Mechanism (source-confirmed)
tasklet_kill() at linux_tasklet.c:169-175:
1. set_bit(TASKLET_IS_DYING, &t->state) β marks dying
2. wakeup(&tasklet_runner) β wakes the runner thread
3. tasklet_unlock_wait(t) β spins until TASKLET_STATE_RUN clears
The flaw: tasklet_unlock_wait() (line 190-196) only waits for TASKLET_STATE_RUN to clear.
RUN is set by tasklet_trylock() (line 88) only after the runner has dequeued the
entry from the STAILQ and cleared TASKLET_STATE_SCHED. If the tasklet_entry is still on
the queue (not yet dequeued), RUN is never set, so tasklet_unlock_wait() returns
immediately.
After tasklet_kill() returns, the caller frees the tasklet_struct. The runner later
dequeues the entry, reads te->ts (pointing to freed memory), and calls t->func(t->data)
β use-after-free.
The runner's PROCESS_TASKLET_LIST macro checks TASKLET_IS_DYING (line 75) and removes
+ frees the entry, but this happens asynchronously β the race window between
tasklet_kill() returning and the runner processing is the vulnerability.
Primitive
- Class: UAF (function pointer call from freed memory)
- The runner calls
t->func(t->data)wheretis freed β attacker controlsfuncif the freed slab is reclaimed - On this guest (no SMEP): redirected function pointer β userspace shellcode β
uid=0
Fix
fix.diff: After tasklet_unlock_wait(), acquire tasklet_lock and manually scan both
tlist and tlist_hi for any entry whose ts == t, remove and free it. This guarantees
no dangling entry remains when tasklet_kill() returns.
Fix verification
not_testablegit apply --check clean + drm.ko compiles
git apply --check clean + drm.ko compiles
Confirmed kernel references
β
Detail
Exploit chain
none (HW-gated)
Evidence (decisive lines)
HW-GATED (no GPU). Source-confirmed: tasklet_kill tasklet_unlock_wait only checks RUN (set after dequeue), caller frees while queued -> UAF funcptr call.
Verified recommended fix
HW-GATED (no GPU). Source-confirmed: tasklet_kill tasklet_unlock_wait only checks RUN (set after dequeue), caller frees while queued -> UAF funcptr call.
Verdict
HW-GATED (no GPU). Source-confirmed: tasklet_kill tasklet_unlock_wait only checks RUN (set after dequeue), caller frees while queued -> UAF funcptr call.
No comments yet.