DragonFlyBSD Kernel Audit
DF-2618 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-2618",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "REPRODUCED on the stock INVARIANTS kernel: a forged hammer2 image whose indirect blockref array contains two overlapping [key,key+2^keybits-1] ranges (INODE(1024)->[1024,1025], INODE(1025)->[1025,1026]) panics deterministically at hammer2_chain_insert's KASSERT (chain.c:314) on mount+stat, with the exact claimed backtrace (hammer2_xop_nresolve -> hammer2_chain_inode_find -> hammer2_chain_lookup -> hammer2_chain_get -> hammer2_chain_insert, panic key=0x401). Verified twice across fresh boots (panic.txt, panic2.txt); control image fully clean; guest down each time. The release-build consequence (RB_INSERT collision -> phantom chain with ONRBTREE set but never linked -> later RB_REMOVE takes the NULL-parent branch and executes RB_ROOT(head)=NULL at sys/sys/tree.h:651-652, wiping the parent's live rbtree -> chain leak + lookup misses) is verified by line-precise source trace (chain.c:203-204 M_ZERO alloc, tree.h:674-677 no RB_SET on collision, chain.c:317-319 flags set anyway, chain.c:618-628 lastdrop RB_REMOVE) but not demoed on a no-INVARIANTS kernel. fix.diff (graceful collision refusal at chain_insert + loop-safe skip-past-corrupt-entry at all chain_get call sites) rebuilt as kernel #1: the identical crafted image mounts, lists, and stats without panic (corrupt-shadowed names ENOENT, one rate-limited console collision message), writes to the corrupt directory succeed, control image unchanged, system stable. fix_status=fixed.",
  "exploit_chain": "unprivileged (post-mount) namei on a crafted/corrupt hammer2 volume -> hammer2_chain_inode_find(inum) -> chain_lookup: base_find selects the overlapping bref while the pinned sibling chain does not cover the key -> chain_get -> RB_INSERT -> hammer2_chain_cmp overlap==match -> collision -> KASSERT panic (INVARIANTS) or phantom chain (release) whose lastdrop RB_REMOVE nulls parent->core.rbtree.rbh_root (tree.h:651-652), structurally corrupting the chain topology. Impact ceiling: kernel DoS (verified) + in-memory chain-cache corruption/leak on release builds (traced); no R/W primitive demonstrated, consistent with the filed Medium severity.",
  "evidence": [
    "panic.txt + panic2.txt: two identical serial captures of 'panic: hammer2_chain_insert: collision ... (key=0000000000000401)' with the full backtrace (fresh boot each)",
    "run.log / run2.log: trigger output stops exactly at 'stat /mnt/h2/c' both times",
    "control.log / fix_control_run.log: pristine image fully functional on stock and patched kernels (forge, not the base image, causes the bug)",
    "forge_df2618.py: walks volhdr->sroot->PFS->indirect array, sets keybits so sibling INODE ranges overlap, CHECK_NONE ancestors, recomputes volhdr CRC32Cs",
    "VERDICT.md section 3: line-by-line release-build root-wipe trace",
    "fix_build.log (38176 lines, BUILD_RC=0) + fix_run.log + fix_write_smoke.log + dmesg.txt: patched kernel #1 handles the same image gracefully (console 'hammer2_chain_insert: collision ... key=...0401' rate-limited message, no panic, write path OK)"
  ],
  "kernel_refs": [
    "sys/vfs/hammer2/hammer2_chain.c:97-118",
    "sys/vfs/hammer2/hammer2_chain.c:203-204",
    "sys/vfs/hammer2/hammer2_chain.c:313-320",
    "sys/vfs/hammer2/hammer2_chain.c:618-628",
    "sys/vfs/hammer2/hammer2_chain.c:2089-2101",
    "sys/vfs/hammer2/hammer2_chain.c:2610-2617",
    "sys/vfs/hammer2/hammer2_chain.c:4975-4986",
    "sys/vfs/hammer2/hammer2_chain.c:5640-5731",
    "sys/sys/tree.h:641-652",
    "sys/sys/tree.h:661-688"
  ],
  "poc_changes": "Seed had no runnable code. Wrote mkbase2618.sh (4-file base image), forge_df2618.py (overlap forge inside the root directory's INDIRECT array - the seed sketch assumed direct blockset entries, but hammer2 pushes children into indirects even at 4 files; cribbed the volhdr/sroot/PFS walk, CHECK_NONE ancestors and CRC32C recompute from DF-2616/DF-2617/DF-2620), and trigger.sh. Deterministic holder: stat a pins chain(1024) via the inode cache; stat c crosses the overlap - the planned fd-pin turned out unnecessary. The readdir-only trigger from the seed does NOT fire (readdir sweeps only the dirent-hash space, vnops.c:673); the inum-keyed inode_find path is the working trigger.",
  "attempts": 2,
  "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": 2520,
  "guest_dirty": 0,
  "build_cmd": "sh build.sh  (guest: newfs_hammer2 -L testvol + files a,b,c,d + sync + umount; host: python3 forge_df2618.py base2618.img craft2618.img)",
  "run_cmd": "sh run.sh  -> sh /root/poc/df2618/trigger.sh control (clean) then craft (panic at stat /mnt/h2/c); serial via dfbsd-qemu/vm.sh log",
  "code_hash": "9a921b58489f8f88440e122dfae0e2f6d2e91b0efdbd763566ae572a80b2b2b9",
  "notes": "Overlap entries were forged on file-INODE brefs (key=inum); DIRENT entries could be forged equally (same cmp path). Mount itself survives (recovery scan drops each chain before inserting the next), so the panic lands on the first lookup that holds one sibling while fetching the overlapping one. Guest left clean: vm.sh reset with-src, stock kernel #0, up. Release-build root-wipe verified by trace only (no-INVARIANTS kernel build out of time budget); INVARIANTS panic proves the overlap primitive. Pre-existing adjacent behavior noted: hammer2_chain_inode_find already validates inum mismatch (chain.c:5713-5723), which is why stat b returns ENOENT rather than confusing inodes.",
  "recommended_fix": "hammer2_chain_insert: on RB_INSERT collision, rate-limited kprintf + return HAMMER2_ERROR_CHECK without setting ONRBTREE/parent/chain_count; hammer2_chain_get returns the errored unlinked chain; lookup/scan (and the two flush-time chain_get sites) skip past bref->key + (1<<keybits) so iteration stays finite (see fix.diff)",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Fri Aug 28 19:52:28 UTC 2026     root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC  x86_64",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "fixed: identical PoC on the patched kernel mounts/lists/stats without panic (corrupt-shadowed names degrade to ENOENT, one rate-limited collision message on console); control image identical to stock; write path on the corrupt volume + clean umount + system stability verified; kernel boots and roots on hammer2 throughout",
  "fix_evidence": "fix.diff (git apply --check clean, 8 hunks applied in-guest), fix_build.log (BUILD_RC=0), fix_control_run.log, fix_run.log, fix_write_smoke.log, dmesg.txt (fix collision message on patched kernel)"
}