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

hpfs_readdir dep-walk unbounded against 2KB bread buffer β€” panic/heap-leak/infinite-loop via crafted dirblk

Summary

hpfs_vnops.c:825 bread(D_BSIZE=2048). :839 dep=D_DIRENT(dp)=bp->b_data+20. :848/871/906 dep=(hpfsdirent_t*)((caddr_t)dep+dep->de_reclen) — de_reclen u16 attacker-controlled NO bounds check dep stays in bp->b_data+D_BSIZE NO check de_reclen>=sizeof(hpfsdirent). Termination only de_flag&DE_END (attacker-controlled). (a) panic: de_reclen=0xFF00 dep jumps past 2048 buffer next de_flag read OOB unmapped = fatal page fault. (b) heap leak: OOB mapped memory interpreted as de_name emitted to user via hpfs_de_uiomove→uiomove up to 255 bytes per fake entry. (c) infinite loop: de_reclen=0 de_flag=DE_SPECIAL no cnum++ no advance = forever spin vnode exclusive locked unkillable. Same defect hpfs_lookup.c:82-96 hpfs_subr.c:572-610. Trigger: crafted HPFS image malicious dirblk mount then ls/stat/cat. Fix: HPFS_DE_VALID macro check dep+reclen<=bp->b_data+D_BSIZE and reclen>=sizeof(hpfsdirent) before each advance.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0830 Β· 16 files
FileTypeDescriptionSize
mk_hpfs.py trigger-source crafts HPFS image with malformed dir block (dep de_reclen=0x0900) 6.7 KB view raw
poc.c trigger-source opens mountpoint, calls getdents, dumps entries 3.2 KB view raw
evil.hpfs test-data the crafted 100 KB HPFS image 100.0 KB ↓ download
build.sh build-script cc -Wall -O0 -o poc poc.c 88 B view raw
run.sh run-script mounts image and runs poc as unprivileged user 624 B view raw
VERDICT.md verdict full narrative: mechanism, impact, fix validation 6.0 KB ↓ raw
fix.diff suggested-fix HPFS_DE_INBOUNDS macro + 5 while-loop + 5 post-loop guards across hpfs.h/vnops.c/subr.c/lookup.c 4.0 KB view raw
build.log build-log PoC build output 17 B view raw
run.log run-log baseline run output (panic) 529 B view raw
fix_build.log build-log hpfs.ko module rebuild with fix applied 10.5 KB view raw
fix_run.log run-log patched-module run output (clean, no panic) 418 B view raw
panic.txt panic-signature fatal trap 12 in hpfs_validateparent+0x146 (2 runs) 1.5 KB view raw
env.txt environment uname, kern.version, cc version, hpfs.ko loaded 383 B view raw
fix_env.txt environment patched module sha256 + kldstat 284 B view 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
VERDICT.md verdict full narrative: mechanism, impact, fix validation
↓ download raw

DF-0830 β€” hpfs_readdir / hpfs_validateparent / hpfs_genlookupbyname dep-walk unbounded against 2 KB bread buffer

Verdict: REPRODUCED β€” kernel panic (OOB read page fault) via crafted HPFS image.

The dep-walk loops in hpfs_readdir (sys/vfs/hpfs/hpfs_vnops.c), hpfs_validateparent (sys/vfs/hpfs/hpfs_subr.c), and hpfs_genlookupbyname (sys/vfs/hpfs/hpfs_lookup.c) advance a struct hpfsdirent *dep pointer by dep->de_reclen β€” a u_int16_t loaded directly from disk with no validation β€” and never bound-check it against the 2 KB (D_BSIZE = DEV_BSIZE*4 = 2048) buffer returned by bread(). A crafted HPFS image with a malformed directory block causes dep to walk past the buffer into unmapped kernel memory, producing a fatal page fault (panic) when the post-loop code reads dep->de_flag.

Threat model (realistic)

The HPFS module is loadable (/boot/kernel/hpfs.ko, shipped with the install). The realistic scenario: an admin has mounted (or made mountable via vfs.usermount=1) an attacker-controlled HPFS filesystem image. Once mounted, any unprivileged user who can access the mountpoint triggers the bug via getdents/stat/ls β€” standard syscalls, no privilege check, no special device access needed. The vulnerability is in filesystem-image parsing (the directory block content), exactly the pattern called out as an acceptable precondition.

