β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2675

allocbuf() maps 17 pages into the 16-page MAXBSIZE per-header KVA slot when (loffset & PAGE_MASK)+size > MAXBSIZE β€” PTE written into the next buffer header's slot, aliasing one page between two live kernel buffers (cross-buffer R/W, info leak, panic)

Field Value
ID DF-2675
Status new
Severity Critical
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
CWE CWE-787 OOB Write (pmap PTE past the slot), CWE-125 OOB Read (aliased page via readdir)
File sys/kern/vfs_bio.c
Lines 638-640 (fixed slots), 3039-3046 + 3211-3216 (qenter), 1885 (dead guard), 2736 (size-only panic); reachability msdosfs_vfsops.c:423-457, msdosfs_vnops.c:513-522/:1368
Area kern
Confidence certain
Discovered 2026-08-29
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

Every buffer header owns a fixed contiguous MAXBSIZE (64KB) KVA slot (bufinit, vfs_bio.c:638-640). allocbuf() computes desiredpages = ((loffset & PAGE_MASK) + roundup2(size, DEV_BSIZE) + PAGE_MASK) >> PAGE_SHIFT and pmap_qenter_noinval()s that many pages at trunc_page(b_data) == b_kvabase (:3044-3046, :3211-3216). Neither guard β€” getblk's size > MAXBSIZE panic (:2736) nor allocbuf's b_kvasize < size panic (:3039) β€” accounts for the page offset, and getnewbuf's size/maxsize parameters (where getblk DOES compute size+pgoff at :2939) are dead code (:1885). Any getblk/bread with (loffset & PAGE_MASK) + size > MAXBSIZE therefore maps 17 pages into the 16-page slot: page 16's PTE is installed at b_kvabase+65536 = the NEXT header's slot base, and teardown (vfs_vmio_release:1854, allocbuf shrink:3077) later removes the neighbor's page-0 PTE.

Reachable from an attacker-supplied filesystem image: msdosfs accepts 64KB clusters (512B sectors Γ— 128 sec/cluster passes the :427 check since 128 is not >128) and with an odd FAT size every data cluster sits at a non-page-aligned device offset, so every directory operation issues bread(devvp, 6144+k*65536, 65536) β€” a plain ls triggers.

Threat model & preconditions

Mounting an attacker-crafted msdosfs image (removable media, downloaded image, restored backup; unprivileged with vfs.usermount=1). Each directory access silently aliases one page of two live kernel buffers: - reads through the 17-page buffer return the neighbor's bytes — readdir leaks kernel-cached file content cross-principal (demonstrated: /boot/kernel/*.ko bytes surfaced as FAT dirents); - writes (dirent names, ~11 attacker-chosen bytes per 32-byte slot, 64 slots per aliased page) land in the neighbor's page — since the buffer cache is merged with the VM cache (B_VMIO), the victim page is a file's VM page, making cached-page corruption of a setuid-root binary the credible unpriv→root route; - recycling the 17-page buffer unmaps the neighbor's page-0, faulting/wedging the kernel; - demonstrated terminal effect: aliased write into root-filesystem hammer2 metadata → flush panic.

Proof of concept

