# DF-3225 — procfs ATTACH lacks the ptrace tracer-cycle check → p_pptr cycle → unbounded kernel ancestor walk (permanent livelock)

**File:** `sys/vfs/procfs/procfs_ctl.c:134-159` (`procfs_control`, PROCFS_CTL_ATTACH)
**Severity:** Medium (unprivileged local DoS, permanent, progressive whole-system wedge)
**Status: REPRODUCED** on DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS, 6 vCPU).

## What the bug is

`ptrace(PT_ATTACH)` refuses to attach to any ancestor of the caller when the
caller is itself traced — this prevents tracer cycles:

```c
if (curp->p_flags & P_TRACED)
    for (pp = curp->p_pptr; pp != NULL; pp = pp->p_pptr)
        if (pp == p) { ... return (EINVAL); }   /* sys_process.c:193-199 */
```

procfs's equivalent attach (`write("attach\n", "/proc/<pid>/ctl")`) has **no
such check** (procfs_ctl.c:134-159 checks only P_TRACED-already, self, and
CHECKIO/p_trespass). The ctl **writer becomes the victim's parent**
(`proc_reparent(p, curp)`, procfs_ctl.c:155). Two cooperating unprivileged
same-uid processes can therefore attach *each other*:

```
A --ctl "attach"--> B   : B.p_pptr = A   (B stopped, then A runs B)
B --ctl "attach"--> A   : A.p_pptr = B   => parent-cycle A <-> B
```

Both remain P_TRACED and runnable (run via ctl `"run"`; note B must arm
`SA_NOCLDSTOP` first — see PoC comment — or the SIGCHLD emitted when A
completes its stop stops B, since B is traced by A).

With the cycle in place, the very check ptrace *has* becomes the weapon:
`ptrace(PT_ATTACH, <any untraced pid C>)` from A (or B) walks
`pp = curp->p_pptr` forever — `A.p_pptr = B`, `B.p_pptr = A`, `pp != NULL`
always, `pp == p` (C) never. The walk spins **inside the kernel holding C's
p_token** (taken at sys_process.c:162), unkillable (SIGKILL pends, never
delivered), and every subsequent kernel operation that needs C's token — or
touches the poisoned parent topology — wedges another CPU: `kill(C)` from
another process, sshd session setup, etc. No panic; a progressive, permanent
livelock from a plain unprivileged user.

## Reproduced evidence (run.log)

```
[main] A=856 B=855 C=854
[A 856] attaching B (855)
[A] running B
[B 855] attaching A (856) back -> cycle
[B] running A again
[main] B reported cycle complete
[main] go marker set; A enters ptrace walk
[A 856] ptrace(PT_ATTACH, 854): if vulnerable, never returns
[main] after 5s: kill -9 A (856)
[main] A SURVIVED SIGKILL -> stuck in kernel spin
[main] now kill -9 C (854): needs C.p_token held by spinning A -- expect HANG
<no further output — kill(C) never returned>
```

* final line `[main] kill(C) returned` never printed (runner hung in kill(2));
* guest ssh became unavailable (banner timeouts) while the console showed no
  panic (wedge_evidence.txt);
* host-side qemu CPU pinned at 300-393% (3-4 of 6 vCPUs spinning in-kernel)
  with the guest otherwise idle — classic livelock, not load;
* guest required `vm.sh reset` — unkillable, unrecoverable.

## Files

* `df3225.c` — PoC (unprivileged; builds the cycle, triggers the walk, proves
  unkillability + wedge spread)
* `run.log` — decisive run (direct output until ssh died)
* `wedge_evidence.txt` — host-side qemu CPU + ssh probe transcripts
* `boot.log` — serial console (no panic; guest alive-but-dead)
* `fix.diff` — verified fix (ancestor check mirroring sys_process.c:193-199)
* `verdict.json`, `manifest.json`

## Build & run

```
cc -O -o df3225 df3225.c
./df3225          # WARNING: wedges the machine; expect to hard-reset after
```
