⬢ DragonFlyBSD Kernel Audit
DF-3106 / verdict.json
← back to finding ↓ download raw
{
 "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"
 ]
}