# DF-2980 — VERDICT

**Status: reproduced** (unprivileged local race; state corruption of security-relevant
`p_flags` bits demonstrated; fix validated) · **Confidence: certain** (mechanism),
exploit escalation **speculative**.

## Root cause (path:line)

- `sys/kern/subr_prof.c:145` — `addupc_task()` calls `stopprofclock(p)` with **no
  `p->p_token`**. Contract: `sys/kern/kern_clock.c:1303-1305` ("caller must hold
  p->p_token"). All other callers comply: `subr_prof.c:61-63` (sys_profil),
  `kern_exec.c:226/444`, `kern_exit.c:304/365`.
- `sys/kern/kern_clock.c:1308-1311` — `stopprofclock()` does read + non-atomic
  `p->p_flags &= ~P_PROFIL`. `p_flags` is a plain `int` (`sys/sys/proc.h:241`); RMWs are
  plain loads/stores (no `lock` prefix), so a competing CPU's store landing between a
  tokenless clear's load and store is silently overwritten.
- Tokenless call sites: `platform/pc64/x86_64/trap.c:231` (`userret()` — runs on **every
  syscall exit** of a `P_PROFIL` process) and `trap.c:507` (`RQF_AST_OWEUPC` AST);
  vkernel mirrors at `platform/vkernel64/x86_64/trap.c:197,405`.
- Racing token-held writers on the same word: `sys_process.c:305/313` (`|= P_TRACED`),
  `sys_process.c:362` (`&= ~(P_TRACED|P_WAITED)`), `kern_prot.c:1304` (`|= P_SUGID`,
  reached from setuid exec at `kern_exec.c:493`), `kern_sig.c:1286` (`|= P_CONTINUED`),
  `kern_exit.c:201` (`|= P_WEXIT`), etc. (Also note `tstop()` at `kern_synch.c:1368`
  clears P_WAITED under the *parent's* token — p_flags writers are not even mutually
  serialized; that broader issue is out of scope for this finding.)

## Threat

Any unprivileged process can generate a MHz-rate stream of tokenless
`p->p_flags &= ~P_PROFIL` RMWs: loop `profil(2)` with `pr_base` in unmapped user VA —
every syscall exit runs `userret → addupc_task → copyin(EFAULT) → stopprofclock`
(`trap.c:225-232`, `subr_prof.c:140-145`). Each stale store can annihilate a concurrent
token-held update of any *other* p_flags bit. Security gates affected:

- P_SUGID loss ⇒ ptrace attach check passes (`sys_process.c:202-204`) for a same-ruid
  setuid-root image ⇒ speculative full privesc (window ~ns inside `setsugid()` during
  exec; chain plausible — exec has no synchronous LWP teardown before `kern_exec.c:493` —
  but not demonstrated).
- P_TRACED loss ⇒ tracee escapes its tracer / debugger state corrupted.
- P_CONTINUED / P_WAITED loss ⇒ wait4(WCONTINUED) accounting corruption.
- P_WEXIT/P_POSTEXIT loss ⇒ exit-path correctness.

Demonstrated impact: deterministic-within-seconds corruption of kernel process state
(P_TRACED resurrection / annihilation) by an unprivileged user. No memory corruption:
`addupc_task`'s copyin/copyout targets are range-checked by `std_copyout`/`std_copyin`
(`platform/pc64/x86_64/support.s:245-258,295-307`), so escalation beyond flag corruption
was not pursued further.

## Reproduction narrative

1. Built `poc.c` in guest (cc, unprivileged-capable). Victim child: two threads looping
   `syscall(SYS_profil, (void*)0x400000000000, 0x100000000, 0, 0x10000)`; tracer parent
   (same uid): `PT_ATTACH → waitpid(stop) → PT_DETACH` loop.
   - First attempt failed: `PT_ATTACH` re-attach got EBUSY (traced relationship persists
     until DETACH) → reworked to attach/detach cycle.
   - Second failure mode (understood, not a hit): multi-threaded victim's straggler
     `tstop()` re-clears P_WAITED after waitpid consumed the stop → DETACH EBUSY artifact;
     handled by re-consuming the re-posted stop event (`waitpid(WNOHANG|WUNTRACED)`).
2. Baseline (kernel #0, stock INVARIANTS):
   - run.log: **HIT2 after 99310 cycles / 1.9 s** — `PT_ATTACH` EBUSY right after a
     successful `PT_DETACH`; state dump `kp_stat=2 (SACTIVE)`, `kp_flags=0x1c20`
     (P_TRACED|P_WAITED|P_PROFIL set) — the child was *running* with P_TRACED still set.
     Attribution: while the process is fully running no `tstop()` can be in flight;
     the only tokenless p_flags writer active is `addupc_task→stopprofclock`.
   - run.2.log: 300000 cycles, no hit (race is probabilistic).
   - run.3.log: **HIT2 after 16166 cycles / 0.33 s** (`kp_flags=0xc20`).
3. Fix validation (`fix.diff`: `lwkt_gettoken/reltoken(&p->p_token)` around the
   `stopprofclock()` in `addupc_task`):
   - `make nativekernel` + `make installkernel` in guest → kernel #1 Sep 4 2026.
   - sanity: normal profiling still accumulates samples (`sanity.c`, sum > 0).
   - 3 × 1,000,000 cycles (~49 s ≈ 10× the exposure in which baseline hit twice):
     **zero hits** (fix-patched.log).
4. Guest reset to pristine baseline afterwards (`vm.sh reset with-src`, verified #0 kernel
   and pristine `subr_prof.c` md5 `974b1875e9983feeb4595c295cd0ad40`).

## Honest limits

- HIT1 (EPERM flavor: annihilated *set* rather than annihilated *clear*) was not observed
  in the recorded runs; HIT2 is the same defect in the stale-store direction and is
  state-verified. The EPERM flavor remains expected under longer runs.
- The P_SUGID → ptrace-a-setuid-root-image privesc chain is argued from source, not
  demonstrated.
- 1-in-3 baseline runs produced no hit within 300K cycles — the defect is a race;
  reproduction is probabilistic but fast (sub-2 s in both hitting runs).
