# DF-2675 VERDICT — REPRODUCED (memcorrupt: cross-buffer KVA aliasing → leak + kernel panic)

## Bottom line

**The finding is real, reproduced, and consequential on the stock INVARIANTS
kernel** (`DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 2026
X86_64_GENERIC`, guest `dfbsd`, 6 vcpu).  A crafted msdosfs image whose
64KB clusters begin at a non-page-aligned device offset makes every
directory operation allocate a **17-page buffer in a 16-page (MAXBSIZE)
per-header KVA slot**; the 17th PTE lands on the *next buffer header's
slot*, silently aliasing one KVA page between two live kernel buffers.
Demonstrated end-to-end: (a) live kernel state shows `xio_npages=17`; (b)
after placing a victim buffer at the neighboring slot, the overflow page
maps the victim's content (kernel-module ELF text); (c) `readdir` on the
FAT directory **leaks the victim's bytes into userspace** (1986 entries vs
1984 baseline, garbage names = `/boot/kernel/*.ko` page bytes); (d) writing
a dirent through the aliased page corrupted an in-cache hammer2 metadata
page of the root filesystem and the next flush **panicked the kernel**
(`panic: base_insert 2 24,16,32 fail 0xfffff8005b34e000:17` in
`hammer2_base_insert → hammer2_flush_core`).  A fix was authored, built
in-guest, and validated: the crafted mount is rejected with `EINVAL`, no
17-page buffers exist on the fixed kernel, and normal msdosfs mounts are
unaffected.

## Root cause (line-accurate)

* `sys/kern/vfs_bio.c:638-640` (bufinit): each header gets
  `b_kvabase = vm_map_min(buffer_map) + MAXBSIZE*n`, `b_kvasize = MAXBSIZE`
  (65536) — slots are exactly MAXBSIZE and contiguous.
* `sys/kern/vfs_bio.c:2736-2737` (getblk): only `size > MAXBSIZE` panics —
  **`(loffset & PAGE_MASK)` is not accounted**.
* `sys/kern/vfs_bio.c:2939-2940` (getblk): `maxsize = size +
  (loffset & PAGE_MASK)` is computed — and passed to `getnewbuf()` whose
  `size`/`maxsize` parameters are **dead code** (never referenced in the
  function body, vfs_bio.c:1885-2199).  The intended guard never existed.
* `sys/kern/vfs_bio.c:3039-3040` (allocbuf): `b_kvasize < size → panic`
  — again ignores the page offset.
* `sys/kern/vfs_bio.c:3044-3047`: `desiredpages = ((loffset & PAGE_MASK) +
  roundup2(size,DEV_BSIZE) + PAGE_MASK) >> PAGE_SHIFT` — with xoff=2048,
  size=65536 → **17**, and `KKASSERT(desiredpages <= XIO_INTERNAL_PAGES)`
  (= btoc(128KB) = 32) does *not* catch it, even on INVARIANTS.
* `sys/kern/vfs_bio.c:3211-3216` (allocbuf step 3): `b_data =
  trunc_page(b_data)` = `b_kvabase`, then
  `pmap_qenter_noinval(b_data, pages, desiredpages=17)` — **page 16's PTE
  is written at `b_kvabase+65536`, the next header's slot base.**
* Symmetric damage on teardown: `pmap_qremove_noinval(kvabase, npages=17)`
  in `vfs_vmio_release` (vfs_bio.c:1854-1855) and the allocbuf shrink path
  (vfs_bio.c:3077-3080) **remove the neighbor's page-0 PTE** — a live
  neighbor then faults on its own `b_data` (wedge/panic class observed
  twice: umount of the tainted mount; touch+sync).

Trigger surface (attacker-supplied filesystem image):

* `sys/vfs/msdosfs/msdosfs_vfsops.c:423-430`: cluster-size check is
  `SecPerClust * pm_BlkPerSec > MAXBSIZE/DEV_BSIZE` — 128*1 = 128 is
  **not > 128**, so 512B-sector/128-sec-per-cluster (64KB) FATs are
  accepted; `pm_firstcluster = ResSectors + NFATs*FATsecs + rootdirsize`
  (msdosfs_vfsops.c:446-457) is sector-granular — an odd FAT size puts
  every data cluster at `≡ 2048 (mod 4096)`.
* `sys/vfs/msdosfs/msdosfs_vnops.c:513-522` (msdosfs_read, isadir):
  `pcbmap(dep, cn, &lbn, NULL, &blsize=65536)` then
  `bread(pm_devvp, de_bn2doff(pmp,lbn), 65536)` — the unaligned 64KB getblk.
  Same pattern in `msdosfs_lookup.c` (all the `bread(pmp->pm_devvp,
  de_bn2doff(...), blsize)` calls) and `msdosfs_vnops.c:1368` (mkdir:
  `getblk(devvp, de_bn2doff(bn), pm_bpcluster)` + 64KB memset).
* A plain `ls` of any subdirectory is sufficient.  With
  `vfs.usermount=1`, an unprivileged user can mount their own image; on
  default configs the attacker supplies the image (removable media,
  downloads, restored backups) and any mount of it triggers.

## What was run (evidence chain)

