β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2886

uiomovez() copyouts unbounded cnt from PAGE_SIZE-sized ZeroPage β€” OOB kernel heap read into userspace (latent: sole in-tree caller passes UIO_SYSSPACE)

Field Value
ID DF-2886
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N
CWE CWE-125 Out-of-bounds Read
File sys/kern/kern_subr.c
Lines 241-246 (ZeroPage kern_slaballoc.c:316 = PAGE_SIZE)
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket kernleak
Reported pending
Known CVE none
CVE match novel

Summary

uiomovez()'s UIO_USERSPACE branch copyouts cnt = min(iov_len, n) bytes from ZeroPage, but ZeroPage is allocated as exactly PAGE_SIZE bytes. cnt is never clamped to PAGE_SIZE, so any call with n > 4096 copies nβˆ’4096 bytes of kernel heap adjacent to ZeroPage into the user's buffer while claiming to zero-fill. Verified on the guest: a KLD harness calling uiomovez(65536) through a plain read() delivered 64 stable non-zero kernel-heap bytes (slab allocator bookkeeping) into an unprivileged (nobody) buffer in 4/4 runs, identical across the first three. Today latent: the only in-tree caller (NFSv3 short-read zero-fill, nfs_vnops.c:1433, up to 32KB on a malicious server reply) reaches it only with UIO_SYSSPACE (safe bzero branch) β€” any current or future caller zero-filling >4096 bytes into a UIO_USERSPACE uio gets an immediate ~60KB-class kernel heap disclosure.

Clamp cnt to PAGE_SIZE per loop iteration (validated fix.diff in findings/poc/DF-2886/; loop re-drives on remaining n/iov so semantics identical β€” patched kernel: 0 non-zero bytes, still 65536 zeros).

Timeline

  • 2026-09-02 Discovered during pass-2 audit of kern_subr.c (GLM 5.3); KLD leak reproduced 4/4 + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2886 Β· 14 files
FileTypeDescriptionSize
uiozleak.c β€” 1.8 KB view raw
uioz_trigger.c β€” 2.2 KB view raw
build.sh β€” 317 B view raw
run.sh β€” 422 B view raw
build.log β€” 5.4 KB view raw
run.log β€” 4.0 KB view raw
run.fixed.log β€” 605 B view raw
leak_sample_run1.txt β€” 2.3 KB view raw
leak_sample_run2.txt β€” 2.3 KB view raw
leak_sample_run3.txt β€” 2.3 KB view raw
env.txt β€” 390 B view raw
fix.diff β€” 440 B view raw
VERDICT.md β€” 3.6 KB ↓ raw
verdict.json β€” 3.9 KB view raw
VERDICT.md
↓ download raw

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:

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied fix.diff to /usr/src/sys/kern/kern_subr.c in-guest, make -j4 nativekernel + make installkernel + reboot into kernel #1; re-ran the identical PoC twice as nobody: 0 non-zero bytes past offset 4096 (vs 64 on baseline), read() still zero-fills the full 65536 bytes; guest stable, no panic.

["run.fixed.log: 2 runs, 'RESULT: no leak observed', 'non-zero bytes >= 4096 : 0 / 61440'", 'run.log: baseline comparison, 4 runs LEAK CONFIRMED', 'fix.diff: git-apply-clean one-hunk clamp']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Wed Sep 2 22:00:57 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["run.log: 4 runs as nobody, each 'RESULT: LEAK CONFIRMED', 64/64/64/63 non-zero bytes at offsets >=4096, bytes[0..4095] zero", 'leak_sample_run1/2/3.txt: identical sparse slab-metadata qwords at fixed offsets (cross-run stability = kernel memory, not noise)', 'run.fixed.log: 2 runs on PAGE_SIZE-clamped kernel #1 - 0 non-zero bytes, read still returns 65536 zeros', 'build.log: module builds -Werror clean via SYSDIR=/usr/src/sys conf/kmod.mk', 'VERDICT.md: reachability trace showing both nfs_readrpc_uio callers pass UIO_SYSSPACE (bzero branch)']

PoC changes

authored fresh (no seed); bring-up fixes: KLD Makefile needs SYSDIR=/usr/src/sys for out-of-tree builds; devfs needs explicit .d_open or open(2) returns ENODEV

Verified recommended fix

Clamp cnt to PAGE_SIZE per loop iteration in uiomovez (see fix.diff).

Verdict

uiomovez()'s UIO_USERSPACE branch copyouts cnt = min(iov_len, n) bytes from ZeroPage, but ZeroPage is exactly PAGE_SIZE (kern_slaballoc.c:316); any n > 4096 reads past the allocation into the user's buffer. KLD harness proved the primitive: 64 stable non-zero kernel-heap bytes (slab bookkeeping) delivered to an unprivileged nobody process in 4/4 runs on stock kernel #0; the one-line PAGE_SIZE clamp (fix.diff, kernel #1) eliminated all leaked bytes while preserving the zero-fill contract. Latent engine defect: the sole in-tree caller (NFSv3 short-read zero-fill, nfs_vnops.c:1433) is only ever reached with UIO_SYSSPACE uios (nfs_bio.c:1080-1117, nfs_vnops.c:433-448) which take the safe bzero() branch, so no stock syscall path triggers it today - filed Low for that reason, certain about the primitive itself.