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

hpfs_validateparent walks directory entries by attacker-controlled de_reclen with no buffer-bound check OOB read

Summary

hpfs_subr.c:576/588/610 dep=(caddr_t)dep+dep->de_reclen no check dep stays in 2048B dirblk. de_reclen u16 on-disk. Crafted large de_reclen walks past buffer -> OOB read dep fields. de_reclen=0 infinite loop. readdone:632 bcopy(dep->de_name,hp->h_name,dep->de_namelen) can leak OOB heap as filename. Same class as DF-0830 but distinct code path (parent-validation).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0865 Β· 16 files
FileTypeDescriptionSize
mk_hpfs.py trigger-source crafts minimal-but-valid HPFS image; root dir block dep0 de_reclen=0x0900 6.4 KB view raw
poc.c trigger-source stat() the mountpoint -> VOP_GETATTR -> hpfs_getattr -> hpfs_validateparent 2.0 KB view raw
build.sh build-script cc -O2 -o poc poc.c + image craft 239 B view raw
run.sh run-script ./poc /mnt/hpfs 266 B view raw
evil.hpfs crafted-image the malicious HPFS image (102400 B) 100.0 KB ↓ download
build.log build-log poc binary build, full output 61 B view raw
run.log run-log baseline run: ssh hung (guest crashed) + full boot.log panic block 1.5 KB view raw
fix_build.log build-log single-fix hpfs.ko module build, full output (rc=0) 10.3 KB view raw
fix_run.log run-log patched-kernel PoC run: stat returns EINVAL, no panic 313 B view raw
panic.txt panic-signature fatal trap 12 in hpfs_validateparent+0x146 (movzwl 0x2(%r15),%edx) 1.5 KB view raw
env.txt environment uname, kern.version, cc version, hpfs.ko sha, INVARIANTS=1 503 B view raw
fix.diff suggested-fix HPFS_DE_INBOUNDS macro + bounds on both validateparent while-loops + 2 post-loop guards 2.1 KB view raw
VERDICT.md verdict full narrative: mechanism, reachability, impact, fix validation 7.5 KB ↓ raw
README.md readme human-facing reproduce + expected-behavior guide 2.2 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-facing reproduce + expected-behavior guide
↓ download raw

DF-0865 β€” hpfs_validateparent dep-walk OOB read

Status: REPRODUCED (kernel panic / local DoS) β†’ FIXED (validated single-module patch).

hpfs_validateparent() (sys/vfs/hpfs/hpfs_subr.c) walks directory entries inside a 2 KB bread buffer advancing a dep pointer by the on-disk de_reclen (u_int16_t, attacker-controlled) with no bound check, so a crafted HPFS image makes dep walk past the buffer β†’ page-fault panic on the next dep->de_flag read. Triggered by an unprivileged stat() on a mounted malicious HPFS image. Same root cause as DF-0830 (duplicate on the hpfs_validateparent path). Read-only primitive β†’ Medium (local DoS / heap info-leak ceiling), no escalation.

Reproduce

# 1. craft the image (host, needs python3)
python3 mk_hpfs.py evil.hpfs

# 2. on the guest, build the trigger (unprivileged user)
cc -O2 -o poc poc.c

# 3. root mounts the crafted image (realistic "admin mounts attacker image")
kldload hpfs                      # if not already loaded
vnconfig -c vn0 /path/to/evil.hpfs
mkdir -p /mnt/hpfs
mount -t hpfs -o ro /dev/vn0 /mnt/hpfs
chmod 755 /mnt/hpfs

# 4. unprivileged user triggers the bug
./poc /mnt/hpfs        # on unpatched kernel: guest panics (check boot.log)
                        # on patched kernel: stat returns EINVAL, no panic

Expected

  • Unpatched #0 kernel: stat() does not return; guest wedges in DDB. Serial boot.log shows Fatal trap 12: page fault … Stopped at hpfs_validateparent+0x146: movzwl 0x2(%r15),%edx.
  • Patched kernel (this fix.diff): stat() returns EINVAL, hpfs_validateparent: dep out of bounds appears in dmesg, guest stays up.

Files

  • mk_hpfs.py β€” image crafter (root dir block dep0 de_reclen=0x0900).
  • poc.c β€” stat() trigger for getattr β†’ hpfs_validateparent.
  • build.sh / run.sh β€” exact build/run commands.
  • fix.diff β€” HPFS_DE_INBOUNDS macro + bounds on both validateparent while-loops + post-loop guards (git apply-able).
  • VERDICT.md β€” full narrative + mechanism + fix validation.
  • manifest.json β€” machine-readable artifact catalog.
  • Logs: build.log, run.log (baseline panic), fix_build.log, fix_run.log (patched clean return), panic.txt, env.txt.
