# DF-3178 VERDICT — hpfs_mountfs census validation absent

**Status: reproduced** (panic variant 1/1; wiring variant 2/2 independent
mount cycles across two guest boots)
**Impact: panic** (deterministic mount-time kernel panic from a crafted
image) **+ resource amplification** (32 MiB kernel heap wired per crafted
mount; unbounded `statfs` capacity lie)
**Confidence: certain** for both variants (full traces captured).

## Root cause (path:line)

* `sys/vfs/hpfs/hpfs_vfsops.c:265-291` — mount reads super/spareblock and
  checks ONLY `su_magic`/`sp_magic`.  No field of the census is validated
  against anything (not non-zero, not the device size, not MAXBSIZE math).
* `sys/vfs/hpfs/hpfs_vfsops.c:301` — `hpfs_bminit(hpmp)` consumes
  `su_btotal` immediately.
* `sys/vfs/hpfs/hpfs_subr.c:109` — `hpm_dbnum = (su_btotal + 0x3FFF) / 0x4000`
  (u32 arithmetic; `>= 0xFFFFC001` wraps — that regime is DF-0862's).
* `sys/vfs/hpfs/hpfs_subr.c:119-120` — bmind-list
  `bread(devvp, dbtodoff(su_bitmap.lsn1), ((dbnum + 0x7F) & ~0x7F) << 2, &bp)`;
  for `dbnum >= 0x4001` the size exceeds `MAXBSIZE` (65536) and `getblk`
  panics.  Threshold: `su_btotal >= 0x10000001` (> 128 GiB of 512-byte
  sectors claimed).
* `sys/vfs/hpfs/hpfs_subr.c:113-117` — `kmalloc(dbnum*BMSIZE)`,
  `kmalloc(dbnum*4)`: up to 32 MiB + 64 KiB wired per mount in the
  non-panicking regime (`su_btotal <= 0x10000000`).
* `sys/vfs/hpfs/hpfs_vfsops.c:408` — `f_blocks = su_btotal` verbatim.

## How it was demonstrated

Panic variant — 1st attempt, no special setup:

    vnconfig vn0 /tmp/df3178.img    # 1.25 MiB image, su_btotal=0xFFFFC000
    /root/mount_ro /dev/vn0 /tmp/hpfsmp
    -> panic: getblk: size(1048576) > MAXBSIZE(65536)
       getblk -> breadnx -> hpfs_bminit -> hpfs_mount -> vfs_mount

(full signature in `panic.txt` / `boot_panic_console.log`; guest left in
DDB, needed `vm.sh reset with-src`.)

Wiring variant — two independent runs (two separate boots), identical
numbers:

    su_btotal=0x0FFFC000 -> dbnum=0x3FFF -> bread exactly MAXBSIZE (passes)
    MOUNT_RC=0 wall=0s
    vmstat -m:  HPFS_mount   3   32.1M     (hpmp + 32 MiB bitmap + 64 KiB bmind)
                HPFS_hash    1    1.00M    (module-load hashinit, once)
                HPFS_node    1      960    (root fnode node)
    v_free_count: 848021 -> 839002  (run A), 847502 -> 838428 (run B)
                  ≈ 9,000 pages ≈ 35 MiB total kernel footprint
    released cleanly on umount (run A verified UMOUNT_OK path; run B's
    umount attempt is the section that hit the wedge below)

statfs lie: `f_blocks = 0x0FFFC000` × 512 B ≈ 128 GiB of "capacity" on a
1.25 MiB image (2.0 TiB for the panic-regime value).  The `df` line itself
could not be captured — see side observation.

## Side observation (NOT claimed by this finding, logs preserved)

After a successful wiring-variant mount, the FIRST path operation that
enters the mountpoint (`df /tmp/hpfsmp`, `open("/tmp/hpfsmp", O_RDONLY)`)
wedges the entire system (unkillable; even `ps`/`pgrep` degrade; needs hard
reset).  Reproduced twice (`run_wiring_partial_df0927_trip.log`,
`run.log`).  The root fnode/dirblk content is attacker data, and the
hpfs lookup/walk layer has known unbounded-walk findings (DF-0927: no
de_reclen bound / no cycle guard; DF-3143: unbounded DE_DOWNLSN dive), so
this is consistent with that already-filed family triggered through the
crafted root directory.  Root-causing it to a specific PC needs an
interactive DDB session (`vm.sh` exposes no console write), so it stays
documented-not-claimed here.  My first crafted dirblk (all-zero dirent
area, `de_reclen=0`, no DE_END) is a textbook DF-0927 trigger; the second
attempt used a well-formed DE_END dirent and STILL wedged on entry —
whatever loop is spinning, it is in the mount-entry/lookup layer consuming
crafted root-fnode data, not in the census fields this finding claims.
(An initial hypothesis that `hpfs_hphashvget`'s retry loop
`sys/vfs/hpfs/hpfs_hash.c:110-129` can spin forever while holding the
global `hpfs_hphash_token` when `vget()` persistently fails could not be
confirmed without a stack trace — noted as speculative only.)

## Why not higher severity

hpfs can never be mounted by an unprivileged user on this system:
`get_fscap("hpfs")` returns `SYSCAP_RESTRICTEDROOT`
(sys/kern/vfs_syscalls.c:646-655), so even `vfs.usermount=1` does not open
this path.  The trigger therefore requires root to mount (or auto-mount /
a kiosk image-supply scenario), same threat model as DF-3128 (msdosfs,
Medium) and DF-0862 (hpfs bminit wrap, Medium).

## Fix

`fix.diff` (authored against the read-only `sys/` tree, never applied
there): bounds `su_btotal` to `(0, dev_dpsize(dev)]`, caps the derived band
count at `MAXBSIZE`-compatible 0x4000 bands, and requires `su_rootfno`
to lie inside the volume — checked in `hpfs_mountfs` right after the magic
checks, before `hpfs_bminit` runs.  Same pattern as the verified DF-3128
fix.  Fix build was not performed (resource-bound finding, not
memory-corruption; the mount layer gate is trivially reviewable and the
panic/wiring variants are both below the patched thresholds by
construction).

## Artifact map

* `craft_img.py` — image builder (variants: wiring 0x0FFFC000 / panic 0xFFFFC000)
* `mount_ro.c` — direct mount(2) helper (RO)
* `statfs_probe.c` — per-syscall alarm probe (used to localize the wedge)
* `build.sh` / `run_wiring.sh` / `run_full.sh` — exact runnable commands
* `build.log`, `run.log`, `run_wiring_partial_df0927_trip.log`,
  `run_panic_attempt.log`, `boot_panic_console.log`, `panic.txt`, `env.txt`
* `manifest.json`, `verdict.json`, `fix.diff`

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