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)
PoC verification
Evidence pack
findings/poc/DF-0865 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
#0kernel:stat()does not return; guest wedges in DDB. Serialboot.logshowsFatal trap 12: page fault β¦ Stopped at hpfs_validateparent+0x146: movzwl 0x2(%r15),%edx. - Patched kernel (this
fix.diff):stat()returnsEINVAL,hpfs_validateparent: dep out of boundsappears indmesg, guest stays up.
Files
mk_hpfs.pyβ image crafter (root dir block dep0de_reclen=0x0900).poc.cβstat()trigger forgetattr β hpfs_validateparent.build.sh/run.shβ exact build/run commands.fix.diffβHPFS_DE_INBOUNDSmacro + 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.
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)
sys/vfs/hpfs/hpfs.h:133βD_BSIZE = DEV_BSIZE*4 = 2048.sys/vfs/hpfs/hpfs_subr.c:556βbread(..., D_BSIZE, &bp)reads a 2 KB directory block.sys/vfs/hpfs/hpfs_subr.c:567βdep = D_DIRENT(dp)=bp->b_data + sizeof(dirblk_t)(= 20). First dep at offset 20.sys/vfs/hpfs/hpfs.h:116-131βstruct hpfsdirent:de_reclenisu_int16_tat offset 0,de_flagat offset 2 β both attacker-controlled from disk.sys/vfs/hpfs/hpfs_subr.c:598-611β main walk:while(!(dep->de_flag & DE_END)) { ... dep = dep + dep->de_reclen; }β no bounds check. Withde_reclen=0x0900, dep jumps frombp->b_data+20tobp->b_data+2324β 276 bytes past the 2048-byte buffer.sys/vfs/hpfs/hpfs_subr.c:613β after the loop,if(dep->de_flag & DE_DOWN)readsdep->de_flagat the OOB address β page fault on unmapped page.- (The same unbounded stride exists at lines 572-577 in the
olsnrestore 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 defaultX86_64_GENERICkernel (options INVARIANTS); the OOB read faults before any INVARIANTS check could matter, so this is a default-kernel result, not anoinv-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_namelenand, on a match atreaddone:(hpfs_subr.c:632),bcopy(dep->de_name, hp->h_name, dep->de_namelen)would copy OOB heap bytes intohp->h_name, exposed to userspace as the filename viagetattr. In this PoC the OOB page was unmapped, so the observable effect is the panic. (With a smaller / page-alignedde_reclenthe 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 touid=0is 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 withde_reclen=0x0900(image structure reused from the proven DF-0829/DF-0830 layout).poc.cβ callsstat()on the mountpoint to directly driveVOP_GETATTR β hpfs_getattr β hpfs_validateparent(the DF-0865-specific caller, vs DF-0830'sgetdents/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 inhpfs_validateparent).
Fix verification
fixedVALIDATED 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)
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).
No comments yet.