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