DF-3035 / verdict.json
{ "finding_id": "DF-3035", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Attacker-offset out-of-bounds kernel READ and WRITE reproduced deterministically on the stock INVARIANTS guest kernel from a crafted UFS1 image whose cylinder-group header carries a magic value plus one poisoned field. ffs_mountfs validates only fs_magic/fs_bsize (ffs_vfsops.c:642-646) and the allocator applies only cg_chkmagic() (fs.h:398, a magic compare) before using on-disk cg fields as kernel-memory offsets and indexes: cg_cgx -> fs_csp[16*cg_cgx].cs_nbfree-- and fs_maxcluster[4*cg_cgx] stores (ffs_alloc.c:1213,1963); cg_freeoff/cg_iusedoff/cg_boff/cg_btotoff/cg_clustersumoff/cg_clusteroff -> bit/int16/int32 read-modify-write through cgbuf+offset (ffs_alloc.c:1208-1216,1383-1463,1532-1609,1893-1955); cg_rotor/cg_frotor/cg_irotor/cg_nclusterblks -> OOB scan bounds (1129,1294,1410,1836); plus inode-derived bpref -> dtog() -> fs_csp[] OOB read before bread (1010,1370). Proven by: cg_cgx=0x40000000 -> Fatal trap 12 supervisor WRITE page-not-present at ffs_clusteracct.isra.0+0x1eb movl %eax,(%rdx,%rcx,4) with fault address = fs_maxcluster + 4*0x40000000 in three independent boots (panic1/2/3.txt); cg_iusedoff=0x20000000 -> Fatal trap 12 supervisor READ at ffs_nodealloccg+0x13f movzbl (%rdi,%rax,1), the inosused[] bitmap read at cgbuf+512MiB (panic4.txt); identical unpatched image allocates cleanly (control run). Panic is only the observable: any offset landing on mapped memory yields a silent attacker-relative int32 inc/dec/store (csum counters) or bit write (block/inode bitmaps), reach +/-8..32GiB around kernel heap objects via the signed 32-bit fields. Fail-stop panics from merely inconsistent in-range cg metadata (dup alloc 630, block-not-in-map 1451, freeing-free 1542/1581, map-mismatch 1317) make the same image a reliable DoS. Trigger model = crafted media mounted by root (DF-0820/DF-3015 class) with a single creat/write as the poisoned allocation; with vfs.usermount=1 or auto-mounted removable media the corruption itself is unprivileged. Root cause is distinct from DF-0820 (superblock geometry at mount) and DF-0794 (fs_ncg==0): this is the per-cylinder-group header consumed lazily at alloc/free time with no validation anywhere.", "exploit_chain": "crafted UFS1 image (cg_chkmagic-valid cg0, one poisoned field) mounted by root -> first creat/write on the mount -> ffs_balloc -> ffs_blkpref -> ffs_alloc -> ffs_hashalloc -> ffs_alloccg -> ffs_alloccgblk/ffs_nodealloccg -> wild write fs_maxcluster[cg_cgx]/fs_csp[cg_cgx] (attacker 32-bit index, store or inc/dec) or wild bitmap RMW at cgbuf+cg_freeoff/cg_iusedoff (attacker bit offset) -> with a heap-layout-stable target (no KASLR on this guest) point cg_cgx at a neighbouring kernel object: single-int32 decrement/Bit-flip corruption at an attacker-chosen relative address; panic when the offset is unmapped (the demonstrated observable). Full uid0 chain not developed: the mount precondition already requires root on the default configuration, so escalation adds nothing; the ceiling is arbitrary-offset kernel memory corruption (and unprivileged corruption wherever users can get images mounted).", "evidence": [ "findings/poc/DF-3035/panic1.txt โ Fatal trap 12, supervisor write, Stopped at ffs_clusteracct.isra.0+0x1eb: movl %eax,(%rdx,%rcx,4), fault 0xfffff8021776a200 (cg_cgx=0x40000000)", "findings/poc/DF-3035/panic2.txt โ same sink, cg_cgx=0x40040000, fault 0xfffff8018dce3600 (linear cg_cgx addressing)", "findings/poc/DF-3035/panic3.txt โ same sink, cg_cgx=0x40000000, fault 0xfffff80218519500 (boot-varying heap base + fixed 4*index displacement)", "findings/poc/DF-3035/panic4.txt โ Fatal trap 12, supervisor READ, Stopped at ffs_nodealloccg+0x13f: movzbl (%rdi,%rax,1), fault 0xfffff8007c136000 (cg_iusedoff=0x20000000)", "findings/poc/DF-3035/run.setup.log โ image build, cgxtool superblock/cg dump, patch confirmation, mount succeeds (mount never validates cg headers)", "findings/poc/DF-3035/run.patched.log โ fix.diff kernel: both poisoned images fail gracefully with ENOSPC, no panic; unpatched control image still allocates (16384-byte file), clean unmount", "findings/poc/DF-3035/cgxtool.c + setup3035.sh โ minimal crafted-image generator/patcher + trigger", "findings/poc/DF-3035/fix.diff โ ffs_chkcg() validator called from all six cg consumers + ffs_hashalloc cg clamp; applied to guest /usr/src, nativekernel -Werror-clean, installed, rebooted" ], "kernel_refs": [ "sys/vfs/ufs/ffs_alloc.c:1010", "sys/vfs/ufs/ffs_alloc.c:1127", "sys/vfs/ufs/ffs_alloc.c:1213", "sys/vfs/ufs/ffs_alloc.c:1215", "sys/vfs/ufs/ffs_alloc.c:1218", "sys/vfs/ufs/ffs_alloc.c:1257", "sys/vfs/ufs/ffs_alloc.c:1294", "sys/vfs/ufs/ffs_alloc.c:1370", "sys/vfs/ufs/ffs_alloc.c:1383", "sys/vfs/ufs/ffs_alloc.c:1415", "sys/vfs/ufs/ffs_alloc.c:1463", "sys/vfs/ufs/ffs_alloc.c:1532", "sys/vfs/ufs/ffs_alloc.c:1836", "sys/vfs/ufs/ffs_alloc.c:1893", "sys/vfs/ufs/ffs_alloc.c:1963", "sys/vfs/ufs/fs.h:394", "sys/vfs/ufs/fs.h:398", "sys/vfs/ufs/ffs_vfsops.c:642" ], "poc_changes": "No prior PoC seed โ authored fresh. cgxtool.c computes all offsets from the image's own superblock via the tree's fs.h (needed the ufs_types.h 32-bit ufs_daddr_t/ufs_time_t typedefs; an int64 first attempt misaligned the superblock). setup3035.sh gained a notrigger mode so setup output could be captured before the panic kills sshd. Two early validator bugs in fix.diff were caught against the real newfs layout before building (btotoff=168 == offsetof(struct cg,cg_space), and the fpg-sized bitmap scan region legitimately overlapping clustersumoff by 4 bytes) โ the fix must reject poison without rejecting legitimate filesystems, and the control run proves it does not.", "attempts": 7, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly v6.5.0.1712.g89e6a-DEVELOPMENT #1: Mon Jun 29 14:18:01 UTC 2026 root@ephemeral-5c2002c44b6c:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 5700, "guest_dirty": 1, "build_cmd": "guest# cc -O -o /tmp/cgxtool /root/poc/cgxtool.c (fix kernel: cd /usr/src && patch -p1 < fix3035.diff && make -j4 nativekernel KERNCONF=X86_64_GENERIC && make installkernel)", "run_cmd": "guest# sh /root/poc/setup3035.sh cgx 0x40000000 ; sh /root/poc/setup3035.sh iusedoff 0x20000000 ; sh /root/poc/setup3035.sh cgx 0 (control)", "code_hash": "cgxtool.c 4e02787b90126976d3cafc4cbf99f231819e0de8d500593218af94d1a02c1484; setup3035.sh fd11fbec44524bc1c87aae00e399dc0ed9ba52c1748852cd4b7a56093c88d29f; fix.diff 03a6693afb9fb938a047b1519bb5dba268c7bbb24b94d9afaff2cc17e1348dde", "notes": "impact field: this is kernel memory corruption (arbitrary-offset relative write + read) with a root-mount precondition on default configs โ filed severity High, bucket memcorrupt, same model as DF-0820/DF-3015. Guest left in panicked/reset cycles is NOT the case โ final state reset with-src (guest_dirty reflects the fix-build during the run). Fail-stop DoS variants via in-range inconsistent csum/bitmap metadata are included in the finding (same fix covers them). The DF-0793 async-TRIM deferred-pointer pattern was swept: the only taskqueue use in this file is the known trim task; ffs_blkfree_trim_completed's double tp read is the same variable. Superblock-field div-by-zero/OOB variants (fs_maxbpg=0 at ffs_blkpref:805, fs_spc/fs_nsect/fs_npsect in cbtocylno/cbtorpos, fs_frag>8 in fragtbl/around/inside) belong to the DF-0820 mount-validation family and were not re-filed.", "recommended_fix": "Add ffs_chkcg() validating every cg-header field (cg_cgx, rotors, and all map offsets/sizes against fs_cgsize) after each cg bread() in ffs_alloc.c, and clamp the bpref-derived cg in ffs_hashalloc() before it indexes fs_csp[] (fix.diff, fix-validated in-guest).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly v6.5.0.1712.g89e6a-DEVELOPMENT #1: Sat Sep 5 14:02:46 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to guest /usr/src (9 hunks clean), make nativekernel KERNCONF=X86_64_GENERIC -Werror-clean (ffs_alloc.o), installkernel, rebooted into #1 Sat Sep 5 14:02:46. Identical crafted images: cgx=0x40000000 -> NO panic, dd fails 'No space left on device' (poisoned cg rejected by ffs_chkcg -> ENOSPC); iusedoff=0x20000000 -> same graceful ENOSPC. Control image (cgx rewritten to its legitimate value 0): first allocation still succeeds (16384-byte file created) and unmounts cleanly, proving the validator does not reject legitimate newfs geometry. Baseline bad behaviour GONE.", "fix_evidence": "findings/poc/DF-3035/run.patched.log" } |