hpfs_mountfs performs no super/spareblock census validation: unvalidated on-disk su_btotal panics the kernel at mount (bmind-list bread > MAXBSIZE), wires attacker-chosen kernel heap (32 MiB demonstrated per mount), and is echoed verbatim into statfs f_blocks
Summary
After checking only the two magic numbers, hpfs_mountfs hands the raw u32 su_btotal (and su_bitmap.lsn1, sp_cpi/sp_cpinum) to hpfs_bminit/hpfs_cpinit. bminit derives dbnum=(su_btotal+0x3FFF)/0x4000 (hpfs_subr.c:109) and issues a single bmind-list bread of ((dbnum+0x7F)&~0x7F)<<2 bytes (:119-120): a crafted su_btotal >= 0x10000001 (claims >128 GiB) makes that bread exceed MAXBSIZE=64KiB and getblk panics at mount time - VERIFIED deterministically with a 1.25 MiB image (su_btotal=0xFFFFC000 -> 1 MiB bread -> 'panic: getblk: size(1048576) > MAXBSIZE(65536)', trace getblk<-breadnx<-hpfs_bminit<-hpfs_mount). Below the panic threshold the mount succeeds and wires dbnum*2048+dbnum*4 bytes of kernel heap (VERIFIED: 'HPFS_mount 3 32.1M' + ~9000 free pages from a 1.25 MiB image claiming su_btotal=0x0FFFC000; ceiling 32 MiB), and hpfs_statfs (:408) reports f_blocks=su_btotal verbatim (128 GiB 'capacity' on a 1.25 MiB image). The hpfs twin of DF-3128 (msdosfs) and the mount-layer enabler of DF-0862's u32-wrap regime and the unvalidated sp_cpinum family (DF-0861/3144/3145). Severity Medium: get_fscap('hpfs')==SYSCAP_RESTRICTEDROOT - the trigger requires root/automount. Fix authored (dev_dpsize + 128 GiB bread bound + su_rootfno < su_btotal; git-apply-clean).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3178 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.3 KB | β raw | |
| VERDICT.md | β | 5.9 KB | β raw | |
| craft_img.py | β | 6.1 KB | view raw | |
| mount_ro.c | β | 1.3 KB | view raw | |
| statfs_probe.c | β | 2.2 KB | view raw | |
| build.sh | β | 65 B | view raw | |
| run_wiring.sh | β | 2.0 KB | view raw | |
| run_full.sh | β | 1.7 KB | view raw | |
| build.log | β | 300 B | view raw | |
| run.log | β | 745 B | view raw | |
| run_wiring_partial_df0927_trip.log | β | 596 B | view raw | |
| run_panic_attempt.log | β | 105 B | view raw | |
| boot_panic_console.log | β | 2.1 KB | view raw | |
| panic.txt | β | 576 B | view raw | |
| env.txt | β | 779 B | view raw | |
| fix.diff | β | 1.9 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 5.4 KB | view raw |
DF-3178 β hpfs_mountfs performs no super/spareblock census validation:
su_btotal flows unvalidated into bminit sizes (mount-time kernel panic
or 32 MiB wired heap + 128 GiB statfs lie per crafted image)
File: sys/vfs/hpfs/hpfs_vfsops.c (finding filed at the mount layer)
Sink: sys/vfs/hpfs/hpfs_subr.c:109-120 (hpfs_bminit), hpfs_vfsops.c:408 (hpfs_statfs)
Severity: Medium (crafted-image, root/auto-mount threat model β hpfs is
never user-mountable: get_fscap("hpfs") = SYSCAP_RESTRICTEDROOT,
sys/kern/vfs_syscalls.c:646-655)
Verification: reproduced on the QEMU guest β panic variant 1/1 runs,
wiring variant 2/2 runs.
What this is
After the two magic checks (hpfs_vfsops.c:282-291) the mount layer does
zero validation of the volume census. su_btotal (a u_int32_t read
straight off the image at hpfs_vfsops.c:268) is consumed as a size/index
in three places with no bound:
-
hpfs_bminit(hpfs_subr.c:109):hpm_dbnum = (su_btotal + 0x3FFF) / 0x4000then a single bmind-list bread of((dbnum + 0x7F) & ~0x7F) << 2bytes (hpfs_subr.c:119-120). Forsu_btotal >= 0x10000001(any volume claiming > 128 GiB in 512-byte sectors) the bread size exceedsMAXBSIZE(64 KiB) andgetblkpanics the kernel at mount time:panic: getblk: size(1048576) > MAXBSIZE(65536) getblk() at getblk+0x4e5 breadnx() at breadnx+0x284 hpfs_bminit() at hpfs_bminit+0xb0 hpfs_mount() at hpfs_mount+0x4b7 vfs_mount() at vfs_mount+0x14f
-
Same function:
kmalloc(dbnum * BMSIZE)+kmalloc(dbnum * 4)β for anysu_btotalin(0x1000 β¦ 0x10000000]the mount succeeds and wiresdbnum Γ 2048 + dbnum Γ 4bytes of kernel heap per mount (demonstrated: 32.1 MiB from a 1.25 MiB image claiming 0x0FFFC000 sectors; ceiling without panicking = 32 MiB; the 512 MiBsu_btotal=0xFFFFC000case panics per (1), andsu_btotal >= 0xFFFFC001wraps the u32 addition and is DF-0862's undersize-OOB regime). -
hpfs_statfs(hpfs_vfsops.c:408):f_blocks = su_btotalβ the mount advertisessu_btotal Γ 512bytes of capacity regardless of the backing device (demonstrated value: 0x0FFFC000 Γ 512 β 128 GiB on a 1.25 MiB image; would be 2.0 TiB for the panic-regime value).
This is the hpfs twin of DF-3128 (msdosfs mountmsdosfs() never bounds
BPB geometry by the backing device size) and the mount-layer enabler of the
already-filed subr-side findings (DF-0862 wrap regime, DF-0861/3144/3145
sp_cpinum family β note sp_cpinum/sp_cpi are likewise unvalidated
here).
Evidence in this pack
craft_img.pyβ builds both variants (same 1.25 MiB image, onlysu_btotaldiffers).mount_ro.c,build.sh,statfs_probe.c,run_full.sh,run_wiring.shβ harness.panic.txt,boot_panic_console.log,run_panic_attempt.logβ the deterministic mount-time panic (su_btotal = 0xFFFFC000, 1st attempt).run.log(run_full) andrun_wiring_partial_df0927_trip.logβ two independent successful wiring-variant mounts:HPFS_mount β¦ 32.1M,vm.stats.vm.v_free_count848021β839002 and 847502β838428 (β9k pages β 35 MiB total kernel footprint),MOUNT_RC=0 wall=0s.VERDICT.mdβ full narrative, including the side observation that entering the crafted mount by path (df/open(2)) wedges the whole system (consistent with the filed DF-0927/DF-3143 unbounded-walk family; root vnode/dirblk content is attacker data β NOT claimed by this finding, logs preserved).fix.diffβ validatesu_btotal/su_rootfnoagainstdev_dpsize()inhpfs_mountfs(same pattern as DF-3128's verified fix).
Reproduce
./build.sh # craft image, push, build mount_ro in guest python3 craft_img.py df3178p.img panic # panic variant # guest: vnconfig vn0 df3178p.img && ./mount_ro /dev/vn0 /tmp/hpfsmp # -> panic: getblk: size(1048576) > MAXBSIZE(65536) python3 craft_img.py df3178w.img wiring # wiring variant # guest: vnconfig vn0 df3178w.img && ./mount_ro /dev/vn0 /tmp/hpfsmp # -> mount OK; vmstat -m | grep HPFS -> 32.1M; v_free_count -~9k pages
Success criterion: (panic variant) Fatal trap/panic: getblk: on the
serial console with the hpfs_bminit β hpfs_mount trace; (wiring variant)
MOUNT_RC=0, HPFS_mount 3 32.1M, free-page drop β9,000, released on
umount.
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 ONLYsu_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)consumessu_btotalimmediately.sys/vfs/hpfs/hpfs_subr.c:109βhpm_dbnum = (su_btotal + 0x3FFF) / 0x4000(u32 arithmetic;>= 0xFFFFC001wraps β that regime is DF-0862's).sys/vfs/hpfs/hpfs_subr.c:119-120β bmind-listbread(devvp, dbtodoff(su_bitmap.lsn1), ((dbnum + 0x7F) & ~0x7F) << 2, &bp); fordbnum >= 0x4001the size exceedsMAXBSIZE(65536) andgetblkpanics. 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_btotalverbatim.
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 commandsbuild.log,run.log,run_wiring_partial_df0927_trip.log,run_panic_attempt.log,boot_panic_console.log,panic.txt,env.txtmanifest.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.
Fix verification
not_testablefix.diff authored and verified to apply cleanly to the read-only sys/ tree; kernel rebuild not performed for this non-memory-corruption finding (resource/validation class) β validation gate is three comparisons and both demonstrated variants are rejected by construction (panic variant: dbnum 0x3FFFF > 0x4000 cap; wiring variant: su_btotal 0x0FFFC000 > dev_dpsize of a 1.25 MiB vn device).
fix.diff (git apply --check clean); DF-3128 fix uses the identical dev_dpsize() pattern and was build-validated in that pack.
Confirmed kernel references
Detail
Evidence (decisive lines)
["panic.txt β 'panic: getblk: size(1048576) > MAXBSIZE(65536)' + getblk->breadnx->hpfs_bminit->hpfs_mount->vfs_mount trace", 'boot_panic_console.log β full serial console of the panic boot', "run.log β wiring variant: MOUNT_RC=0 wall=0s; vmstat -m 'HPFS_mount 3 32.1M', 'HPFS_hash 1 1.00M', 'HPFS_node 1 960'; v_free_count 847502->838428; then the documented path-entry wedge (open(2) probe)", 'run_wiring_partial_df0927_trip.log β independent wiring run: same 32.1M row, v_free_count 848021->839002', 'craft_img.py β builds both variants (wiring su_btotal=0x0FFFC000 / panic su_btotal=0xFFFFC000) in a 1.25 MiB image', 'fix.diff β git-apply-able dev_dpsize()-based census validation (applies cleanly to sys/)']
PoC changes
pack authored fresh in this run (no orchestrator seed); harness conventions (vnconfig, direct mount(2) helper, amd64 struct offsets) adopted from the DF-3141 evidence pack; the initial all-zero dirent area tripped DF-0927's de_reclen=0 walk and was replaced with a DE_END dirent (still wedged on entry -- documented, not claimed)
Verified recommended fix
In hpfs_mountfs, after the magic checks, validate the census before hpfs_bminit: su_btotal != 0, su_btotal <= dev_dpsize(dev), derived band count <= 0x4000 (keeps the bmind-list bread within MAXBSIZE), and su_rootfno < su_btotal; return EINVAL otherwise (see fix.diff).
Verdict
REPRODUCED. hpfs_mountfs validates nothing but the two magic numbers (sys/vfs/hpfs/hpfs_vfsops.c:282-291); the on-disk su_btotal flows straight into hpfs_bminit (hpfs_vfsops.c:301 -> hpfs_subr.c:109-120), where dbnum=(su_btotal+0x3FFF)/0x4000 sizes a single bmind-list bread of ((dbnum+0x7F)&~0x7F)<<2 bytes and two kmallocs. A 1.25 MiB crafted image with su_btotal=0xFFFFC000 (dbnum=0x3FFFF) made that bread 1 MiB and the kernel panicked deterministically at mount: 'panic: getblk: size(1048576) > MAXBSIZE(65536)' with the full getblk->breadnx->hpfs_bminit->hpfs_mount->vfs_mount trace (1/1 run; threshold su_btotal>=0x10000001, i.e. any volume claiming >128 GiB in 512-byte sectors). Below the panic threshold the mount SUCCEEDS and wires attacker-chosen kernel heap: su_btotal=0x0FFFC000 (dbnum=0x3FFF) mounted in 0 s with 'HPFS_mount 3 32.1M' (hpmp + 32 MiB bitmap + 64 KiB bmind) and a free-page drop of ~9,000 pages (~35 MiB total footprint), reproduced identically across two independent boots (848021->839002 and 847502->838428), released on umount. hpfs_statfs (hpfs_vfsops.c:408) additionally echoes su_btotal verbatim into f_blocks (128 GiB advertised on a 1.25 MiB image). This is the hpfs twin of DF-3128 (msdosfs) and the mount-layer enabler of DF-0862's wrap regime (su_btotal>=0xFFFFC001) and the unvalidated sp_cpinum family (DF-0861/3144/3145). Severity is capped by get_fscap('hpfs')==SYSCAP_RESTRICTEDROOT (sys/kern/vfs_syscalls.c:646-655): an unprivileged user can never mount hpfs even with vfs.usermount=1, so the trigger is a crafted image supplied to root/an automount path. SIDE OBSERVATION (not claimed): entering the crafted mount by path (df/open) hard-wedged the whole system twice -- consistent with the already-filed unbounded-walk family (DF-0927/DF-3143) consuming the crafted root fnode/dirblk; logs preserved (run_wiring_partial_df0927_trip.log, run.log); a speculative hphashvget spin hypothesis (hpfs_hash.c:110-129 holding hphash_token across a persistently failing vget retry) could not be confirmed without DDB access.
No comments yet.