DragonFlyBSD Kernel Audit
DF-2616 / verdict.json
← back to finding ↓ download raw
{
 "finding_id": "DF-2616",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "panic",
 "confidence": "certain",
 "verdict": "REPRODUCED end-to-end. (1) Stock X86_64_GENERIC (INVARIANTS ON): a crafted hammer2 image whose sroot_blockset[0].data_off = 0x180fd0a (radix 10 = PERFECTLY VALID, lbase 0x180fd00 misaligned and crossing the 64KB DIO window) panics the kernel at mount time at hammer2_io.c:126 (panic_A.txt); the file-data variant (data_off=0x1c0ff0a) panics on the first read of the file (panic_B.txt) - deterministic local DoS from a mounted image. (2) A rebuilt kernel without INVARIANTS (release-style, build_noinv.log) removes the KKASSERT: the unconditional 'Illegal:' kprintf at hammer2_io.c:122-125 fires and execution continues, giving chain->data = bp->b_data + 0xFF00 with chain->bytes = 0x400: the OOB bcopy either page-faults in kernel mode (fault_B_noinv.txt: Fatal trap 12 at memmove+0x28, fault VA = bp->b_data+0x10000, DDB stop, ssh dead - DoS) or, with 32 marker files groomed one-per-64KB-window so the adjacent buffer KVA slot is mapped, read(2) returns out-of-bounds kernel-buffer bytes to userspace and write(2) plants attacker content through the crossed pointer (run_E_noinv.log + forensic_E.txt: pattern recovered from the flushed image at media offset 0x200ff05, exactly where chain->data pointed). uid=0 was NOT achieved: the adjacent-slot victim object could not be placed deterministically (fixed per-header KVA scheme, vfs_bio.c:638; the observed neighbor was a mapped but clean zero-filled buffer, so the past-end write half could not be recovered from media). One refinement to the finding discovered during verification: hammer2's freemap allocates at 1KB granularity, so radix-N blocks at merely-1KB-aligned offsets are LEGAL on real filesystems - the actually-missing invariant is exactly what io.c asserts (pbase!=0, radix<=16, no 64KB-window crossing); an alignment-based fix (v1) broke the root fs boot (fix_v1_too_strict.txt). Fix v2 (fix.diff) implements the correct predicate at every I/O issue point and was VALIDATED on the guest: root fs boots with zero false rejections, variant A mount fails cleanly (EINVAL), crossed file reads fail cleanly (EIO, 'illegal data_off geometry'), no panic, no fault (fix_run.log).",
 "exploit_chain": "crafted hammer2 image (root or vfs.usermount+owned device) -> mount -> chain load of crossed bref -> [INVARIANTS: panic; release: OOB] -> read(2) discloses bytes past the 64KB DIO buffer; write(2) via CHECK_NONE in-place path (chain.c:1504-1516) + hammer2_write_bp (strategy.c:1356) plants chosen content 0x300-0xFF00 bytes past the buffer end into the adjacent kernel buffer object. Demonstrated mechanically + forensically (in-window half on media); deterministic victim-object placement (->uid0) not achieved.",
 "evidence": [
  "panic_A.txt: mount-time panic, 'Illegal: 0000000001800000 000000000180fd00+00000400' + KKASSERT panic at hammer2_io.c:126 (stock kernel, valid radix 10)",
  "panic_B.txt: same panic loading the crafted FILE DATA chain (mount OK, cat /mnt/h2/f1)",
  "fault_B_noinv.txt: no-INVARIANTS kernel #1, Fatal trap 12 in memmove+0x28, fault VA 0xfffff8006be66000 = bp->b_data+0x10000 (OOB bcopy source off the 64KB buffer)",
  "run_E_noinv.log: groomed run on kernel #1 - crossed read survives (ILLEGAL_AFTER_READ=1), full write sequence, clean umount",
  "forensic_E.txt + h2_E_flushed.img: attacker pattern DF2616OOBWRITE recovered at media 0x200ff05 (write went through chain->data = bdata+0xFF00)",
  "fix_v1_too_strict.txt: legitimate radix-11/12/13 brefs at 1KB granularity that the over-strict v1 alignment check rejected (proof of the real invariant)",
  "fix_run.log: fixed kernel (#1 Aug 28 13:14, no-INVARIANTS + fix v2): variant A mount -> EINVAL clean; E10/E16 crossed reads -> EIO clean; root fs zero false rejections; guest stays up",
  "build_noinv.log / build_fix.log: full kernel build logs (rc=0)"
 ],
 "kernel_refs": [
  "sys/vfs/hammer2/hammer2_chain.c:189-192",
  "sys/vfs/hammer2/hammer2_chain.c:938-939",
  "sys/vfs/hammer2/hammer2_chain.c:994-1000",
  "sys/vfs/hammer2/hammer2_chain.c:1048",
  "sys/vfs/hammer2/hammer2_chain.c:1100",
  "sys/vfs/hammer2/hammer2_chain.c:1504-1516",
  "sys/vfs/hammer2/hammer2_chain.c:1819-1827",
  "sys/vfs/hammer2/hammer2_io.c:113-126",
  "sys/vfs/hammer2/hammer2_io.c:555-566",
  "sys/vfs/hammer2/hammer2_strategy.c:487",
  "sys/vfs/hammer2/hammer2_strategy.c:1310-1356",
  "sys/vfs/hammer2/hammer2_disk.h:458-461",
  "sys/kern/vfs_bio.c:638"
 ],
 "poc_changes": "Seed sketch replaced entirely: wrote a python3 image forger that walks volhdr->sroot->PFS-dir (incl. INDIRECT blockset arrays, BREF_TYPE_INDIRECT=2) to the file DATA brefs, sets methods=0x00 (CHECK_NONE, testcheck returns 1 - avoids all CRC recomputation) + modify_tid (enables overwrite-in-place), crosses data_off to lbase|0xFF00 with the ORIGINAL VALID radix, and recomputes only the three volhdr CRC32Cs (DF-0763's proven technique). Added grooming: 32 marker files each relocated (valid geometry) into their own 64KB window so their DIO buffers occupy adjacent KVA slots. Learned en route: hammer2 compresses small files (markers must be incompressible), radix-16 allocations burn ~2MB freemap each on small volumes, and buffer KVA slots are preassigned per header (vfs_bio.c:638).",
 "attempts": 8,
 "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Aug 28 13:14:29 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (stock baseline #0: Thu Jul 2 06:02:54 UTC 2026, INVARIANTS ON)",
 "runtime_sec": 13200,
 "guest_dirty": 1,
 "build_cmd": "guest: sh mkbase.sh; host: python3 forge_E.py base.img h2_E_groom10.img; guest: vnconfig+mount; fix kernel: cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel KERNCONF=X86_64_GENERIC",
 "run_cmd": "sh trigger_E.sh (groomed sequence) / vnconfig -c vn0 img && mount -t hammer2 /dev/vn0@testvol /mnt/h2 && cat /mnt/h2/f1",
 "code_hash": "2413428caffd82cd36167bf084b45d8880e63dc463e292284f08ba76c80db2bb",
 "notes": "Guest reset (with-src snapshot) performed after validation so the next spawn starts clean. Impact recorded as 'panic' (the deterministic demonstrated end-state on the stock kernel); the release-kernel primitive is a chosen-offset kernel-heap OOB read (bytes reached userspace; zeros in the observed run because the adjacent slot held a clean buffer) and OOB write of attacker content (mechanically proven via the crossed-pointer media forensics; the past-end half landed in an unflushable clean buffer). DF-0763's radix-magnitude class is incidentally also closed by fix v2 (radix<=16 bound).",
 "recommended_fix": "Validate data_off geometry (radix<=16 and nonzero, lbase>=64KB, block within one 64KB window - exactly the io.c KKASSERT invariant) in hammer2_chain_load_data, the OPTDATA path of hammer2_chain_modify, and dedup_off installation; fix.diff validated on-guest.",
 "fix_status": "fixed",
 "fix_log": "build_fix.log (v2 build rc=0, install rc=0) + fix_run.log (root fs zero false rejections; variant A mount EINVAL clean; E10/E16 crossed reads EIO clean; no panic/fault)",
 "fix_kernel_uname": "DragonFly 6.5-DEVELOPMENT no-INVARIANTS + fix v2 (built in-guest from /usr/src, build_fix.log)",
 "fix_baseline_reproduced": 1,
 "fix_patched_reproduced": 0,
 "fix_verdict": "fix v2 (data_off geometry validation at every I/O issue point: radix 1..16, lbase>=64KB, block contained in one 64KB window) VALIDATED on guest: root fs boots with zero false rejections (legit radix-11/12/13 at 1KB granularity accepted), crafted mount fails cleanly EINVAL, crossed file reads fail cleanly EIO, no panic, no fault. v1 alignment-based predicate was too strict and rejected legal blocks (fix_v1_too_strict.txt).",
 "fix_evidence": "fix_build.log (rc=0); fix_run.log (EINVAL/EIO clean rejections, root fs OK); fix_v1_too_strict.txt (why v1 wrong); fix.diff"
}