DF-3106 / verdict.json
{ "finding_id": "DF-3106", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Reproduced on the stock INVARIANTS guest kernel: a crafted UFS image (4K/512 geometry, one 512-byte directory fragment) makes ufs_direnters compaction loop bcopy 264-byte blocks from poison entries whose DIRSIZ(255)=264 dwarfs their d_reclen of 4-8, walking the destination pointer past the end of the buffer-cache buffer. Variant A proves the precise 16-byte OOB write (bcopy at dirbuf+264..528 in a 512-byte buffer) followed by panic: ufs_direnter: compact2; variant B proves massive uncontrolled-extent corruption (Fatal trap 12, supervisor WRITE page fault inside memmove after ~22.7KB/86 iterations past the buffer). Content is fully attacker-crafted on-disk bytes; extent is attacker-chosen (region layout x 264B). The post-loop compact1/compact2 panic is arithmetically guaranteed after the writes, so the corrupting thread always dies right after smashing adjacent kernel heap - uid=0 escalation was not achieved in this run (would require an SMP consumer of the corrupted objects in the pre-panic window). fix.diff (compaction-loop entry validation) validated on a rebuilt kernel: both variants return clean EIO with no panic and normal create/delete compaction regression passes.", "exploit_chain": "crafted UFS image -> mount RW (root or vfs.usermount=1) -> unprivileged creat(2) with 160-char name in crafted 512B-frag dir -> ufs_lookup returns COMPACT region [slotoffset, slotoffset+slotsize) containing poison entries -> ufs_direnter compaction bcopy writes 264B per poison entry at dirbuf+264*k, walking past the 512B buffer -> adjacent kernel heap (buffer-cache neighbors) corrupted with attacker bytes -> panic (compact1/compact2) immediately after. OOB write established; privilege escalation blocked by the guaranteed post-corruption panic (no surviving corruptor thread) - documented as the hard blocker.", "evidence": [ "findings/poc/DF-3106/panicA.txt: panic: ufs_direnter: compact2 with ufs_direnter<-ufs_makeinode<-ufs_create backtrace", "findings/poc/DF-3106/panicB.txt: Fatal trap 12 ... supervisor write data, page not present stopped in memmove+0x32 - the compaction bcopy faulting on its own OOB write", "findings/poc/DF-3106/runA.patched.log: fixed kernel #1 returns Input/output error (EIO), guest stays up", "findings/poc/DF-3106/runB.patched.log: fixed kernel, variant B: clean EIO, no page fault", "findings/poc/DF-3106/VERDICT.md: full deterministic derivation of the 16B OOB (variant A) and 22.7KB walk (variant B)" ], "kernel_refs": [ "sys/vfs/ufs/ufs_lookup.c:808", "sys/vfs/ufs/ufs_lookup.c:831", "sys/vfs/ufs/ufs_lookup.c:843", "sys/vfs/ufs/ufs_lookup.c:857", "sys/vfs/ufs/ufs_lookup.c:862", "sys/vfs/ufs/ufs_lookup.c:264", "sys/vfs/ufs/ufs_lookup.c:286", "sys/vfs/ufs/dir.h:117" ], "poc_changes": "Seed sketch had no on-disk layout; authored patch_dir.c with two layouts. Two craft corrections were needed during verification: (1) a poison entrys d_reclen bytes collide with the next entrys d_ino bytes, so the first poison needs a hand-built 8-byte header (reclen=8) to hop onto the free trigger entry; (2) the 4-periodic W pattern must cover the tail entrys inode field and the chain must land exactly on the buffer end (an entry header straddling the end trips the walks mangled-entry check - which itself exposed a 2-6 byte OOB read in ufs_lookup:264, noted in the finding).", "attempts": 5, "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": 0, "build_cmd": "cc -O2 -Wall -o patch_dir patch_dir.c (guest, gcc 8.3 DragonFly)", "run_cmd": "./craft_image.sh A && ./trigger.sh A (guest, root; trigger exec as unpriv user maxx)", "code_hash": "5fdb3df1207adbc000aaae6ec0bdc3fd4279350bbc0985af35af2513a552d698", "notes": "debug.dircheck=0 on stock kernels (DIAGNOSTIC only) is what leaves d_namlen unvalidated; the fix validates each nep inside the compaction loop (reclen!=0, reclen<=i_count-loc, reclen>=DIRSIZ for live entries; B_INVAL+brelse+EIO on failure). Fix validated: baseline panics vs patched EIO + compaction regression (50/50 files) clean. First two craft attempts panicked earlier via ufs_dirbad (mangled entry) - those logs became DF-3108 corroboration.", "recommended_fix": "In ufs_direnters compaction loop, validate nep (d_reclen != 0, d_reclen <= dp->i_count - loc, and d_reclen >= DIRSIZ(nep) for live entries) before any ep mutation/bcopy; bail with B_INVAL+brelse+EIO on corrupted regions (see fix.diff).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Sep 6 10:46:38 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 (compaction-loop entry validation, B_INVAL+brelse+EIO on corrupt regions) applied in-guest, nativekernel rebuilt (KERNEL_RC=0), installed, rebooted into #1: variant A and B triggers both return EIO with no panic and no page fault; guest stays up. Normal create/delete compaction regression passes (50/50 files). Baseline (#0) had panicked on both variants.", "fix_evidence": [ "findings/poc/DF-3106/fix_run.log (patched runs A+B + regression)", "findings/poc/DF-3106/fix_build.log", "findings/poc/DF-3106/panicA.txt", "findings/poc/DF-3106/panicB.txt" ] } |