DF-5784 / verdict.json
{ "finding_id": "DF-5784", "status": "reproduced", "reproduced": true, "impact": "dos", "confidence": "certain", "verdict": "hammer2_chain_cmp() (hammer2_chain.c:109/111) never validates that key + (1<<keybits) fits the 64-bit keyspace; a crafted-image bref (key=0xC000000000000001, keybits=63, defined shifts) wraps its comparator range end below its begin, making the chain compare fully-left of every other chain. The mis-placed chain is pruned by hammer2_chain_find()s RB_SCAN (comp<0 descends right-only), so every lookup re-allocates a duplicate chain (chain_lookup:2610 -> chain_get -> chain_alloc), and iteration keys regress (key_next = key + (1<<keybits) wraps below key_beg, chain_next:2748) so directory iteration can never pass the element. Reproduced on the stock INVARIANTS guest: one directory read on the mounted image wedges getdents forever (process R2, kill -9 -> EPERM), umount hangs, interrupt-livelock messages on the console, and vfs.hammer2.chain_allocs creeps upward monotonically. Causality control (same patched key with keybits=0, no wrap) is completely benign: 200k entries, delta 0, umount OK. Fix (hammer2_bref_sane() + skip in hammer2_base_find) validated on a rebuilt kernel: crafted image now reads cleanly, CRAFTED_DELTA=0 over 1M entries, umount succeeds.", "exploit_chain": "", "evidence": "run_wedge_transcript.txt (unkillable R2 process, kill -9 EPERM, umount rc=124, chain_allocs 818->830 creep); run_control_craftC.log (keybits=0 causality control: CRAFTC_DELTA=0, umount rc=0); run.log + run_fix.log (baseline wedge vs patched CRAFTED_DELTA=0, AFTER_UMOUNT=821); forge_df5784.py (image forger); VERDICT.md for the full analysis", "kernel_refs": [ "sys/vfs/hammer2/hammer2_chain.c:108-117", "sys/vfs/hammer2/hammer2_chain.c:1931-1951", "sys/vfs/hammer2/hammer2_chain.c:1955-1969", "sys/vfs/hammer2/hammer2_chain.c:2606-2617", "sys/vfs/hammer2/hammer2_chain.c:2748-2749", "sys/vfs/hammer2/hammer2_chain.c:2594-2597", "sys/vfs/hammer2/hammer2_chain.c:4975-4986", "sys/vfs/hammer2/hammer2_admin.c:1000-1019" ], "poc_changes": "Wrote the forger fresh (DF-2616 rig technique: methods=CHECK_NONE along the crafted path + volhdr CRC32C recompute); wrote leakloop.c (readdir drain with live vfs.hammer2.chain_allocs sampling via sysctlbyname); added the keybits=0 causality control craftC.img after observing the wedge to isolate the wrap as the trigger", "attempts": 3, "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 (baseline); fix kernel #1 Mon Oct 12 15:27:54 UTC 2026", "runtime_sec": 4200, "guest_dirty": 0, "build_cmd": "guest: newfs_hammer2+mount+create f1 -> base.img; host: python3 forge_df5784.py base.img craftA.img craftB.img (+ craftC.img variant); guest kernel: cd /usr/src/sys/config && config -d /usr/obj/usr/src/sys/X86_64_GENERIC X86_64_GENERIC && cd /usr/obj/usr/src/sys/X86_64_GENERIC && make -j6 && make install", "run_cmd": "sh /root/poc/df5784/run_df5784.sh", "code_hash": "forge_df5784.py 00929839f27a3e2a5edede64ca3f5980ec16e48fb37666d1cd32c68849bec2fa; leakloop.c 2a6945277435ed3994270b8df550497984750997978e7a97ed75e6a602c7520e", "notes": "Escalation to memory corruption not found: duplicates are properly allocated chains; RB_REMOVE is linkage-based (no comparator), tree stays structurally valid; the loop allocates/frees within bounds. Recovery requires reboot. Guest left on the validated fix kernel; guest_dirty=0 (no stale mounts; image files under /root/poc only).", "recommended_fix": "Validate media blockrefs at array-scan time: add hammer2_bref_sane() (type in 1..6, no key-range wrap) and skip insane elements in hammer2_base_find() exactly like EMPTY slots", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to guest /usr/src, kernel rebuilt+installed (#1 Oct 12 15:27:54 2026), rebooted, exact PoC re-run: craftB leakloop completes 1,000,000 entries with CRAFTED_DELTA=0 (baseline on stock kernel: unkillable wedge + monotone chain growth), umount succeeds (AFTER_UMOUNT=821). Bad behavior gone.", "fix_evidence": "run_fix.log; fix_build.log; env.txt (fix kernel uname)" } |