# DF-0927 — PoC: unbounded dirent traversal in `hpfs_genlookupbyname`

## Goal

Prove four impacts of the unbounded dirent-chain walk in
`hpfs_genlookupbyname` (`hpfs_lookup.c:82-102`) on a crafted HPFS image:

- **Variant A (OOB read):** `de_reclen = 0xFFFF` and `DE_END` clear →
  first stride jumps ~64 KiB past the 2048-byte `bread`'d buffer → kernel
  page-fault panic reading `dep->de_flag`. (CWE-125 OOB read.)
- **Variant B (hang #1):** `de_reclen = 0` → `dep` never advances →
  infinite `while` loop (kernel thread wedged). (CWE-835.)
- **Variant C (hang #2):** two dirblks whose `DE_DOWN` pointers reference
  each other → `dive` loop never terminates. (CWE-835 + CWE-400.)
- **OOB→info-leak ceiling:** if the kernel survives the OOB read, the
  bytes are copied to userspace via the readdir name-copy path
  (`hpfs_vnops.c:745-747` `hpfs_de_uiomove`).

Plus a **deterministic userspace harness** (`harness.c`) transcribing the
`hpfs_genlookupbyname` loop byte-for-byte against `sys/vfs/hpfs/hpfs.h`
struct layouts, proving all three variants and the fix rejection.

## Files

| File                | Purpose                                                    |
|---------------------|------------------------------------------------------------|
| `craft_img.py`      | Builds a full HPFS image from scratch (no base.hpfs needed). Three variants: `--oob`, `--hang1`, `--cycle`. |
| `harness.c`         | Deterministic userspace transcription of `hpfs_genlookupbyname:82-102`. Models the buffer bound, de_reclen minimum, and depth cap; proves all three variants. |
| `build.sh`          | `cc -O2 -Wall -o harness harness.c`                        |
| `run.sh`            | `./harness`                                                |
| `df0927_oob.img`    | Variant A image                                            |
| `df0927_hang1.img`  | Variant B image                                            |
| `df0927_cycle.img`  | Variant C image                                            |
| `panic.txt`         | Kernel panic signature from `boot.log` (Variant A, baseline)|
| `run.log`           | Full reproduction log (panic + harness output)             |
| `fix_run.log`       | Patched-kernel validation (all 3 variants → EINVAL)        |
| `fix_build.log`     | Build log for patched `hpfs.ko`                            |
| `fix.diff`          | `git apply`-able fix (lookup.c + subr.c)                   |
| `VERDICT.md`        | Full narrative analysis                                    |
| `manifest.json`     | Machine-readable catalog                                   |

## Build & run (DragonFlyBSD guest)

The harness is pure userspace and reproduces the bug deterministically:

```
ssh dfbsd-maxx
cd poc/DF-0927
./build.sh && ./run.sh
```

For the in-kernel reproduction (root to mount; trigger as unprivileged
`maxx`):

```
python3 craft_img.py --oob df0927_oob.img    # on host (no python3 in guest)
scp df0927_oob.img dfbsd-maxx:poc/DF-0927/

# in guest as root:
vnconfig -c vn0 /home/maxx/poc/DF-0927/df0927_oob.img
mount -t hpfs -o ro /dev/vn0 /mnt/df0927

# in guest as unprivileged maxx:
stat /mnt/df0927/zzz     # immediate panic (Variant A)
```

## Expected output

### Variant A (panic, baseline)
```
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff80058e36015
fault code = supervisor read data, page not present
instruction pointer = 0x8:0xffffffff826032f6
current process = 910
Stopped at hpfs_validateparent+0x146: movzwl 0x2(%r15),%edx
```
(`movzwl 0x2(...)` reads `dep->de_flag` at offset 0x2; `%r15` is the
poisoned `dep` after `dep += de_reclen(0xFFFF)`. The panic fires in
`hpfs_validateparent` (sibling of `hpfs_genlookupbyname` sharing the same
unbounded `dep += de_reclen` pattern); the harness proves the bug
deterministically in `hpfs_genlookupbyname` itself.)

### Variant B/C (hard hang, baseline)
`stat` never returns; ssh stops responding within ~12s; guest wedged.
The kernel thread spins forever inside `hpfs_validateparent` /
`hpfs_genlookupbyname` holding VFS locks.

### Patched kernel (fix applied)
```
$ stat /mnt/df0927/zzz
stat: /mnt/df0927/zzz: stat: Invalid argument
$                                  # guest up; dmesg: hpfs_validateparent: corrupt dirblk
```

## Notes

- HPFS mount is root-only (`vfs.usermount=0`). But once root mounts the
  crafted image, any unprivileged user who can stat/ls/open files in the
  mounted tree triggers the bug. The PoC uses `mount -t hpfs -o ro` (no
  `uid=`/`gid=` remapping) — `maxx` (uid 1001) can `stat /mnt/df0927/zzz`
  because the mountpoint dir is world-readable. The trigger is fully
  unprivileged post-mount.
- The same unbounded `dep += dep->de_reclen` pattern exists in
  `hpfs_readdir` (`hpfs_vnops.c:823-915`) — same bug class, would need
  the same fix; out of scope for this finding's `fix.diff`.