1. **Fresh-boot baseline (run.log)**: mount accepted; `ls /mnt/D00`
   returns the crafted 1984 entries; `ovrdump` (kvm `/dev/kmem` +
   `/dev/mem` page-table walk) reports `OVRBUF n=3541 kva=…5e226000
   npages=17 kvasize=65536 loff=0x1800` and `n=5387 … loff=0xd1800`.
   `findfield` (array snapshot diff around one `ls`) decoded the new
   buffer's fields: `b_loffset(176)=0xd1800`, `b_bufsize(996)=0x10000`,
   `b_bcount(1004)=0x10000`, `b_data(1016)=kvabase|0x800`,
   `xio_npages(1056)=0x11`.
2. **Neighbor placement (ladder.sh)**: buffer headers are per-cpu
   round-robin (header n ⇒ cpu n%6) while the overflow always targets
   header n+1 (next cpu's queue, same position).  Using
   `usched_set(USCHED_SET_CPU)`-pinned allocations (`pindir`/`pinread`/
   `sweep`), D-buffers were placed on cpu5 and fresh `.ko` reads swept on
   cpu0 until the neighbors were consumed.  `p16watch` then showed the
   overflow pages mapping **foreign content**: `7f454c46` (ELF), module
   strings (`deflt.mo`, `d_iic_on`, `r4.ext_i`) — two live buffers
   sharing one KVA page, 25 of them simultaneously.
3. **Leak**: `ls /mnt/D07` = **1986** entries, `/mnt/D10` = **1986**,
   `/mnt/D12` = **1985** (baseline exactly 1984 for all); the extra
   entries are `????????.???`, `B???????.??s` — raw `/boot/kernel/*.ko`
   page-cache bytes returned through a FAT directory listing
   (cross-principal disclosure; the same "mount-an-image → read foreign
   kernel data" class as DF-2663).
4. **Corruption → panic**: `touch /mnt/D09/PWNED2675; touch
   /mnt/D10/PWNED2675B; sync` — the dirent bytes are written through the
   aliased PTE into a live page of the root hammer2 filesystem's cache;
   the flush then panicked (panic.txt, serial console):
   `panic: base_insert 2 24,16,32 fail 0xfffff8005b34e000:17`,
   `hammer2_base_insert+0x639 → hammer2_flush_core+0x954 →
   hammer2_flush_recurse → Debugger("panic")`.  `base` points into a
   buffer-cache KVA page (slot kvabase+0x8000) whose contents the alias
   replaced with dirent bytes; the `validate` loop at
   `sys/vfs/hammer2/hammer2_chain.c:5364` detects the overlapping keys.
   An earlier independent run wedged identically on `umount` of the
   tainted mount (silent; console showed the same DDB stop on the second
   occurrence).
5. **Fix validation**: `fix.diff` (getblk bound incl. page offset +
   allocbuf defense-in-depth + msdosfs mount-time rejection of unaligned
   64KB clusters) applied to the guest's `/usr/src`, `make nativekernel`,
   rebooted: crafted image now **rejected with EINVAL** ("msdosfs: 64KB
   clusters at non-page-aligned device offset 6144"), `ovrdump` finds
   **zero** npages>kvasize/4096 buffers, and a standard msdosfs image
   (newfs_msdos, 512B clusters) still mounts and reads normally.

## Exploitability toward uid=0

The primitive is a **controlled cross-buffer write of attacker-chosen
bytes** (dirent names: ~11 arbitrary bytes per 32-byte entry, repeated at
every slot from 1984 up — 64 entries per aliased page) into the *first
cache page of a victim buffer of the attacker's choosing* (placement
control proven with the cpu-pinning ladder), plus the read direction
(info disclosure).  Because DragonFly's buffer cache is merged with the
VM cache (`B_VMIO`), a victim buffer's page *is* the file's VM page: an
attacker who arranges the neighbor slot to hold a setuid-root binary's
page (read it, then place the 17-page buffer) corrupts the very page
`execve` will map — the write persists in the page cache until eviction.
That chain was not completed in this run (the guest is single-tenant and
the slot-placement around a specific exec path needs longer grooming);
the demonstrated leak + root-filesystem-corruption panic already
establishes Critical-class impact.  The panic-on-demand variant (recycle
the 17-page buffer while the neighbor is live → neighbor's page-0 PTE
removed → any access faults) is trivially reachable via cache pressure.

## Why the stock INVARIANTS kernel stayed silent

No assertion covers "pages needed vs slot size": the only bounds are
`size > MAXBSIZE` (without the offset) and
`desiredpages <= XIO_INTERNAL_PAGES` (32, sized for MAXPHYS).  The
overflow is pure pmap state — invisible until two buffers collide.

## Honesty notes

* The `touch`-write demo's grep over `/boot/kernel` never completed (the
  `sync` panicked the box first — see 4); the write direction is instead
  proven by (i) the hammer2 metadata corruption panic it caused and (ii)
  the symmetric read direction.  `p16watch` snapshots bracketing the
  write show the aliased page's contents changing to the victim's bytes
  on placement, establishing the shared mapping in both directions.
* Two guest resets occurred during the session (both after the bug's own
  effects); the final state is the fix-validated kernel's baseline.
