# DF-3157 — forged msdosfs file handle → OOB read/write in deget/readep/deupdat (NFS-reachable)

## What this is

`msdosfs_fhtovp()` (sys/vfs/msdosfs/msdosfs_vfsops.c:848-863) passes the raw
`struct defid` fields (`defid_dirclust`, `defid_dirofs`) of an NFS file handle
— fully attacker-controlled opaque bytes — straight into `deget()`
(sys/vfs/msdosfs/msdosfs_denode.c:236) with **no validation**. `deget()` →
`readep()` (msdosfs_lookup.c:880) computes the direntry location as
`bp->b_data + (dirofs & pm_crbomask)` (`bptoep`), while the block number
(`detobn`) **ignores `dirofs` for non-root clusters**. Results:

1. **31-byte kernel heap OOB read** whenever `dirofs & (bpcluster-1)` ∈
   [bpcluster-31, bpcluster-1] (e.g. 511 for 512-byte clusters):
   `DE_INTERNALIZE` (denode.c:377 / denode.h:191-203) copies 32 bytes out of
   the buffer into the denode; the fields are handed to userspace by
   `stat()` (`va_size = de_FileSize`, msdosfs_vnops.c:250) — a repeatable
   kernel-memory info leak.
2. **Arbitrary in-volume 32-byte read via forged cluster numbers**:
   `detobn(dirclust)` maps any `dirclust` to a device block in the data area;
   a forged handle turns `stat()` into a 32-byte reader at any device offset
   ≥ the data-area start (proven with the planted marker "ABCD" read back as
   `st_size = 0x44434241`).
3. **OOB write** through `deupdat()` (msdosfs_denode.c:453-463): any SETATTR
   on a forged handle (utimes/chmod/truncate) externalizes the denode into
   the same unbounded `bptoep` slot (`*dirp = dir`, 32 bytes) — up to 31
   bytes past kernel heap buffers, or a 32-byte structured write at any
   device offset (≥ 9 attacker-chosen bytes: mtime/mdate/size/attributes).
4. **Deterministic kernel panic** from an unprivileged RPC: a forged
   `(dirclust=0, dirofs=32)` GETATTR killed the stock INVARIANTS kernel in
   `deget+0x2b5` (inlined `DE_INTERNALIZE`, page fault on an unmapped page)
   — reproduced 3/3 times in the nfsd server thread. A forged SETATTR
   `(0,511)` wedged the nfsd server (all subsequent RPCs time out).

Threat: any host permitted by an NFS export of an msdosfs filesystem
(FAT/FAT32 USB sticks, shares) with any AUTH_SYS credential — no crafted
on-disk image needed. Locally, the identical `deget` path is reachable
through `fhstat(2)`/`fhopen(2)` (root-gated in DF, unprivileged via NFS).

## Files

- `mkfat.py` — deterministic FAT16 test image (512 B clusters; SUB at
  cluster 2, VICTIM.DAT at cluster 3 with the size-marker "ABCD" at entry
  offset +28).
- `df3157.c` — unprivileged raw ONC-RPC client (mountd GETFH bypassed; fh
  forged from the mount fsid). Modes: `mountdump`, `null`, `getattr
  <clust> <dirofs> [n]`, `setattr <clust> <dirofs> <unix_sec>`.
- `fhtest.c` — local `fhstat`/`fhopen` harness driving the same
  `VFS_FHTOVP → deget` path (diagnostic; root-gated).
- `setup.sh` — guest setup: vnconfig, mount, exports, rpcbind/mountd/nfsd.
- `hammer.c` — DF-3158 hammer (see that pack).
- `panic_capture.txt` — serial-console capture of baseline panic #3.
- `fix.diff` — validated two-part fix (fh bounds validation + deget refcnt
  ordering; the latter belongs to DF-3158).

## Reproduce (baseline, stock kernel)

```
# host: build image + push
python3 mkfat.py && gzip -9 fat16.img
scp fat16.img.gz df3157.c fhtest.c setup.sh dfbsd-maxx:/tmp/
# guest root:
cd /tmp && gunzip -f fat16.img.gz && sh setup.sh
cc -O2 -o /tmp/df3157 df3157.c && cc -O2 -o /tmp/fhtest fhtest.c

# guest UNPRIVILEGED (maxx), baseline kernel:
DF3157_SRCPORT=800 DF3157_FSID0=0x1e110807 DF3157_FSID1=3 \
    /tmp/df3157 getattr 3 0      # -> GETATTR size=1145258561 (0x44434241 "ABCD") : arbitrary in-volume read
DF3157_SRCPORT=800 ... /tmp/df3157 getattr 3 511    # -> size/mtime = kernel heap garbage : 31B OOB read leak
DF3157_SRCPORT=800 ... /tmp/df3157 getattr 0 32     # -> kernel panic (deget+0x2b5, DE_INTERNALIZE), guest down
# write: guest root, local harness (same deget path):
/tmp/fhtest 3 0 write ; sync      # 32-byte externalized dirent lands in VICTIM.DAT data cluster
hexdump -C /dev/vn0 | ...         # bytes 11,22..25 of victim cluster replaced
```

Note on the fsid seed: DF masks `f_fsid` for non-root callers, so the local
harness seeds it via env (obtained once as root). A network client does not
need this — mountd hands the full fh of the export root to any permitted
client (that is how the attack obtains fsid+format in the real threat
model). All forged-handle RPCs themselves run unprivileged.
