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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2675 Β· 28 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
- Live 17-page buffers:
ovrdump/findfield(kvm + /dev/mem PTE walk) showxio_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 2026kernel. - Cross-buffer alias: after placing victim buffers at the neighboring
slots (cpu-pinned allocation ladder),
p16watchshows 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. - Info disclosure (leak):
ls /mnt/D10on the crafted image returns 1986 entries instead of the crafted 1984, the extra entries (????????.???,B???????.??s) being raw bytes of root-owned/boot/kernel/*.kocache pages leaked out through a FAT directory listing (cross-principal disclosure, same class as DF-2663). - Memory corruption β kernel panic: writing a dirent through the
aliased page (
touch /mnt/D09/PWNED2675) plussynccorrupted an in-cache hammer2 metadata page of the root filesystem, and the next flush panicked:panic: base_insert 2 24,16,32 fail 0xfffff8005b34e000:17inhammer2_base_insert() β hammer2_flush_core()(see panic.txt). A second, earlier silent wedge (umount of the tainted msdosfs mount) reproduced the hang class. - 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 thebufarray, deriveb_kvabase(offset 1024) by its +65536 signature, dumpb_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,lsnormal, thenOVRBUF ... npages=17Γ2.panic.txtβ serial console capture of the hammer2 flush panic.fix.diffβ the verified fix (git-apply-able againstsys/).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.
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 getsb_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): onlysize > MAXBSIZEpanics β(loffset & PAGE_MASK)is not accounted.sys/kern/vfs_bio.c:2939-2940(getblk):maxsize = size + (loffset & PAGE_MASK)is computed β and passed togetnewbuf()whosesize/maxsizeparameters 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, andKKASSERT(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, thenpmap_qenter_noinval(b_data, pages, desiredpages=17)β page 16's PTE is written atb_kvabase+65536, the next header's slot base.- Symmetric damage on teardown:
pmap_qremove_noinval(kvabase, npages=17)invfs_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 ownb_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 isSecPerClust * 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)thenbread(pm_devvp, de_bn2doff(pmp,lbn), 65536)β the unaligned 64KB getblk. Same pattern inmsdosfs_lookup.c(all thebread(pmp->pm_devvp, de_bn2doff(...), blsize)calls) andmsdosfs_vnops.c:1368(mkdir:getblk(devvp, de_bn2doff(bn), pm_bpcluster)+ 64KB memset).- A plain
lsof any subdirectory is sufficient. Withvfs.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)
- Fresh-boot baseline (run.log): mount accepted;
ls /mnt/D00returns the crafted 1984 entries;ovrdump(kvm/dev/kmem+/dev/mempage-table walk) reportsOVRBUF n=3541 kva=β¦5e226000 npages=17 kvasize=65536 loff=0x1800andn=5387 β¦ loff=0xd1800.findfield(array snapshot diff around onels) 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. - 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.koreads swept on cpu0 until the neighbors were consumed.p16watchthen 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. - 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/*.kopage-cache bytes returned through a FAT directory listing (cross-principal disclosure; the same "mount-an-image β read foreign kernel data" class as DF-2663). - 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").basepoints into a buffer-cache KVA page (slot kvabase+0x8000) whose contents the alias replaced with dirent bytes; thevalidateloop atsys/vfs/hammer2/hammer2_chain.c:5364detects the overlapping keys. An earlier independent run wedged identically onumountof the tainted mount (silent; console showed the same DDB stop on the second occurrence). - 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"),ovrdumpfinds 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/kernelnever completed (thesyncpanicked 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.p16watchsnapshots 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
fixedFIXED. 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)
Confirmed kernel references
- sys/kern/vfs_bio.c:638
- sys/kern/vfs_bio.c:1885
- sys/kern/vfs_bio.c:2736
- sys/kern/vfs_bio.c:2939
- sys/kern/vfs_bio.c:2962
- sys/kern/vfs_bio.c:3039
- sys/kern/vfs_bio.c:3044
- sys/kern/vfs_bio.c:3211
- sys/kern/vfs_bio.c:3077
- sys/kern/vfs_bio.c:1854
- sys/vfs/msdosfs/msdosfs_vfsops.c:423
- sys/vfs/msdosfs/msdosfs_vfsops.c:446
- sys/vfs/msdosfs/msdosfs_vfsops.c:505
- sys/vfs/msdosfs/msdosfs_vnops.c:513
- sys/vfs/msdosfs/msdosfs_vnops.c:1368
- sys/vfs/msdosfs/msdosfs_lookup.c:639
- sys/vfs/hammer2/hammer2_chain.c:5364
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.
No comments yet.