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)
PoC verification
Evidence pack
findings/poc/DF-0830 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
sys/vfs/hpfs/hpfs.h:133βD_BSIZE = DEV_BSIZE*4 = 2048.sys/vfs/hpfs/hpfs_vnops.c:825βbread(hp->h_devvp, dbtodoff(lsn), D_BSIZE, &bp)reads a 2 KB directory block.sys/vfs/hpfs/hpfs_vnops.c:839βdep = D_DIRENT(dp)=bp->b_data + sizeof(dirblk_t)(= 20 bytes). 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 are attacker-controlled bytes from the disk image.sys/vfs/hpfs/hpfs_vnops.c:906βdep = (hpfsdirent_t *)((caddr_t)dep + dep->de_reclen)β no bounds check. Withde_reclen=0x0900, dep jumps frombp->b_data+20tobp->b_data+2324, which is 276 bytes past the 2048-byte buffer.sys/vfs/hpfs/hpfs_vnops.c:909(and equivalentlyhpfs_subr.c:613) β after the while loop exits,if(dep->de_flag & DE_DOWN)readsdep->de_flagat the OOB address β unconditional, no bounds check.- 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/statsyscall 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_nameand emit it to userspace viahpfs_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=0is 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 withde_reclen=0x0900. Reuses the fnode/superblock/spareblock/bitmap structure proven in DF-0829.poc.cβ opens the mountpointO_RDONLY|O_DIRECTORYand callsgetdents, dumping returned entries. If the kernel panics (guest goes down), the proof is inboot.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
fixedVALIDATED 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)
Confirmed kernel references
- sys/vfs/hpfs/hpfs.h:133
- sys/vfs/hpfs/hpfs.h:116
- sys/vfs/hpfs/hpfs_vnops.c:825
- sys/vfs/hpfs/hpfs_vnops.c:839
- sys/vfs/hpfs/hpfs_vnops.c:882
- sys/vfs/hpfs/hpfs_vnops.c:906
- sys/vfs/hpfs/hpfs_vnops.c:909
- sys/vfs/hpfs/hpfs_subr.c:556
- sys/vfs/hpfs/hpfs_subr.c:567
- sys/vfs/hpfs/hpfs_subr.c:576
- sys/vfs/hpfs/hpfs_subr.c:598
- sys/vfs/hpfs/hpfs_subr.c:610
- sys/vfs/hpfs/hpfs_subr.c:613
- sys/vfs/hpfs/hpfs_lookup.c:82
- sys/vfs/hpfs/hpfs_lookup.c:96
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
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.
No comments yet.