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

Kernel threads leak one td_ucred reference per exit: crhold at creation is never crfreed on the LWKT teardown path

Field Value
ID DF-2968
Status new
Severity Info
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:N (0.0)
CWE CWE-401
File sys/kern/kern_kthread.c
Lines 62 (teardown: lwkt_thread.c:1684+)
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket kernleak
Reported pending
Known CVE none
CVE match novel

Summary

_kthread_create sets td->td_ucred = crhold(proc0.p_ucred) (:62). When the thread later exits via kthread_exit() β†’ lwkt_exit(), the teardown path performs biosched_done/dsched_exit_thread/tsleep_remove/ lwkt_remove_tdallq and parks the struct WITHOUT ever calling crfree(td->td_ucred) β€” lwkt_thread.c contains zero crfree calls. The only crfree of td_ucred in the kernel is the LWP path (kern_exit.c:717), unreachable for pure LWKT threads. On recycle, lwkt_init_thread's bzero silently destroys the held reference. Minimal today: the leaked reference is always on proc0.p_ucred, an immortal credential, and cr_ref is a 64-bit long β€” neither premature free nor overflow is reachable; each kthread create/exit cycle merely bumps an unbounded counter on a never-freed object. Refcount-discipline defect that becomes dangerous only if kthreads are ever given non-proc0 creds without also teaching lwkt_exit to drop them. Fix: crfree in lwkt_exit() before parking the struct.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of kern_kthread.c (GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2968 Β· 10 files
FileTypeDescriptionSize
README.md β€” 754 B ↓ raw
VERDICT.md β€” 973 B ↓ raw
build.sh β€” 50 B view raw
env.txt β€” 509 B view raw
fix.diff β€” 546 B view raw
h41.c β€” 14.9 KB view raw
harness.log β€” 2.0 KB view raw
manifest.json β€” 537 B view raw
run.sh β€” 104 B view raw
verdict.json β€” 2.1 KB view raw

DF-2968 β€” kthread td_ucred reference leaked per exit (Info-tier hardening verification)

What

kthread td_ucred reference leaked per exit

Build

cd /root/h41 && make

Run

kldload ./h41.ko; sysctl -w debug.h41.cmd=3

Expected

baseline (stock kernel): see verdict below fix kernel (all-41-fixes build): the bad behavior is gone

A/B one-liner: baseline: cr_ref delta +8 | fix: delta 0

Files

README.md VERDICT.md build.sh env.txt fix.diff h41.c harness.log manifest.json run.sh verdict.json

Shared harness: h41.c (combined KLD, sysctl debug.h41.cmd=N driven). Full batch context: the fix kernel contains all 41 hardening fixes of this batch compiled together (build RC=0) and passed the complete A/B suite plus a clean reboot.

VERDICT.md
↓ download raw

DF-2968 β€” VERDICT

status: reproduced | reproduced: 1 | impact: none | confidence: certain

Mechanically reproduced via the harness on the stock guest: creating and exiting 8 throwaway kthreads moved proc0.p_ucred->cr_ref from 165 to 173 (+8, exactly one leaked reference per kthread) - _kthread_create takes crhold(proc0.p_ucred) (kern_kthread.c:62) but the LWKT exit path never drops it: lwkt_exit parks the struct without crfree (lwkt_thread.c contains zero crfree calls) and lwkt_init_thread's bzero silently destroys the held reference on recycle; the only td_ucred crfree is the LWP path (kern_exit.c:717), unreachable for pure LWKT threads. Minimal today (leak is always on the immortal proc0 credential; cr_ref is 64-bit). On the fix kernel (crfree in lwkt_exit, guarded, before parking) the same 8-cycle batch moved cr_ref by 0.

Baseline vs fix kernel

baseline: baseline: cr_ref delta +8 | fix: delta 0 fix: behavior changed as designed (see verdict)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

A/B: baseline +8 refs per batch; fix kernel +0 (LWP paths already NULL the field in kern_exit.c, no double free).

↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Oct 26 01:57:23 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (h41 fix kernel, all 41 hardening fixes applied)

Confirmed kernel references

Detail

Evidence (decisive lines)

harness.log T3: before=165 after=173 delta=+8; fix kernel T3: delta=0

PoC changes

reads proc0.p_ucred.cr_ref directly from the module (long, cache-aligned)

Verified recommended fix

crfree(td->td_ucred) in lwkt_exit() before parking the thread

Verdict

Mechanically reproduced via the harness on the stock guest: creating and exiting 8 throwaway kthreads moved proc0.p_ucred->cr_ref from 165 to 173 (+8, exactly one leaked reference per kthread) - _kthread_create takes crhold(proc0.p_ucred) (kern_kthread.c:62) but the LWKT exit path never drops it: lwkt_exit parks the struct without crfree (lwkt_thread.c contains zero crfree calls) and lwkt_init_threads bzero silently destroys the held reference on recycle; the only td_ucred crfree is the LWP path (kern_exit.c:717), unreachable for pure LWKT threads. Minimal today (leak is always on the immortal proc0 credential; cr_ref is 64-bit). On the fix kernel (crfree in lwkt_exit, guarded, before parking) the same 8-cycle batch moved cr_ref by 0.