β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0862

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0862 Β· 13 files
FileTypeDescriptionSize
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
README.md readme human-readable summary + reproduce instructions
↓ download 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)

  1. hpfs_vfsops.c β€” validate su_btotal against the backing device via DIOCGSECTORSIZE+DIOCGMEDIASIZE; reject (EINVAL) if su_btotal*secsize > mediasize.
  2. hpfs_subr.c β€” compute the band count in 64-bit (((u_long)su_btotal + 0x3FFF) / 0x4000; hpm_dbnum is already u_long) and reject su_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.

VERDICT.md verdict full narrative: mechanism, reachability, exploit-chain assessment, fix, validation
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (kernel text unchanged; the bug lives entirely in the hpfs.ko module, so the single-fix artifact is the rebuilt module /boot/kernel/hpfs.ko sha256 d5dbde0c50f7deee435641cd9855d128b5c3fae2b8087daa4a713c58ce6e4a33)

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