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

dirfs_node_chsize destroys the guest buffer cache/pager BEFORE the host truncate() is known to succeed β€” failed truncates (EFAULT on unlinked/over-length paths, EPERM by non-owner) silently discard acknowledged writes and desync the VM object; absolute_path NULL return never checked

Summary

dirfs_node_chsize (:778-793) runs nvtruncbuf/nvextendbuf on the vnode FIRST and only then calls truncate(tmp, nsize); tmp comes from dirfs_node_absolute_path (:778) which returns NULL for unlinked-but-open nodes and for paths > MAXPATHLEN - chsize is the only ch* wrapper with no NULL check. On any failed truncate the committed damage is never rolled back: nvtruncbuf destroys ALL buffers at/beyond the truncation point WITHOUT flushing dirty ones (vfs_vm.c:280-306) and resizes the VM object. Demonstrated: a write(2)-acknowledged dirty buffer silently destroyed, subsequent dirfs_read re-fetches the OLD host data through the still-open dn_fd (acked write reverted), mmap view SIGBUSes, and - because the setattr size branch has no ownership check (vnops:491-503) and chsize lacks the IMMUTABLE|APPEND guard chtimes has - any guest user can inflict this on the shared cached view of files they cannot modify. Over-length paths fully constructible by an unpriv user (dirfs itself creates entries via openat from cached-fd ancestors). VERIFIED 3/3 via deterministic harness transcription: (A) acked-write reversion on unlink+ftruncate (absolute_path NULL -> damage then EFAULT); (B) >MAXPATHLEN tree via fd-relative openat -> same damage; (C) root-owned file EPERM-after-damage with host file byte-identical. FIXED variant (NULL check + host-first): zero damage. No escalation (data-integrity/DoS; dirfs vkernel-only).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3133 Β· 14 files
FileTypeDescriptionSize
harness.c trigger-source deterministic transcription of chsize + absolute_path with vfs_vm.c-faithful buffer stubs and real truncate/pread outcomes 18.1 KB view raw
build.sh build-script cc command line 65 B view raw
run.sh run-script runs harness; rc=2 == BUG CONFIRMED + FIX VALIDATED 89 B view raw
build.log build-log final successful build 966 B view raw
run.log run-log decisive run 1.7 KB view raw
run.2.log run-log determinism check 2 1.7 KB view raw
run.3.log run-log determinism check 3 1.7 KB view raw
fix.diff suggested-fix git-apply-able unified diff (never applied to sys/) 1.4 KB view raw
fix_base_cc.log fix-log baseline compile error (vkernel64 env) 155 B view raw
fix_patched_cc.log fix-log patched compile error β€” identical to baseline (compile-neutral) 127 B view raw
env.txt environment uname, compiler, dirfs absence, guest restoration 764 B view raw
VERDICT.md verdict full narrative 6.1 KB ↓ raw
verdict.json verdict machine verdict (persist_poc.py schema) 5.6 KB view raw
README.md readme how to reproduce 3.2 KB ↓ raw
README.md readme how to reproduce
↓ download raw

DF-3133 β€” dirfs_node_chsize() destroys the guest page cache before the host

truncate() is known to succeed (and ignores absolute_path()==NULL)

What

dirfs_node_chsize (sys/vfs/dirfs/dirfs_subr.c:778-793) runs nvtruncbuf()/nvextendbuf() on the vnode first, and only then attempts the host-side truncate(tmp, nsize). If that truncate fails β€” deterministically EFAULT when dirfs_node_absolute_path() returned NULL (unlinked-but-open node with dn_parent already cleared by dirfs_nremove/nrmdir/nrename, or absolute path > MAXPATHLEN), or EPERM/ EACCES/ENOSPC from the host β€” the damage is already done and is never rolled back:

  • nvtruncbuf destroys every buffer at/beyond the truncation point without flushing (B_INVAL|B_RELBUF|B_NOCACHE, sys/kern/vfs_vm.c:300-302) β€” including B_DELWRI buffers holding write(2)-acknowledged data β†’ the acknowledged writes are silently lost and subsequent reads resurrect the OLD host data;
  • the VM object is resized (nvnode_pager_setsize) to nsize while the host file is unchanged β†’ mmap view desyncs (SIGBUS on data that read(2) still returns);
  • chsize has no dn_flags & (IMMUTABLE|APPEND) guard (unlike chtimes :656-657) and the setattr size branch has no ownership check (dirfs_vnops.c:491-503 only checks MNT_RDONLY), so any guest user can inflict this on the shared cached view of any dirfs file whose host truncate then fails.

tmp (the dirfs_node_absolute_path() return, :778) is never NULL-checked β€” unlike the sibling ch* wrappers whose KKASSERT(tmp) family is DF-0855.

Reproduce

dirfs is vkernel-only, so proof is by deterministic harness transcription (DF-3054/DF-0806/DF-0807 precedent) β€” verbatim absolute_path + chsize control flow, nvtruncbuf/nvextendbuf stubbed to the documented vfs_vm.c semantics, real host files and real truncate()/pread so all errno outcomes are real.

ssh dfbsd-maxx        # unprivileged (uid 1001)
cd poc/DF-3133
./build.sh && ./run.sh   # expect rc=2: 9/9 BUG confirmations + FIXED variants

