# DF-2675 — allocbuf() KVA-slot overflow: 17-page buffer mapping crosses into the next buffer header's slot (cross-buffer page aliasing)

## What this is

`sys/kern/vfs_bio.c` gives every buffer header a fixed, `MAXBSIZE` (64KB) KVA
slot (`bufinit()`, `b_kvabase = vm_map_min(buffer_map) + MAXBSIZE * n`,
`b_kvasize = MAXBSIZE`).  `allocbuf()` computes

```
desiredpages = ((loffset & PAGE_MASK) + roundup2(size,DEV_BSIZE) + PAGE_MASK) >> PAGE_SHIFT
```

and `pmap_qenter_noinval(trunc_page(b_data)=b_kvabase, pages, desiredpages)`
— but the only guards are `getblk(): size > MAXBSIZE → panic` and
`allocbuf(): b_kvasize < size → panic`, **neither of which accounts for
`(loffset & PAGE_MASK)`** (and `getnewbuf()`'s `size`/`maxsize` parameters,
where `getblk()` *does* compute `maxsize = size + (loffset & PAGE_MASK)`,
are dead code — never used in the body).

So any `getblk(vp, loffset, size)` with `(loffset & PAGE_MASK) + size >
MAXBSIZE` maps **17 pages into a 16-page slot**: the PTE for page 16 lands at
`b_kvabase + 65536` = **the next buffer header's slot base**, silently
aliasing one page of two live kernel buffers.

## Reachability (no prior privileges beyond mounting an attacker-supplied image)

msdosfs accepts `BytesPerSec=512, SecPerClust=128` → 64KB clusters — the
mount check at `msdosfs_vfsops.c:427` is `SecPerClust*BlkPerSec >
MAXBSIZE/DEV_BSIZE` (128 > 128 is false → **allowed**).  With an odd FAT
size the first data cluster starts at an odd 512-byte block, so *every*
directory cluster is read via `bread(devvp, 6144 + k*65536, 65536)`
(`msdosfs_vnops.c:515-522`, `msdosfs_lookup.c`, `msdosfs_vnops.c:1368` mkdir)
→ `xoff = 2048` → **17 pages**.  Directory reads/writes are enough; the
trigger is a plain `ls`.  (With `vfs.usermount=1` this is reachable by an
unprivileged user mounting their own image; on default configs the attacker
supplies the image and any mount — removable media, restored backup, etc.)

## Consequences demonstrated on the stock INVARIANTS guest

1. **Live 17-page buffers**: `ovrdump`/`findfield` (kvm + /dev/mem PTE walk)
   show `xio_npages=17`, `b_bufsize=65536`, `b_data = b_kvabase|0x800`,
   `b_loffset=0x1800/0xd1800/...` for every directory read — on the
   unmodified `#0 Thu Jul 2 06:02:54 2026` kernel.
2. **Cross-buffer alias**: after placing victim buffers at the neighboring
   slots (cpu-pinned allocation ladder), `p16watch` shows the overflow PTE
   mapping *another file's* page — ELF headers / kernel module text
   (`.ELF....`, `deflt.mo`, `d_iic_on`) — i.e. two live buffers share one KVA
   page.
3. **Info disclosure (leak)**: `ls /mnt/D10` on the crafted image returns
   **1986 entries instead of the crafted 1984**, the extra entries
   (`????????.???`, `B???????.??s`) being raw bytes of root-owned
   `/boot/kernel/*.ko` cache pages leaked out through a FAT directory
   listing (cross-principal disclosure, same class as DF-2663).
4. **Memory corruption → kernel panic**: writing a dirent through the
   aliased page (`touch /mnt/D09/PWNED2675`) plus `sync` corrupted an
   in-cache hammer2 metadata page of the *root filesystem*, and the next
   flush panicked:
   `panic: base_insert 2 24,16,32 fail 0xfffff8005b34e000:17` in
   `hammer2_base_insert() → hammer2_flush_core()` (see panic.txt).
   A second, earlier silent wedge (umount of the tainted msdosfs mount)
   reproduced the hang class.
5. The write primitive is attacker-influenced (dirent name bytes) into a
   chosen victim's first cache page — e.g. the merged-cache page of a
   setuid-root executable would be corrupted in place (exec path uses the
   same vm_page), giving a plausible uid=0 route; not built in this run
   (see VERDICT.md).

## Files

* `mkfat64k.py` / `fat64k.img(.gz)` — FAT12 forger: 512B sectors, 128
  sec/cluster (64KB), first data cluster at block 12 (byte 6144, xoff 2048),
  D00..D19 pre-filled with 1982 entries so readdir reaches the aliased
  region (terminator at slot 1984 = byte 63488 = exactly where the alias
  starts).
* `mkfat64kB.py` / `fat64kB.img.gz` — 100-directory variant used by the
  allocation ladder (`ladder.sh`).
* `ptecheck.c`, `pml4dump.c`, `bufscan.c`, `ovrdump.c`, `findfield.c`,
  `p16watch.c` — kvm//dev/mem introspection: locate the `buf` array, derive
  `b_kvabase` (offset 1024) by its +65536 signature, dump
  `b_loffset`(176)/`b_bufsize`(996)/`b_data`(1016)/`xio_npages`(1056) and
  the *physical page mapped at the overflow KVA*.
* `pindir.c`, `pinread.c`, `sweep.c`, `ladder.sh` — cpu-pinned allocation
  (`usched_set(USCHED_SET_CPU)`) to place victim buffers exactly at
  neighbor slots.
* `run.log` — fresh-boot baseline: mount accepted, `ls` normal, then
  `OVRBUF ... npages=17` ×2.
* `panic.txt` — serial console capture of the hammer2 flush panic.
* `fix.diff` — the verified fix (git-apply-able against `sys/`).
* `verdict.json` / `manifest.json` — machine verdict.

## Build / run

All tools build in-guest with `cc -O2 -o t t.c -lkvm` (needs /dev/mem+kvm as
root).  Exact sequence: `build.sh`, `run.sh`.
