⬢ DragonFlyBSD Kernel Audit
DF-3223 / verdict.json
← back to finding ↓ download raw
{
 "finding_id": "DF-3223",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "panic",
 "confidence": "certain",
 "verdict": "REPRODUCED and FIX VALIDATED. prune_check_nlinks() (sys/vfs/hammer/hammer_prune.c:330-332) calls hammer_btree_extract_data() and, on a 0 return, immediately dereferences cursor->data->inode.nlinks \u2014 but hammer_btree_extract() returns 0 with cursor->data == NULL when the on-disk leaf has data_offset == 0 (hammer_btree.c:705 + 728-731, early return BEFORE the CRC gate), making the read a load from absolute kernel address 0x48 (nlinks offset in struct hammer_inode_data). Any live (delete_tid==0) HAMMER_RECTYPE_INODE record forged/corrupted to data_offset=0 panics the kernel the moment HAMMERIOC_PRUNE scans it (hammer prune / hammer cleanup / direct ioctl; SYSCAP_NOVFS_IOCTL-gated at hammer_ioctl.c:72 \u2014 untrusted-media + root-maintenance threat model, same gate as DF-0910). 3/3 baseline runs on stock kernel #0 from fresh resets: Fatal trap 12, fault VA 0x48, Stopped at hammer_ioc_prune+0x37f: cmpq $0,0x48(%rax) \u2014 the literal compiled form of the nlinks test \u2014 guest dead each time. fix.diff (NULL guard) built as kernel #2: identical PoC returns ioctl=0 with normal prune stats (6 scanned, 1 destroyed, 128 bytes), 0 Fatal traps, fs read/write/umount verified, guest up. No escalation: the deref address is the fixed struct offset 0x48 and both this and the forged-offset variants are read-only \u2014 no write primitive, no copyout.",
 "exploit_chain": "",
 "evidence": [
  "panic.txt \u2014 full serial console of baseline run 1: Fatal trap 12, fault virtual address = 0x48, Stopped at hammer_ioc_prune+0x37f: cmpq $0,0x48(%rax)",
  "run.log / run.2.log / run.3.log \u2014 3/3 identical baseline panics on stock kernel #0, guest down each time",
  "fixrun.log \u2014 kernel #2 + fix.diff: ioctl returned 0, scanrecords=6 rawrecords=1 bytes=128, grep -c Fatal trap -> 0, fs functional, UMOUNT_OK, guest up",
  "build.log \u2014 both in-guest kernel builds (incl. the documented mangled-patch artifact on kernel #1)",
  "patch_inode.c \u2014 offline forger: rewrites live inode leaf data_offset (elm+48) to 0 and fixes the node CRC (iscsi/v7)",
  "trigger_prune.c \u2014 HAMMERIOC_PRUNE (HAMMER_IOC_PRUNE_ALL, full PFS#0 range) trigger"
 ],
 "kernel_refs": [
  "sys/vfs/hammer/hammer_prune.c:330",
  "sys/vfs/hammer/hammer_prune.c:332",
  "sys/vfs/hammer/hammer_prune.c:321-350",
  "sys/vfs/hammer/hammer_prune.c:227",
  "sys/vfs/hammer/hammer_btree.c:705",
  "sys/vfs/hammer/hammer_btree.c:726-731",
  "sys/vfs/hammer/hammer_btree.c:736-738",
  "sys/vfs/hammer/hammer_disk.h:886-901",
  "sys/vfs/hammer/hammer_ioctl.c:72",
  "sys/vfs/hammer/hammer_ioctl.c:78-84"
 ],
 "poc_changes": "Adapted the DF-3187 offline image patcher to target live PFS#0 inode records (rec_type=0x0001, delete_tid==0, obj_id>1, localization PFS-bits==0) rewriting data_offset (leaf elm offset 48) to 0 instead of data_len; wrote a new trigger issuing HAMMERIOC_PRUNE with HAMMER_IOC_PRUNE_ALL and a localization 0..2 / full obj_id range (kernel takes localization/obj_id only). Image built per DF-3187 harness (newfs_hammer v7, f1, synctid). RW mount required (prune rejects ronly).",
 "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 (baseline); fix kernel #2: Tue Sep 8 18:43:09 UTC 2026",
 "runtime_sec": 3000,
 "guest_dirty": 0,
 "build_cmd": "cc -O -o patch_inode patch_inode.c && cc -O -I/usr/src/sys/vfs/hammer -o trigger_prune trigger_prune.c (guest); fix kernel: cd /usr/src && patch -p1 < fix.diff && make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel",
 "run_cmd": "./patch_inode /home/maxx/h1.img; vnconfig -c -S 10g /dev/vn0 /home/maxx/h1.img; mount -t hammer /dev/vn0 /mnt; ./trigger_prune /mnt",
 "code_hash": "30246674410957610afc728f8df374e1003e643058540c4ee01005d8d55dc3f0 (trigger_prune.c); cf8033bc58cc5d7f62af1c73f03e1d1de33ba5ab8eea00864df22cd69d9edc33 (patch_inode.c)",
 "notes": "Upstream master (github.com/DragonFlyBSD/DragonFlyBSD, 2026-09-08) carries the identical unguarded dereference \u2014 file frozen since 2008 (v 1.19). Process artifact documented in VERDICT.md/build.log: first fix build crashed identically because a wrong hand-written hunk header made patch(1) drop the added return; GCC folded the NULL deref into the absolute movq 0x48,%rax at hammer_ioc_prune+0x65d \u2014 re-confirming the faulting source line; corrected diff applied cleanly and validated. Forged data_offset != 0 variants of the same call site yield the DF-3187-class wild read (CRC walk) \u2014 read-only, no escalation either.",
 "recommended_fix": "Add a cursor->data == NULL guard between hammer_btree_extract_data() and the nlinks read in prune_check_nlinks() (hammer_prune.c:331); see fix.diff.",
 "fix_status": "fixed",
 "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #2: Tue Sep 8 18:43:09 UTC 2026   root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
 "fix_baseline_reproduced": 1,
 "fix_patched_reproduced": 0,
 "fix_verdict": "kernel #2 built in-guest from pristine tree + corrected fix.diff (Hunk #1 succeeded at 329); exact PoC re-run on an identically forged fresh image: ioctl returned 0, scanrecords=6 rawrecords=1 bytes=128, zero Fatal trap in /var/log/messages, filesystem write/read/remove and clean umount verified, guest stayed up. Baseline (stock #0) panicked 3/3 at the same input.",
 "fix_evidence": "fixrun.log (patched-kernel run: rc=0, 0 traps, UMOUNT_OK); run.log/run.2.log/run.3.log + panic.txt (baseline 3/3 fatal trap 12 @ 0x48); build.log (both kernel builds)"
}