# DF-0864 — hpfs_toupper indexes hpm_cpdblk[cp] with unchecked on-disk de_cpid OOB read

**Verdict:** REPRODUCED (OOB read / heap info-leak via crafted HPFS image,
post-mount). **Fix VALIDATED** (single-fix hpfs.ko module).
**Severity:** Medium. **Impact:** kernel heap OOB read up to ~34 KB past the
allocation, reachable post-mount by any user who can stat() a name.

## What the bug is
The `hpfs_toupper` macro (`sys/vfs/hpfs/hpfs_subr.h:55`) indexes
`hpm_cpdblk[cp].b_upcase[c & 0x7F]` using a `cp` value that comes from the
on-disk directory entry's `de_cpid` field (`u_int8_t`, `sys/vfs/hpfs/hpfs.h:126`)
via `hpfs_genlookupbyname` (`hpfs_lookup.c:87`) → `hpfs_cmpfname`
(`hpfs_subr.c:180`). No code in this chain validates `cp` against `sp_cpinum`
(the number of entries allocated in `hpfs_cpinit` at `hpfs_subr.c:276`).

With `sp_cpinum=1` (136-byte allocation) and `de_cpid=0xFF`, the access reads
at offset `255*136+6+0x7F = 34813` bytes from the base — 34677 bytes past the
end of the allocation. On amd64 the kernel direct-map covers all physical
memory, so the read is silent (no page fault); the OOB byte is kernel heap
data, used in a name comparison.

## Threat model / reachability
HPFS is `optional hpfs` (shipped as `/boot/kernel/hpfs.ko`). An admin enables
it with `kldload hpfs` (registers the parser; not a privilege action). The
attacker supplies a crafted image; the mounter (root, or unprivileged after
`vfs.usermount=1` + owned memory disk) mounts it. After mount, ANY user with
search permission triggers the OOB by looking up any name (VOP_LOOKUP →
hpfs_genlookupbyname → hpfs_cmpfname → hpfs_toupper OOB).

## Reproduce
```sh
./build.sh                       # cc -O2 -Wall -o craft_img craft_img.c
./craft_img crafted.img 0xFF     # sp_cpinum=1, de_cpid=0xFF, name=0xFF*4
# as root (the victim mounting the attacker image):
kldload hpfs
DEV=$(vnconfig -c vn $(pwd)/crafted.img | grep -oE 'vn[0-9]+' | head -1)
mount -t hpfs -o ro /dev/$DEV /mnt/df0864
stat /mnt/df0864/x               # triggers VOP_LOOKUP -> hpfs_toupper OOB
```
Expected on the **unpatched** kernel: stat returns ENOENT (the OOB happened
silently; the slab memory at the OOB offset is zeroed). Guest stays up. The
kernel harness (`harness.c`, loaded via `kldload`) DEFINITIVELY shows the OOB:
in-bounds read at 0xAA vs OOB read at 0x00 from +34677 bytes past the alloc.

Expected on the **fixed** module: identical userspace behavior (ENOENT for "x"),
but the OOB does NOT occur internally — `cp` is clamped to 0 by the bounds check
(verified by disassembly: `cmp 0x94(%rdi),%eax; cmovae %ebx,%r9d`).

## The fix (`fix.diff`)
Single change in `sys/vfs/hpfs/hpfs_subr.c`: validate `cp` at the start of
`hpfs_cmpfname` and `hpfs_cpstrnnicmp`:
```c
if (cp >= hpmp->hpm_sp.sp_cpinum)
    cp = 0;
```
This clamps the code-page index to [0, sp_cpinum-1], preventing the OOB in
`hpfs_toupper`. `sp_cpinum` is the exact bound (used for both the kmalloc size
and the cpinit loop count).

Validated: built the single-fix `hpfs.ko` (sha256
a48cdce17538302dfb0aec1a36f34a90eb4e68cceb2062899d1dfd6ad5ffd804),
`kldload`'d it, re-ran the same PoC — mount + lookup works correctly, no panic,
bounds check confirmed by disassembly. See `VERDICT.md`, `fix_run.log`,
`fix_build.log`, `dmesg.txt`.
