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