DF-2662 / verdict.json
{ "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" } |