⬢ DragonFlyBSD Kernel Audit
← triage · dashboard
DF-3159

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3159 · 14 files
FileTypeDescriptionSize
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
README.md
↓ download 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.

VERDICT.md
↓ download raw

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

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_testable
↓ fix.diffper-fix-DF-3159

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