# DF-3235 VERDICT — reproduced (kernel heap UAF read → cross-process info disclosure)

## Bottom line

`procfs_docmdline()` snapshots a pointer to the target's `p_args`
(`sys/vfs/procfs/procfs_status.c:241-242`), then performs a potentially
blocking `uiomove_frombuf()` (procfs_status.c:292) that copies from that
pointer to userland.  The whole procfs read path runs under the target's
`p_token` (acquired in `pfs_pfind()`, `sys/vfs/procfs/procfs_subr.c:298`),
but DragonFly LWKT tokens are *dropped whenever the holding thread blocks*
(`sys/kern/lwkt_token.c:40`: "If the thread blocks all tokens are released,
then reacquired").  A page fault on the *reader's* destination buffer inside
`uiomove()`'s `copyout()` is such a block.  During the block, a concurrent
`execve()` in the target frees the pargs chunk
(`sys/kern/kern_exec.c:589-596`: `p->p_args = NULL;
refcount_release(&pa->ar_ref) → kfree(pa, M_PARGS)`), and any process's
next exec re-allocates the same slab chunk and fills it with ITS argv.
When the reader resumes, it copies the recycled chunk's bytes to userland:
a kernel heap use-after-free READ that discloses another process's
argument vector, bypassing the CHECKIO/p_trespass gate that
procfs_docmdline itself enforces (procfs_status.c:203-205, 220-222,
237-239).

The identical structural flaw exists for the two other data sources in the
same function: `lp->lwp_lpmap->thread_title` (procfs_status.c:202,213-217)
and `p->p_upmap->proc_title` (procfs_status.c:219,230-234), freed by
`lwp_userunmap()`/`proc_userunmap()` during target exec/exit
(`sys/kern/kern_exec.c:432-433,981-982`; `sys/kern/kern_exit.c:688,1269`)
— also with no reference that survives a block.

The kernel itself proves the required discipline in the sibling sysctl
path: `sysctl_kern_proc_args()` takes `refcount_acquire(&pa->ar_ref)`
around its (blocking) `SYSCTL_OUT` for exactly this reason
(`sys/kern/kern_proc.c:1943-1949`).  procfs_docmdline omitted it.

## How it was reproduced (guest, unprivileged user maxx)

racer.c stages three unprivileged actors:

1. **victim** (pinned to cpu1) alternately execs itself with a small argv
   (~176 bytes ⇒ `struct pargs` lands in the 192-byte slab zone, cached
   because 8+176 ≤ ps_arg_cache_limit=256) and with a 6KB argv
   (> 256 ⇒ kern_exec.c:601 does NOT cache ⇒ p_args freed and left NULL).
   Each small→big exec is one guaranteed `kfree(M_PARGS)` with no
   same-process realloc to hide it.
2. **fillers** (pinned cpu1/cpu2) exec themselves with a byte-count-
   identical MARKER argv — constant same-zone churn that recycles the
   victim's freed chunk.
3. **readers** `readv()` /proc/<victim>/cmdline with 184 single-byte
   iovecs into 184 never-touched 4KB-strided pages of a 3.5GB file
   (> guest RAM, so every fault is a cold disk fault).  Each iovec
   copyout faults and blocks; the victim's exec+free+filler-realloc land
   inside the block; the remaining iovecs copy the recycled chunk.

Detection: the victim's argv alphabet is only {'/','home','maxx','racer',
'victim','A','A'-padding}.  Any run of 'F' bytes (the filler's pad) or the
MARKER string in the returned 184 bytes is impossible content ⇒ proof the
bytes came from a different process's recycled pargs chunk.

Observed torn reads (stock kernel #0):

* run.12: `184B 8.00ms` — 'A'*134 then 'F'*~49 (tear mid-chunk)
* run.13: `184B 4.62ms` — 'A'*~107 then 'F'*~75 (HIT)
* run.15: `184B 3.38ms` — 'F'*2 at the very tail (2-byte tear) and
  `184B 5.27ms` — 'A'*~93 then 'F'*~90 (HIT; controller printed
  `RESULT: UAF-READ REPRODUCED`)

4 torn reads in 3 consecutive runs (~780s of racing) ⇒ reproducible, not a
fluke.  `reproduced=1`, `impact=leak` (kernel heap UAF read disclosed to
unprivileged userland; confidentiality of other processes' argv broken;
the recycled chunk can belong to ANY local process, any uid — the PoC's
filler is same-uid only to satisfy the gate on the *victim*).

Not a write primitive: `buflen` is snapshotted (victim's ar_length), so
the overrun direction is a bounded read of one ≤ps_arg_cache_limit-byte
M_PARS chunk (default 256B; sysctl-raisable).  No uid=0 chain; ceiling is
repeated cross-user argv disclosure plus stale-heap content within the
M_PARS zone.

## Why earlier attempts missed (engineering notes)

* single-page faults into cached/BCACHE-resident files never sleep
  (tokens never dropped) — needed a 3.5GB backing file and strided cold
  pages;
* ms-scale latencies measured on plain reads were pre-snapshot p_token
  waits (victim's exec holds p_token), useless for the race — the 184×1B
  readv turns the *post-snapshot* copy into ~184 serialized cold faults
  (35-500ms windows);
* the victim's small→small exec reallocates the same chunk itself — the
  big-argv exec (no realloc) is what exposes the freed chunk to the
  filler;
* slab zones are per-16-bytes in the 128-255 range (kern_slaballoc.c:55)
— victim and filler argv byte counts must be exactly equal (184) to share
the 192 zone;
* `usched_set` (syscall 481, USCHED_SET_CPU) pins victim+fillers so the
  filler's kmalloc sees the victim's kfree through the per-CPU magazine.

## Fix validation (mandatory: memory-corruption class)

`fix.diff` (regenerated clean, `patch -p1 --dry-run` validated):
* pargs branch: `refcount_acquire(&pa->ar_ref)` + snapshot-copy (or hold
  the ref across uiomove when ar_length > snapshot buffer) — mirrors
  `sysctl_kern_proc_args()` (kern_proc.c:1943-1949);
* lpmap/upmap branches: non-blocking `bcopy` of the title into a
  pre-allocated snapshot buffer while p_token is genuinely held (no
  allocation between scan and copy), then uiomove from the snapshot.

Guest: `vm.sh reset with-src`, patch applied to /usr/src, first build
failed on a redundant `MALLOC_DECLARE(M_PARGS)` (already declared in
sys/proc.h:454) — removed; `make nativekernel KERNCONF=X86_64_GENERIC`
(1117s) + `make installkernel` + reboot into kernel
`#1: Wed Sep  9 02:37:12 UTC 2026`.

Re-run of the exact PoC on the patched kernel: 2 × 300s runs
(run.fix.1.log, run.fix.2.log): ~22k legit p_args reads, every one of
them blocked ms-scale in the copyout fault (slow==args), **zero torn/F/
MARKER bytes**, `RESULT: no hit within timeout` both times.  cmdline
functionality regression-checked (`/proc/self/cmdline`, `ps`, status
nodes all fine).  Baseline (stock) reproduced 4 torn reads in less total
racing time.  `fix_status=fixed`.

Guest reset to clean `with-src` snapshot after validation.

## Kernel references

* sys/vfs/procfs/procfs_status.c:202-247 (three snapshot branches),
  :292 (blocking uiomove_frombuf from the captured pointers)
* sys/vfs/procfs/procfs_subr.c:281-306 (pfs_pfind: p_token held),
  :346-433 (procfs_rw)
* sys/kern/kern_exec.c:226,589-608 (pargs free/realloc under p_token),
  :432-433,981-982 (lwp_userunmap/proc_userunmap during exec)
* sys/kern/kern_proc.c:1943-1949 (correct refcount discipline, the model)
* sys/kern/kern_proc.c:1300-1310,1356-1383 (proc_userunmap/lwp_userunmap)
* sys/kern/kern_exit.c:688,1269 (teardown call sites)
* sys/kern/lwkt_token.c:40 (tokens dropped on block)
* sys/kern/kern_subr.c:96-160 (uiomove copyout), :272-284 (uiomove_frombuf)

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