DF-3178 / verdict.json
{ "finding_id": "DF-3178", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "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-0862s 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.", "exploit_chain": "", "evidence": [ "panic.txt \u2014 panic: getblk: size(1048576) > MAXBSIZE(65536) + getblk->breadnx->hpfs_bminit->hpfs_mount->vfs_mount trace", "boot_panic_console.log \u2014 full serial console of the panic boot", "run.log \u2014 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 \u2014 independent wiring run: same 32.1M row, v_free_count 848021->839002", "craft_img.py \u2014 builds both variants (wiring su_btotal=0x0FFFC000 / panic su_btotal=0xFFFFC000) in a 1.25 MiB image", "fix.diff \u2014 git-apply-able dev_dpsize()-based census validation (applies cleanly to sys/)" ], "kernel_refs": [ "sys/vfs/hpfs/hpfs_vfsops.c:265-291", "sys/vfs/hpfs/hpfs_vfsops.c:301", "sys/vfs/hpfs/hpfs_vfsops.c:408", "sys/vfs/hpfs/hpfs_subr.c:109", "sys/vfs/hpfs/hpfs_subr.c:113-117", "sys/vfs/hpfs/hpfs_subr.c:119-120" ], "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-0927s de_reclen=0 walk and was replaced with a DE_END dirent (still wedged on entry -- documented, not claimed)", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 1140, "guest_dirty": 0, "build_cmd": "./build.sh (python3 craft_img.py + scp + in-guest cc -O2 -I/usr/src/sys -o /root/mount_ro /tmp/mount_ro.c)", "run_cmd": "./run_wiring.sh (panic variant: vnconfig vn0 df3178p.img && /root/mount_ro /dev/vn0 /tmp/hpfsmp -> panic)", "code_hash": "5670148d93d9c18530654c25239a1631836e4bdbf0567921be9ceb58ddba8da8", "notes": "Guest needed vm.sh reset with-src after the panic and after both wedges; final state re-verified clean. Fix build deliberately not performed: this is a resource/validation finding (not memory corruption), the gate is three comparisons, and both variants sit below the patched thresholds by construction (panic threshold su_btotal>=0x10000001 > cap; wiring bitmap capped by devsize bound).", "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).", "fix_status": "not_testable", "fix_kernel_uname": "", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.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) \u2014 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_evidence": "fix.diff (git apply --check clean); DF-3128 fix uses the identical dev_dpsize() pattern and was build-validated in that pack." } |