DF-3212 / verdict.json
{ "finding_id": "DF-3212", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "verdict": "REPRODUCED kernel-memory disclosure via OOB read in ffs_read() on a crafted-geometry UFS1 mount. ffs_read computes blkoffset from the on-disk fs_qbmask (loaded verbatim: ffs_vfsops.c:642-646 validates only magic/bsize, ffs_oldfscompat:802-816 re-derives masks only for pre-4.4 inodefmt) while bp->b_bufsize comes from blksize() via ffs_blkatoff_ra (ffs_subr.c:97-119). When the mask is wider than fs_bsize, xfersize = b_bufsize - blkoffset goes negative and the unsigned size_t promotion at ufs_readwrite.c:143 clamps it to a small positive value BEFORE the xfersize <= 0 panic guard at :147 (dead code for exactly its intended case), so uiomovebp at :155 copies from b_data + k*64KB + 0x123 into adjacent MAXBSIZE buffer-cache KVA slots (vfs_bio.c:640). Unprivileged pread(2) at valid offsets returned: kernel .rodata format strings (ahci/wlan), raw kernel .text (rip-relative code, kernel link-address immediates), and other files cached pages (ngctl strings); unmapped neighbors yield EFAULT (also impossible on a clean fs). Deterministic: control on clean image 15/15 zeros; crafted runs stable across 3 runs x 2 guest lives (nonzero=2/10, efault=10/22 for trigger/trigger2). Fix validated in-guest (make nativekernel, kernel #1): invalid-geometry preads -> EIO, legitimate-blkoffset read (0x80123 & 0x7ffff = 0x123) still succeeds, clean-fs control unchanged 15/15 zeros. No panic on this paths leak runs (the xfersize==0 sub-case remains a reachable panic). Not a uid=0 primitive (read-only disclosure); leak fully characterized.", "exploit_chain": "", "evidence": [ "findings/poc/DF-3212/run2.trigger2.log \u2014 full hexdumps: kernel .text (48 8b 25 8a 9f 20 00 rip-relative), ahci_get_err_ccb format string, ngctl string tables; SCAN nonzero=10 clean=3 efault=22", "findings/poc/DF-3212/run.control_and_trig1.log \u2014 clean-image control SUMMARY nonzero=0 clean=15 efault=0 vs crafted nonzero=2 efault=10", "findings/poc/DF-3212/fix_patched_run.log \u2014 fixed kernel: errno=5 EIO at all invalid offsets, k=8 (legit blkoffset) returns 512", "findings/poc/DF-3212/fix_build.log \u2014 kernel #1 build+install, zero errors", "findings/poc/DF-3212/fix.diff \u2014 blkoffset >= bp->b_bufsize guard" ], "kernel_refs": [ "sys/vfs/ufs/ufs_readwrite.c:141", "sys/vfs/ufs/ufs_readwrite.c:142", "sys/vfs/ufs/ufs_readwrite.c:143", "sys/vfs/ufs/ufs_readwrite.c:147", "sys/vfs/ufs/ufs_readwrite.c:155", "sys/vfs/ufs/fs.h:487", "sys/vfs/ufs/ffs_subr.c:97", "sys/vfs/ufs/ffs_vfsops.c:642", "sys/vfs/ufs/ffs_vfsops.c:802", "sys/kern/vfs_bio.c:640" ], "poc_changes": "No prior seed existed (new pass-2 finding). Harness reuses DF-0903s craft_img mechanics with a wider mask (0x7ffff vs 0x3fff) so blkoffset reaches 7 neighboring 64KB buffer slots; trigger uses pread() at valid offsets (< i_size) and classifies returns (zeros/EFAULT/non-zero) with marker search for a root-only 0600 file; mounts use -o noatime,noclusterr for deterministic b_bufsize == fs_bsize.", "attempts": 6, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline); fixed kernel #1: Tue Sep 8 13:58:18 UTC 2026", "runtime_sec": 4200, "guest_dirty": 1, "build_cmd": "cc -O -o craft_img craft_img.c && cc -O -o trigger trigger.c && cc -O -o trigger2 trigger2.c (guest); fix: cd /usr/src && patch -p1 < fix.diff && make nativekernel && make installkernel", "run_cmd": "sh setup.sh; awk BEGIN{for(i=0;i<1000000;i++)printf \"DF3212-ROOT-SECRET-%08d\\n\",i} > /root/.df3212_secret; chmod 600 /root/.df3212_secret; cat /root/.df3212_secret > /dev/null; su -m pwn -c /tmp/trigger2 /mnt/test/leak.bin", "code_hash": "sha256(trigger.c)=bb477631212796f834cd57a96e9e884185d2a60bdc6197183309c29856fa5d6b sha256(trigger2.c)=4f0eb418803181a7bf36bcf225e00b63e82db4d9485ff6dedfbf20986e0e0ec2 sha256(craft_img.c)=3b81de317df3bc56177e13badb85bcbcdd8ddef8952be44688bd2acd4beadb23", "notes": "su -c quoting pitfall documented: su -m pwn -c /tmp/trigger /path drops the path arg (becomes $0) \u2014 always quote the whole command. The root-only marker files buffers did not land in the 35 sampled slots (LEAK=0 for the marker specifically), but cross-file disclosure is proven directly (ngctl pages) plus kernel-image .text/.rodata windows. The xfersize==0 sub-case (blkoffset == b_bufsize exactly) still panics on stock; the fix.diff guard closes it too.", "recommended_fix": "In ffs_read, reject impossible geometry before the clamp: if (blkoffset >= bp->b_bufsize) { error = EIO; break; } (see fix.diff); complement with DF-3190s mount-time validation of the whole mask/shift family.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly 6.5-DEVELOPMENT #1: Tue Sep 8 13:58:18 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Kernel rebuilt in-guest with fix.diff (make nativekernel && make installkernel, fix_build.log, zero errors). Patched kernel: crafted-fs preads at all 14/15 invalid-geometry offsets return -1 EIO (guard) and trigger2 35/35 probes return EIO with SCAN nonzero=0 clean=0 efault=0 LEAK=0; the single legitimate-blkoffset offset (k=8: 0x80123 & 0x7ffff = 0x123) still returns 512 zero bytes, proving the guard is surgical. Clean-image control on patched kernel: SUMMARY nonzero=0 clean=15 efault=0 \u2014 identical to stock, no false positives. No panic, no leak.", "fix_evidence": [ "findings/poc/DF-3212/fix_patched_run.log", "findings/poc/DF-3212/fix_patched_mount.log", "findings/poc/DF-3212/fix_build.log" ] } |