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

Beyond-EOF VM pages retained for the last buffer remain faultable: nvnode_pager_setsize()'s unmap is the only EOF enforcement, so mmap reads/writes past EOF succeed on every filesystem, pre-truncate content (incl. recycled disk-block residue) is served during the truncate zero-fill window, and on tmpfs beyond-EOF writes become file data

Field Value
ID DF-2920
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
CWE CWE-284 / CWE-200
File sys/kern/vfs_vm.c
Lines 42-47, 165, 179-201, 463-495 (sink: vm_fault.c:1906-2015)
Area kern/vfs + vm
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

vfs_vm.c's design keeps fully-valid VM pages beyond file EOF that fit in the last buffer cache buffer and claims 'we simply unmap them and do not allow userland to fault them in' (:46-47). nvnode_pager_setsize() implements only a one-shot pmap unmap (:486-495). vm_fault_object() maps any present+valid page with no v_filesize test; its only bounds check compares against object->size, which deliberately covers the whole last buffer (:463-466), and the pager-side EOF test is only reached for missing pages — so vnode_pager.c's claim that 'the VM fault code tests against v_filesize' is false. Verified on the guest as an unprivileged user on tmpfs, UFS and HAMMER2: a fresh mmap created after ftruncate() reads @70000 (beyond EOF, page retained) with no SIGBUS while @200000 (page absent) correctly SIGBUSes; WRITE faults beyond EOF succeed; on tmpfs the written byte survives extension as file data because tmpfs_truncate→tmpfs_reg_resize(...,trivial=1) makes nvextendbuf skip the covering zero-fill; and during nvtruncbuf's window between the v_filesize update/unmap (:165) and the bzero (:184) racing faults serve pre-truncate contents — 3,072,242 fstat-correlated hits in 10 s on tmpfs. The window also exposes raw on-disk beyond-EOF tail bytes pulled in by the truncate-time bread_kvabio — residue of previously-deleted files on UFS/HAMMER2; zeroing is additionally skipped entirely by NVEXTF_TRIVIAL (:179) and the bread-error path (:196-201). Unprivileged local user with read access to a file can read pre-truncate contents and on-disk recycled-block residue beyond EOF; modify the last buffer beyond EOF (on tmpfs becomes visible file data after a later extend); programs relying on SIGBUS past EOF silently see zeros. No user→root route (file-data boundary bug, not memory corruption).

Proof of contest

VERIFIED (findings/poc/DF-2920/): truncfault.c β€” fill 320KiB file with 0x5A; ftruncate to 65536+1231 (mid-block); fresh mmap; read/write @70000. Vulnerable kernel: read returns 0x00 (no SIGBUS), write succeeds, tmpfs extend then pread returns 'X'; race thread reads pattern @70000 twice with fstat-verified size<70000 (3.07M hits/10s tmpfs). Fixed kernel (vm_fault_object EOF check): all beyond-EOF probes SIGBUS; partial EOF-page behavior stays POSIX-correct.

Enforce EOF at fault time in vm_fault_object(): for OBJT_VNODE first objects, fail with KERN_PROTECTION_FAILURE when IDX_TO_OFF(first_pindex)

= vp->v_filesize (pages must stay valid for VMIO buffers, so the check cannot live in vfs_vm.c). Validated by full nativekernel rebuild + reboot: all probes flip to SIGBUS, exec/file-IO healthy.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of vfs_vm.c (GLM 5.3); reproduced on tmpfs/UFS/HAMMER2 as unpriv user + fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2920 Β· 13 files
FileTypeDescriptionSize
truncfault.c β€” 8.3 KB view raw
build.sh β€” 130 B view raw
run.sh β€” 195 B view raw
run.tmpfs.log β€” 1.0 KB view raw
run.ufs.log β€” 1.0 KB view raw
run.h2.log β€” 1.0 KB view raw
run.fixed.log β€” 919 B view raw
run.fixed.h2.log β€” 910 B view raw
env.txt β€” 247 B view raw
fix-build.log β€” 443 B view raw
VERDICT.md β€” 4.7 KB ↓ raw
README.md β€” 1.7 KB ↓ raw
fix.diff β€” 1.1 KB view raw

DF-2920 β€” beyond-EOF mmap fault enforcement missing (vfs_vm.c)

Unprivileged PoC proving that after a mid-block ftruncate(), VM pages beyond file EOF (retained for the last buffer cache buffer) can be re-faulted by userland: reads return data instead of SIGBUS, writes modify the last block, and during the truncate zero-fill window the pre-truncate contents are served (3.07M correlated observations/10s on tmpfs). On tmpfs, bytes written beyond EOF through such a mapping become file data after a later extend (trivial=1 extend skips the covering zero-fill).

Build (in guest)

