suspend_kproc holds no reference on the target thread: concurrent kthread_exit yields use-after-free read/write on a recycled struct thread and an unkillable sleeper
| Field | Value |
|---|---|
| ID | DF-2969 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:L/A:L (3.3) |
| CWE | CWE-362 / CWE-416 |
| File | sys/kern/kern_kthread.c |
| Lines | 188-206 (lifetime: lwkt_thread.c:1697-1701) |
| Area | kern |
| Confidence | speculative |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
suspend_kproc operates on a caller-supplied struct thread *td without taking a reference (no lwkt_hold(td)) and without checking for exit: if the target daemon calls kthread_exit() while a suspender is inside the protocol, (a) wakeup(td) at :194 and tsleep(td,...) at :196 use a stale sleep identity β with timo=0 (the u4b idiom) the suspender sleeps forever on an identity that will never be woken for it; (b) the loop test td->td_mpflags at :195 reads freed memory; (c) atomic_clear_int(&td->td_mpflags, TDF_MP_STOPREQ) at :200 WRITES to a struct that lwkt_exit has already parked and which may be recycled β clearing STOPREQ on an unrelated thread being legitimately suspended. lwkt_exit's td_refs drain loop is the intended lifetime mechanism this code fails to use. NO currently reachable in-tree instance: the three callers are shutdown_kproc (targets daemons that never exit during shutdown), uvc self-suspend (curthread cannot exit while sleeping), and u4b usb_proc_drain (cold-gated; those threads ignore STOPREQ entirely β that path hangs rather than UAFs). Becomes live memory corruption the day any caller suspends a daemon that can error-exit its main loop. Not triggerable by an unprivileged user (no syscall/ioctl entry). Fix: lwkt_hold/lwkt_drop bracket + TDF_EXITING re-check (row diff).
Timeline
- 2026-09-02 Discovered during pass-2 audit of kern_kthread.c (GLM 5.3). DF-0239..0242 re-verified still present, not re-reported.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2969 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 680 B | β raw | |
| VERDICT.md | β | 1.8 KB | β raw | |
| build.sh | β | 54 B | view raw | |
| df2969_run.log | β | 760 B | view raw | |
| dfv_2969.c | β | 2.7 KB | view raw | |
| env.txt | β | 401 B | view raw | |
| fix.diff | β | 532 B | view raw | |
| manifest.json | β | 655 B | view raw | |
| run.sh | β | 133 B | view raw | |
| verdict.json | β | 1.8 KB | view raw |
DF-2969 PoC evidence pack
What
suspend_kproc holds no reference on the target thread: exited-target protocol operates on freed state.
Build
make (KMOD dfv_2969)
Run
kldload dfv_2969.ko; sysctl -w dfv69.s2969=1; sysctl -w dfv69.s2969x=1; ps
Expected
suspend_kproc on exited kthread RETURNS (hang refuted); stale-flag write logged
Guest: DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:04 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 All logs are full untrimmed captures unless noted. fix.diff is the git-apply-able verified fix (authored after verification; never applied to the audit tree).
DF-2969 - verification verdict
Status: not_reproduced | reproduced: 0 | impact: denial_of_service | confidence: speculative
Protocol violation confirmed statically; the claimed hang REFUTED live. The harness created a kthread that immediately kthread_exit()ed, then called suspend_kproc(dead_td, 0) - the log shows the full sequence (atomic_set_int on the parked thread = the stale write, wakeup/tsleep on a dead identity), but suspend_kproc RETURNED: the exit path clears or the allocator recycles td_mpflags before the while-test, so the unkillable-sleeper never materialized; a second suspend_kproc on a LIVE kthread also completed (kpsus_token not wedged). No in-tree trigger exists (finding itself says speculative). Fix (lwkt_hold/lwkt_rele bracket) compiled into the fix-all kernel.
Evidence pointers
df2969_run.log: suspend_kproc returned?! + live suspend_kproc returned (kpsus_token not held?)
Kernel references
PoC changes vs the filing sketch
drove the exact claimed call shape (timo=0 on an exited kthread); hang did not occur
Fix
recommendation: lwkt_hold/lwkt_drop bracket + TDF_EXITING re-check
fix status: not_testable
compile-validated in fix-all kernel
Guest: DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:04 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
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.
Fix verification
not_testablecompile-validated in fix-all kernel
Confirmed kernel references
Detail
Evidence (decisive lines)
df2969_run.log: suspend_kproc returned?! + live suspend_kproc returned (kpsus_token not held?)
PoC changes
drove the exact claimed call shape (timo=0 on an exited kthread); hang did not occur
Verified recommended fix
lwkt_hold/lwkt_rele bracket + TDF_EXITING re-check
Verdict
Protocol violation confirmed statically; the claimed hang REFUTED live. The harness created a kthread that immediately kthread_exit()ed, then called suspend_kproc(dead_td, 0) - the log shows the full sequence (atomic_set_int on the parked thread = the stale write, wakeup/tsleep on a dead identity), but suspend_kproc RETURNED: the exit path clears or the allocator recycles td_mpflags before the while-test, so the unkillable-sleeper never materialized; a second suspend_kproc on a LIVE kthread also completed (kpsus_token not wedged). No in-tree trigger exists (finding itself says speculative). Fix (lwkt_hold/drop bracket) compiled into the fix-all kernel.
No comments yet.