# DF-3128 — VERDICT

**Status: reproduced (impact: dos, confidence: certain). Fix validated (fixed).**

## Root cause

`mountmsdosfs()` (`sys/vfs/msdosfs/msdosfs_vfsops.c`) validates BPB fields for
internal consistency but never bounds the described volume by the size of the
backing device:

- vfsops.c:432-437 — the only size sanity check is an overflow test that can
  never trigger in 64-bit arithmetic (`(off_t)pm_HugeSectors * pm_BytesPerSec`
  ≤ 2^32·2^15 = 2^47). The comment on line 434 — `XXX FreeBSD also checks
  media size in above` — marks exactly the check DragonFly dropped. FreeBSD
  (msdosfs_vfsops.c ~667) rejects `HugeSectors * BytesPerSec > mediasize`
  with `/* past end of vol */`.
- vfsops.c:463-464 — `pm_maxcluster = (pm_HugeSectors - pm_firstcluster)/SecPerClust + 1`
  is derived purely from BPB numbers.
- vfsops.c:484-490 — the only clamp is the FAT-capacity clamp
  (`clusters = FATsecs*512*fatdiv/fatmult`), which the *attacker also
  controls* via `bpbBigFATsecs` (FAT32, 32-bit).
- vfsops.c:565-566 — `pm_inusemap = kmalloc(howmany(pm_maxcluster+1,32)*4, M_WAITOK)`
  — attacker-sized, wired for the mount's lifetime.
- vfsops.c:578-579 / msdosfs_fat.c:884-955 — `fillinusemap()` scans every
  cluster 0..pm_maxcluster, reading the whole FAT: minutes of kernel time on
  a forged-but-sparse image.

Related census note: `pm_HugeSectors` is a 16-bit/32-bit-store alias
(`pm_bpb.bpbHugeSectors`, uint32_t), so `pm_HugeSectors *= pm_BlkPerSec`
(:439) truncates — harmless here because everything downstream is derived
from the truncated value consistently.

## Threat model

Mounting a crafted filesystem image: a malicious USB stick / disk image
mounted by an operator (root), or — with `vfs.usermount=1` — any unprivileged
user against a device node they can open (e.g. their own md/vn). Guest
default `vfs.usermount: 0`. Each crafted mount:

1. consumes attacker-chosen wired kernel heap (128 MiB demonstrated; up to
   512 MiB per mount given the 32-bit BigFATsecs and 32-bit HugeSectors store,
   i.e. 2^32 clusters × 1 bit + 1 bit/32 slack),
2. pins the mounting thread (and the mount path) for the attacker-chosen scan
   duration — 38 s demonstrated with a 4 GiB sparse FAT; the work is not
   interruptible by SIGKILL (proven: mount completed after the caller was
   killed),
3. stacks linearly with concurrent mounts (256 MiB for two demonstrated),
4. reports bogus statfs (512 GiB "free" on a 9 GiB device) — amusing but
   secondary.

## Reproduction (stock kernel #0, full log: run.log)

```
./craftfat32 /root/DF-3128a.img       # 9 GiB sparse, claims 1.5 TiB FAT32
vnconfig -c -s labels vn3 /root/DF-3128a.img
mount_msdos -m 644 /dev/vn3 /mnt/DF-3128a   # rc=0, 38 s
df -k /mnt/DF-3128a    # 536870912 1K-blocks
vmstat -m | grep -i msdos   # MSDOSFS_FAT 1 128M (2 256M with a second mount)
```

Decisive lines in `run.log`: `mount rc=0, wall time 38s`,
`/dev/vn3 536870912 1 536870911 0%`, `MSDOSFS_FAT 1 128M`, and TEST 2's
post-SIGKILL `mount` output showing **both** vn3 and vn1 mounted (the killed
mount finished anyway) with `MSDOSFS_FAT 2 256M`.

## Fix validation (kernel #2, full log: run.fixed2.log)

`fix.diff` adds the missing bound, in DEV_BSIZE-block units matching
DragonFly's `dev_dpsize()` convention (vm_swap.c treats it as a block count;
vn.c:562 divides by secsize):

```c
devsize = dev_dpsize(dev);	/* DEV_BSIZE blocks, 0 if unknown */
...
if ((off_t)pmp->pm_HugeSectors * pmp->pm_BytesPerSec <
    pmp->pm_HugeSectors /* overflow */ ||
    (devsize > 0 &&
     (off_t)pmp->pm_HugeSectors * pmp->pm_BlkPerSec > devsize)
    /* past end of volume */) { EINVAL }
```

- `make -j6 nativekernel KERNCONF=X86_64_GENERIC` BUILD_RC=0, installkernel
  INSTALL_RC=0, reboot into `#2`.
- Crafted image: `mount rc=71 (EINVAL) elapsed=0s`, 0 mounts,
  `MSDOSFS_FAT 0` — attack gone.
- Regression: `newfs_msdos -F 32 -c 1` (legit FAT32, HugeSectors == device)
  mounts rc=0, file create/read/delete OK; `newfs_msdos` (FAT16) mounts rc=0,
  RW OK.
- First fix iteration compared `HugeSectors*BytesPerSec` (bytes) against
  `dev_dpsize` (blocks) and wrongly rejected legit images — caught by the
  regression test, corrected to blocks. (Units gotcha documented for upstream.)

Baseline-vs-patched table:

| kernel | crafted 1.5 TiB image | legit FAT32 | legit FAT16 |
|---|---|---|---|
| stock #0 | rc=0, 38 s, 128 MiB wired, 512 GiB in df | mounts | mounts |
| fixed  #2 | rc=71 in 0 s, no alloc | mounts, RW OK | mounts, RW OK |

## Non-goals / notes

- Secondary observation: SIGKILL-interrupted mounts can leave one leaked
  `msdosfsmount` (`MSDOS_mount` inUse +1) — a much smaller, separate
  error-path leak; noted, not part of this finding's fix.
- No memory *corruption*: all FAT/inusemap indexing stays consistent with the
  (forged) geometry; the damage is resource exhaustion + unkillable kernel
  work + statfs fiction.
- Guest left clean: `vm.sh reset with-src` after validation (stock #0).

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