# DF-3224 — /proc/pid/ctl signal delivery performs no authorization (bypasses kill(2)'s CANSIGNAL: p_trespass + reaper_sigtest + pid-1 gate)

**File:** `sys/vfs/procfs/procfs_ctl.c:319-337` (`procfs_doctl`, signal branch)
**Severity:** Medium — authorization bypass (CWE-862), unprivileged local
**Status: REPRODUCED** on DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), 2 independent runs (before and after a clean-snapshot reset), both deltas.

## What the bug is

Writing a signal name (e.g. `"term"`, `"kill"`) to `/proc/<pid>/ctl` reaches
`procfs_doctl()`. When the writer is not the tracer of a stopped tracee
(`TRACE_WAIT_P` false), the only branch taken is:

```c
} else {
    ksignal(p, nm->nm_val);      /* procfs_ctl.c:333 — NO permission check */
}
error = 0;
```

`ksignal()` is the raw delivery primitive with **zero checks inside**
(kern_sig.c:1118). Every gate that `kill(2)` applies in `kern_kill()`
(kern_sig.c:803 `CANSIGNAL(p, sig, 1)`) is skipped:

* `p_trespass()` (kern_prot.c:1023) — uid/ruid rules incl. full-cred-drop cases;
* `reaper_sigtest()` (kern_fork.c:1285) — DragonFly's reaper-subtree signal scoping;
* the `pid == 1` NOREBOOT capability gate (kern_sig.c:786).

The only authorization that ever happens is the **open-time** vnode check:
`/proc/<pid>/ctl` is mode 0200 with `va_uid =` target's *current* effective uid
(procfs_subr.c:216, procfs_vnops.c:607), enforced once at open; P_SUGID targets
have the write bits stripped only for *new* getattr/open calls
(procfs_vnops.c:584-588). A pre-opened fd keeps write permission across any
later credential transition of the target.

## Reproduced deltas (both unprivileged, uid 1001)

1. **Reaper-subtree bypass (fully unprivileged).** A same-uid attacker inside a
   `PROC_REAP_ACQUIRE` subreaper subtree cannot `kill(2)` a same-uid process
   outside it (`reaper_sigtest` → EPERM, enforcement working as designed), yet
   `write("term\n")` to `/proc/<victim>/ctl` delivers SIGTERM. This defeats the
   reaper isolation boundary exactly where it is designed to protect
   (supervised/sandboxed same-uid service trees).
2. **Kill of a root process after credential sanitization.** Attacker opens the
   ctl fd while the victim is uid 1001; victim execs a setuid-root helper that
   fully drops credentials (`setresuid(0,0,0)`, su/login-style). `kill(2)`
   returns EPERM (all four uid combinations fail in `p_trespass`); the stale
   ctl fd still delivers SIGKILL — the now-root process dies.

## Files

* `df3224.c` — PoC (part 1 unprivileged; part 2 takes the setuid helper path)
* `dropdemo.c` — setuid-root credential-drop helper (one-time root install:
  `cc -o dropdemo dropdemo.c && chown root:wheel dropdemo && chmod 4555 dropdemo`)
* `build.log`, `run.log`, `run.2.log` — full logs (run.2 = after clean-snapshot reset)
* `fix.diff` — verified fix (CANSIGNAL gate, mirrors kern_kill)
* `verdict.json`, `manifest.json`

## Build & run

```
cc -O -o df3224 df3224.c
cc -O -o dropdemo dropdemo.c && chown root:wheel dropdemo && chmod 4555 dropdemo   # root, once
./df3224 /full/path/to/dropdemo      # or without arg for part 1 only
```

Expected (vulnerable): part 1 `kill → REFUSED (Operation not permitted)` then
`ctl write → DELIVERED`, victim exits 57 (42+15); part 2 `kill → REFUSED` then
`root-dropped victim KILLED by signal 9 via /proc ctl`.

Expected (patched fix.diff): ctl writes fail with EPERM; victims survive.