VERDICT.md verdict full narrative: mechanism, reachability, impact, fix validation
↓ download raw

DF-0865 β€” hpfs_validateparent dep-walk OOB read past 2 KB bread buffer

Verdict: REPRODUCED β€” kernel panic (OOB read page fault) via crafted HPFS image; FIXED by the validated single-module patch.

The bug

hpfs_validateparent() in sys/vfs/hpfs/hpfs_subr.c reads a 2 KB directory block with bread(dhp->h_devvp, dbtodoff(lsn), D_BSIZE=2048, &bp) (line 556), then walks the hpfsdirent chain inside it advancing the pointer by dep->de_reclen:

  • hpfs_subr.c:576 β€” dep = (hpfsdirent_t *)((caddr_t)dep + dep->de_reclen); (restore/olsn path)
  • hpfs_subr.c:610 β€” dep = (hpfsdirent_t *)((caddr_t)dep + dep->de_reclen); (main walk)

de_reclen is a u_int16_t loaded directly from disk (sys/vfs/hpfs/hpfs.h:117) with no validation that dep stays within [bp->b_data, bp->b_data + D_BSIZE). After the loop, the post-loop read if(dep->de_flag & DE_DOWN) at line 613 also runs without a bounds check. A crafted directory block with a large de_reclen walks dep past the buffer; the next read of dep->de_flag faults.

Reachability (realistic, unprivileged)