./build.sh            # cc -O2 -pthread -o /tmp/truncfault truncfault.c

Run (as unprivileged user)

./run.sh /tmp/df2920.tmpfs.bin 8      # tmpfs
./run.sh /mnt/ufs/df2920.ufs.bin 8    # UFS (md/vn image)
./run.sh /home/maxx/df2920.h2.bin 8   # HAMMER2 root

Expected on a VULNERABLE kernel

S1 read @70000 (beyond EOF): value 0x00 served, NO SIGBUS
   read @200000 (beyond object): SIGBUS        <- contrast proves mechanism
S3 write @70000: SUCCEEDED; on tmpfs pread after extend returns 0x58
S4 WINDOW-HITS(pattern twice while small) > 0  (tmpfs: ~300k/10s)

Expected on a FIXED kernel (fix.diff applied)

All beyond-EOF probes SIGBUS; S3 write fails; S4 window hits 0.

Files

truncfault.c   probe (S0 sanity, S1/S2 reads, S3 write, S4 race)
run.tmpfs.log  baseline run on tmpfs (vulnerable kernel)
run.ufs.log    baseline run on UFS image (vn1)
run.h2.log     baseline run on HAMMER2 root
run.fixed.log  same probe on kernel patched with fix.diff
env.txt        guest uname / mounts
VERDICT.md     full narrative
fix.diff       git-apply-able fix (sys/vm/vm_fault.c)
VERDICT.md
↓ download raw

DF-2920 β€” beyond-EOF mmap pages remain faultable after mid-block truncate

Verdict (baseline, stock kernel): REPRODUCED

File: sys/kern/vfs_vm.c (nvnode_pager_setsize(), called from nvtruncbuf()/nvextendbuf()) Class: improper access control / information disclosure window + memory-mapping semantics violation Impact: local info disclosure (racy) + deterministic integrity violation (writes beyond EOF become file data on tmpfs) Confidence: certain β€” deterministically reproduced on tmpfs, UFS and HAMMER2, unprivileged user, stock INVARIANTS kernel.

What the code intends

sys/kern/vfs_vm.c:42-47 states the design: pages beyond file EOF that still fit inside the last buffer cache buffer are kept (valid, part of the VMIO buffer) but "We simply unmap them and do not allow userland to fault them in." nvnode_pager_setsize() implements only the first half: a one-shot vm_page_protect(m, VM_PROT_NONE) unmap (sys/kern/vfs_vm.c:486-495). There is no second mechanism. The comment in sys/vm/vnode_pager.c:99 ("the VM fault code tests against v_filesize") is false: vm_fault_object() maps any present and fully valid page (sys/vm/vm_fault.c:1906-1968) with no EOF test whatsoever; the only size test (vm_fault.c:2015) compares against object->size, which deliberately covers the whole last buffer including pages beyond EOF (vfs_vm.c:463-466). The pager-side EOF test (vnode_pager_generic_getpages(), sys/vm/vnode_pager.c:505) is only reached for missing pages.

Proof (guest, unprivileged user maxx)

truncfault.c: file of 320 KiB filled with 0x5A, ftruncate() to 65536+1231 (mid-block for every blksize 4K..64K), then:

subtest tmpfs UFS (vn1) hammer2 (/)
S1 fresh mmap after truncate, read @70000 (beyond EOF, page retained) served, 0x00, no SIGBUS served, 0x00 served, 0x00
S1 read @200000 (beyond object->size, page absent) SIGBUS SIGBUS SIGBUS
S2 pre-existing mapping, read @70000 after truncate served 0x00 served 0x00 served 0x00
S3 fresh mmap WRITE @70000 after truncate succeeded succeeded succeeded
S3 then extend, pread @70000 0x58 'X' β€” beyond-EOF write became file data 0x00 (extend zero-fill covered it) 0x00 (covered)
S4 strict race: pattern read twice at @70000 with fstat-verified size<70000 3,072,242 hits / 10 s 0 (window closes faster than 2nd sample; 3110 1st-sample observations) 0 (2 1st-sample observations)

S3-tmpfs persistence mechanism: tmpfs_truncate() calls tmpfs_reg_resize(vp, length, 1) (sys/vfs/tmpfs/tmpfs_subr.c:1404) β€” trivial=1 even for ftruncate()-extends β€” so nvextendbuf() skips the zero-fill of the block straddling the old EOF (sys/kern/vfs_vm.c:386-408) and the bytes the user wrote beyond EOF through the (illegitimate) mapping become in-file data.

