# DF-3212 — ffs_read OOB read into buffer-cache KVA via dead `xfersize` guard (crafted fs_qbmask)

## One line
An unprivileged `read(2)`/`pread(2)` at a **valid** file offset on a crafted-geometry
UFS1 mount returns **other kernel buffer-cache pages' contents** (kernel .text/.rodata
from the kernel-image file's cache pages, other files' data — e.g. `/usr/sbin/ngctl`
string tables) or `EFAULT`, because the `xfersize <= 0` panic guard in `ffs_read()` is
**dead code** for exactly the case it exists for: the unsigned `size_t` promotion at
`ufs_readwrite.c:143` clamps the negative `xfersize` *up* to `uio_resid` before the
guard, and `uiomovebp(bp, bp->b_data + blkoffset, ...)` then copies from up to
`fs_qbmask`-width bytes past the buffer — into adjacent 64 KB buffer-cache KVA slots
(`sys/kern/vfs_bio.c:640`: `b_kvabase = vm_map_min(buffer_map) + MAXBSIZE * n`).

## Where
- `sys/vfs/ufs/ufs_readwrite.c:141-155` (ffs_read xfersize math + dead guard + sink)
- geometry root cause shared with DF-0903/DF-3190 (unvalidated `fs_qbmask`,
  `sys/vfs/ufs/ffs_vfsops.c:642-646` validation gap; `ffs_oldfscompat` re-derives masks
  only for `fs_inodefmt < FS_44INODEFMT`, `ffs_vfsops.c:802-816`) — but this is a
  **different sink and a different primitive** (read-side info leak vs write-side
  balloc panic / undersized-buffer overrun).

## Reproduce (guest, root then unprivileged)
```
# setup as root (~1 min):
sh setup.sh          # builds craft_img + trigger, makes clean 8192/1024 UFS image,
                     # creates 1 MB zero file leak.bin, unmounts, patches fs_qbmask
                     # 0x1fff -> 0x7ffff, mounts crafted RW at /mnt/test
                     # (noatime,noclusterr)
# marker (optional root-only content to hunt for in the leak):
awk 'BEGIN{for(i=0;i<1000000;i++)printf "DF3212-ROOT-SECRET-%08d\n",i}' \
    > /root/.df3212_secret && chmod 600 /root/.df3212_secret
cat /root/.df3212_secret > /dev/null     # warm buffer cache

# decisive run as the unprivileged user:
su -m pwn -c /tmp/trigger2 /mnt/test/leak.bin
```

## Expected
- **Control** (clean image `base.img` mounted on /dev/vn1): every pread returns 512
  **zero** bytes (file is all zeros) — `SUMMARY nonzero=0 clean=15 efault=0`.
- **Stock kernel + crafted image**: `n=-1 errno=14 EFAULT` for unmapped adjacent
  slots AND `NON-ZERO KERNEL DATA` windows for mapped ones — kernel .rodata format
  strings (`ahci_get_err_ccb: ci not 0 (%08x)`, `...rejected because of the security
  policy`), raw kernel .text (x86-64, rip-relative), and other files' cached pages
  (ngctl help strings). Stable counts across runs
  (`nonzero=2/10 clean=3 efault=10/22` for trigger/trigger2).
- **Fixed kernel (fix.diff)**: every crafted-offset pread returns `-1 errno=5 (EIO)`,
  control still returns zeros, no leak, no panic.

## Files
- `craft_img.c` — superblock `fs_qbmask` patcher (file offset SBOFF+1336)
- `trigger.c` — 15-slot single-offset prober (k*0x10000+0x123)
- `trigger2.c` — 7-slot × 5-page scanner with hexdump + marker search
- `setup.sh` / `run.sh` — guest orchestration
- `fix.diff` — in-file guard (`blkoffset >= bp->b_bufsize` → EIO)
- logs: `run.control_and_trig1.log`, `run2.trigger2.log`, `fix_build.log`,
  `fix_baseline_reproduced.log`, `fix_patched_run.log`, `env.txt`
