# DF-5822 — sysctl_kern_proc uses un-held `cr1 = curproc->p_ucred` across blocking copyouts → ucred UAF read → visibility bypass (ps_showallprocs=0)

## What

`sysctl_kern_proc()` caches a **raw, unreferenced** credential pointer:

```c
1669:	struct ucred *cr1 = curproc->p_ucred;
```

and then uses it across dozens of blocking `SYSCTL_OUT` copyouts inside the
per-process loop:

```c
1715:	if (ps_showallprocs == 0) {
1716:		crcache = pcredcache(crcache, p);
1717:		if (crcache == NULL ||
1718:		    p_trespass(cr1, crcache)) {      /* cr1 dereferenced per row */
1768:	if (!PRISON_CHECK(cr1, crcache))
1789:	if (!ps_showallthreads || jailed(cr1))
```

The trap layer caches `td_ucred` with a held reference per syscall
(sys/platform/pc64/x86_64/trap.c:181-206), but that protects only the cred
current at *entry*: any cred installed between entry and line 1669 is
returned by the raw read with **no reference held by this thread**.  When
another thread of the same process then replaces credentials —
`cratom_proc()` (sys/kern/kern_prot.c:1170-1190) installs `newcr` and
`crfree(oldcr)` unconditionally on every setuid-family change — the
pointed-to `struct ucred` is freed while `cr1` keeps dereferencing it for
the remainder of the (multi-millisecond, user-copy-blocked) sysctl.

`p_trespass()` (sys/kern/kern_prot.c:1023-1059) then reads the freed
chunk; a recycled chunk (crget() zeroes on allocation, kern_prot.c:1065)
reads `cr_uid == 0`, hitting:

```c
1057:	if (cr1->cr_uid == 0)
1058:		return (0);      /* blanket no-trespass */
```

→ other users' `kinfo_proc` rows are emitted to a poller whose real euid
is unprivileged.

## Reproduced on the guest

Under `security.ps_showallprocs=0` (the documented hardening config for
multi-user boxes):

| run | poll iterations | bypass iterations | root rows leaked |
|-----|-----------------|-------------------|------------------|
| 1 (30 s) | 632,433 | **78,481 (12.4 %)** | **80,584** |
| 2 (25 s) | 845,892 | **73,310** | **73,620** |
| 3 (25 s) | 854,135 | **60,297** | **60,635** |
| control (churn disabled, 719,556 iters) | — | **0** | **0** |

The churn uses `setegid(0) <-> setegid(1001)` (legal for the setuid-root
scaffold with svgid 0) which **never touches `cr_uid`**: a *fresh* `cr1`
always has `cr_uid == 1001` and can never legitimately pass
`p_trespass()` against root-owned processes — every observed root row is
therefore attributable to the stale-pointer read.

Default-config impact (`ps_showallprocs=1`): the p_trespass gate is
skipped; remaining consumers are `PRISON_CHECK`/`jailed()` comparisons
against freed memory — wrong jail visibility decisions on multi-jail
hosts, no crash.

## Rig

`df5822_rig.c` — setuid-root scaffold (mode 6755 so svgid==0 survives the
euid drop): main drops to euid 1001, thread B churns setegid, thread A
polls `kern.proc.uid.0`.  Root scaffolding (one-time):
`sysctl -w security.ps_showallprocs=0; chown root:wheel df5822_rig;
chmod 6755 df5822_rig`.

## Fix

```diff
-	struct ucred *cr1 = curproc->p_ucred;
+	struct ucred *cr1 = curthread->td_ucred;
```
(td_ucred is the trap-layer-cached, held credential — the same idiom
kern_descrip.c:984/2095 already use; likewise for the two other raw
caches at kern_proc.c:1878 and kern_proc.c:2038.)

Validated by kernel rebuild + re-run: bypass disappears (see VERDICT.md).

## Files

`df5822_rig.c`, `build.sh`, `run.sh`, `run.log`, `run.2.log`,
`control.log`, `VERDICT.md`, `manifest.json`, `verdict.json`, `fix.diff`,
`env.txt`, `build_fix.log`, `run_patched.log`
