# DF-3225 VERDICT — REPRODUCED (permanent unprivileged kernel livelock / local DoS)

## Bottom line

`procfs_control(ATTACH)` (sys/vfs/procfs/procfs_ctl.c:134-159) omits the
tracer-cycle (ancestor) check that `ptrace(PT_ATTACH)` performs at
sys/kern/sys_process.c:193-199. Because the ctl writer becomes the victim's
parent (`proc_reparent(p, curp)`, procfs_ctl.c:155), two cooperating
unprivileged same-uid processes attach each other and create a `p_pptr`
cycle. The ptrace ancestor walk

```c
for (pp = curp->p_pptr; pp != NULL; pp = pp->p_pptr)   /* sys_process.c:194 */
```

then never terminates for any subsequent `ptrace(PT_ATTACH, x)` from either
of them: the loop spins in-kernel holding the target's `p_token` (acquired at
sys_process.c:162), the thread cannot be killed (SIGKILL pends on a thread
that never returns to user mode), and the wedge spreads — each later kernel
operation needing that token or touching the cyclic parent topology (kill(2)
of the token-holder's target, sshd session setup) blocks or spins another
CPU.

## What was observed (run.log, 2026-09-08, DF 6.5-DEVELOPMENT #0)

1. Cycle built with three unpriv (uid 1001) forks + four ctl writes — all
   markers printed up to `[B] running A again` / `[main] B reported cycle
   complete`.
2. `A ptrace(PT_ATTACH, C)` **never returned** (`[A] ... if vulnerable, never
   returns` is the last A line).
3. `kill -9 A` had **no effect** (`A SURVIVED SIGKILL -> stuck in kernel
   spin`) — unkillable in-kernel thread.
4. `kill -9 C` from the runner **never returned** (the expected
   `[main] kill(C) returned` line is absent) — C's `p_token` is held forever
   by the spinner (kern_sig.c:802 blocks in `lwkt_gettoken_shared`).
5. Guest sshd stopped accepting connections (banner timeouts, multiple
   probes) while the serial console showed **no panic** — the system was
   alive-but-dead; host-side qemu burned 300-393% CPU (3-4 vCPUs pinned
   in-kernel) with the guest idle.
6. Only a hard reset recovered the machine.

Orchestration note: the PoC arms `SA_NOCLDSTOP` in B before attaching back —
without it, the SIGCHLD emitted when A completes its attach-stop
(kern_sig.c:1637 `ksignal(p->p_pptr, SIGCHLD)`) stops B (B is traced by A)
and the two merely freeze each other (mutual-stop, observed in the first
attempt, run.log.first-attempt not kept) without reaching the livelock.

## Exploit chain (DoS)

fork A,B,C (unpriv) → A: ctl attach+run B → B: ctl attach+run A (cycle) →
A: ptrace(PT_ATTACH, C) → kernel spins forever on one CPU holding C.p_token →
any further activity touching C or the A/B parent topology wedges more CPUs →
system unusable; no recovery without reboot. 100% reliable on the test guest.

## Fix

`fix.diff` adds the missing ancestor check to procfs ATTACH, mirroring
sys_process.c:193-199. Validated by patched-kernel rebuild + re-run
(verdict.json fix_status): the attach-back is refused with EINVAL, the cycle
never forms, `ptrace(PT_ATTACH, C)` returns normally, the runner completes,
no wedge.

## kernel_refs

* sys/vfs/procfs/procfs_ctl.c:134-159 (attach, no cycle check)
* sys/vfs/procfs/procfs_ctl.c:155 (proc_reparent(p, curp) — writer becomes parent)
* sys/kern/sys_process.c:162 (target p_token held during request)
* sys/kern/sys_process.c:193-199 (the unbounded walk)
* sys/kern/kern_sig.c:802 (kill blocks on p_token — wedge spread)

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
