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`):

```c
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)
```
