DF-4842 / verdict.json
{ "finding_id": "DF-4842", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Reproduced on a fresh-reset guest with a kld module driving the real in-tree drm_sched_* code (amdgpu.ko/drm.ko). kthread_stop (linux_kthread.c:89-101) sleeps a fixed 1s and then unconditionally lwkt_free_thread(ts->dfly_td) on a thread DragonFly kthreads reclaim themselves (body return -> kthread_exit -> lwkt_exit parks the td in gd_freetd, next exiting thread on that cpu objcache_puts it). Case B (drm_sched_main busy inside run_job() at stop time, reached via drm_sched_fini sched_main.c:652-656): panic: assertion \"(td->td_flags & (TDF_RUNNING|...)) == 0\" failed in lwkt_free_thread with trace lwkt_free_thread <- kthread_stop <- drm_sched_fini (panic.txt); on an assert-less interleave this is a silent free of a live, schedulable struct thread. Case A (idle thread exits <1s, the common amdgpu unload path): probe proves gd_freetd[cpu]==tdS after drm_sched_fini returned while kthread_stop already objcache_put the same td into thread_cache (one pointer, two freelists); a forced thread exit on that cpu consumes gd_freetd and puts the td into the cache a SECOND time, and a scanner allocation then received tdS from the cache (run.log) - double-free of the global thread cache, i.e. two live kernel threads can be handed the same struct thread. Trigger is driver-teardown (privileged), but the corruption is deterministic per teardown and hits the global thread allocator; a uid=0 chain via td reclaim was not constructed (weak attacker write control into recycled td fields) - demonstrated impact is hard panic + thread-cache double-entry. Fix validated: patched drm.ko+amdgpu.ko, kthread_stop now waits for a kt_done handshake and never frees the td: test1 clean ~3s return, no panic, guest up (run_fixed.log); slow-body variant shows the wait blocking 120s instead of freeing the running thread (run_fixed_slowbody.log).", "exploit_chain": "driver teardown (amdgpu unload/rebind, i915 breadcrumbs fini) -> drm_sched_fini -> kthread_stop: (A) idle thread: td parked in gd_freetd + kthread_stop objcache_put -> next thread exit on that cpu double-puts the td -> two lwkt_alloc_thread calls can receive the SAME struct thread (cross-thread td_sp/td_pri/td_lwp corruption) or a live td is freed; (B) busy thread (run_job >1s): lwkt_free_thread on TDF_RUNNING td -> KKASSERT panic, or silent free of a live schedulable td -> subsequent td reuse -> UAF write through any td field mutation. uid0 escalation would require reclaiming the freed td with controlled contents (td_sp/td_ucred forge) - thread-cache allocation-side write control is weak, not demonstrated.", "evidence": "panic.txt (case B: assertion panic + lwkt_free_thread<-kthread_stop trace); run.log (case A: gd_freetd==tdS probe after fini, 2nd put, cache dispense of tdS to a new allocation); run_fixed.log (patched: clean stop, no panic); run_fixed_slowbody.log (patched: waits 120s for stuck body instead of freeing); fix.diff; VERDICT.md", "kernel_refs": [ "sys/dev/drm/linux_kthread.c:89", "sys/dev/drm/linux_kthread.c:97", "sys/dev/drm/linux_kthread.c:98", "sys/kern/lwkt_thread.c:1684", "sys/kern/lwkt_thread.c:1741", "sys/kern/lwkt_thread.c:510", "sys/dev/drm/scheduler/sched_main.c:652", "sys/dev/drm/i915/intel_breadcrumbs.c:891" ], "poc_changes": "Seed was authored fresh for this finding. Two iterations needed: (1) -Werror unused-variable cleanups; (2) the case-B run_job busy-spin read ticks non-volatile - under -O2 the compiler register-cached it and the 3-4s spin became infinite (this also produced a confusing patched-kernel wedge where kthread_stop correctly waited 120s while the PoC starved a cpu - diagnosed, fixed with a volatile read, and kept as slow-body fix evidence).", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 3900, "guest_dirty": 0, "build_cmd": "kldload amdgpu; make in /tmp/df4842 (guest)", "run_cmd": "kldload /tmp/df4842/df4842.ko; sysctl hw.df4842_test=1 (panic ~1s); after reset: sysctl hw.df4842_test=2 (double-put staging)", "code_hash": "02ab0c0cd7b727443d689fd83bca6596f18e464a4b910654e7c1bc549b5d7c88", "notes": "Baseline from vm.sh reset with-src; guest reset with-src at end of run. Case-A second-copy dispense not observed (per-cpu objcache magazine locality) - double-entry itself is proven by probe+code+observed single dispense from the 2nd put. kthread_stop also leaked the task_struct (fix includes kfree). The unpark raw lwkt_schedule on running/tsleeping threads (runq/sleepq invariant breakage) is fixed by the same patch via KTHREAD_IS_PARKED.", "recommended_fix": "kthread_stop must wait for the body to return (kt_done handshake set by linux_ktfn_wrapper before it returns into kthread_exit) and must never call lwkt_free_thread - the DFly thread reclaims its own td via lwkt_exit/gd_freetd; also kfree(task_struct) and only lwkt_schedule threads that actually parked (KTHREAD_IS_PARKED).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fixed: fix.diff applied to guest /usr/src, drm.ko+amdgpu.ko rebuilt; baseline test1 panics at +1s (panic.txt) vs patched test1 clean ~3s return with guest up (run_fixed.log); patched slow-body variant blocks 120s on the handshake instead of freeing the running td (run_fixed_slowbody.log); patched test2 shows the normal single park/dispense lifecycle.", "fix_evidence": "fix.diff; run_fixed.log; run_fixed_slowbody.log; VERDICT.md" } |