# DF-4842 VERDICT — kthread_stop() frees a struct thread it does not own

## Status: REPRODUCED (panic + demonstrated thread_cache double-entry; fix validated)

## Root cause

`sys/dev/drm/linux_kthread.c:89-101`:

```c
int
kthread_stop(struct task_struct *ts)
{
	set_bit(KTHREAD_SHOULD_STOP, &ts->kt_flags);

	kthread_unpark(ts);
	wake_up_process(ts);

	/* XXX use a better mechanism to wait for the thread to finish running */
	tsleep(kthread_stop, 0, "kstop", hz);	/* fixed 1s — proves nothing */
	lwkt_free_thread(ts->dfly_td);		/* frees a thread it does not own */

	return ts->kt_exitvalue;
}
```

DragonFly kthreads reclaim themselves: when the body returns,
`cpu_set_thread_handler()`'s exit handler runs `kthread_exit()` →
`lwkt_exit()` (sys/kern/lwkt_thread.c:1684-1744), which leaves the exiting
td in `gd->gd_freetd` for the next exiting thread on that CPU to
`objcache_put()` into `thread_cache`.  `lwkt_free_thread()` from
`kthread_stop()` is a second, uncoordinated free:

* **Case B (body busy > 1s at stop time):** the td is `TDF_RUNNING` (or
  blocked on a sleepq) when the fixed 1-second sleep elapses;
  `lwkt_free_thread()`'s KKASSERT (lwkt_thread.c:513-514) fires — or, if
  the thread merely sits descheduled (e.g. via the broken `kthread_park`),
  the asserts pass and a **live, still-schedulable `struct thread` is
  returned to the cache** while it remains on `gd_tdallq`.
* **Case A (body exited < 1s):** the td is parked in `gd_freetd[cpu]` AND
  `objcache_put()` by `kthread_stop()` — one pointer in two freelists.  The
  next thread exit on that CPU consumes `gd_freetd` and puts the td into
  the cache a **second time**; subsequent `lwkt_alloc_thread()` calls can
  hand the same td to two different new threads (shared td → td_sp/td_pri/
  td_lwp cross-corruption of unrelated kernel threads), or free the td of
  a thread that is still using it.

Secondary defects in the same function: `kthread_unpark()` (called from
`kthread_stop`) does a raw `lwkt_schedule()` on a thread that may be
running or tsleeping (`_lwkt_enqueue()` has no TDF_RUNNING/TDF_TSLEEPQ
guard — lwkt_thread.c:1287-1315 enqueues whatever it is handed), and the
`task_struct` is leaked (never `kfree`'d).

## Reachability

* `drm_sched_fini()` — sys/dev/drm/scheduler/sched_main.c:652-656 — every
  amdgpu ring teardown / driver unload / rebind.  This is the teardown
  companion of the scheduler/entity files audited in this batch
  (sched_entity.c destroy path → scheduler fini).
* `intel_engine_fini_breadcrumbs()` — sys/dev/drm/i915/intel_breadcrumbs.c:891
  (i915 engine teardown).

Trigger is privileged (driver unload/device teardown), but the corruption
is deterministic on every teardown and lands in the **global thread
cache** — every subsequent kernel-thread creation on the box shares the
corrupted objects.

## Reproduction (guest, no GPU required — module drives the real in-tree
drm_sched_* code from amdgpu.ko/drm.ko)

### Case B — free of a live TDF_RUNNING thread (deterministic panic)

`sysctl hw.df4842_test=1` (panic.txt):

```
[DF4842] run_job(...): entered on td=0xfffff800905fc100 cpu=1 - now BUSY-SPINNING ...
[DF4842] calling drm_sched_fini(): ...
panic: assertion "(td->td_flags & (TDF_RUNNING | TDF_PREEMPT_LOCK | TDF_RUNQ | TDF_TSLEEPQ | TDF_KERNELFP)) == 0" failed in lwkt_free_thread at /usr/src/sys/kern/lwkt_thread.c:514
cpuid = 0
Trace:
lwkt_free_thread() at lwkt_free_thread+0x119
kthread_stop() at kthread_stop+0xb4
sysctl_df4842_test() at ...
Debugger("panic") ... db>
```

