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)
PoC verification
Evidence pack
findings/poc/DF-2968 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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
fixedA/B: baseline +8 refs per batch; fix kernel +0 (LWP paths already NULL the field in kern_exit.c, no double free).
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.
No comments yet.