hpfs_bminit integer overflow in hpm_dbnum computation undersizes bitmap arrays OOB read/write
Summary
hpfs_subr.c:109 hpm_dbnum=(su_btotal+0x3FFF)/0x4000 u32 arithmetic. su_btotal>0xFFFFC000 addition wraps. su_btotal=0xFFFFC001 -> hpm_dbnum=0 -> kmalloc(0) degenerate. :156 for(i=0;i<su_btotal>>5;i++) reads 512MB past allocation = panic. bmmark writes OOB. su_btotal from disk unvalidated in hpfs_vfsops.c:268.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0862 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_img.c | trigger-source | builds crafted HPFS image with su_btotal=0xFFFFC001 (configurable) | 3.1 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o craft_img craft_img.c | 157 B | view raw |
| run.sh | run-script | kldload hpfs + vnconfig + mount (root; triggers the bug) | 1.7 KB | view raw |
| fix.diff | suggested-fix | validated git-apply-able fix: device-size check (hpfs_vfsops.c) + 64-bit band-count (hpfs_subr.c) | 2.7 KB | view raw |
| panic.txt | panic-signature | unpatched Fatal trap 12 / VA=0xfffffffffffffff8 / hpfs_bminit+0x230 (crash proof) | 1.7 KB | view raw |
| run.log | run-log | baseline BEFORE run output (panic) | 1.4 KB | view raw |
| fix_run.log | run-log | fixed-module AFTER run output + before/after contrast | 1.7 KB | view raw |
| fix_build.log | build-log | full nativekernel build log of the fixed hpfs.ko (rc=0) | 5.6 MB | β download |
| env.txt | environment | uname, cc 8.3, fixed-module sha256, vfs.usermount | 453 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, exploit-chain assessment, fix, validation | 7.6 KB | β raw |
| README.md | readme | human-readable summary + reproduce instructions | 2.9 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0862 β hpfs_bminit integer overflow in hpm_dbnum (OOB read / panic)
Verdict: REPRODUCED (panic/DoS via crafted HPFS image). Fix VALIDATED. Severity: Medium. Impact: kernel panic at mount (local DoS).
What the bug is
su_btotal (a u_int32_t read verbatim off the HPFS SuperBlock at mount and
never validated) feeds (su_btotal + 0x3FFF) / 0x4000 in hpfs_bminit
(sys/vfs/hpfs/hpfs_subr.c:109), evaluated in u32 arithmetic. For
su_btotal in [0xFFFFC001, 0xFFFFFFFF] the +0x3FFF wraps mod 2^32, so
hpm_dbnum becomes 0; the two kmalloc(hpm_dbnum * β¦) calls then hit the
slab allocator's kmalloc(0) special case and return the sentinel
ZERO_LENGTH_PTR == (void*)-8 == 0xFFFFFFFFFFFFFFF8 (kern_slaballoc.c:888/193).
The bitmap-bitcount loop at :156, bounded by the un-wrapped su_btotal >> 5
(up to ~134M iterations), then dereferences hpm_bitmap[0] at that non-canonical
address β immediate page fault on mount.
Threat model / reachability
HPFS is optional hpfs (not in X86_64_GENERIC) but shipped as /boot/kernel/hpfs.ko.
An admin enables it with kldload hpfs (standard FS-enable action). The attacker
supplies a crafted image; the mounter (root, or an unprivileged user after
vfs.usermount=1 + an owned memory disk) mounts it β kernel panic. Classic
filesystem-image-parsing memory-safety bug.
No unprivβroot escalation: the fault is a single OOB read of a sentinel that
panics on the first iteration; the write path (bmmark/hpfs_bmdeinit) is never
reached because mount dies first. Impact ceiling = local DoS / panic.
Reproduce
./build.sh # cc -O2 -Wall -o craft_img craft_img.c
./craft_img crafted.img 0xFFFFC001
# as root (the victim mounting the attacker image):
kldload hpfs
DEV=$(vnconfig -c vn $(pwd)/crafted.img | grep -oE 'vn[0-9]+' | head -1)
mount -t hpfs -o ro /dev/$DEV /mnt/df0862 # UNPATCHED: Fatal trap 12, guest dies
Expected on the unpatched kernel (serial boot.log):
Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffffffffffffff8 Stopped at hpfs_bminit+0x230: movl (%rax),%edi
Expected on the fixed module: mount_hpfs: /dev/vnN: Invalid argument (EINVAL),
guest stays up, dmesg: hpfs_mountfs: su_btotal 4294950913 exceeds device capacity 262144.
The fix (fix.diff)
- hpfs_vfsops.c β validate
su_btotalagainst the backing device viaDIOCGSECTORSIZE+DIOCGMEDIASIZE; reject (EINVAL) ifsu_btotal*secsize > mediasize. - hpfs_subr.c β compute the band count in 64-bit
(
((u_long)su_btotal + 0x3FFF) / 0x4000;hpm_dbnumis alreadyu_long) and rejectsu_btotal == 0. This defends the arithmetic itself even when the geometry ioctls are unavailable.
Validated: built the single-fix hpfs.ko, installed it, re-ran the same PoC β
panic is gone, mount returns EINVAL promptly and the guest stays up (deterministic
over 2 runs). See VERDICT.md, fix_run.log, panic.txt.
DF-0862 β hpfs_bminit integer overflow in hpm_dbnum computation
VERDICT: REPRODUCED (panic / DoS via crafted HPFS image). FIX VALIDATED.
Severity (per finding): Medium. Impact demonstrated: kernel panic (Fatal trap 12, page fault on the ZERO_LENGTH_PTR sentinel) at mount of a crafted HPFS image.
1. THE BUG (root cause)
su_btotal (total blocks) is a u_int32_t read verbatim off the SuperBlock
at mount and NEVER validated against the device (sys/vfs/hpfs/hpfs_vfsops.c:268).
In hpfs_bminit (sys/vfs/hpfs/hpfs_subr.c) it is used in two inconsistent ways:
hpfs_subr.c:109 hpm_dbnum = (su_btotal + 0x3FFF) / 0x4000; / u32 arith / hpfs_subr.c:113 hpm_bmind = kmalloc(hpm_dbnum * sizeof(lsn_t), ...); hpfs_subr.c:116 hpm_bitmap = kmalloc(hpm_dbnum * BMSIZE, ...); ... hpfs_subr.c:156 for (i=0; i < su_btotal >> 5; i++) / u32, UN-wrapped / hpfs_subr.c:159 if (((u_int32_t *)hpm_bitmap)[i] & mask) dbavail++;
The +0x3FFF is evaluated in u_int32_t (su_btotal is u32, the literals are int,
so the usual arithmetic conversions keep the whole expression in unsigned int).
For su_btotal in [0xFFFFC001, 0xFFFFFFFF], su_btotal + 0x3FFF wraps mod 2^32 to
[0, 0x3FFE], so hpm_dbnum becomes 0. The allocations then call kmalloc(0),
which the DragonFly slab allocator special-cases (kern_slaballoc.c:888) by
returning the sentinel ZERO_LENGTH_PTR = (void*)-8 = 0xFFFFFFFFFFFFFFF8
(kern_slaballoc.c:193) -- NOT a real slab chunk.
The bitmap-bitcount loop, however, is bounded by the UN-wrapped su_btotal >> 5
(== up to 0x07FFFE00 = 134,217,728 iterations), completely independent of the
wrapped hpm_dbnum. Its first dereference of hpm_bitmap[0] reads address
0xFFFFFFFFFFFFFFF8 -- a non-canonical address on amd64 -> immediate page fault.
=> hpfs_bminit+0x230 movl (%rax),%edi (rax = 0xfffffffffffffff8)
Confirmed on the default GENERIC kernel (6.5-DEVELOPMENT #0, INVARIANTS ON):
INVARIANTS does not interfere here because ZERO_LENGTH_PTR is an intentional
sentinel, not a slab-corruption artifact.
2. REACHABILITY / THREAT MODEL
HPFS is optional hpfs -- NOT compiled into X86_64_GENERIC, but shipped as the
loadable module /boot/kernel/hpfs.ko. An admin enables HPFS support by
kldload hpfs (a one-time, standard action analogous to enabling any filesystem
type -- it only registers the parser; it is NOT part of any privilege chain).
The attacker supplies a crafted HPFS image; the victim (root, or an unprivileged
user after vfs.usermount=1 + an image/memory-disk owned by them -- both
realistic admin preconditions per AGENT.md) mounts it and the kernel panics.
This is a classic filesystem-image-parsing memory-safety bug.
The panic kills the guest (local DoS). No unpriv->root escalation is reachable:
the bug fires at MOUNT time, before any HPFS file operation, and the bitmap scan
is a READ (the bmmark write path is never reached because mount dies first).
=> Impact ceiling = kernel panic / local DoS. Matches Medium severity.
3. REPRODUCTION EVIDENCE (unpatched #0)
Trigger: crafted.img, su_btotal = 0xFFFFC001 ( craft_img.c ). kldload hpfs; vnconfig -c vn crafted.img; mount -t hpfs -o ro /dev/vnN /mnt
Serial console (boot.log): Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffffffffffffff8 <-- ZERO_LENGTH_PTR Stopped at hpfs_bminit+0x230: movl (%rax),%edi guest: DOWN. (See panic.txt + run.log.)
4. EXPLOIT-CHAIN ASSESSMENT (why this stops at panic, not uid0)
The reachable primitive is a single OOB READ of a non-canonical sentinel address that faults on the first iteration. It is not a controllable write, the write path (hpfs_bmdeinit/bmmark) is unreachable post-panic, and nothing about the fault leaks or corrupts a victim object that could be converted to privilege. This is a VALID hard blocker per Phase 6 ("read-only primitive that panics; no write, no corruption to convert"). Correctly classified as a DoS / memory-safety bug, not an escalation candidate.
5. THE FIX (fix.diff) -- two complementary hunks
(a) hpfs_vfsops.c (hpfs_mountfs, after the magic checks): validate su_btotal against the actual backing device via DIOCGSECTORSIZE + DIOCGMEDIASIZE; reject (EINVAL) images whose su_btotal * sectorsize exceeds the device. This closes the practical threat model (a 256 KiB image can no longer claim 4 billion blocks). Best-effort: if the device does not answer the geometry ioctls, it falls through to (b).
(b) hpfs_subr.c (hpfs_bminit): compute the band count in 64-bit
(((u_long)su_btotal + 0x3FFF) / 0x4000, hpm_dbnum is already u_long) so the
+0x3FFF can never wrap to 0 regardless of the device, and reject su_btotal==0.
This defends the cited arithmetic itself and bounds the later kmallocs.
The first attempt at a fix (b alone) eliminated the ZERO_LENGTH_PTR panic but the
absurd su_btotal still crashed via a RELATED path -- with hpm_dbnum correctly
=262144, the bitmap-directory bread (hpfs_subr.c:119-120) requests 1 MiB >
MAXBSIZE(64 KiB) -> panic: getblk: size(1048576) > MAXBSIZE. That is the same
root cause (unvalidated su_btotal) surfacing elsewhere; adding the device-size
check (a) closes the whole threat model. (This intermediate finding is recorded
for completeness -- it confirms su_btotal validation, not just the arithmetic
widening, is required.)
6. FIX VALIDATION (single-fix hpfs.ko)
Built the fixed hpfs.ko (make -j6 nativekernel, warm obj; the bug lives in the
module, not the kernel text, so the single-fix artifact is the rebuilt module:
/boot/kernel/hpfs.ko sha256 d5dbde0c50f7deee435641cd9855d128b5c3fae2b8087daa4a713c58ce6e4a33).
BEFORE (unpatched): mount -> Fatal trap 12, VA=0xfffffffffffffff8, PANIC, guest DOWN. AFTER (fixed): mount -> EINVAL (71) promptly, guest UP; dmesg: "hpfs_mountfs: su_btotal 4294950913 exceeds device capacity 262144". Deterministic over 2 runs. => fix_status: fixed. (See fix_run.log.)
7. FILES IN THIS EVIDENCE PACK
craft_img.c -- builds the crafted HPFS image (su_btotal configurable, default 0xFFFFC001) build.sh -- cc -O2 -Wall -o craft_img craft_img.c run.sh -- kldload hpfs + vnconfig + mount (root; triggers the bug) fix.diff -- the validated git-apply-able fix (hpfs_vfsops.c + hpfs_subr.c) panic.txt -- unpatched panic signature from boot.log (crash proof) run.log -- baseline (BEFORE) run output fix_run.log -- fixed-module (AFTER) run output + before/after contrast fix_build.log -- full nativekernel build log of the fixed module (rc=0) env.txt -- guest uname, cc, module sha256, sysctls VERDICT.md -- this file manifest.json -- machine-readable catalog
Fix verification
fixedVALIDATED the fix: the same crafted HPFS image (su_btotal=0xFFFFC001) that PANICS on the unpatched #0 baseline (Fatal trap 12, VA=0xfffffffffffffff8, Stopped at hpfs_bminit+0x230) instead returns EINVAL (71) promptly on the single-fix hpfs.ko with dmesg 'hpfs_mountfs: su_btotal 4294950913 exceeds device capacity 262144', guest staying UP -- deterministic over 2 runs. The integer-overflow ZERO_LENGTH_PTR-deref is gone AND the related getblk>MAXBSIZE path is pre-empted by the device-size check. => fix closes the bug.
BEFORE (unpatched hpfs.ko): Fatal trap 12: page fault, fault virtual address=0xfffffffffffffff8, Stopped at hpfs_bminit+0x230: movl (%rax),%edi -> guest DOWN. AFTER (fixed hpfs.ko d5dbde0c...): MOUNT_RC=71, mount_hpfs: /dev/vn4: Invalid argument, dmesg: hpfs_mountfs: su_btotal 4294950913 exceeds device capacity 262144, GUEST_STILL_UP (2nd run identical).
Confirmed kernel references
Detail
Exploit chain
Not an escalation candidate -- VALID hard blocker applies (Phase 6: read-only primitive that panics, no write to convert). The reachable primitive is a single OOB READ of the ZERO_LENGTH_PTR sentinel that faults on the FIRST loop iteration; nothing is leaked or corrupted because hpm_bitmap is the sentinel, not a real slab chunk. The write path (hpfs_bmdeinit/bmmark) is unreachable because mount dies in hpfs_bminit before any file operation. Impact ceiling = local kernel panic / DoS at mount, matching the Medium classification. No bucket/victim-object grooming is applicable. The chain file is the trigger itself (craft_img.c + run.sh); no exploit.c was warranted since there is no write primitive to convert to uid0.
Evidence (decisive lines)
UNPATCHED (boot.log): Fatal trap 12: page fault while in kernel mode / fault virtual address = 0xfffffffffffffff8 / Stopped at hpfs_bminit+0x230: movl (%rax),%edi / db> (guest DOWN). FIXED module: mount_hpfs: /dev/vn4: Invalid argument / MOUNT_RC=71 / dmesg: hpfs_mountfs: su_btotal 4294950913 exceeds device capacity 262144 / GUEST_STILL_UP (deterministic over 2 runs).
PoC changes
Created the full evidence pack from scratch (finding had only a DB record, no poc dir): craft_img.c (builds a minimal HPFS image with su_btotal configurable, default 0xFFFFC001, with correct SU_MAGIC/SP_MAGIC and struct sublock layout), build.sh, run.sh (kldload hpfs + vnconfig + mount), README.md, VERDICT.md, panic.txt, run.log, fix_run.log, fix_build.log, env.txt, manifest.json, and fix.diff. No upstream PoC existed to fix.
Verified recommended fix
fix.diff has two complementary hunks. (1) hpfs_vfsops.c hpfs_mountfs: after the magic checks, validate su_btotal against the backing device via VOP_IOCTL(DIOCGSECTORSIZE)+VOP_IOCTL(DIOCGMEDIASIZE) and reject (EINVAL) when su_btotal*secsize > mediasize -- this closes the practical crafted-image threat model (best-effort; falls through if the geometry ioctls are unavailable). (2) hpfs_subr.c hpfs_bminit: compute the band count in 64-bit ((u_long)su_btotal + 0x3FFF) / 0x4000 (hpm_dbnum is already u_long) so the +0x3FFF can never wrap to 0, and reject su_btotal==0. Hunk (2) alone eliminated the ZERO_LENGTH_PTR panic but the absurd su_btotal still crashed via a related path (bitmap-directory bread 1MiB > MAXBSIZE -> getblk panic); adding the device-size check (1) closes the whole threat model. Supersedes any arithmetic-only proposal (the finding's DB summary cites only the integer overflow; device-size validation is additionally required to fully close it).
Verdict
REPRODUCED. The bug is real: hpfs_subr.c:109 computes hpm_dbnum=(su_btotal+0x3FFF)/0x4000 in u32 arithmetic; for su_btotal in [0xFFFFC001,0xFFFFFFFF] the +0x3FFF wraps mod 2^32 so hpm_dbnum=0, making the kmalloc(0)s at :113/:116 return the slab sentinel ZERO_LENGTH_PTR (== (void*)-8 == 0xFFFFFFFFFFFFFFF8, kern_slaballoc.c:193/888), while the bitmap-scan loop at :156 is bounded by the UN-wrapped su_btotal>>5 (up to ~134M iters) and dereferences hpm_bitmap[0] at the non-canonical address. su_btotal is read verbatim off the SuperBlock (hpfs_vfsops.c:268) and never validated. Confirmed on the default GENERIC kernel (INVARIANTS ON) -- INVARIANTS does not interfere because ZERO_LENGTH_PTR is an intentional sentinel, not a slab artifact. Trigger: mount a crafted HPFS image (su_btotal=0xFFFFC001); the mounter (root, or unpriv after vfs.usermount=1 + owned memory disk) is the victim. The proof panic is Fatal trap 12: page fault, fault virtual address 0xfffffffffffffff8, Stopped at hpfs_bminit+0x230: movl (%rax),%edi (boot.log).
No comments yet.