DF-3175 / verdict.json
{ "finding_id": "DF-3175", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "REPRODUCED on stock 6.5-DEVELOPMENT #0 (INVARIANTS): an unprivileged ftruncate() on a crafted FIFO inode (mode 0666) on a root-mounted ext2 image runs the full ext2 truncate engine over the inodes i_db[] overlay \u2014 kern_ftruncate rejects only VDIR (vfs_syscalls.c:4105), ext2_setattrs default:break passes special files to ext2_truncate (ext2_vnops.c:338), and ext2_truncate has no vnode-type gate (ext2_inode.c:468). Variant A (i_db[]=clear-bit blocks): panic(\"ext2_blkfree: freeing free block 499\") with backtrace kern_ftruncate->vop_setattr->ext2_setattr->ext2_truncate->ext2_blkfree; guest dead in DDB. Variant B (i_db[0]=live block of file victim): ftruncate returns 0, f_bfree 483->484, and after unmount the bitmap shows block 26 not in use while victim still references it \u2014 silent cross-file block free/aliasing (in-fs disclosure+corruption), no kernel message. No kernel-memory corruption (ext2_blkfree bounds-checks bno < bcount), so no uid0 chain; ceiling = deterministic DoS panic + fs-integrity break, trigger unprivileged post-mount, mount itself root-only (same reachability ceiling as DF-0888). Fix (ext2_setattr default: return 0 matching FreeBSDs ufs_setattr, plus VREG/VDIR/VLNK gate in ext2_truncate) validated on rebuilt kernel #1: both variants return 0 as no-ops, f_bfree unchanged, block 26 still marked in use, guest stays up.", "exploit_chain": "root mounts crafted ext2 (8MiB, 3 patched inodes) -> unprivileged user opens targetA/targetB (FIFO 0666) O_RDWR -> ftruncate(fd,0) -> ext2_setattr default-case falls through -> ext2_truncate/ext2_ind_truncate frees i_db[0..11] as blocks -> ext2_blkfree: clear bit => panic (DoS); set bit => live block freed while owned (cross-file aliasing, in-fs data disclosure on reallocation). Also reachable via unlink through ext2_inactive (ext2_inode.c:511-512).", "evidence": [ "panic.txt: panic: ext2_blkfree: freeing free block 499 + full backtrace kern_ftruncate->ext2_setattr->ext2_truncate->ext2_blkfree", "run.log: variant B f_bfree 483->484 (delta 1) with rc=0 + host-side debugfs proof Block 26 not in use while victim BLOCKS: (0):26", "run.2.log: patched kernel #1 \u2014 variant A no panic (STILL-ALIVE rc=0), variant B delta 0", "VERDICT.md: full narrative with path:line citations" ], "kernel_refs": [ "sys/vfs/ext2fs/ext2_inode.c:458-491", "sys/vfs/ext2fs/ext2_inode.c:383-393", "sys/vfs/ext2fs/ext2_inode.c:511-512", "sys/vfs/ext2fs/ext2_vnops.c:324-342", "sys/vfs/ext2fs/ext2_alloc.c:1403-1431", "sys/kern/vfs_syscalls.c:4105-4122" ], "poc_changes": "n/a (pack authored fresh in this run); craft_img.py needed two parser fixes vs its first draft (debugfs BLOCKS:(0):NN continuation line; free-block scan restricted to blocks < s_blocks_count because debugfs answers not in use for out-of-range numbers too).", "attempts": 2, "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) / #1: Mon Sep 7 21:02:49 UTC 2026 (fix)", "runtime_sec": 1450, "guest_dirty": 0, "build_cmd": "guest: cc -O2 -Wall -o trigger trigger.c (host: python3 craft_img.py df3175.ext2)", "run_cmd": "vm.sh run_root vnconfig -c vn0 /root/df3175.ext2 && mount -t ext2fs /dev/vn0 /mnt/df3175; vm.sh run_user poc/DF-3175/trigger /mnt/df3175/targetB; vm.sh run_user poc/DF-3175/trigger /mnt/df3175/targetA", "code_hash": "sha256(trigger.c)=fef4fcdf72c9d17da0dc2f61969fc3c06cce173f277e0c05a92e46f9acfd816a sha256(craft_img.py)=461c3a1b8b77a346265fd55975975001741e6b1634a48660321882c0582e31bd", "notes": "DF-0888 fix-state checked as instructed: STILL UNFIXED in-tree (ext2_inode.c:138-140 has no clamp; ext2_truncate at :465-466 has no structural-limit check); the validated fix.diff lives only in findings/poc/DF-0888/. DragonFly ufs_setattr shares the same default:break fallthrough (ffs affected too) \u2014 FreeBSD ufs_setattr returns 0 for special files, confirming upstream divergence. ext2_update early i_flag clear (ext2_inode.c:81 before bread at :85) is inherited from ffs_update \u2014 filed as Info finding, not verified (no memory-safety impact).", "recommended_fix": "ext2_setattr va_size switch: default: return (0) (match FreeBSD ufs_setattr); plus defense-in-depth VREG/VDIR/VLNK vnode-type gate at the top of ext2_truncate covering the ext2_inactive unlink path \u2014 see fix.diff.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Sep 7 21:02:49 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Applied fix.diff inside the guest /usr/src (never to the read-only sys/), make nativekernel + installkernel -> kernel #1. Re-ran the exact PoC: variant A no longer panics (ftruncate rc=0 no-op, guest stays up, STILL-ALIVE), variant B f_bfree delta 0, and the unmounted image still shows Block 26 marked in use (no on-disk corruption). Baseline #0 had panicked (guest down) / freed the live block.", "fix_evidence": "run.2.log (patched A+B outputs), env.txt (uname #1, vfs.usermount=0), fix_build.log (full nativekernel build), VERDICT.md fix section" } |