DF-3232 / verdict.json
{ "finding_id": "DF-3232", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Verified on the stock INVARIANTS guest (X86_64_GENERIC #0) across three fresh boots: with fs_fragshift patched 3->0 (fs_frag left at the valid 8, fs_fpg raised to keep the forged block in cg0), a single unprivileged open()+ftruncate(0) drives ffs_blkfree_cg -> fragstoblks(bno)=bno>>0 -> ffs_isfreeblock/ffs_setblock at cp[bno] where the bitmap is only fpg/8 bytes. bno=16000: Fatal trap 12 supervisor READ page-not-present stopped at ffs_isfreeblock+0x19 (ffs_subr.c:250), fault VA minus (cg_freeoff 1628 + 16000) equals the page-aligned buffer base - a 9.4KB OOB read past the 8192-byte cg buffer. bno=6000: panic ffs_blkfree: freeing free block dev=vn0 block=6000 - the OOB read returned a live nonzero kernel byte from past the bitmap. bno=5648: the free SURVIVES and the clean unmount persists 0xff at image offset 31852 = cg0+freeoff+5648 - ffs_setblocks cp[5648]=0xff (ffs_subr.c:294) executed 16 bytes past the bitmap. Control with stock fs_fragshift survives cleanly. Primitive: single-byte 0x00/0xff write and byte read at attacker-chosen offset up to fs_fpg past the bitmap (spans neighbouring buffer-cache KVA slots), trigger one syscall by uid 1001. Impact recorded as panic (deterministic reboot-equivalent) with the silent-corruption write demonstrated on disk; uid0 chain not completed - the write value is one constrained byte and the adjacent-slot victim population is not reliably steerable on this guest (per-buffer MAXBSIZE KVA reservations), documented in VERDICT.md.", "exploit_chain": "crafted image (root mount, or unprivileged with vfs.usermount=1) -> victim files di_db[0] forged to chosen frag B -> unprivileged ftruncate -> ffs_truncate -> ffs_blkfree_cg:1538 blkno=B>>fs_fragshift(=B) -> :1539 ffs_isfreeblock OOB READ (trap on unmapped reservation / panic on nonzero byte) -> :1544 ffs_setblock OOB WRITE cp[B]=0xff (observed persisted to media at +16 bytes past the bitmap; layout permits offsets past 64KB into neighbouring buffers). uid0 open: single-byte 0x00/0xff at chosen buffer-cache-relative offset; no deterministic credential-object grooming achieved.", "evidence": [ "findings/poc/DF-3232/panic.txt: Fatal trap 12 supervisor READ, Stopped at ffs_isfreeblock+0x19 (run bno=16000)", "findings/poc/DF-3232/panic2.txt: panic ffs_blkfree: freeing free block, dev=vn0 block=6000, stack ffs_truncate->ffs_blkfree->ffs_blkfree_cg (run bno=6000)", "findings/poc/DF-3232/run.3.log: pre-trigger 00-byte vs post-trigger ff at image offset 31852 after unprivileged ftruncate + clean umount (silent OOB write persisted)", "findings/poc/DF-3232/run.control.log: stock-geometry control survives", "findings/poc/DF-3232/fix_patched.log: patched kernel #1 rejects the crafted image at mount; stock image unaffected" ], "kernel_refs": [ "sys/vfs/ufs/ffs_subr.c:250", "sys/vfs/ufs/ffs_subr.c:294", "sys/vfs/ufs/ffs_subr.c:270", "sys/vfs/ufs/ffs_subr.c:227", "sys/vfs/ufs/fs.h:518-521", "sys/vfs/ufs/ffs_alloc.c:1534-1544", "sys/vfs/ufs/ffs_alloc.c:1208-1209", "sys/vfs/ufs/ffs_alloc.c:1137", "sys/vfs/ufs/ffs_vfsops.c:642-646" ], "poc_changes": "authored in this pass; two fixes during bring-up: victim inode is 3 not 4 (auto-detect added), and 8-alignment of the forged bno is required by ffs_blkfrees fragnum check (setup.sh auto-picks an aligned zero target byte for the write-proof run)", "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", "runtime_sec": 2400, "guest_dirty": 1, "build_cmd": "sh /root/df3232/build.sh", "run_cmd": "sh /root/df3232/setup.sh 16000 (or 6000 / pick) then su -m maxx -c /tmp/trig3232 /mnt/victim", "code_hash": "c8b14c5d063af2aa26adb8c5cc25c2b77f178ceb6d557f0db190bc39353ba1e9 (sbtool2.c), 9eb091af3f9bcd73c5f278cd4c07f63fd68eb31eaf35470fc627cd15de37d99b (trigger.c)", "notes": "Beyond DF-0919: fs_frag is valid here (all switch() cases satisfied) - the OOB comes solely from the unvalidated fs_fragshift index scaling. fragacct itself is safe for valid fs_frag (blkmap-masked fragmap <= 0xff; around[]/inside[]/fragtbl[]/cg_frsum[] indices bounded) - negative result documented in the audit. cg-offset-driven OOB variants belong to DF-3035 (ffs_alloc.c).", "recommended_fix": "Mount-time fragment-geometry validation in ffs_mountfs (fs_frag power-of-2 <= MAXFRAG, 1<<fs_fragshift==fs_frag, bsize==fsize<<fragshift, bshift/fshift/bmask/fmask/qbmask/qfmask consistent) - validated in-guest: crafted image rejected at mount, stock images and the guests own /boot+hammer2 unaffected.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Wed Sep 9 00:49:30 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Patched kernel #1 (fix.diff applied to guest /usr/src, make nativekernel + installkernel): DF-3232 crafted image is rejected at mount with ffs_mountfs: /mnt: inconsistent fragment geometry / mount EINVAL (fix_patched.log); stock newfs image mounts and operates normally; guest boots cleanly (its UFS /boot and hammer2 root pass the gate). Previously-observed trap/panic/on-disk-write are unreachable because the trigger filesystem can no longer mount.", "fix_evidence": "findings/poc/DF-3232/fix_patched.log" } |