⬢ DragonFlyBSD Kernel Audit
DF-2606 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-2606",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "dos",
  "confidence": "certain",
  "verdict": "REPRODUCED on the DragonFly 6.5-DEVELOPMENT stock kernel (guest /usr/src md5-identical to the audited tree). A fully checksum-valid crafted hammer2 image (newfs_hammer2, then keybits of an INDIRECT blockref in the PFS-root inode patched 63->65 with XXH64 chain checks and volume-header iCRC32s repaired) mounts with no rejection; on x86-64 the UB shift 1<<65 masks to 1<<1 and collapses a 2^63-byte key range to 2 bytes, so readdir still lists all 25 names while every name lookup returns ENOENT - lookup misses files that provably exist on media (pristine mount before/after lists and md5s them). Additionally, a file create on the rw-mounted crafted image drives hammer2_chain_indkey_dir over the keybits=65 entry and its keybits>64 check (hammer2_chain.c:4728-4731, twin of the cited :4555-4560) prints bad bref chain and calls Debugger(fubar), halting all CPUs and freezing the machine at the db> prompt - reproduced on two independent boots (serial-hang.log, serial-hang.2.log). Honest ceiling: no memory-safety primitive (all scans index-bounded; UB only produces masked shift counts), no unprivileged or remote trigger (vfs.usermount=0; vnconfig+mount require root) - impact is filesystem logic corruption plus a system-halt DoS, classified dos. privesc bucket closed. fix.diff (validate keybits<=63 in hammer2_chain_load_data, hammer2_base_find scan, hammer2_base_insert validate) was built via make nativekernel, installed and A/B validated: pristine image bit-identical (same entries and md5s), crafted image cleanly handled, and the exact rw stress that froze the stock kernel twice completes with zero kernel complaints.",
  "exploit_chain": "root crafts image: newfs_hammer2 -> populate -> clean unmount -> h2keybits patch sets INDIRECT keybits=65 and repairs XXH64 checks + volhdr iCRCs -> mount_hammer2 accepts (no rejection) -> (logic corruption) all name lookups under the collapsed range return ENOENT while readdir still lists the names, files unreachable though intact on media; (DoS) one file create on the rw mount hits hammer2_chain.c:4728 kprintf + Debugger(fubar) -> machine frozen in DDB until power cycle. No user-to-root chain exists: trigger is root-only malicious media, corruption is logical not memory-unsafe.",
  "evidence": "run.b.patched.log (mount not rejected; 25 names listed; stat/cat/ls all ENOENT), run.a.pristine.log + run C in run.sh output (same 25 entries and md5s before/after - data intact on media), dump.patched.log (keybits=65 entry, computed range [0..1], all XXH64/iCRC checks OK), serial-hang.log and serial-hang.2.log (bad bref chain + Debugger(fubar) + db> prompt, two boots), runfix.a.pristine.log and runfix.stress.log (fixed kernel: pristine bit-identical, stress completes alive), fix.diff, VERDICT.md",
  "kernel_refs": ["sys/vfs/hammer2/hammer2_chain.c:109", "sys/vfs/hammer2/hammer2_chain.c:1962", "sys/vfs/hammer2/hammer2_chain.c:2025", "sys/vfs/hammer2/hammer2_chain.c:4546", "sys/vfs/hammer2/hammer2_chain.c:4555", "sys/vfs/hammer2/hammer2_chain.c:4719", "sys/vfs/hammer2/hammer2_chain.c:4728", "sys/vfs/hammer2/hammer2_chain.c:4731", "sys/vfs/hammer2/hammer2_chain.c:4977", "sys/vfs/hammer2/hammer2_chain.c:4992", "sys/vfs/hammer2/hammer2_chain.c:5307", "sys/vfs/hammer2/hammer2_disk.h:429", "sys/vfs/hammer2/hammer2_disk.h:623", "sys/vfs/hammer2/hammer2_ondisk.c:501"],
  "poc_changes": "The original sketch assumed plain icrc checks and a raw byte-patch. Reality: default check algorithm is XXHASH64 (HAMMER2_CHECK_DEFAULT, hammer2_disk.h:736) with seed 0x4d617474446c6c6e, so the patcher had to recompute XXH64 of the PFS-root inode (1024B) into the sroot array entry, XXH64 of the sroot inode into the volume header, and the three volume-header iCRC32s in dependency order SECT1 then SECT0 then volheader (icrc_sects[6] lives inside sect0s covered range - computing sect0 first produces an invalid header, discovered and fixed during bring-up). Wrote a full structural walker (h2keybits.c) instead of scanning for bytes; also switched the demo target from the volume-header root bref to an INDIRECT entry inside the PFS-root inode because that is what base_find actually misroutes.",
  "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": 6300,
  "guest_dirty": 0,
  "build_cmd": "cc -O2 -Wall -o h2keybits h2keybits.c /usr/src/sys/libkern/icrc32.c /usr/src/sys/vfs/hammer2/xxhash/xxhash.c -I/usr/src/sys -I/usr/src/sys/vfs/hammer2/xxhash   (guest, see build.sh)",
  "run_cmd": "./mkimage.sh && ./run.sh   (stress: ./stress.sh freezes stock kernel; fix kernel: ./stress-fix.sh)",
  "code_hash": "bec127ccdd49a07134366b201027df3d1cacf5c62b650d902405f64a4965e190",
  "notes": "vn devices stay wedged for a while after hammer2 unmount on this old snapshot (kernel iod threads hold the devvp) - run.sh therefore uses one vn device per section. The DDB-halt trigger depends on where the fsid-salted dirhash of the new filename lands; a 60-name create batch guarantees a hit (2/2 boots). The finding cited :4555 (freemap variant); the live-reachable variant of the identical check is :4728-4731 (directory). Guest reset to clean snapshot after validation.",
  "recommended_fix": "Validate bref keybits <= 63 where on-media blockrefs enter range math: reject chains with keybits > 63 in hammer2_chain_load_data (chain->error = HAMMER2_ERROR_CHECK), skip such entries in hammer2_base_find forward scan and in hammer2_base_insert validate - see fix.diff (validated A/B)."
}