⬢ DragonFlyBSD Kernel Audit
DF-5836 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5836",
  "status": "reproduced",
  "reproduced": true,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "hammer2_inode_create_normal() guards the inode_get() duplicate-inum return with only a KKASSERT (sys/vfs/hammer2/hammer2_inode.c:1158-1159), compiled out on production kernels. The pmp inum allocator is seeded from untrusted on-media iroot meta.pfs_inum (hammer2_vfsops.c:1977), so a crafted image makes the first allocated inum collide with a live materialized inode; create_normal then stomps the live inodes meta (type/uid/mode/nlinks/DIRECTDATA with stale size), flags it CREATING, and the next fs sync double-inserts a duplicate chain into the media topology. Reproduced on the guest: crafted image (pfs_inum 1026->1023) + mount + stat origfile (inum 1024) + mkdir -> panic assertion ... failed in hammer2_inode_create_normal at hammer2_inode.c:1159 (trace create_normal <- vop_nmkdir <- kern_mkdir <- sys_mkdir). Production kernels skip the assert and proceed with the stomp (code-verified straight-line). Fix validated A/B on the same image: runtime check + EEXIST refusal -> no panic, origfile byte-identical, sync/umount clean.",
  "exploit_chain": "root (or vfs.usermount=1) mounts crafted hammer2 image whose iroot meta.pfs_inum=1023 seeds inode_tid=1024 == existing origfile inum -> unprivileged mkdir on the mount -> hammer2_trans_newinum returns 1024 -> hammer2_inode_get returns the live origfile inode -> (INVARIANTS) panic at inode.c:1159 / (production) live-inode metadata clobber + CREATING set on fully-linked inode -> next sync attempts duplicate chain insertion (media topology corruption, inode identity confusion).",
  "evidence": "run.log & boot_panic_raw.txt (full serial: mount, panic at hammer2_inode.c:1159 with stack); run.fixed.log (patched kernel: mkdir refused EEXIST, dmesg duplicate inode number 1024 detected, create refused, origfile unchanged, clean umount); craft_image.py (self-verifying image patcher: pfs_inum + check-disable + CRC-32C recompute); build_image.sh / run.sh; fix.diff (runtime duplicate-inum check); env.txt",
  "kernel_refs": [
    "sys/vfs/hammer2/hammer2_inode.c:1156",
    "sys/vfs/hammer2/hammer2_inode.c:1158",
    "sys/vfs/hammer2/hammer2_inode.c:1160",
    "sys/vfs/hammer2/hammer2_inode.c:1165",
    "sys/vfs/hammer2/hammer2_inode.c:1212",
    "sys/vfs/hammer2/hammer2_inode.c:1257",
    "sys/vfs/hammer2/hammer2_inode.c:1743",
    "sys/vfs/hammer2/hammer2_vfsops.c:1977",
    "sys/vfs/hammer2/hammer2_flush.c:308",
    "sys/vfs/hammer2/hammer2_flush.c:902",
    "sys/vfs/hammer2/hammer2_vfsops.c:2685"
  ],
  "poc_changes": "PoC authored from scratch (no seed): guest-side image builder (newfs_hammer2 via vnconfig), host-side pure-python CRC-32C patcher disabling the two parent-link check codes (bref methods=0x00) and rewriting iroot meta.pfs_inum 1026->1023, plus trigger script (mount + stat origfile to materialize inum 1024 + mkdir). Two patcher iterations: icrc_sects are u32 not u64, and icrc_volheader lives at 0xFFFC (last 4 bytes of the 64KB header), not 0xFFC.",
  "attempts": 3,
  "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": 1500,
  "guest_dirty": 0,
  "build_cmd": "guest: make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel (for fix validation); host: python3 craft_image.py h2.img h2c.img",
  "run_cmd": "sh run.sh  (mount TESTFS@vn0 /mnt/h2; stat origfile; mkdir boomdir)",
  "code_hash": "a5688234d7f0e31c655a2eb3a1231b161b2ebceaaf9f9fb65077cf6d1dadb097",
  "notes": "First fix attempt double-dropped the found inode (unlock+drop vs the single lookup ref) and crashed in hammer2_vop_getattr on the guest; corrected to unlock() only and re-validated. Guest kernel is an INVARIANTS build, hence panic-on-trigger; the production-kernel stomp path (lines 1160-1215) is straight-line with no further guards. Non-crafted crash-consistency trigger theoretically possible (stale on-media pfs_inum after lost iroot flush); not reproduced.",
  "recommended_fix": "Replace the KKASSERT in hammer2_inode_create_normal() with a runtime duplicate-inum check that refuses the create with EEXIST (see fix.diff); optionally also sanity-scan the seed at mount.",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Baseline stock kernel: panic at hammer2_inode.c:1159 on mount+stat+mkdir of the crafted image. Patched kernel (fix.diff, nativekernel rebuild, reboot): same image and commands -> mkdir fails EEXIST, duplicate inode number 1024 detected, create refused on console, origfile metadata and content byte-identical, second create also refused, sync+umount clean. Bad behavior gone.",
  "fix_evidence": "run.log vs run.fixed.log; fix.diff"
}