# DF-3248 — OOB heap WRITE through hpfs_genlookupbyname's unvalidated dirent walk (hpfs_updateparent sink)

## What

`hpfs_genlookupbyname` (sys/vfs/hpfs/hpfs_lookup.c:82-97) walks an on-disk
dirent chain by attacker-controlled `de_reclen` (u16) with **no bound against
the 2048-byte D_BSIZE bread buffer** and no self-consistency check. The read
side of this walk is DF-0927. **DF-3248 is the write side**: when the same
walk is re-run by `hpfs_updateparent` (sys/vfs/hpfs/hpfs_subr.c:699-709) at
file-deactivation time, a name match at an out-of-bounds `dep` makes the
kernel execute

```c
dep->de_mtime = hp->h_mtime;      /* u_long, 8 bytes  — set by utimes()      */
dep->de_size  = hp->h_fn.fn_size; /* u32,   4 bytes  — crafted-image value   */
dep->de_atime = hp->h_atime;      /* u_long, 8 bytes  — set by utimes()      */
bdwrite(bp);                      /* flushes only the in-bounds 2048 bytes   */
```

i.e. **20 attacker-chosen bytes at an attacker-chosen offset (u16 reclen,
≤64 KB) past the end of a kernel buffer-cache chunk**, silently (the bdwrite
persists only the in-bounds part of the origin chunk).

## Reproduced on guest (decisive console output, /root/df3248_verdict_console.txt)

```
DF3248: origin lsn 0x02a0 @ 0xfffff8007eec6000, plant @ 0xfffff8007eec6800 (same page, +2048 past chunk end)
DF3248F: h_no=0x58 h_flag=0xb (H_PARCHANGE=1 H_PARVALID=1)
DF3248F: pending attacker values: h_mtime=0x4242424242424242 h_atime=0x4141414141414141 fn_size=0xcafebabe
DF3248F: plant BEFORE: mtime=0xc1c1c1c1c1c1c1c1 size=0xc2c2c2c2 atime=0xc3c3c3c3c3c3c3c3
DF3248F: hpfs_updateparent() -> 0
DF3248F: plant AFTER:  mtime=0x4242424242424242 size=0xcafebabe atime=0x4141414141414141
DF3248F: *** OOB WRITE CONFIRMED: 20 attacker-chosen bytes written 2048 bytes past the 2048-byte dirblk chunk ***
```

## Build / run

Prereqs: single-tenant QEMU guest up (`dfbsd-qemu/vm.sh up`), stock
INVARIANTS kernel, `maxx` uid 1001 (ssh alias `dfbsd-maxx`), root alias
`dfbsd`, gcc + `/usr/src` in guest.

```
sh run_full.sh          # upload, build modules, mount, precache,
                        # plant+pin (kld df3248patch), UNPRIVILEGED trigger
                        # (uid 1001: open+futimes+close), forced-deactivation
                        # verify (kld df3248fin2) — verdict on console
```

Expected: `TRIG_RC=0` from the uid-1001 trigger (the open matched the dirent
**past** the chunk), then the `DF3248F: *** OOB WRITE CONFIRMED ***` block.

### Harness roles (read VERDICT.md "what is harness vs attack" first)

* `craft_img.py` — builds the crafted HPFS image (root dirblk with 24
  "D*" dirents, an XSTEP dirent + END marker that DE_DOWN-dive into a second
  dirblk "root2", whose "XSTEP2" dirent carries the OOB hop `de_reclen`;
  24 spray dirblks each holding forged "ZFILE" dirents at every 128-byte
  phase so that a walk landing in any of them terminates deterministically).
* `poc.c` — the unprivileged phases: `precache` (caches root/root2/spray
  chunks via graceful "QZ" walks), `trigger` (open+`futimes`+close as the
  mount owner — sets the 8+8 byte marker values and H_PARCHANGE),
  `observe` (stat-based readback variant).
* `autopatch.c` — KLD: pins the dirblk buffers (B_LOCKED, against buffer
  recycling — the #1 practical obstacle), plants a forged dirent at
  `origin_chunk+2048` (the same 4 KB page, i.e. guaranteed-mapped memory one
  object past the chunk, emulating attacker heap grooming of the neighbor),
  and patches the cached walk (dive lsn + hop reclen) to hit it.
* `finalizer.c` — KLD: locates the planted dirent and the ZFILE hpfsnode,
  prints the pending unprivileged values, invokes `hpfs_updateparent(hp)` —
  the exact call `hpfs_inactive` (hpfs_vnops.c:607) makes at deactivation —
  and reads the planted bytes back, all in one MOD_LOAD (no readback race).
* `mount_rw.c` — direct `mount(2)` RW helper (stock `mount_hpfs` forces
  MNT_RDONLY), from the DF-3137 pack.
* `memscan.c`, `bufdump.c`, `hscan.c` — measurement tools used during
  development (physical-memory and buffer-queue layout discovery).
  `bufdump.c`'s queue walk is what produced the measured chunk addresses.

### Files

* `run.log` — decisive orchestrated run (build → plant → uid-1001 trigger →
  verdict).
* `run.smoke.log`, `run.k1.log`, `run.k2.log` — development runs: smoke
  (in-bounds control; exposed the namei-cache observer pitfall), first
  layout attempts (kernel livelock wedges — the OOB walk landing on garbage
  `reclen==0` chains; documented as the DoS variant).
* `fix.diff` — the fix; validated by in-guest kernel rebuild
  (`fix_build.log`, `fix_run.log`).
