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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0103 Β· 12 files
FileTypeDescriptionSize
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
README.md readme reproduce instructions
↓ download 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.

VERDICT.md verdict full analysis: ktrops mutates p_tracenode without p_token -> double-free
↓ download raw

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:

  1. snapshot oldnode = p->p_tracenode = A,
  2. set p->p_tracenode = ktrinherit(new),
  3. 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=0 is not realistic on GENERIC: INVARIANTS trap the kn_refs underflow at ktrdestroy:481 before 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 a vn_close() on a reclaimed/forged kn_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

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Fri Jul 17 17:35:56 UTC 2026

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.