# DF-3133 — dirfs_node_chsize() is destructive-before-validate

## Verdict
**REPRODUCED** (deterministic harness, 3/3 runs byte-identical) —
`dirfs_node_chsize` (dirfs_subr.c:778-793) mutates the vnode's buffer cache
and pager (`nvtruncbuf`/`nvextendbuf`) BEFORE the host `truncate()` is
attempted, never checks `dirfs_node_absolute_path()` for NULL, and never
rolls anything back when the host truncate fails. Demonstrated failure
modes, each with the guest-side damage already committed:

1. **unlinked-but-open** (dn_parent cleared by dirfs_nremove vnops.c:914-916
   / nrmdir :1128-1130 / nrename :1000-1002): absolute_path → NULL →
   `truncate(NULL)` → EFAULT. nvtruncbuf had already destroyed the B_DELWRI
   buffer holding a `write(2)`-acknowledged write (vfs_vm.c:300-302 — dirty
   buffers are destroyed, not flushed) and truncated the VM object. The host
   file's data is still fully alive through the still-open descriptor
   (POSIX unlink semantics hold on the host), and the guest's next read
   re-fetches the OLD data: the acknowledged write is silently REVERTED.
   The mmap view SIGBUSes on data read(2) still returns.
2. **absolute path > MAXPATHLEN** (>1024 bytes of components, constructed
   for real on the host through fd-relative openat() — exactly the mechanism
   dirfs's own passive fd cache uses, so such trees are fully constructible
   by an unprivileged user): same NULL → EFAULT → committed damage. The
   4x240-byte boundary (count ≤ MAXPATHLEN) truncates normally — sanity.
3. **host refuses the truncate** (root-owned file, valid path): EPERM
   returned, but the guest's cached buffers/pager for that file were
   destroyed anyway. The setattr size branch (vnops :491-503) has no
   ownership check (only MNT_RDONLY) and chsize lacks the
   `IMMUTABLE|APPEND` guard chtimes has (:656-657), so any guest user can
   inflict this on the shared cached view of files they cannot modify.

Impact ceiling: guest (vkernel) data-integrity + availability — silent loss
of acknowledged writes, resurrection of stale data, mmap/read desync, and
denial of the cached contents of arbitrary dirfs files. dirfs is
vkernel-only (`sys/platform/vkernel64/conf/files:45-47`), so this is a
vkernel integrity/DoS issue, not a host-kernel crash.

## The bug — line by line

```c
:778  tmp = dirfs_node_absolute_path(dmp, dnp, &pathfree);   /* may be NULL */
:779  if (nsize < osize) {
:780      error = nvtruncbuf(vp, nsize, biosize, -1, 0);     /* DESTROYS buffers
                                                                + pager FIRST */
:781  } else {
:782      error = nvextendbuf(vp, osize, nsize, ...);        /* extends FIRST */
:785  }
:786  if (error == 0 && truncate(tmp, nsize) < 0)            /* host SECOND;
                                                                tmp may be NULL */
:787      error = errno;                                     /* damage stays */
:788  if (error == 0)
:789      dnp->dn_size = nsize;
```

`nvtruncbuf_bp_trunc` (sys/kern/vfs_vm.c:280-306) takes both the clean AND
the dirty rb-tree; dirty buffers get `bremfree` + `B_INVAL|B_RELBUF|B_NOCACHE`
+ `brelse` — the data is dropped, never written. `nvnode_pager_setsize`
(vfs_vm.c:165) then resizes the VM object. (The `bawrite` at vfs_vm.c:343 is
only in the *metasync* callback, which matches negative-loffset metadata
buffers only — vfs_vm.c:316.)

## Distinction from known findings

* DF-0855 owns the `KKASSERT(tmp)` family in chtimes:660/chflags:698/
  chmod:718/chown:736 (panic sites). chsize :778 has NO KKASSERT — its NULL
  flows into `truncate(NULL)` (EFAULT) *after* the destructive helpers ran.
  This finding is about the destructive ordering, which no known finding
  covers.
* DF-3057 is dirfs_write's bread()-error clobber (vnops:740-753) — different
  function, different mechanism.
* DF-0808 owns nrename's absolute_path_plus NULL → rename(NULL,...) (no
  damage to the page cache).
* DF-3054/DF-0855/DF-0856 own the findfd-family NULL derefs and the
  alloc_file leak. Untouched here.

## Reproduction (harness; dirfs is vkernel-only — DF-0806/0807/3054 precedent)

```
A) chsize returned 14 (Bad address)
   BUG A1: truncate(NULL) -> EFAULT after damage already done
   BUG A2: acknowledged (dirty) write DESTROYED without flush
   BUG A3: pager/VM object truncated to 0 while host file unchanged
   BUG A4: guest re-read resurrects OLD data — acknowledged write REVERTED
   FIXED A: clean ENAMETOOLONG / zero damage, dirty buffer intact
B) BUG precondition: >MAXPATHLEN tree IS constructible via fd-relative
   openat() (dirfs does the same through the passive fd cache)
   BUG B1/B2: absolute_path NULL -> EFAULT after buffer+pager damage
   B-sanity: 4x240 boundary path truncates normally
   FIXED B: ENAMETOOLONG, zero damage
C) chsize returned 13 (Permission denied)
   BUG C1/C2: host refuses truncate, guest pager/buffers destroyed anyway
   host file verified untouched
   → exit code 2, 9/9 confirmations, 0 mismatches; 3/3 identical runs
```

## Fix validation

* `fix.diff` (NULL check → ENAMETOOLONG; host truncate FIRST; buffer/pager
  updates only on success) — `git apply --check` RC=0 on the local sys/ tree
  AND on the guest /usr/src.
* Compile-neutral: patched vs unpatched `dirfs_subr.c` compiled in the
  vkernel64 ad-hoc env fail with IDENTICAL first errors (pre-existing
  `errno.h` include breakage, same as DF-3054/DF-0806 documented).
  (`fix_base_cc.log` vs `fix_patched_cc.log`.)
* Behavior: harness FIXED variants show zero damage in all scenarios.
* Live boot test: not_testable — dirfs is not compiled into the guest host
  kernel and no vkernel boot is available on this guest (same as DF-3054).

## How to reproduce

```
ssh dfbsd-maxx   # unprivileged (uid 1001)
cd poc/DF-3133
./build.sh && ./run.sh     # expect: rc=2 "BUG CONFIRMED + FIX VALIDATED"
```

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