# DF-5822 VERDICT

**status: reproduced / impact: visibility-bypass info disclosure
(kernleak) / confidence: certain / fix validated (fixed)**

## Root cause (path:line)

1. `sysctl_kern_proc()` caches an unreferenced credential:
   sys/kern/kern_proc.c:1669 `struct ucred *cr1 = curproc->p_ucred;`
2. It dereferences `cr1` per scanned row across blocking `SYSCTL_OUT`
   copyouts: kern_proc.c:1715-1721 (`p_trespass(cr1, crcache)` under
   `ps_showallprocs==0`), 1768 (`PRISON_CHECK`), 1789 (`jailed(cr1)`).
3. The per-syscall trap-layer credential cache
   (sys/platform/pc64/x86_64/trap.c:181-206) only guarantees a held
   reference for the cred installed at *entry*; a cred installed between
   entry and line 1669 is returned by the raw read with no reference.
4. Any other thread of the same process replacing credentials frees it:
   `cratom_proc()` sys/kern/kern_prot.c:1170-1190 (`p->p_ucred = newcr;
   ... crfree(oldcr)` — invoked by every setuid-family change, e.g.
   change_euid/change_egid).
5. `p_trespass()` sys/kern/kern_prot.c:1023-1059 dereferences the freed
   chunk; `crget()` (kern_prot.c:1065, M_ZERO) recycles chunks zeroed, so
   the stale read sees `cr_uid == 0` and hits `if (cr1->cr_uid == 0)
   return (0);` (kern_prot.c:1057-1058) — blanket no-trespass.

The dangling `cr1` persists for the whole multi-millisecond sysctl (the
pointer is read once at 1669), so each poisoned call misfilters every row
it scans.

## Why the rows prove the UAF (not a legit euid window)

The churn is `setegid(0) <-> setegid(1001)`, which goes through
`cratom_proc()` but **never modifies `cr_uid`**: a *fresh* `cr1` always
has `cr_uid == 1001` (ruid 1001 as well), so `p_trespass(fresh, root)`
can never return 0 against root-owned processes.  Every emitted root row
under `ps_showallprocs=0` is therefore attributable solely to the stale
(freed/recycled) pointer read.

## Baseline results (unpatched snapshot kernel, 2026-10-13)

| run | duration | poll iters | bypass iters | root rows |
|-----|----------|-----------|--------------|-----------|
| 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 | 30 s (churn inert: setegid EPERM, scaffold mode 4555) | 719,556 | **0** | **0** |

Marker line (run.log): `[rig] CHURN  iter 632204: 1 ROOT rows visible with
euid=1001  <== STALE cr1 BYPASS`.

## Fix validation (same guest, rebuilt kernel)

* fix.diff applied to the guest's /usr/src (all 3 sites: kern_proc.c
  1669/1878/2038 `cr1 = curthread->td_ucred`), `make nativekernel
  KERNCONF=X86_64_GENERIC` + installkernel, reboot into
  `#1: Tue Oct 13 06:36:10 UTC 2026`.
* Same rig, same scaffold, same `ps_showallprocs=0`:
  - patched run 1: 922,345 poll iterations → **0 bypass, 0 root rows**
  - patched run 2: 894,484 poll iterations → **0 bypass, 0 root rows**
  (run_patched.log)
* Baseline vs patched: 60k-78k bypass iterations per ~25-30 s → zero.
  The visibility bypass is gone; churn rate and poll rate unchanged
  (poll iterations actually higher — no row copies happening).

## Impact / severity reasoning

* Under `security.ps_showallprocs=0` (the documented hardening used on
  shared multi-user machines): full `kinfo_proc` rows (argv-adjacent
  metadata, rusage, tty, exit status) of every other uid — including
  root — disclosed to an unprivileged poller, ~12% of polls.  Info leak,
  no crash, no memory-content disclosure beyond the row data.
* Default config (`ps_showallprocs=1`): the p_trespass gate is skipped;
  remaining uses of the stale pointer are `PRISON_CHECK` (1768) and
  `jailed()` (1789) — on multi-jail hosts these can mis-judge prison
  visibility from freed memory; no crash observed or expected
  (M_CRED zone-resident).
* UAF *read* only; no write primitive; no uid change.  Severity: Medium.

## Reproduce

```
# one-time root scaffolding
cc -O2 -pthread -o df5822_rig df5822_rig.c
chown root:wheel df5822_rig && chmod 6755 df5822_rig
sysctl -w security.ps_showallprocs=0
# unprivileged
./df5822_rig 25
# unpatched: "[rig] CHURN ... <== STALE cr1 BYPASS" + "root rows observed: NNNNN"
# patched:   "root rows observed   : 0"
```

## 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.