Expected (see run.log): * A) unlinked-but-open: chsize β†’ EFAULT after the dirty acknowledged write was destroyed and the pager truncated; host data still alive through the open fd; guest re-read resurrects OLD data. * B) >MAXPATHLEN absolute path β€” tree really constructed on the host via fd-relative openat() (the dirfs fd-cache trick); same damage; the 4x240 boundary path truncates normally (sanity). * C) valid path, root-owned file: EPERM from the host after the guest buffers/pager for that file were destroyed; host file verified untouched. * FIXED variant (NULL check + host-truncate-first): ENAMETOOLONG / errno propagated with ZERO damage in every scenario.

3/3 runs byte-identical (run.log, run.2.log, run.3.log β€” same MD5).

Fix

fix.diff (also validated): NULL check β†’ ENAMETOOLONG, host truncate() first, buffer/pager updates only after success. Applies cleanly (git apply --check RC=0 local tree + guest /usr/src), compile-neutral (fix_base_cc.log vs fix_patched_cc.log: identical first error β€” the pre-existing vkernel-env errno.h breakage documented in DF-3054). Live vkernel boot not available on this guest β†’ boot validation not_testable (same as DF-3054).

VERDICT.md verdict full narrative
↓ download raw

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

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

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched reproduced

fix.diff (NULL check -> ENAMETOOLONG; host truncate first; buffer/pager mutation only on success) applies cleanly (git apply --check RC=0 on the local sys/ tree and on the guest /usr/src). Compile-neutral: patched vs unpatched dirfs_subr.c 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 validated by the harness FIXED variants (zero damage, clean error propagation) in every scenario. Live boot validation not_testable: dirfs is vkernel-only and not compiled into the guest host kernel, so the patched path cannot be exercised by a host-kernel reboot.

fix.diff; fix_base_cc.log vs fix_patched_cc.log (identical first error); harness FIXED variant output in run.log (FIXED A/B lines)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

vkernel user -> (own file) open+ftruncate after unlink, or deep >1024-byte path built via fd-relative creation, or ftruncate of a host-root-owned file -> VOP_SETATTR size branch (no ownership check) -> dirfs_node_chsize -> nvtruncbuf destroys buffers/pager (dirty B_DELWRI data lost, VM object resized) -> host truncate() fails EFAULT/EPERM -> error returned but damage committed: acknowledged writes gone, reads resurrect old host data, mmap SIGBUSes; persists for the vnode's cache lifetime

Evidence (decisive lines)

harness.c (verbatim absolute_path :377-443 + chsize :748-799 control flow; nvtruncbuf/nvextendbuf stubbed to documented vfs_vm.c semantics; real truncate/pread errno outcomes); run.log β€” 'BUG A1..A4', 'BUG B1/B2' + fd-relative constructibility precondition, 'BUG C1/C2' + 'host file untouched'; exit code 2, 9/9 confirmations, 3/3 runs identical MD5; fix.diff applies cleanly (git apply --check RC=0 local + guest /usr/src); fix_base_cc.log vs fix_patched_cc.log compile-neutral

PoC changes

Harness written fresh (no seed). Iterations: (1) helper that joined path fragments ignored its format arg (scenario B silently tested the wrong directory) β€” replaced with a concatenating helper and re-verified scenario A unchanged; (2) added debug prints mid-debug, removed. All scenario results were re-captured after the fix; 3/3 identical runs.

Verified recommended fix

dirfs_node_chsize: check absolute_path for NULL (ENAMETOOLONG), perform the host truncate() FIRST, and only run nvtruncbuf/nvextendbuf + dn_size update after it succeeds (fix.diff).

Verdict

REPRODUCED (deterministic harness; dirfs is vkernel-only so no live-boot test β€” DF-0806/0807/3054 precedent). dirfs_node_chsize (dirfs_subr.c:778-793) runs nvtruncbuf/nvextendbuf on the vnode BEFORE the host truncate() is attempted, never checks dirfs_node_absolute_path() for NULL, and never rolls back on failure. Demonstrated: (A) unlinked-but-open file β€” absolute_path NULL (dn_parent cleared by nremove vnops:914-916) -> truncate(NULL) EFAULT after the B_DELWRI buffer holding a write(2)-acknowledged write was destroyed WITHOUT flush (vfs_vm.c:300-302) and the VM object truncated; host data still alive through the open fd; guest re-read resurrects the OLD data (acknowledged write silently reverted); mmap view desyncs. (B) absolute path >MAXPATHLEN β€” the deep tree is really constructed on the host via fd-relative openat() (the dirfs passive-fd-cache mechanism), proving constructibility by an unprivileged user; same EFAULT-after-damage; 4x240 boundary path truncates normally. (C) valid path, root-owned file: host truncate EPERM, guest buffers/pager destroyed anyway (no ownership check in the setattr size branch vnops:491-503; chsize also lacks the IMMUTABLE/APPEND guard chtimes has at :656-657); host file verified untouched afterwards. FIXED variant (NULL check + host-first): ENAMETOOLONG/errno with zero damage in every scenario. Impact ceiling: vkernel data integrity + availability (silent loss of acknowledged writes, stale-data resurrection, cached-view denial of arbitrary dirfs files); no host-kernel crash, no escalation.