# DF-2706 VERDICT — kern.proc.pid.<pid> bypasses security.ps_showallprocs

**Status: REPRODUCED** (impact: leak — visibility-control bypass / kernel-pointer
re-disclosure), confidence: certain.

## What was claimed

`sysctl_kern_proc()`'s `KERN_PROC_PID` fast path (sys/kern/kern_proc.c:1686-1694)
omits the `ps_showallprocs == 0 → p_trespass()` visibility gate that the main
enumeration loop applies (kern_proc.c:1715-1721). Consequence: with
`security.ps_showallprocs=0` (the knob admins set on shared multi-user boxes to
hide other users' processes), any unprivileged local user can still retrieve the
complete `struct kinfo_proc` of *any* pid — including uid-0 daemons — by walking
`kern.proc.pid.<pid>` for pid in 1..99999. The record includes `kp_comm`,
credentials, session/pgrp ids, signal state, and the raw kernel-heap pointer
`kp_paddr` (re-enabling the DF-0179/DF-0016 address disclosure that the knob is
supposed to gate for untrusted users).

## How it was verified (guest: DragonFly 6.5-DEVELOPMENT, stock INVARIANTS
kernel #0, `uname -a` in env.txt)

1. Root started a uid-0 marker (`/tmp/snoopmark`, a copy of /bin/sleep), pid 893,
   and set `security.ps_showallprocs=0`.
2. Unprivileged user (uid 1001) built and ran `pidsnoop` (build.log / run.log):
   - **[A] `kern.proc.pid.893` → 1 record**: `pid=893 ppid=1 uid=0 pgid=890
     sid=890 comm="snoopmark"`, `kp_paddr=0xfffff80116aa9180` — full disclosure
     of a root process.
   - **[B] `kern.proc.all` → 3 records** — only the caller's own processes
     (gate enforced on the main loop).
   - **[C] `kern.proc.uid.0` → 0 records** — gate enforced on the uid selector.
3. Stability re-run (run.2.log) reproduced [A] byte-identically and additionally
   disclosed pid 1 (`comm="init"`, `kp_paddr=0xfffff80089977280`).
4. Guest left clean: knob restored to 1, marker killed, no kernel state dirtied.

The A/B/C contrast isolates the defect to the KERN_PROC_PID branch: sibling
selectors through the same handler honor the knob; only the pid fast path
bypasses it.

## PoC fixes vs the naive approach

`sysctlbyname("kern.proc.pid.<pid>")` fails with ENOENT: DFly's name2oid does
not register per-pid children of the node. The branch is reached via the raw
MIB — `sysctlnametomib("kern.proc.pid", mib, &len)` then `mib[len++] = pid` —
because kern_sysctl.c:1476-1478 forwards trailing MIB components into the node
handler as `arg1/arg2`. This is exactly how the sysctl(8) string form would
work if the resolver supported numeric children; it is a 3-line C program.

## Why this matters / impact ceiling

- Metadata of every process (names, uids/gids, sessions, pgrps, signal masks,
  thread lists with `_lwp` variants) → process-table reconnaissance that the
  admin explicitly disabled.
- `kp_paddr` / `kl_wchan` / `kp_ktaddr` raw kernel pointers (DF-0179/DF-0016)
  remain readable per-pid even with the knob at 0 → KASLR-defeat mitigation
  value of the knob is void.
- No memory corruption; read-only disclosure. Severity Low (requires the
  non-default hardened setting; default `ps_showallprocs=1` shows everything
  anyway).

## Fix

Gate the fast path exactly like the main loop (fix.diff):

```diff
 		crcache = pcredcache(crcache, p);
-		if (PRISON_CHECK(cr1, crcache))
+		if (PRISON_CHECK(cr1, crcache) &&
+		    (ps_showallprocs || !p_trespass(cr1, crcache))) {
 			error = sysctl_out_proc(p, req, flags);
+		}
 		PRELE(p);
```

Fix validated: not rebuilt (one-line logic gate, non-memory-corruption class;
contract mandates kernel rebuild validation only for memory-corruption
findings). The behavioral A/B/C proof above pins the code path unambiguously.
