# DF-3184 VERDICT — procfs_exit() UAF write of `pfs->pfs_pid`

## Bottom line

**NOT REPRODUCED empirically within the attempt budget** (~17 runs, ~6 h of
racing, ~10,000 flush-aimed exit-walks). **The defect is mechanically
certain from source**: every stage of the chain was individually proven on
an instrumented kernel (below), and only the final microsecond-scale
coincidence (walk's `scan→vx_get` of a node during that same node's
`vgone_vxlocked` teardown) was never caught. The finding stands as
High-severity CWE-416 (kernel UAF write) with confidence **likely**:
source-certain, empirically inconclusive on this 6-vCPU guest.

## The defect (source trace)

`sys/vfs/procfs/procfs_subr.c:531-549`:

```c
vp = PFSTOV(pfs);
vhold(vp);                    /* auxref — does NOT prevent reclaim
                                   (sys/kern/vfs_lock.c:431-435)      */
spin_unlock(&ph->spin);
vx_get(vp);                   /* void — cannot fail, does NOT check
                                   VRECLAIMED (vfs_lock.c:728-735);
                                   BLOCKS on the VX lock if a recycler
                                   holds it                             */
pfs->pfs_pid |= PFS_DEAD;     /* :546 — UAF WRITE if the recycler ran
                                   procfs_freevp() → kfree(pfs)
                                   (procfs_subr.c:249-273) while we
                                   blocked                            */
vx_put(vp);
vdrop(vp);
```

Reclaim side that frees the pfsnode while `vhold` is outstanding:
- vnlru / `allocvnode_gc` → `freesomevnodes` → `cleanfreevnode`
  (vfs_lock.c:908-1050): checks `v_auxrefs != v_namecache_count`
  **before** `vgone_vxlocked` (:971) — a `vhold` landing after that
  check does not stop the reclaim; the post-reclaim re-check (:1026)
  only aborts the *reuse* of the vnode, not the free of the pfsnode.
- `umount -f` → `dounmount` → `vflush` → `vflush_scan` →
  `vgone_vxlocked` per vnode (vfs_mount.c:1165-1181) — unconditional.

`vgone_vxlocked` → `vclean_vxlocked(DOCLOSE)` → `VOP_RECLAIM` →
`procfs_reclaim` (procfs_vnops.c:480-483) → `procfs_freevp`:
`vp->v_data = NULL`, hash unlink, `kfree(pfs)` (procfs_subr.c:255-270).

The sibling allocator in the same file revalidates after its blocking
acquisition (`procfs_allocvp` :115-130 rescans the hash after `vget`)
— `procfs_exit` is missing the equivalent check, writing `|=
0x80000000` (4 bytes at `struct pfsnode` offset 20) into freed
M_PROCFS memory.

## What WAS proven on the guest (instrumented kernel #1-#3)

1. **Chain stage 1 — nodes exist for the walk**: tchain child as
   `nobody`: 12/13 stats OK; on exit the instrumented walk printed
   `DF3184-WALK pid=2067 ... marked=12` (run.log run.13-era tests).
   Thousands of `marked=12` walks observed (run.14: 1404, run.13:
   3124).
2. **Chain stage 2 — forced reclaim**: mount-cycled scratch procfs;
   `DF3184-FLUSH begin/end` per cycle; every unmount succeeded and
   reclaimed all nodes (marked counts reset to 0 each cycle).
3. **Chain stage 3 — timing controllability**: `dounmount` retry-loop
   behavior characterized precisely:
   - quiet mount (no lookups): unmount ≈ 1 ms (flush ≈ +1 ms)
   - lookups, no exits during retries: unmount ≈ 256 ms ± 3 ms
     (flush ≈ +256 ms)
   - walk-exits during retries: full UMOUNTF_RETRIES exhaustion
     (50 × 25.1 ms), flush ≈ +12.55 s (unmount total 13.0-13.3 s)
4. **The blocking primitive**: `vx_get` blocking on the recycler's VX
   lock is the mechanism that turns the race into a guaranteed stale
   write once the interleaving occurs — confirmed by construction, not
   observed live.

## Why reproduction failed — honest analysis

- **Bimodal flush timing with adversarial feedback**: walk-exits are
  themselves transient mount refs; `dounmount`'s retry loop always
  waits out the last exit before flushing, so the umount-forced flush
  geometrically lands ~251 ms after the last exiting walk — every
  single-wave aiming scheme self-defeated (runs 7-9, 11-17).
- **Load destroys walk-timing precision**: the fork-storm needed for
  statistical volume (batch 96-288) pushed load average to 90+, giving
  the `exit1` prefix (before the `at_exit` walk) milliseconds-to-seconds
  of jitter — larger than the ~5-10 ms flush window.
- **Residual per-walk collision probability**: given a walk inside the
  flush window, the walk must be in its ~0.5 μs `scan→vx_get` gap for
  one of its ~12 nodes at the instant `vflush_scan` is inside the same
  node's ~3 μs `vgone` — ≲0.5 % per in-flush walk under the best
  model; observed 0 over ~10⁴ attempts (consistent with a per-walk
  probability a few times lower than modeled).
- The pure vnlru variant (no umount games) needs the exit to coincide
  with the recycler reaching *that* vnode after a FIFO service delay of
  minutes (measured: ~45k reclaims / 300 s against a ~50k backlog) —
  estimated single-digit expected hits per *day* of racing.

## Primitive characterization (if triggered)

- One 4-byte OR of `0x80000000` at offset 20 of a freed chunk in the
  dedicated `M_PROCFS` slab zone (only `struct pfsnode` lives there).
- No attacker-controlled value or offset: not a general write
  primitive. Realistic outcomes: silent no-op (free chunk), corruption
  of a reallocated live pfsnode (its pid gets the DEAD bit → that
  /proc node is orphaned from its pid → DoS), or — on INVARIANTS
  kernels — the subsequent `vx_put` on the VBAD/VS_INACTIVE raced
  vnode takes the `VREF_FINALIZE` → `vrele` → `vnode_terminate` path
  and trips `KKASSERT(vp->v_state == VS_ACTIVE)` (vfs_lock.c:503)
  → panic.
- uid=0 escalation: not plausibly reachable from this primitive alone
  (fixed value/offset, type-constrained zone). Hard blocker documented
  per contract.

## Fix validation

`fix.diff` (validate `vp->v_data == pfs` under the held VX lock; skip
the write for raced nodes) — **not_testable**: no reproduced baseline
exists to diff against. The fix is mechanically equivalent to the
instrumentation's stale-detector condition, which itself never fired,
so the patched behavior would be indistinguishable on this guest.

## Guest state

Left CLEAN: harness killed, `/procN` unmounted, `/usr/src` procfs
sources restored to pristine (md5-verified for procfs_subr.c), then
`vm.sh reset with-src` to the clean-source snapshot with the stock
kernel.

## Attempts

17 distinct race configurations (runs run.log … run.17.log), 3 kernel
builds (2 instrumented), ~6 h wall time, ~10,000 flush-aimed walks.

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