p_tracenode/p_traceflag mutated without target p_token -> refcount double-drop/UAF and NULL-deref TOCTOU
Summary
sys_ktrace by-pid ktrops(td,p,...) (:386) and by-pg LIST_FOREACH (:369) and ktrace_clear_callback allproc_scan (:412-427) and ktrwrite (:608-610 comment XXX not MP safe) all mutate/read target p_tracenode/p_traceflag WITHOUT holding p->p_token (code self-documents NOT MPSAFE yet :405). KTROP_SET oldnode=p_tracenode assign new ktrdestroy(&oldnode) (:514-518); two concurrent ktrace() on same pid each snapshot oldnode=A then each ktrdestroy(&A) -> double-free/UAF (ktrdestroy atomic_fetchadd on freed :481-486). ktrwrite NULL-check+inherit TOCTOU :608-610 if concurrent clearer NULLs between -> vn_lock(NULL) panic :633. Unprivileged self/child trace no special config. Impact: kernel panic (DoS) controlled UAF plausible. Fix: acquire p_token around ktrops/ktrsetchildren/clear_callback/ktrwrite.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0103 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| race.c | trigger-source | 3 caller processes racing ktrace(KTROP_SET/CLEAR) on same target pid | 3.1 KB | view raw |
| build.sh | build-script | cc -O2 -o race race.c | 98 B | view raw |
| run.sh | run-script | ./race | 238 B | view raw |
| run.log | run-log | race run output (panics; see panic.txt) | 404 B | view raw |
| panic.txt | panic-signature | panic: ktrdestroy KKASSERT kn_refs>0; ktrops->sys_ktrace | 416 B | view raw |
| VERDICT.md | verdict | full analysis: ktrops mutates p_tracenode without p_token -> double-free | 3.6 KB | β raw |
| README.md | readme | reproduce instructions | 1.1 KB | β raw |
| fix.diff | suggested-fix | acquire target p_token around ktrops in by-pid and by-pg paths | 1.0 KB | view raw |
| fix_build.log | build-log | single-fix kernel build (kern_ktrace.c) rc=0 | 5.6 MB | β download |
| fix_run.log | run-log | patched kernel -> RACE_DONE (no panic) over 2x30s runs | 161 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0103 β ktrace() mutates target p_tracenode without p_token (double-free)
| Verdict | REPRODUCED |
| Impact | kernel panic / local DoS (tracenode double-free race) |
| File | sys/kern/kern_ktrace.c:386,369 (ktrops :514-518, ktrdestroy :481) |
Build
cc -O2 -o race race.c
Run (unprivileged)
./race
Expected (bug present)
Within ~seconds the kernel panics and the guest drops to DDB:
panic: assertion "tracenode->kn_refs > 0" failed in ktrdestroy at kern_ktrace.c:481 ktrdestroy() -> ktrops() -> sys_ktrace()
ssh session dies; panic captured in dfbsd-qemu/boot.log.
Expected (fixed)
The process runs to completion and prints RACE_DONE: no panic.
Mechanism (short)
sys_ktrace holds only the caller's curp->p_token, not the target's. Two
separate caller processes (same uid) both call ktrace(KTROP_SET, target) and
each snapshots oldnode = p->p_tracenode then ktrdestroy(&oldnode) β the
second destroy sees kn_refs == 0 β KKASSERT β panic.
See VERDICT.md for the full analysis and fix.diff for the patch.
DF-0103 β ktrace() mutates target p_tracenode without p_token (double-free)
Verdict: REPRODUCED (kernel panic / DoS via tracenode double-free race)
Mechanism
sys_ktrace() acquires only the caller's token curp->p_token
(sys/kern/kern_ktrace.c:305). The by-pid path (:374-387) and by-pg path
(:363-372) then call ktrops(td, p, ...) on the target p without
acquiring p->p_token (ktrsetchildren self-guards at :544, but the plain
ktrops path does not). ktrops KTROP_SET (:514-518):
if ((oldnode = p->p_tracenode) != tracenode) { /* snapshot, no lock */
p->p_tracenode = ktrinherit(tracenode); /* atomic ++new.refs */
ktrdestroy(&oldnode); /* atomic --refs, free if 0 (:482) */
}
Two separate caller processes P1, P2 (each with its own curp->p_token,
so they do NOT serialize) both targeting the same pid T can each:
- snapshot
oldnode = p->p_tracenode = A, - set
p->p_tracenode = ktrinherit(new), - call
ktrdestroy(&A).
ktrdestroy (:474-488): KKASSERT(tracenode->kn_refs > 0) (:481) then
atomic_fetchadd_int(&kn_refs, -1) (:482). The second ktrdestroy(&A) sees
A->kn_refs == 0 (already decremented to 0 and freed by the first) and trips
the KKASSERT (:481) β or, racing the slab free, dereferences freed/poisoned
memory β slab INVARIANTS. Either way: panic.
ktrcanset() (:667-685) lets an unprivileged user trace its own (same-uid,
non-sugid) processes, so the whole race is reachable unprivileged, on the
default GENERIC kernel, with no special config.
Proof (decisive run)
3 caller processes (2 forked callers + parent) racing ktrace(KTROP_SET/CLEAR)
on the same target pid; panicked within seconds:
panic: assertion "tracenode->kn_refs > 0" failed in ktrdestroy at /usr/src/sys/kern/kern_ktrace.c:481 Trace beginning at frame 0xfffff801181b7758 ktrdestroy() at ktrdestroy+0x89 ktrdestroy() at ktrdestroy+0x89 ktrops() at ktrops+0x65 sys_ktrace() at sys_ktrace+0xe0 Stopped at Debugger+0x7c db>
The ktrops -> ktrdestroy stack and the kn_refs > 0 assertion pinpoint the
double-free of the shared p->p_tracenode. Reliably reproduced on the first
30s run. ssh dies; guest sits in DDB.
Impact / realism & escalation assessment
- Reachable unprivileged (same-uid target), default GENERIC, no config.
- Class: tracenode refcount double-free / UAF race. On the default GENERIC kernel (INVARIANTS ON) it is a deterministic kernel panic (local DoS).
- Escalation to
uid=0is not realistic on GENERIC: INVARIANTS trap thekn_refsunderflow atktrdestroy:481before any silent reclamation. The victim object (struct ktrace_node) holds only{ kn_vp (vnode *), kn_refs }; even on an INVARIANTS-OFF build the only corruption handle would be avn_close()on a reclaimed/forgedkn_vp(vnode confusion), which is far more effort than this Medium-severity finding warrants and is not a default-kernel path. The realistic default-kernel ceiling is local DoS.
Fix
fix.diff: acquire the target p->p_token around the ktrops call in both
the by-pid (:386) and by-pg (:369) paths (ktrsetchildren already
self-guards at :544, so the descend branch is left as-is). This serializes
concurrent ktrops mutations of the same target's p_tracenode, killing the
double-free. Supersedes (specifies) the finding's proposal.
Reproduce
cc -O2 -o race race.c # build.sh ./race # run.sh (panics the default kernel)
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
REPRODUCED (live panic). sys_ktrace by-pid ktrops no p_token -> tracenode double-free KKASSERT panic. Unprivileged same-uid.
No comments yet.