Mechanism (every hop cited)

  1. sys/vfs/hpfs/hpfs.h:133 β€” D_BSIZE = DEV_BSIZE*4 = 2048.
  2. sys/vfs/hpfs/hpfs_vnops.c:825 β€” bread(hp->h_devvp, dbtodoff(lsn), D_BSIZE, &bp) reads a 2 KB directory block.
  3. sys/vfs/hpfs/hpfs_vnops.c:839 β€” dep = D_DIRENT(dp) = bp->b_data + sizeof(dirblk_t) (= 20 bytes). First dep at offset 20.
  4. sys/vfs/hpfs/hpfs.h:116-131 β€” struct hpfsdirent: de_reclen is u_int16_t at offset 0, de_flag at offset 2. Both are attacker-controlled bytes from the disk image.
  5. sys/vfs/hpfs/hpfs_vnops.c:906 β€” dep = (hpfsdirent_t *)((caddr_t)dep + dep->de_reclen) β€” no bounds check. With de_reclen=0x0900, dep jumps from bp->b_data+20 to bp->b_data+2324, which is 276 bytes past the 2048-byte buffer.
  6. sys/vfs/hpfs/hpfs_vnops.c:909 (and equivalently hpfs_subr.c:613) β€” after the while loop exits, if(dep->de_flag & DE_DOWN) reads dep->de_flag at the OOB address β€” unconditional, no bounds check.
  7. The OOB read faults because the virtual page at that address is not mapped β†’ Fatal trap 12: page fault while in kernel mode.

The identical defect exists in: - sys/vfs/hpfs/hpfs_subr.c:576, 588, 610 β€” hpfs_validateparent (where the crash actually occurred in our PoC, called from hpfs_getattr at hpfs_vnops.c:467). - sys/vfs/hpfs/hpfs_lookup.c:96 β€” hpfs_genlookupbyname.

Demonstration

mk_hpfs.py crafts a minimal-but-valid HPFS image whose root directory block contains a single dep with de_reclen=0x0900, de_flag=0x0000 (no DE_END, no DE_SPECIAL). Mounting it and calling getdents (via poc.c) triggers the dep-walk:

