DragonFlyBSD Kernel Audit
DF-2662 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-2662",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "hammer2_update_spans() (hammer2_iocom.c:285) walks the super-root PFS entries asynchronously after every mount (invoked from hammer2_autodmsg's VOLDATA DUMP branch at iocom.c:237) and dereferences chain->data->ipdata without checking chain->error: an inode chain whose load failed with EIO comes back from hammer2_chain_lookup with data == NULL (chain.c:996-1001), producing a NULL+offset read (meta.pfs_clid at +0x90). Reproduced in isolation on kernel B (stock + DF-2661 fix + EIO injector): Fatal trap 12, fault VA 0x90, 'Stopped at hammer2_autodmsg+0x273: movq 0x90(%r15),%rax' with r15 = chain->data = NULL; objdump of the faulting region matches the ripdata->meta.pfs_clid/pfs_fsid/pfs_type/filename field copies (offsets 0x90/0x98/0xa0/0x100). This bug is pre-existing and was masked on stock kernels by DF-2661's earlier panic in the same EIO workload. Unlike DF-2661 it needs no concurrency - the async post-mount walk alone suffices if the PFS inode read fails. Fix (skip chains with data == NULL, and advance the iteration instead of the old bare continue which could spin forever on non-INODE entries) validated on kernel C: identical 5-round EIO storm survives completely (twice).",
  "exploit_chain": "N/A for privilege escalation: pure NULL-pointer dereference (read of fixed offsets from NULL), no attacker-controlled memory content. Impact ceiling is local DoS on device read error during/after mount.",
  "evidence": "panic.txt (Fatal trap 12 transcript + root-cause annotation); disasm_autodmsg.txt (objdump pinning the faulting loads to the ripdata field copies); fix_run.log (kernel C: ROUND_SURVIVED x5 + ALL_ROUNDS_SURVIVED); build.log (kernel B baseline build), fix_build.log (kernel C build); VERDICT.md narrative",
  "kernel_refs": [
    "sys/vfs/hammer2/hammer2_iocom.c:285-325",
    "sys/vfs/hammer2/hammer2_iocom.c:237",
    "sys/vfs/hammer2/hammer2_chain.c:985-1006"
  ],
  "poc_changes": "No seed existed (found while validating DF-2661's fix). The DF-2661 trigger is reused verbatim; the discrimination experiments (ARM=0 control surviving 5/5 on kernel B; storm crash on kernel B; storm survival on kernel C) isolate this bug from both DF-2661 and generic mount/umount racing.",
  "attempts": 2,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Aug 30 06:22:36 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (kernel B, crash baseline); kernel C #2 Sun Aug 30 06:51:32 UTC 2026 (fix validation)",
  "runtime_sec": 900,
  "guest_dirty": 1,
  "build_cmd": "cd /usr/src && patch -p1 < /root/df2661/inject.diff && patch -p1 < /root/df2661/fix_b.diff && patch -p1 < /root/df2662/fix.diff && make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel KERNCONF=X86_64_GENERIC",
  "run_cmd": "sh /root/df2661/trigger_df2661.sh golden && sh /root/df2661/trigger_df2661.sh 5",
  "code_hash": "232bdf6aeb801fc52c75bc87367a6dda3a244557896755899ba18a40fca2e1ec",
  "notes": "Found during DF-2661 Phase V: fixing the first panic unmasked this second one. Secondary latent bug fixed in the same hunk: the bare 'continue' for non-INODE chains never advanced the iteration (infinite kernel loop if the super-root ever contains a non-INODE entry). Guest reset with-src afterwards.",
  "recommended_fix": "In hammer2_update_spans(): treat chains with chain->data == NULL (failed load) like non-INODE chains, and advance the iteration with hammer2_chain_next() before continuing.",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #2: Sun Aug 30 06:51:32 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (kernel C)",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Baseline kernel B crashed at hammer2_autodmsg+0x273 (NULL deref of chain->data in the update_spans walk). Kernel C (fix applied) survived the identical 5-round EIO storm twice: ROUND_SURVIVED x5, ALL_ROUNDS_SURVIVED, RC=0. fix_baseline_reproduced=1 (trap before fix), fix_patched_reproduced=0 (no trap after fix).",
  "fix_evidence": "fix_run.log (kernel C storm survival); panic.txt + disasm_autodmsg.txt (kernel B baseline crash); build.log / fix_build.log"
}