{
  "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"
}