baseline (unpatched #0):
  Fatal trap 12: page fault while in kernel mode
  fault virtual address    = 0xfffff80055487116
  Stopped at  hpfs_validateparent+0x146:  movzwl  0x2(%r15),%edx

The instruction movzwl 0x2(%r15),%edx is reading dep->de_flag (offset +2) from the OOB dep pointer in r15. The fault address (...7116) is bp->b_data + 2324 + 2 β€” 278 bytes past the 2 KB buffer.

Impact

  • Kernel panic (DoS). Deterministic, unprivileged, via a standard getdents/stat syscall on a mounted malicious HPFS image. Two independent runs produced the identical crash signature.
  • Potential heap info-leak. If the OOB memory past the buffer is mapped (adjacent kernel heap), the walk would read dep->de_name and emit it to userspace via hpfs_de_uiomove β†’ vop_write_dirent β†’ uiomove. In our PoC the OOB page was unmapped, so the observable effect is the panic rather than a leak.
  • No write primitive. The dep-walk is read-only (it reads dep fields and advances the pointer). No escalation chain to uid=0 is derivable; this is a pure OOB-read / DoS class bug. Correctly classified as Medium.

Fix

fix.diff adds an HPFS_DE_INBOUNDS(bp, dep) macro to hpfs.h and uses it in all five dep-walk while loop conditions across the three files. The macro checks that dep + sizeof(hpfsdirent_t) fits within bp->b_data + D_BSIZE before any dep field is read (short-circuit && in the while condition). A de_reclen >= sizeof(hpfsdirent_t) check prevents de_reclen=0 infinite loops. Additionally, post-loop dep->de_flag reads are guarded with if (!HPFS_DE_INBOUNDS(bp, dep)) goto <error_exit> β€” this catches the case where the loop exits with dep OOB and the post-loop code tries to read dep->de_flag & DE_DOWN.

Fix validation (Phase 8)

Kernel kern.version hpfs.ko PoC result Verdict
baseline (unpatched #0) 6.5-DEVELOPMENT #0 original panic hpfs_validateparent+0x146 BUG
patched (#0 + fixed hpfs.ko) 6.5-DEVELOPMENT #0 rebuilt with fix.diff clean return (3/3 runs, no panic) FIXED

The fix was validated by building only the hpfs.ko module (the bug is entirely in the loadable module, not the kernel proper), installing it at /boot/kernel/hpfs.ko, and re-running the identical PoC. The baseline panic is deterministic (2/2 runs); the patched module survives 3/3 runs.

PoC changes from the seeded draft

The folder was empty when this run started; the entire evidence pack was authored from scratch:

  • mk_hpfs.py β€” Python crafter for a minimal-but-valid HPFS image whose root directory block has a dep with de_reclen=0x0900. Reuses the fnode/superblock/spareblock/bitmap structure proven in DF-0829.
  • poc.c β€” opens the mountpoint O_RDONLY|O_DIRECTORY and calls getdents, dumping returned entries. If the kernel panics (guest goes down), the proof is in boot.log/panic.txt.
  • fix.diff β€” the verified, git apply-able fix (5 while-loop bounds checks + 5 post-loop guards across 3 files, plus the macro).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: baseline (unpatched #0 + original hpfs.ko) panics deterministically (2/2 runs, Fatal trap 12 in hpfs_validateparent+0x146 reading dep->de_flag past 2KB bread buffer). Patched (#0 + rebuilt hpfs.ko with fix.diff) does NOT panic (3/3 clean runs, getdents returns entries normally, guest stays up). The fix was applied to the hpfs.ko loadable module (which contains all vulnerable code -- hpfs_readdir, hpfs_validateparent, hpfs_genlookupbyname are all in hpfs.ko, not compiled into GENERIC), built with 'cd /usr/src/sys/vfs/hpfs && make', and installed at /boot/kernel/hpfs.ko. Note: a full nativekernel build also completed successfully (rc=0) but the resulting kernel could not be booted by the DragonFly loader (loader format issue unrelated to the fix); the module-only approach is equivalent since the bug is entirely in the loadable module.

=== BASELINE (before) -- unpatched ===
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff80055487116
Stopped at  hpfs_validateparent+0x146:  movzwl  0x2(%r15),%edx
(guest down, 2/2 runs)
=== PATCHED (after) -- fixed hpfs.ko ===
[*] getdents returned 72 bytes
  ino=30 type=4 namelen=1 name='.'
  ino=30 type=4 namelen=2 name='..'
RUN_EXIT=1
(guest up, 3/3 runs, no panic)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (kernel binary unchanged; fix applied to the hpfs.ko loadable module which contains 100% of the vulnerable code -- hpfs.ko sha256=91016a98b0cfca6560222436208f7d7a74bf069f0c46b9928432e804c545e707)

Confirmed kernel references

Detail

Exploit chain

none -- pure OOB read / DoS class, no write primitive. The dep-walk reads dep fields (de_flag, de_reclen, de_name) and advances the pointer; it never writes through dep. The three theoretical impact modes are: (a) panic (OOB read hits unmapped page) -- DEMONSTRATED, deterministic; (b) heap info-leak (if OOB memory is mapped, dep->de_name emitted via hpfs_de_uiomove -> vop_write_dirent -> uiomove) -- possible variant, not observed because the OOB page was unmapped in our test; (c) infinite loop (de_reclen=0, no DE_END/SPECIAL) -- possible variant, bounded by uio_resid so not a permanent kernel hang. No escalation to uid=0 is derivable from this bug. Correctly classified as Medium.

Evidence (decisive lines)

=== BASELINE (unpatched #0 kernel, original hpfs.ko) -- Run 1 ===
Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0xfffff80055487116
instruction pointer      = 0x8:0xffffffff826032f6
Stopped at  hpfs_validateparent+0x146:  movzwl  0x2(%r15),%edx
=== Run 2 (identical signature, different VA) ===
fault virtual address    = 0xfffff80058877116
Stopped at  hpfs_validateparent+0x146:  movzwl  0x2(%r15),%edx
=== PATCHED (fixed hpfs.ko module) -- 3/3 clean runs ===
[*] getdents returned 72 bytes
  ino=30 type=4 namelen=1 name='.'
  ino=30 type=4 namelen=2 name='..'
  ino=0  type=8 namelen=0 name=''
RUN_EXIT=1 (guest stays up, no panic)

PoC changes

The folder was empty when this run started; the entire evidence pack was authored from scratch. mk_hpfs.py crafts a minimal-but-valid HPFS image (same superblock/spareblock/bitmap/fnode structure proven in sibling DF-0829) whose root directory block contains a single dep with de_reclen=0x0900, de_flag=0x0000. poc.c opens the mountpoint O_RDONLY|O_DIRECTORY and calls getdents, dumping returned entries. If the kernel panics, the proof is in boot.log/panic.txt. fix.diff adds the HPFS_DE_INBOUNDS macro and applies it to all 5 dep-walk while loops + 5 post-loop guards across hpfs.h/hpfs_vnops.c/hpfs_subr.c/hpfs_lookup.c.

Verified recommended fix

Add HPFS_DE_INBOUNDS(bp, dep) macro to hpfs.h that checks (caddr_t)dep + sizeof(hpfsdirent_t) <= bp->b_data + D_BSIZE. Use it in all 5 dep-walk while conditions across hpfs_vnops.c (hpfs_readdir lines 844+882), hpfs_subr.c (hpfs_validateparent lines 572+598), hpfs_lookup.c (hpfs_genlookupbyname line 82) -- short-circuit && ensures dep->de_flag is never read if dep is OOB. Add de_reclen >= sizeof(hpfsdirent_t) to prevent de_reclen=0 infinite loops. Add post-loop guards: after each while loop, if (!HPFS_DE_INBOUNDS(bp, dep)) goto -- this catches the unconditional dep->de_flag & DE_DOWN read at vnops:909 / subr:613 that caused the actual crash. The first fix attempt bounded only the while-loop conditions but NOT the post-loop reads -- the patched kernel still panicked at hpfs_validateparent+0x1d6 (post-loop dep->de_flag read). The revised fix.diff adds the post-loop guards and was validated. Full git-apply-able diff in findings/poc/DF-0830/fix.diff. Supersedes the finding proposal (which suggested an HPFS_DE_VALID macro but did not address the post-loop reads).

Verdict

REPRODUCED. The dep-walk loops in hpfs_readdir (sys/vfs/hpfs/hpfs_vnops.c:882-906), hpfs_validateparent (sys/vfs/hpfs/hpfs_subr.c:598-610), and hpfs_genlookupbyname (sys/vfs/hpfs/hpfs_lookup.c:82-96) advance a struct hpfsdirent dep pointer by dep->de_reclen -- a u_int16_t loaded directly from disk (hpfs.h:117) with NO validation -- and never bound-check it against the 2 KB bread(D_BSIZE=DEV_BSIZE4=2048) buffer (hpfs.h:133, bread at hpfs_vnops.c:825 / hpfs_subr.c:556). Additionally, the post-loop code at hpfs_vnops.c:909 and hpfs_subr.c:613 reads dep->de_flag unconditionally after the loop exits, even when dep has walked past the buffer. A crafted HPFS image (mk_hpfs.py) with a directory block containing dep de_reclen=0x0900 makes dep jump from bp->b_data+20 to bp->b_data+2324 (276 bytes past the 2048-byte buffer). The read of dep->de_flag at the OOB address faults on an unmapped kernel page: Fatal trap 12 page fault, hpfs_validateparent+0x146: movzwl 0x2(%r15),%edx. Confirmed by TWO independent baseline runs with the identical crash signature (different fault VA per boot: 0xfffff80055487116, 0xfffff80058877116 -- same relative offset past the buffer). The crash is deterministic and triggered by an unprivileged user calling getdents/stat on a mounted malicious HPFS image.