DF-2675 / verdict.json
{ "finding_id": "DF-2675", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "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+k*65536, 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.", "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+k*65536, 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": [ "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)" ], "kernel_refs": [ "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" ], "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)", "attempts": 9, "guest_uname": "baseline: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (stock INVARIANTS); fix: #1: Sun Aug 30 12:36:11 UTC 2026 (nativekernel + fix.diff)", "runtime_sec": 7800, "guest_dirty": 0, "build_cmd": "in-guest root: cd /root && sh build.sh (cc -O2 ... -lkvm for ptecheck/pml4dump/bufscan/ovrdump/findfield/p16watch; cc for pindir/pinread/sweep; gunzip fat64k.img.gz); fix kernel: cd /usr/src && patch -p1 < /root/fix.diff && make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel", "run_cmd": "in-guest root: sh /root/run.sh (vnconfig -c vn0 fat64k.img; mount_msdos /dev/vn0 /mnt; ls /mnt/D00; /root/ovrdump 20000; /root/findfield '/root/pindir 5 /mnt/D13'); alias/leak/panic: sh /root/ladder.sh then demo per VERDICT.md", "code_hash": "ee6fbeebc507316eddc2cd6eb0f71661edd6971b6b1a86e7882c6a6665311bf8 (mkfat64k.py); 77e07c86e7010b7641822b4a72cf1cc1a7c56de33bb1fa2068136394da6b68b8 (fat64k.img)", "notes": "No prior findings on this file (pass-1 covered, none recorded) - nothing to avoid re-reporting except the orchestrator context: DF-2663 short-read family (root cause NOT here: bpdone:3610 sets B_CACHE on short reads without B_ERROR, but the un-zeroed-page consumption is fixed at the io.c layer per instructions), DF-2616 KVA slot lifetime (different defect: this is slot SIZE overflow), DF-2633/2655 silent-error family (bpdone re-dirty loop at :3556-3565 is the designed retry mechanism; error stays in bp->b_error with infinite bufdaemon retry - noted, not re-filed). Additional pass-2 notes recorded in negative_notes of the JSON result. Two guest resets during verification were both caused by the bug's own effects (silent umount wedge; hammer2 flush panic) - final reset restored the clean snapshot after fix validation. Guest left CLEAN after final vm.sh reset with-src.", "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)", "fix_status": "fixed", "fix_kernel_uname": "DragonFly 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", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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.", "fix_evidence": "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)" } |