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)
PoC verification
Evidence pack
findings/poc/DF-3181 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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[]
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.
No comments yet.