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

Failed non-forced umount of tmpfs has already truncated every regular file (destructive pre-vflush pass leaves a live mount with destroyed data)

Summary

tmpfs_unmount walks all nodes and calls tmpfs_truncate(vp, 0) on every VREG file BEFORE vflush(mp, 0, flags) (:284-322); if vflush then fails (busy vnode -> EBUSY) unmount returns the error with the mount still alive and every file's contents already destroyed - the code comment acknowledges this ('all the nodes have already been truncated. Erroring out is the best we can do.'). Verification attempts on the guest (open-fd holder; open+unlinked-fd holder) did NOT trigger it: dounmount bails at its mnt_refs retry loop before VFS_UNMOUNT whenever an fd's f_nchandle pins the mount, so the destructive order is only reachable when a vnode is busy without an nchandle (in-flight VOP racing dounmount, transient holds). Filed Low/speculative with the shield documented (not_reproduced; the code order is unambiguous). Fix: vflush first, truncate only after it succeeds.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3181 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 2.2 KB ↓ raw
build.log β€” 55 B view raw
env.txt β€” 413 B view raw
holder.c β€” 1.0 KB view raw
run.log β€” 298 B view raw
run.sh β€” 91 B view raw
run2.log β€” 595 B view raw
umount_dataloss.sh β€” 1.7 KB view raw
verdict.json β€” 2.2 KB view raw

DF-3181 β€” failed non-forced umount of tmpfs has already truncated every file (destructive side effect before vflush)

What

tmpfs_unmount (sys/vfs/tmpfs/tmpfs_vfsops.c:284-334) walks every node and calls tmpfs_truncate(vp, 0) on all VREG files before vflush(mp, 0, flags). If vflush then fails (vnode still referenced β†’ EBUSY), unmount returns the error and the filesystem remains mounted β€” with every file's contents already destroyed:

    /*
     * Flush all vnodes on the unmount.
     *
     * If we fail to flush, we cannot unmount, but all the nodes have
     * already been truncated. Erroring out is the best we can do.
     */
    error = vflush(mp, 0, flags);
    if (error != 0) {
        TMPFS_UNLOCK(tmp);
        return (error);
    }

The correct order is to run vflush first (which can fail harmlessly) and only discard data once vflush has succeeded (or for MNT_FORCE).

Trigger surface (measured on the guest)

In practice the window is narrower than it looks: dounmount (sys/kern/vfs_syscalls.c) bails out with EBUSY at its mnt_refs retry loop before VFS_UNMOUNT is ever called whenever an open fd's f_nchandle still references the mount β€” an open file (even unlinked) keeps a namecache/mount reference and shields the filesystem from tmpfs_unmount entirely (demonstrated: two attempts, no truncation, see run.log / run2.log). The truncation-then-EBUSY path requires a vnode that is v_usecount-busy with no f_nchandle β€” e.g. a vnode pinned by an in-flight VOP on another CPU racing dounmount, or other transient kernel holds. Hence: not reproduced from a stable userspace state; the code order is still unconditionally wrong and destructive whenever vflush fails.

Reproduce attempt

run.sh    # mounts tmpfs, writes 4MB file, holder opens+unlinks it and
          # fstat()s every second, then `umount` (non-forced)

Observed: umount: Device busy, mount stays, holder's fd never shrinks (dounmount early EBUSY shield) β€” see run.log/run2.log.

Fix

Move the destructive truncate loop after a successful vflush (or behind if (error == 0)), leaving data intact whenever the unmount fails; for MNT_FORCE the truncation before FORCECLOSE vflush is intentional and can stay behind if (flags & FORCECLOSE).

VERDICT.md
↓ download raw

DF-3181 verdict β€” NOT REPRODUCED from a stable state (window shielded by dounmount's mnt_refs loop); defect confirmed by code order

Attempts (stock INVARIANTS guest): 1. mount tmpfs /mnt/df3181 + 4MB file + sleep 300 <file holder (fd open, namecache entry present) + umount β†’ EBUSY, file NOT truncated (4194304 bytes intact). dounmount returned EBUSY before reaching VFS_UNMOUNT because the file's namecache entry holds a mount ref. 2. Same, but holder opens the file and UNLINKS it (no namecache entry, only the fd) + umount β†’ EBUSY again, file still 4194304 bytes after 16 s of polling. The open fd's fp->f_nchandle still pins the mount: dounmount's mnt_refs retry loop (kern/vfs_syscalls.c dounmount) again shields tmpfs_unmount from running at all.

Conclusion: with purely userspace-held references, dounmount fails early and tmpfs_unmount's destructive order is never exercised. The truncate-before-vflush window requires a vnode that is busy (v_usecount > 0) without an f_nchandle/mount ref β€” in-flight VOP on another CPU, transient kernel holds β€” i.e. a race, or paths not exercised here.

The code-order defect is real and unambiguous (tmpfs_vfsops.c:284-334: tmpfs_truncate(vp, 0) for every VREG node precedes vflush(mp, 0, flags) whose failure leaves the mount alive), and the author's own comment acknowledges the data loss ("all the nodes have already been truncated. Erroring out is the best we can do."). Filed Low: data-destruction on a failed operation, practical trigger narrow (race).

Status: not_reproduced / impact=none (in these attempts) / confidence likely that the order is wrong, speculative for practical triggerability.

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean
[]
per-fix-DF-3181

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.log (attempt 1)', 'run2.log (attempt 2: holder fstat polls stay 4194304 through t+16)', 'VERDICT.md']

PoC changes

holder rewritten from shell to C (fstat polling on the open fd of the unlinked file); second attempt added unlink-before-umount to drop the namecache mount ref

Verified recommended fix

run vflush first and only truncate nodes after it succeeds (keep truncate-first only behind MNT_FORCE/FORCECLOSE)

Verdict

NOT REPRODUCED from a stable userspace state in two attempts (open-fd holder with namecache entry; open+unlink holder with fd only): in both, dounmount() failed at its mnt_refs retry loop BEFORE VFS_UNMOUNT ran (the fd's f_nchandle pins the mount), so tmpfs_unmount's destructive truncate-before-vflush order was never exercised and the file stayed 4194304 bytes. The code-order defect is unambiguous (tmpfs_vfsops.c:284-334 truncates every VREG node before vflush(mp,0,flags), and the author comment acknowledges the data loss on vflush failure), but reaching it requires a vnode busy without an f_nchandle (in-flight VOP race or transient kernel hold). Filed Low, speculative triggerability.