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

linux_task_alloc() leaks the task_struct of every pure kernel thread that evaluates `current` - no drop path exists on the kthread-exit (lwkt_exit) route

Summary

The only drop hook is on lwp_exit; pure kthreads exit via lwkt_exit which has none - every kernel thread touching drm code (irq/fence/syncer paths) permanently leaks a task_struct + spinlock, and the parked td keeps a dangling back-pointer (recycled-td aliasing killed: lwkt_init_thread bzeroes on reuse). Guest: 8/8 leaked baseline vs 8/8 clean with the lwkt_exit hook rebuilt into the kernel. Distinct from DF-2196 (different site/trigger).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5201 Β· 11 files
FileTypeDescriptionSize
df5201.c β€” 7.6 KB view raw
Makefile β€” 326 B ↓ download
build.sh β€” 91 B view raw
run.sh β€” 136 B view raw
build.log β€” 1.4 KB view raw
run.log β€” 1.6 KB view raw
run_fixed.log β€” 1.1 KB view raw
fix.diff β€” 757 B view raw
env.txt β€” 431 B view raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 3.7 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff adds the lwkt_exit() drop hook; X86_64_GENERIC kernel rebuilt with it and booted: baseline (original kernel) 8/8 exited kthreads left leaked task_structs on their parked tds; patched 8/8 clean (task dropped at lwkt_exit), leak gone.

run.log (baseline 8/8 'task LEAKED') vs run_fixed.log (8/8 'task dropped at lwkt_exit'); objdump of lwkt_exit in the rebuilt kernel shows the td_linux_task check + linux_task_drop_callback indirect call
↓ fix.diffper-fix-DF-5201

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (baseline, original kernel: 8/8 'PARKED: exited td=... still carries td_linux_task=... -> task LEAKED', plus W1/W2 td-recycling rounds entering clean); run_fixed.log (patched kernel: 8/8 'exited td=... is clean (task dropped at lwkt_exit)')

PoC changes

no seed PoC; authored fresh. Fixes during bring-up: sysctl node hw.df5201.test; sys/kthread.h include for kthread_create; ASCII-only comments; added deterministic parked-td metric (read W1's td_linux_task after exit) after the address-recycle indicator proved cpu-migration sensitive (0/8 on both kernels).

Verified recommended fix

Hook lwkt_exit() to call linux_task_drop_callback when td_linux_task is set (mirrors the lwp_exit hook), in the may-block cleanup region after dsched_exit_thread().

Verdict

REPRODUCED on the QEMU guest via a KLD driving the real linux_task_alloc (drm.ko) from pure kthreads created with kthread_create(). Every kernel thread that evaluates the current macro gets a task_struct installed at linux_sched.c:88, but the only drop path (linux_task_drop_callback) fires from lwp_exit (kern_exit.c:708-709), which pure kthreads never execute (they exit via kthread_exit -> lwkt_exit, lwkt_thread.c:1684-1744). Baseline: 8/8 exited kthreads left their parked td still carrying the leaked, never-dropped task_struct pointer (8 distinct leaked allocations, addresses never recycled). Suspected escalation to cross-thread current aliasing via recycled tds was investigated and KILLED: lwkt_init_thread (lwkt_thread.c:440) bzeroes the whole struct thread on reuse, so a recycled td always starts clean (observed: rounds where W2 received exactly W1's td entered clean) - the surviving defect is the unbounded permanent leak, one task_struct+spinlock per kernel thread that ever executes drm code (dma_fence_wait/prepare_to_wait/set_current_state/... on irq, callout, syncer, bufdaemon threads).