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