S4 window mechanism: nvtruncbuf() updates vp->v_filesize and unmaps beyond-EOF pages in nvnode_pager_setsize() before the zero-fill bzero(bp->b_data + boff, blksize - boff) runs (vfs_vm.c:165 vs vfs_vm.c:179-195). In that window any fault re-maps the retained valid page and serves the pre-truncate contents (0x5A) while fstat() already reports the smaller size β€” 3.07M correlated observations on tmpfs in 10 s. The window also exposes recycled disk-block content: when the last buffer is not cached, the truncate-time bread_kvabio() reads the whole block from disk β€” including the beyond-EOF tail, which on UFS/HAMMER2 contains residue of previously-deleted files β€” into the valid page before the bzero() erases it. The zero-fill is additionally skipped entirely by the NVEXTF_TRIVIAL path and by the bread error path (vfs_vm.c:196-201), leaving pre-truncate content exposed indefinitely.

Security impact

  • Read side: an unprivileged user with read access to a file can read file contents beyond EOF (a) during the truncate window (proven), (b) permanently whenever the zero-fill is skipped (TRIVIAL / bread error), including on-disk residue of deleted files pulled in by the truncate-time bread. POSIX/SUSv require SIGBUS.
  • Write side: an unprivileged user with write access can modify the last buffer beyond EOF through mappings created after the truncate; on tmpfs (trivial extend) those bytes become file content.
  • The SIGBUS contract break also silently substitutes zeros where programs expect a fault, defeating "map generously, catch SIGBUS" bounds patterns.

Fix validation

Kernel rebuilt with fix.diff (vm_fault.c: reject faults beyond v_filesize for OBJT_VNODE first objects): see run.fixed.log. Baseline behavior on the same guest before the patch: run.*.log.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Rebuilt kernel with fix.diff (EOF check in vm_fault_object); identical PoC now gets SIGBUS for every beyond-EOF read/write probe on tmpfs and hammer2 (run.fixed*.log); partial EOF-page reads within the EOF page still served as zeros (POSIX-conformant); boot, exec of dynamic binaries, file I/O all healthy.

['findings/poc/DF-2920/run.fixed.log', 'findings/poc/DF-2920/run.fixed.h2.log', 'findings/poc/DF-2920/fix-build.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Thu Sep 3 12:10:26 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

unprivileged user maps a readable file -> another thread/proc (or same) ftruncate()s it mid-block -> user re-faults beyond EOF: (a) during the setsize->bzero window reads pre-truncate content including recycled disk-block residue read by the truncate-time bread (3.07M correlated observations/10s on tmpfs); (b) via TRIVIAL/bread-error paths permanently; (c) write-faults beyond EOF modify the last buffer block and on tmpfs persist as file data after extend

Evidence (decisive lines)

["findings/poc/DF-2920/run.tmpfs.log (S1 served-no-SIGBUS, S3 val=0x58 'X' became file data, S4 3,072,242 window hits)", 'findings/poc/DF-2920/run.ufs.log and run.h2.log (same S1-S3 behavior)', 'findings/poc/DF-2920/run.fixed.log + run.fixed.h2.log (all probes SIGBUS on patched kernel)', 'findings/poc/DF-2920/VERDICT.md']

PoC changes

Fixed vs the conceptual sketch: added SIGSEGV handling (failed vnode faults surface as SIGSEGV on this kernel), unbuffered stdout, per-subtest isolation, and the strict S4 protocol (pattern at v1, fstat-verified size < probe offset, pattern again at v2, size still small) to make window hits unambiguous.

Verified recommended fix

In vm_fault_object(), reject faults on OBJT_VNODE first objects whose page offset is >= vp->v_filesize (KERN_PROTECTION_FAILURE); see fix.diff

Verdict

Deterministically reproduced on stock INVARIANTS kernel as an unprivileged user on tmpfs, UFS and HAMMER2: after a mid-block ftruncate(), VM pages beyond EOF retained for the last buffer cache buffer are re-faultable because nvnode_pager_setsize() (sys/kern/vfs_vm.c:486-495) only unmaps them while vm_fault_object() (sys/vm/vm_fault.c:1906-1968) maps any present+valid page with no v_filesize check. Fresh mappings created long after the truncate read the retained page (zeros) instead of receiving SIGBUS, and WRITE faults beyond EOF succeed; on tmpfs the written bytes become file data after a later ftruncate-extend because tmpfs_reg_resize passes trivial=1 (tmpfs_subr.c:1404) so nvextendbuf skips the covering zero-fill. During the truncate itself the window between the v_filesize update/unmap (vfs_vm.c:165) and the bzero (vfs_vm.c:184) serves PRE-TRUNCATE contents to racing faults: 3,072,242 fstat-correlated hits in 10 s on tmpfs (run.tmpfs.log); the same window exposes on-disk residue pulled in by the truncate-time bread of the last block. Patched kernel (fix.diff adds the EOF check to vm_fault_object) flips every deterministic probe to SIGBUS and eliminates re-fault exposure; only the inherent in-flight-truncate pmap race remains.