msdosfs_setattr→detrunc/deextend lack the va_size upper-bound check the write path has: ftruncate beyond FAT's 32-bit max silently drives deextend past the on-disk size field instead of EFBIG
Summary
msdosfs_write enforces offset+resid <= MSDOSFS_FILESIZE_MAX (vnops:618), but msdosfs_setattr:371-399 passes any va_size straight to detrunc(); detrunc:509-511 forwards length>de_FileSize to deextend, whose count math (:636) accepts sizes above 2^32 on LP64. On a volume with enough free clusters (64KiB clusters, >4GiB volume - mountable per DF-3128) de_FileSize becomes >0xFFFFFFFF and DE_EXTERNALIZE's putulong truncates the dirent size field to the low 32 bits - after remount the file silently shrinks. On typical volumes ENOSPC instead of the POSIX-mandated EFBIG. Own-file data loss / spec violation; no cross-privilege impact. Not executed (Low). Fix: reject va_size > MSDOSFS_FILESIZE_MAX EFBIG in setattr's VREG case. [VERIFIED pass2: reproduced - ftruncate 4GiB-plus-64KiB succeeds then file silently shrinks to 65536 after remount; EFBIG fix A/B validated legal sizes unaffected]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3159 · 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_fat32big.py | — | 3.2 KB | view raw | |
| trunc.c | — | 1.6 KB | view raw | |
| trunc_step.c | — | 1.2 KB | view raw | |
| run_guest.sh | — | 808 B | view raw | |
| run.log | — | 531 B | view raw | |
| README.md | — | 718 B | ↓ raw | |
| VERDICT.md | — | 2.4 KB | ↓ raw | |
| build.sh | — | 213 B | view raw | |
| run.sh | — | 190 B | view raw | |
| manifest.json | — | 920 B | view raw | |
| verdict.json | — | 2.2 KB | view raw | |
| fix.diff | — | 583 B | view raw | |
| env.txt | — | 272 B | view raw | |
| compile_validation.txt | — | 1.8 KB | view raw |
DF-3159 -- verification pack
Finding: setattr/detrunc lack the FILESIZE_MAX bound; dirent field truncates silently
Status: reproduced / impact corruption / confidence certain
What was run
host: python3 craft_fat32big.py head; guest: truncate -s 4831838208 img + dd head at 0; cc trunc.c / trunc_step.c
How to reproduce
guest: vnconfig -c -S 4608m, mount_msdos, paced growth then final ftruncate 0x100010000, umount, remount, stat
Expected
ftruncate succeeds; file shrinks to 65536 after remount (0x10000 = low 32 bits)
See VERDICT.md for the full narrative and verdict.json for the persisted machine verdict. Artifacts: craft_fat32big.py, trunc.c, trunc_step.c, run_guest.sh, run.log.
DF-3159 verification
Kernel: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
Bottom line
On a FAT32 volume with 64-KiB clusters and 4.5 GiB capacity, ftruncate(0x100010000) (4 GiB + 64 KiB) SUCCEEDS - msdosfs_setattr has no upper bound, unlike msdosfs_write which enforces MSDOSFS_FILESIZE_MAX - and the in-memory size reads 4295032832. DE_EXTERNALIZE stores the dirent size field with putulong, so after unmount+remount the same file is 65536 bytes: 4 GiB of file silently vanished (silent data loss; POSIX expects EFBIG). The 4-GiB deextend zero-fill exceeds this 3.7-GiB-RAM guest when issued as a single syscall (two full-guest wedges recorded), so the trigger paces the growth in 64-MiB steps with sync. Own-file data loss on a volume the mounter chose; no cross-privilege impact.
Kernel references
- sys/vfs/msdosfs/msdosfs_vnops.c:371-399
- sys/vfs/msdosfs/msdosfs_denode.c:509-511
- sys/vfs/msdosfs/msdosfs_denode.c:636
- sys/vfs/msdosfs/msdosfs_vnops.c:618
Exploit chain
(none: availability/correctness finding; no privilege chain)
Fix
reject va_size > MSDOSFS_FILESIZE_MAX with EFBIG in setattr; combined fixed kernel: the same ftruncate returns EFBIG and the file stays 4 GiB minus one step
See fix.diff in this directory (git-apply-able against the read-only sys/ tree; applied only inside the disposable verification guest).
Reproduction
build: host: python3 craft_fat32big.py head; guest: truncate -s 4831838208 img + dd head at 0; cc trunc.c / trunc_step.c
run: guest: vnconfig -c -S 4608m, mount_msdos, paced growth then final ftruncate 0x100010000, umount, remount, stat
expected: ftruncate succeeds; file shrinks to 65536 after remount (0x10000 = low 32 bits)
Notes on the PoC (what changed vs the finding sketch)
single-shot ftruncate of the full 4 GiB wedged the guest twice (deextend zero-fills every new cluster through the buffer cache; dirty backlog starves sshd); the pacing harness (trunc_step.c / 64-MiB shell steps with sync) completes in ~2 minutes
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_testableConfirmed kernel references
Detail
Evidence (decisive lines)
run.log: ftruncate OK, in-memory size 0x100010000, size-after-remount=65536
PoC changes
single-shot ftruncate of the full 4 GiB wedged the guest twice (deextend zero-fills every new cluster through the buffer cache; dirty backlog starves sshd); the pacing harness (trunc_step.c / 64-MiB shell steps with sync) completes in ~2 minutes
Verified recommended fix
reject va_size > MSDOSFS_FILESIZE_MAX with EFBIG in setattr; combined fixed kernel: the same ftruncate returns EFBIG and the file stays 4 GiB minus one step
Verdict
On a FAT32 volume with 64-KiB clusters and 4.5 GiB capacity, ftruncate(0x100010000) (4 GiB + 64 KiB) SUCCEEDS - msdosfs_setattr has no upper bound, unlike msdosfs_write which enforces MSDOSFS_FILESIZE_MAX - and the in-memory size reads 4295032832. DE_EXTERNALIZE stores the dirent size field with putulong, so after unmount+remount the same file is 65536 bytes: 4 GiB of file silently vanished (silent data loss; POSIX expects EFBIG). The 4-GiB deextend zero-fill exceeds this 3.7-GiB-RAM guest when issued as a single syscall (two full-guest wedges recorded), so the trigger paces the growth in 64-MiB steps with sync. Own-file data loss on a volume the mounter chose; no cross-privilege impact.
No comments yet.