The scheduler thread was mid-`run_job()` on cpu 1 when `kthread_stop()`
(on cpu 0) hit its fixed 1-second sleep and freed the running thread's td.
On a build where the KKASSERT does not fire first, this is a silent
free-of-live-thread (UAF).

### Case A — thread_cache double-entry (staged, guest stays up)

`sysctl hw.df4842_test=2` (run.log):

```
[DF4842] round 1: sched thread td=0xfffff800905fd480 on cpu=1; calling drm_sched_fini() ...
[DF4842] probe: cpu=1 gd_freetd=0xfffff800905fd480 tdS=0xfffff800905fd480 -> == tdS STILL PARKED
           in gd_freetd (while kthread_stop already objcache_put() it into thread_cache: DOUBLE OWNERSHIP)
[DF4842] sacr: exiting on cpu=1 - lwkt_exit() will now objcache_put(gd_freetd=tdS ...) AGAIN (2nd put)
[DF4842] scan(cpu=1) alloc 1: thread_cache handed out tdS 0xfffff800905fd480 — SAME struct thread
           in a NEW allocation while the 2nd put also sits in the cache
```

After `drm_sched_fini()` returned, the exited td was still parked in
`gd_freetd` while `kthread_stop()` had already cache-put it (code path at
linux_kthread.c:98); the sacrificial exit drives the second put; the cache
then hands the same td to a fresh allocation — an allocation that must
never see that td again while a second copy of it sits in the cache.  Two
live threads receiving the same td is the direct consequence (observed as
a single dispense in this run; per-CPU objcache magazine locality kept the
second copy from being dispensed to the scanners, which the round loop
retries).

## Exploitability

Primitive: deterministic `struct thread` UAF / thread_cache double-entry
on every drm scheduler (amdgpu) teardown, and on i915 breadcrumbs teardown.
The escalated route (uid=0) would reclaim a freed td via thread-cache
allocations and forge td fields (td_sp / td_lwp / td_ucred) — the
allocation-side write control into a recycled td is weak (thread fields are
kernel-set at init; td_comm via kthread names is the main attacker-influen-
ced bytes), so a reliable local privesc chain was not constructed in this
run.  Demonstrated impact: hard kernel panic (case B) + proven double-entry
of the global thread cache (case A) — kernel memory corruption of the
highest-value allocator in the system.

## Fix validation

`fix.diff` (authored after verification; never applied to the read-only
audit tree): `kthread_stop()` now waits for the wrapper to publish
`kt_done` (handshake before the body returns into `kthread_exit()`), never
frees the td (the DFly thread reclaims itself through
`lwkt_exit()/gd_freetd`), frees the leaked `task_struct`, and stops doing
a raw `lwkt_schedule()` on non-parked threads (`KTHREAD_IS_PARKED` bit set
by `kthread_parkme()`, honored by `kthread_unpark()`/`kthread_stop()`).

Applied to the guest's /usr/src, drm.ko and amdgpu.ko rebuilt:

* test 1 baseline: panic at +1 s (panic.txt) — test 1 patched:
  `drm_sched_fini RETURNED (no panic?!)` in ~3 s (the true body runtime),
  guest up, load+unload clean (run_fixed.log).
* slow-body variant (body stuck >120 s): patched `kthread_stop()` blocked
  for its 120 s safety timeout and returned **without freeing the running
  thread** — no panic (run_fixed_slowbody.log); baseline would have
  panicked at +1 s.  (The ssh loss afterwards in that log was the PoC's own
  accidentally-infinite spin — `ticks` read non-volatile under -O2 — since
  fixed with a volatile read; not a kernel issue.)
* test 2 patched: probe still parks the td in `gd_freetd` (normal exit
  path), one legit cache dispense — the kthread_stop extra put is gone.

## Guest state

Baseline from `vm.sh reset with-src`; guest reset with-src at end of run.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
