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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2145 Β· 6 files
FileTypeDescriptionSize
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
VERDICT.md verdict source-level analysis with path:line citations
↓ download 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) where t is freed β†’ attacker controls func if 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_testable
baseline no→ patch + rebuild →patched clean

git 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.