FAT32 active-FAT number pm_curfat (bpbExtFlags & FATNUM, 0..15) never validated against pm_FATs β crafted BPB relocates every FAT read AND write outside the FAT region (fatblock adds curfat*FATsecs), kernel-mediated disk corruption on a RW mount of a crafted image
Summary
vfsops.c:409-410 sets pm_curfat = bpbExtFlags & 0xf whenever the FATMIRROR bit is clear, with no check that curfat < pm_FATs (bpbFATs may be 1). fatblock() (msdosfs_fat.c:87) then computes bn += pm_fatblk + pm_curfat * pm_FATsecs for ALL FAT I/O: with FATs=1 and curfat=15, every FAT entry read is taken from (and, on a RW mount, every FAT update written to) fatblk + 15*FATsecs - the root-directory/data area or past the volume entirely. The mount succeeds (the census clamps maxcluster against the claimed FAT capacity regardless of where the FAT bytes come from), so fatentry/fatchain/updatefats write cluster-chain updates through the wrong device blocks: silent cross-region corruption of the mounted medium. FreeBSD shares this validation gap; impact limited to the device the mounter chose to trust. Crafted-image RW mount: kernel writes FAT updates to attacker-chosen offsets outside the volume's FAT - data destruction (extendable to another mounted fs's area when the vn image is shared). No kernel memory corruption. Phase V skipped (Low, disk-only). Fix: reject pm_curfat >= pm_FATs (and FATs==0). [VERIFIED pass2: reproduced - FATs 1 extflags 0x8F mounts RW and writes NEW.TXT FAT chain inside CANARY.BIN data clusters with real FAT untouched; fix A/B mount rejected]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3132 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_curfat.py | β | 4.9 KB | view raw | |
| run_guest.sh | β | 1.5 KB | view raw | |
| run.log | β | 1.2 KB | view raw | |
| README.md | β | 717 B | β raw | |
| VERDICT.md | β | 2.4 KB | β raw | |
| build.sh | β | 198 B | view raw | |
| run.sh | β | 216 B | view raw | |
| manifest.json | β | 824 B | view raw | |
| verdict.json | β | 2.3 KB | view raw | |
| fix.diff | β | 1.0 KB | view raw | |
| env.txt | β | 272 B | view raw | |
| compile_validation.txt | β | 1.8 KB | view raw |
DF-3132 -- verification pack
Finding: pm_curfat never validated against pm_FATs; FAT I/O displaced cross-region
Status: reproduced / impact corruption / confidence certain
What was run
host: python3 craft_curfat.py out.img (FATs=1 extflags=0x8F; landing zone seeded with a FAT copy)
How to reproduce
guest: vnconfig -c -S 64m /dev/vn0 img && mount_msdos /dev/vn0 /mnt && echo hi > /mnt/NEW.TXT && umount, then hexdump sectors 32 and 152
Expected
real FAT unchanged; NEW.TXT chain (entry 7 EOC) written at sector 152 inside CANARY.BIN clusters
See VERDICT.md for the full narrative and verdict.json for the persisted machine verdict. Artifacts: craft_curfat.py, run_guest.sh, run.log.
DF-3132 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
A FAT32 BPB with bpbFATs=1 and bpbExtFlags=0x8F (mirror bit set, active FAT 15) mounts successfully: msdosfs_vfsops.c:409-410 sets pm_curfat=15 without ever checking it against pm_FATs, and every subsequent FAT read AND write is displaced 15*FATsecs sectors past the real FAT by fatblock (msdosfs_fat.c:87). After creating NEW.TXT on the RW mount and unmounting, the image shows the real FAT sector 32 unchanged (entry 7 still free) while the EOC chain write for cluster 7 landed at sector 152 inside CANARY.BIN data clusters - kernel-mediated writes outside the FAT region, silently corrupting file data. The landing zone had been pre-seeded with a copy of the real FAT so the displaced reads see a workable filesystem, proving the kernel operates consistently against the wrong blocks.
Kernel references
Exploit chain
(none: availability/correctness finding; no privilege chain)
Fix
reject pm_curfat >= pm_FATs and pm_FATs == 0 at mount; combined fixed kernel: the crafted volume is refused with bad active FAT and no FAT byte is written anywhere
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_curfat.py out.img (FATs=1 extflags=0x8F; landing zone seeded with a FAT copy)
run: guest: vnconfig -c -S 64m /dev/vn0 img && mount_msdos /dev/vn0 /mnt && echo hi > /mnt/NEW.TXT && umount, then hexdump sectors 32 and 152
expected: real FAT unchanged; NEW.TXT chain (entry 7 EOC) written at sector 152 inside CANARY.BIN clusters
Notes on the PoC (what changed vs the finding sketch)
initial canary filled the landing zone with 0xAA, which made the displaced FAT census read all clusters as busy (ENOSPC before any write); the zone now carries a copy of the real FAT so the displaced reads work and the misplaced WRITE is observable
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 hexdumps: sector 32 (real FAT) entry 7 = 0 after umount vs sector 152 (displaced) entry 7 = 0x0FFFFFFF EOC; mount line shows the crafted volume accepted RW
PoC changes
initial canary filled the landing zone with 0xAA, which made the displaced FAT census read all clusters as busy (ENOSPC before any write); the zone now carries a copy of the real FAT so the displaced reads work and the misplaced WRITE is observable
Verified recommended fix
reject pm_curfat >= pm_FATs and pm_FATs == 0 at mount; combined fixed kernel: the crafted volume is refused with bad active FAT and no FAT byte is written anywhere
Verdict
A FAT32 BPB with bpbFATs=1 and bpbExtFlags=0x8F (mirror bit set, active FAT 15) mounts successfully: msdosfs_vfsops.c:409-410 sets pm_curfat=15 without ever checking it against pm_FATs, and every subsequent FAT read AND write is displaced 15*FATsecs sectors past the real FAT by fatblock (msdosfs_fat.c:87). After creating NEW.TXT on the RW mount and unmounting, the image shows the real FAT sector 32 unchanged (entry 7 still free) while the EOC chain write for cluster 7 landed at sector 152 inside CANARY.BIN data clusters - kernel-mediated writes outside the FAT region, silently corrupting file data. The landing zone had been pre-seeded with a copy of the real FAT so the displaced reads see a workable filesystem, proving the kernel operates consistently against the wrong blocks.
No comments yet.