hpfs_validateparent is called from hpfs_getattr (sys/vfs/hpfs/hpfs_vnops.c:467) when H_PARVALID is not yet set β€” i.e. on the first stat() of a vnode. The HPFS module is shipped loadable (/boot/kernel/hpfs.ko). The realistic threat model (acceptable precondition per the audit's realism test): an admin has mounted, or made mountable via vfs.usermount=1, an attacker-controlled HPFS image. Once mounted, any unprivileged user who can stat() a path on the mount triggers the bug β€” a standard syscall, no privilege check, no special device access. The malicious bytes live entirely in the filesystem image (directory-block content).

Mechanism (every hop cited)

  1. sys/vfs/hpfs/hpfs.h:133 β€” D_BSIZE = DEV_BSIZE*4 = 2048.
  2. sys/vfs/hpfs/hpfs_subr.c:556 β€” bread(..., D_BSIZE, &bp) reads a 2 KB directory block.
  3. sys/vfs/hpfs/hpfs_subr.c:567 β€” dep = D_DIRENT(dp) = bp->b_data + sizeof(dirblk_t) (= 20). 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 attacker-controlled from disk.
  5. sys/vfs/hpfs/hpfs_subr.c:598-611 β€” main walk: while(!(dep->de_flag & DE_END)) { ... dep = dep + dep->de_reclen; } β€” no bounds check. With de_reclen=0x0900, dep jumps from bp->b_data+20 to bp->b_data+2324 β€” 276 bytes past the 2048-byte buffer.
  6. sys/vfs/hpfs/hpfs_subr.c:613 β€” after the loop, if(dep->de_flag & DE_DOWN) reads dep->de_flag at the OOB address β†’ page fault on unmapped page.
  7. (The same unbounded stride exists at lines 572-577 in the olsn restore path and at line 588.)

Demonstration (baseline, unpatched #0 kernel)

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_DOWN, de_fnode=0 so it never matches root's h_no). Mounting it and calling stat("/mnt/hpfs") (via poc.c) drives getattr β†’ hpfs_validateparent over that block:

baseline (unpatched #0, original hpfs.ko):
  Fatal trap 12: page fault while in kernel mode
  fault virtual address    = 0xfffff80057f97116
  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. Guest is wedged in DDB (db>). This is DF-0865's exact cited path.

Impact

  • Kernel panic (local DoS). Deterministic, unprivileged, via a standard stat() syscall on a mounted malicious HPFS image. INVARIANTS is ON on the default X86_64_GENERIC kernel (options INVARIANTS); the OOB read faults before any INVARIANTS check could matter, so this is a default-kernel result, not a noinv-only one.
  • Potential heap info-leak. If the OOB memory past the buffer were mapped (adjacent kernel heap), the walk would read dep->de_name / dep->de_namelen and, on a match at readdone: (hpfs_subr.c:632), bcopy(dep->de_name, hp->h_name, dep->de_namelen) would copy OOB heap bytes into hp->h_name, exposed to userspace as the filename via getattr. In this PoC the OOB page was unmapped, so the observable effect is the panic. (With a smaller / page-aligned de_reclen the leak is the realistic ceiling.)
  • No write primitive. The dep-walk is read-only (reads dep fields, advances the pointer, and on success copies dep fields into hp). No corruption, no escalation chain to uid=0 is derivable. Correctly classified as Medium. (exploit_chain = none.)

Overlap with DF-0830

DF-0830 ("hpfs_readdir dep-walk unbounded") is the same root-cause defect across the three HPFS dep-walk sites: hpfs_readdir (vnops), hpfs_validateparent (subr), and hpfs_genlookupbyname (lookup). DF-0830's PoC crash actually landed in hpfs_validateparent+0x146 (the same site this finding cites), and DF-0830's proposed fix already patches all three sites including the two hpfs_subr.c loops cited here. DF-0865 is therefore a duplicate of DF-0830 on the hpfs_validateparent path. The fix.diff in this folder is the hpfs_validateparent-focused subset (the HPFS_DE_INBOUNDS macro + the two subr.c while-loop bounds + two post-loop guards) and is byte-identical to the hpfs.h + hpfs_subr.c hunks of DF-0830's fix.

Fix

fix.diff adds an HPFS_DE_INBOUNDS(bp, dep) macro to sys/vfs/hpfs/hpfs.h and applies it to both hpfs_validateparent dep-walk while-loops (hpfs_subr.c:572, :598) plus a post-loop guard before each dep->de_flag & DE_DOWN read (:579, :613). 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), and a de_reclen >= sizeof(hpfsdirent_t) term prevents the de_reclen=0 infinite loop. On an OOB dep, the function prints hpfs_validateparent: dep out of bounds and returns EINVAL instead of faulting.

Fix validation (Phase 8)

Kernel kern.version hpfs.ko PoC result Verdict
baseline (unpatched #0) 6.5-DEVELOPMENT #0 original shipped panic hpfs_validateparent+0x146 (movzwl 0x2(%r15)) BUG
patched (#0 + rebuilt hpfs.ko) 6.5-DEVELOPMENT #0 rebuilt with fix.diff, sha256 53fa1c66… clean EINVAL return (3/3 runs, no panic, guest up; hpfs_validateparent: dep out of bounds in dmesg) FIXED

The bug is entirely in the loadable hpfs.ko module (both touched files hpfs.h and hpfs_subr.c compile into the module), so validation rebuilt only the module (cd /usr/src/sys/vfs/hpfs && make), installed it at /boot/kernel/hpfs.ko, and re-ran the identical PoC. The baseline panic is deterministic; the patched module survives 3/3 runs.

PoC changes

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 (image structure reused from the proven DF-0829/DF-0830 layout).
  • poc.c β€” calls stat() on the mountpoint to directly drive VOP_GETATTR β†’ hpfs_getattr β†’ hpfs_validateparent (the DF-0865-specific caller, vs DF-0830's getdents/readdir trigger).
  • build.sh / run.sh β€” exact repro commands.
  • fix.diff β€” the verified, git apply-able fix (macro + 2 while-loop bounds + 2 post-loop guards in hpfs_validateparent).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: on the unpatched #0 baseline (original hpfs.ko) the PoC deterministically panics in hpfs_validateparent+0x146 (movzwl 0x2(%r15),%edx, fault va 0xfffff80057f97116, guest wedged in DDB); on the same #0 kernel with the single-fix hpfs.ko rebuilt from fix.diff, the identical stat() returns EINVAL with 'hpfs_validateparent: dep out of bounds' in dmesg and the guest stays up -- reproduced 3/3 times. The bounds check trips cleanly before any OOB dep field is read, closing the OOB read / panic / leak. Fix is the hpfs_validateparent-focused subset of DF-0830's validated fix (same root cause).

baseline (unpatched #0, original hpfs.ko):
  Fatal trap 12: page fault while in kernel mode
  fault virtual address = 0xfffff80057f97116
  Stopped at hpfs_validateparent+0x146: movzwl 0x2(%r15),%edx
  db>   (vm.sh status = down)

patched (#0 + rebuilt hpfs.ko, sha256 53fa1c66...):
  $ ./poc /mnt/hpfs
  [!] stat returned error (no panic): Invalid argument
  dmesg: hpfs_validateparent: dep out of bounds   (x3 over 3 runs)
  vm.sh status = up   (3/3 runs, no panic)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (same #0 kernel; only /boot/kernel/hpfs.ko rebuilt with fix.diff, sha256 53fa1c66e494b2880580787ee7051f69a5fc9644f807897a4f5f7a3abf43b0da)

Confirmed kernel references

Detail

Exploit chain

none -- pure OOB-read / DoS class bug, no write primitive. hpfs_validateparent's dep-walk only reads dep fields and advances the pointer (no write to attacker-chosen kernel memory); on a name match at readdone: it copies dep->de_name/de_*time into the vnode's hp (kernel-internal), not into attacker-controlled memory. The realistic impact ceiling is a heap info-leak (if the OOB page past the buffer were mapped, dep->de_name/de_namelen would be OOB heap bytes copied into hp->h_name and exposed via getattr) plus the deterministic local DoS (panic). No uid=0 chain derivable; correctly classified Medium.

Evidence (decisive lines)

baseline (unpatched #0):
Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0xfffff80057f97116
fault code               = supervisor read data, page not present
instruction pointer      = 0x8:0xffffffff826032f6
Stopped at      hpfs_validateparent+0x146:      movzwl  0x2(%r15),%edx
db>
(guest wedged; vm.sh status=down)

patched (rebuilt hpfs.ko, sha256 53fa1c66...):
$ ./poc /mnt/hpfs
[!] stat returned error (no panic): Invalid argument
(dmesg: 'hpfs_validateparent: dep out of bounds' x3; vm.sh status=up, 3/3 runs)

PoC changes

Folder was empty when this run started; entire evidence pack authored from scratch. mk_hpfs.py crafts a minimal-but-valid HPFS image (root dir block dep0 de_reclen=0x0900, de_flag=0, de_fnode=0) reusing the proven DF-0829/DF-0830 superblock/spareblock/fnode/bitmap layout. poc.c calls stat() on the mountpoint to directly drive VOP_GETATTR->hpfs_getattr(hpfs_vnops.c:467)->hpfs_validateparent (the DF-0865-specific caller, vs DF-0830's getdents trigger). build.sh/run.sh wrap the exact commands. fix.diff is the validateparent-focused subset of DF-0830's fix (byte-identical hpfs.h + hpfs_subr.c hunks): adds HPFS_DE_INBOUNDS macro to hpfs.h and bounds both validateparent while-loops + 2 post-loop guards. git apply --check passes.

Verified recommended fix

Add an HPFS_DE_INBOUNDS(bp, dep) macro to sys/vfs/hpfs/hpfs.h (checks dep+sizeof(hpfsdirent_t) fits within bp->b_data+D_BSIZE before any dep field is read) and use it as the leading short-circuit term of both hpfs_validateparent while-loops (hpfs_subr.c:572 and :598) together with a de_reclen>=sizeof(hpfsdirent_t) term to also kill the de_reclen=0 infinite loop; add a post-loop 'if(!HPFS_DE_INBOUNDS(bp,dep)){kprintf(...);error=EINVAL;goto failed;}' guard before each 'dep->de_flag & DE_DOWN' read (hpfs_subr.c:579 and :613). Matches the finding proposal's intent and is byte-identical to the hpfs.h+hpfs_subr.c hunks of DF-0830's validated fix (same root cause). The full git-apply-able diff lives in findings/poc/DF-0865/fix.diff.

Verdict

REPRODUCED -- kernel panic (OOB read page fault) in hpfs_validateparent via a crafted HPFS image, triggered by an unprivileged stat(); FIXED by the validated single-module patch. hpfs_validateparent (sys/vfs/hpfs/hpfs_subr.c:598-611) advances its dep pointer by the on-disk u16 de_reclen with no bound check against the 2 KB (D_BSIZE=2048) bread buffer, so dep0 de_reclen=0x0900 walks dep 276 bytes past the buffer; the post-loop/next-iteration read of dep->de_flag (movzwl 0x2(%r15),%edx) faults on the unmapped OOB page. Confirmed by the boot.log panic 'Stopped at hpfs_validateparent+0x146' on the unpatched #0 kernel. Read-only primitive (dep-walk reads + copies dep fields into hp on match) -> Medium: local DoS, heap info-leak ceiling; no write, no escalation. INVARIANTS is ON on the default X86_64_GENERIC (#0) kernel, so this is a default-kernel result. Same root cause as DF-0830 (duplicate on the hpfs_validateparent path; DF-0830's PoC also crashed in hpfs_validateparent+0x146 and its fix already patches this site).