# DF-2886 VERDICT — uiomovez() reads past ZeroPage into userspace

## Bottom line

**REPRODUCED (impact: leak).** `uiomovez()`'s `UIO_USERSPACE` branch
performs `copyout(ZeroPage, iov->iov_base, cnt)` with `cnt` bounded only
by `min(iov_len, n)`, while `ZeroPage` is a fixed `PAGE_SIZE` (4096)
allocation (sys/kern/kern_slaballoc.c:316). For any `cnt > 4096` the
kernel copies `cnt - 4096` bytes of adjacent kernel heap into the
calling process's buffer. A KLD harness driving the engine's public API
delivered 64 stable non-zero kernel bytes (slab-allocator bookkeeping)
into an unprivileged (`nobody`) process's read buffer across 3+1 runs,
identical byte-for-byte across the first three. The one-hunk fix
(clamp `cnt` to PAGE_SIZE) was built as kernel #1 and eliminates every
non-zero byte while preserving the zero-fill contract (`read()` still
returns 65536 zeros). No panic; system stable before/after.

## Why it leaks (path:line)

- sys/kern/kern_subr.c:241-242 — `if (cnt > n) cnt = n;` — no PAGE_SIZE clamp
- sys/kern/kern_subr.c:246 — `error = copyout(ZeroPage, iov->iov_base, cnt);`
- sys/kern/kern_slaballoc.c:316 — `ZeroPage = kmem_slab_alloc(PAGE_SIZE, PAGE_SIZE, M_WAITOK|M_ZERO);`
- Leak content on this guest: sparse small integers in the 60KB window
  after ZeroPage (per-CPU slab-zone bookkeeping); identical across runs
  within a boot — see leak_sample_run{1,2,3}.txt. Content is live kernel
  heap (run 4 showed 63 vs 64 non-zero bytes as the allocator churned).

## Reachability analysis (why this is filed Low and not High)

`uiomovez` has exactly one in-tree caller: the NFSv3 short-read
zero-fill at sys/vfs/nfs/nfs_vnops.c:1433
(`uiomovez(len - retlen, uiop)`, `len = min(tsiz, nm_rsize)`,
default `nm_rsize = NFS_MAXDATA = 32768` — sys/vfs/nfs/nfs_vfsops.c:106,
sys/vfs/nfs/nfsproto.h:57). A malicious/compromised NFSv3 server can
legally reply `count < len, eof = 0`, which is precisely the zero-fill
trigger — **but** every uio reaching `nfs_readrpc_uio()` today is
`UIO_SYSSPACE`:

- sys/vfs/nfs/nfs_bio.c:1080-1104 (`nfs_doio`): `io.iov_base = bp->b_data`,
  `uio_segflg = UIO_SYSSPACE` → takes the `bzero()` branch (safe);
- sys/vfs/nfs/nfs_vnops.c:433-448: 1-byte `UIO_SYSSPACE` probe (safe).

Therefore no stock syscall path currently reaches the OOB `copyout`;
the defect is a live booby-trap in a core subroutine: any caller that
zero-fills >4096 bytes into a `UIO_USERSPACE` uio (e.g. someone
"optimizing" nfs_doio to copy directly into user buffers) gets an
immediate 60KB-class kernel heap disclosure into unprivileged hands.
Engine defect: certain. Current reachability: latent.

## Exploit chain

None (information disclosure class; no corruption primitive). The leak
window is the fixed 60KB following a boot-time allocation; on this
guest it holds allocator metadata (no kernel pointers observed in 4
runs). On a system whose post-ZeroPage pages hold pointer-bearing
structures it would aid heap/KASLR mapping. No user→root route.

## Fix validation

- Baseline (kernel #0, stock): `run.log` — 4/4 runs LEAK CONFIRMED as
  `nobody` (64, 64, 64, 63 non-zero bytes ≥4096).
- Patched (kernel #1, `fix.diff` applied, `make nativekernel` +
  `make installkernel` + reboot): `run.fixed.log` — 2/2 runs: all 65536
  bytes zero, 0 non-zero bytes, `read()` semantics unchanged.
- fix.diff is `git apply`-clean against sys/kern/kern_subr.c.

## PoC changes vs the seed

No seed existed for this finding (discovered during pass 2 of
sys/kern/kern_subr.c). Harness authored fresh; needed two fixes during
bring-up: (1) out-of-tree KLD builds require `SYSDIR=/usr/src/sys` in
the Makefile; (2) DragonFly devfs requires an explicit `.d_open`
handler or open(2) fails ENODEV.