VERIFIED on the stock INVARIANTS kernel (findings/poc/DF-2675/): mkfat64k.py forges FAT12 (bps=512, spc=128, odd FATsecs → xoff=2048; dir entries pre-filled to slot 1983). ls /mnt/D00 → ovldump (kvm /dev/mem PTE walk) shows live buffers with xio_npages=17, b_bufsize=65536, b_data=b_kvabase|0x800. ladder.sh uses usched_set-pinned allocations (header n lives on cpu n%6; overflow always targets header n+1) to place neighbor buffers at the aliased slots: p16watch shows the overflow PTEs mapping /boot/kernel/*.ko content (ELF headers, module text) — 25 simultaneous aliases. Success criteria, all met: (1) ls returns 1986 entries vs crafted 1984, the extras are raw kernel-cache bytes (info leak); (2) touch /mnt/D09/PWNED2675; sync corrupts an in-cache hammer2 page of the root fs → panic: base_insert 2 24,16,32 fail in hammer2_flush_core (panic.txt); (3) umount of the tainted mount reproduces the wedge class. user→root route (not completed): placement control + write primitive demonstrated; exec-path grooming documented in VERDICT.md.

fix.diff (build-validated in-guest, kernel #1): bound the requested KVA by the page offset in getblk (size + (loffset & PAGE_MASK) > MAXBSIZE panic), defense-in-depth in allocbuf (b_kvasize < size + pgoff), and reject the dangerous mount configuration in msdosfs (64KB clusters at non-page-aligned first-cluster offset β†’ EINVAL; page-aligned 64KB clusters still mount and read β€” fix is precise; normal newfs_msdos filesystems unregressed).

References

  • vfs_bio.c:638 KVA slot scheme (DF-2616's verification localized it), DF-2663 (bio_page_alloc family), DF-2618 (the RB overlap panic the corrupted hammer2 flush reproduced)

Timeline

  • 2026-08-29 Discovered during pass-2 audit of vfs_bio.c (GLM 5.3); leak + corruption + panic all reproduced on stock, fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2675 Β· 28 files
FileTypeDescriptionSize
mkfat64k.py β€” 4.3 KB view raw
fat64k.img.gz β€” 96.7 KB ↓ download
mkfat64kB.py β€” 4.3 KB view raw
fat64kB.img.gz β€” 472.6 KB ↓ download
mkfat64k_aligned.py β€” 4.3 KB view raw
fat64k_aligned.img.gz β€” 97.0 KB ↓ download
ptecheck.c β€” 2.3 KB view raw
pml4dump.c β€” 1.6 KB view raw
bufscan.c β€” 4.1 KB view raw
ovrdump.c β€” 3.2 KB view raw
findfield.c β€” 1.7 KB view raw
p16watch.c β€” 3.0 KB view raw
pindir.c β€” 793 B view raw
pinread.c β€” 990 B view raw
sweep.c β€” 1.1 KB view raw
ladder.sh β€” 1.2 KB view raw
runs17.sh β€” 620 B view raw
build.sh β€” 336 B view raw
run.sh β€” 899 B view raw
run.log β€” 1.2 KB view raw
panic.txt β€” 13.7 KB view raw
fix.diff β€” 2.5 KB view raw
fix_build.log β€” 1.0 KB view raw
fix_run.log β€” 383 B view raw
README.md β€” 5.0 KB ↓ raw
VERDICT.md β€” 8.8 KB ↓ raw
manifest.json β€” 2.7 KB view raw
verdict.json β€” 10.5 KB view raw

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.

VERDICT.md
↓ download raw

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 β€” 1281 = 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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

FIXED. Baseline (stock #0): crafted msdosfs image mounts and every directory read creates npages=17 buffers overflowing the 16-page KVA slot (ovrdump), with demonstrated cross-buffer aliasing, readdir info leak of /boot/kernel/*.ko bytes, and a hammer2 flush panic after an aliased write. Patched (#1, nativekernel+fix.diff): the crafted mount is rejected with 'msdosfs: 64KB clusters at non-page-aligned device offset 6144' (EINVAL, rc=71) and ovrdump finds ZERO overflow buffers system-wide; the page-ALIGNED 64KB-cluster variant still mounts and reads correctly (1984 entries, 0 overflow buffers) proving the fix is precise, and a normal newfs_msdos filesystem mounts/mkdir/write/umounts unregressed.

findings/poc/DF-2675/fix_run.log (mount rejection + ovrdump counts + aligned-64K + normal-image regression); findings/poc/DF-2675/fix_build.log (kernel build tail, BUILD-DONE); fix.diff applied with patch -p1 (hunks at vfs_bio.c:2733/:3045, msdosfs_vfsops.c:507)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Aug 30 12:36:11 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

crafted FAT image (512B sectors, 128 sec/cluster = 64KB clusters, odd FATsecs -> first data cluster block 12 = byte 6144, xoff 2048) -> victim mounts it (root, or unprivileged with vfs.usermount=1; real-world vector: any mount of attacker-supplied media/image) -> any directory read (ls/getdents/mkdir) -> bread(devvp, 6144+k65536, 65536) -> getblk/allocbuf maps 17 pages at b_kvabase -> PTE for page 16 lands on next header's slot -> [placement] cpu-pinned neighbor allocation puts victim buffer (kernel module page, setuid binary page, fs metadata page) at slot n+1 -> two live buffers share one KVA page -> read direction: readdir returns victim's bytes as dirents (info disclosure, proven with /boot/kernel/.ko bytes); write direction: dirent bytes (touch) written into victim's page (proven by the hammer2-metadata corruption panic); teardown direction: recycling the 17-page buffer unmaps victim's page-0 -> victim access faults (wedge/panic class, observed twice)

Evidence (decisive lines)

["findings/poc/DF-2675/run.log - fresh-boot baseline: mount accepted, then 'OVRBUF n=3541 kva=0xfffff8005e226000 npages=17 kvasize=65536 loff=0x1800' and 'n=5387 ... loff=0xd1800' (live 17-page buffers in 16-page slots, stock kernel)", 'findings/poc/DF-2675/findfield.c + session transcript - record decode: off176 b_loffset=0x000d1800, off996 b_bufsize=0x00010000, off1004 b_bcount=0x00010000, off1016 b_data=kvabase|0x800, off1056 xio_npages=0x11', "p16watch output (transcript + run3.log) - after ladder placement, overflow pages map foreign content: '7f454c46 .ELF....', 'deflt.mo', 'd_iic_on', 'r4.ext_i' (/boot/kernel module pages), 25 simultaneous aliases", "leak demo (transcript) - 'ls /mnt/D07: 1986 entries', '/mnt/D10: 1986', '/mnt/D12: 1985' vs crafted baseline 1984; garbage entries '????????.???', 'B???????.??s' = raw .ko cache bytes via readdir", 'findings/poc/DF-2675/panic.txt - serial console: \'panic: base_insert 2 24,16,32 fail 0xfffff8005b34e000:17\' hammer2_base_insert+0x639 -> hammer2_flush_core+0x954 -> Debugger("panic") after touch+sync wrote dirents through the aliased page into root-fs metadata', "findings/poc/DF-2675/fix_run.log - fixed kernel #1: crafted image rejected with 'msdosfs: 64KB clusters at non-page-aligned device offset 6144', mount rc=71, ovrdump OVRBUF count 0; aligned-64KB image mounts fine (1984 entries, 0 OVRBUF); normal newfs_msdos regression clean", 'findings/poc/DF-2675/fix.diff - git-apply-able (checked against sys/); applied cleanly in-guest (patch -p1)']

PoC changes

no seed existed (pass-2 new finding); everything authored from scratch: FAT12 forger with exact geometry control (odd FATsecs -> xoff 2048; entries pre-filled to slot 1983 so readdir reaches the aliased page), kvm//dev/mem introspection suite (buf-array scan by b_kvabase +65536 signature to defeat unknown struct offsets; PTE walk via KPML4phys; live record diffing to decode field offsets), usched_set(USCHED_SET_CPU)-pinned allocation ladder to defeat per-cpu buffer queues (header n is cpu n%6; overflow target is always header n+1)

Verified recommended fix

getblk(): bound size + (loffset & PAGE_MASK) <= MAXBSIZE; allocbuf(): account page offset in the b_kvasize check; msdosfs: reject 64KB clusters at non-page-aligned data offsets at mount (fix.diff, validated)

Verdict

REPRODUCED (kernel memory corruption, Critical). sys/kern/vfs_bio.c gives every buffer header a fixed MAXBSIZE (64KB) KVA slot (bufinit, :638-640) but neither getblk() (:2736 'size > MAXBSIZE') nor allocbuf () (:3039 'b_kvasize < size') accounts for (loffset & PAGE_MASK) when allocbuf computes desiredpages (:3044-3046) and pmap_qenter_noinval()s that many pages at b_kvabase (:3211-3213); getnewbuf()'s size/maxsize parameters - where getblk does compute size+pgoff (:2939) - are dead code (:1885). Any getblk/bread with (loffset & PAGE_MASK)+size > MAXBSIZE therefore maps 17 pages into the 16-page slot and writes page 16's PTE at b_kvabase+65536 = the NEXT buffer header's slot base, silently aliasing one KVA page between two live kernel buffers; teardown (vfs_vmio_release :1854, allocbuf shrink :3077) removes the neighbor's page-0 PTE. Reachable from an attacker-supplied filesystem image: msdosfs accepts 512B-sector/128-sec-per-cluster FATs (64KB clusters pass the :427 check since 128 is not > 128) and with an odd FAT size every data cluster sits at a 512B-granular, non-page-aligned device offset; every directory operation then does bread(devvp, 6144+k65536, 65536) (msdosfs_vnops.c:515-522, msdosfs_lookup.c, mkdir :1368) - a plain 'ls' triggers. Demonstrated on the stock INVARIANTS kernel (#0 Thu Jul 2 06:02:54 2026): (1) kvm//dev/mem introspection (ovrdump/findfield/p16watch) shows live buffers with xio_npages=17, b_bufsize=65536, b_data=b_kvabase|0x800, b_loffset=0x1800/0xd1800 and the overflow PTE installed in the next slot; (2) with cpu-pinned allocation placement (usched_set USCHED_SET_CPU) the overflow pages were made to map neighbor buffers holding /boot/kernel/.ko content (25 simultaneous aliases; p16watch shows ELF headers/module text); (3) readdir then LEAKS those kernel-cached bytes to userspace: 'ls' of crafted dirs returns 1986/1985 entries instead of the crafted 1984, extra entries are raw .ko page bytes ('????????.???', 'B???????.??s') - cross-principal info disclosure (same class as DF-2663); (4) writing a dirent through the aliased page (touch) plus sync corrupted an in-cache page of the root hammer2 filesystem and the flush PANICKED: 'panic: base_insert 2 24,16,32 fail 0xfffff8005b34e000:17' in hammer2_base_insert->hammer2_flush_core (panic.txt; base points into a buffer-cache page whose contents the alias replaced, tripping the validate check at hammer2_chain.c:5364). A second independent wedge reproduced on umount of the tainted mount. The write primitive places ~11 attacker-chosen bytes per 32-byte dirent at any offset in the victim's first cache page; since the buffer cache is merged with the VM cache (B_VMIO), a victim buffer page IS the file's VM page - corrupting the cached page of a setuid-root binary is the credible uid=0 route (placement control proven; exec-path grooming not completed in this single-tenant run). No INVARIANTS assertion covers pages-vs-slot (only XIO_INTERNAL_PAGES=32), so stock kernels corrupt silently. FIX VALIDATED: fix.diff (getblk bound incl. page offset + allocbuf defense-in-depth + msdosfs mount-time rejection of unaligned 64KB clusters) built via make nativekernel (#1 Sun Aug 30 12:36:11 2026): crafted image now rejected 'msdosfs: 64KB clusters at non-page-aligned device offset 6144' (mount rc=71), zero overflow buffers system-wide, page-ALIGNED 64KB-cluster images still mount and read (fix is precise), normal newfs_msdos filesystems unregressed.