DF-3111 / verdict.json
{ "finding_id": "DF-3111", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "hammer_direntry_namekey() ALG1 returns namekeys up to 0x7fffffffffff0000 with max_iterations=0xffffff and no guarantee that key+max_iterations stays positive; the nlookup/nrename/dounlink consumers do an unchecked signed cursor.key_end.key += max_iterations that wraps negative for crafted filenames, and hammer_btree_iterate() then truncates the directory hash-chain scan to its first element. An unprivileged user (GF(2)-inverting CRC-32 to craft two distinct names with an identical wrap-zone namekey) creates a permanently unfindable, permanently unremovable dirent that persists across remounts/reboots, plus an unremovable dirent+inode leak on every re-create: durable local filesystem poisoning / disk-space exhaustion. No memory corruption.", "exploit_chain": "solve wrap-zone name pair via 4-byte CRC-32 GF(2) inversion (segment-sum -> 0x7fffffff top bucket) + 3 alphabet brute bytes (whole-name crc zone) -> create nameA at key K -> create nameB (collision-bumped to K+1) -> lookup(nameB): btree_search returns nameAs record (first element >= K), bcmp fails, hammer_btree_iterate rejects K+1 vs wrapped negative key_end -> ENOENT forever; readdir still lists it; each further O_CREAT after a namecache miss appends another unremovable dirent at K+2, K+3, ...", "evidence": [ "run.log: stage1 unlink nameB : No such file or directory <-- BUG + readdir nameB cnt : 1", "run.log: stage3 persist stat nameB : No such file or directory <-- POISONED, persist unlink nameB : No such file or directory <-- UNREMOVABLE, after 5 re-creates ... cnt = 2 (was 1) <-- LEAK", "fix_run.log: same pair on fix.diff kernel: persist stat nameB : OK, persist unlink nameB : OK, cnt stays 1 after 5 re-creates" ], "kernel_refs": [ "sys/vfs/hammer/hammer_subs.c:943", "sys/vfs/hammer/hammer_subs.c:963", "sys/vfs/hammer/hammer_subs.c:1024", "sys/vfs/hammer/hammer_vnops.c:1234", "sys/vfs/hammer/hammer_vnops.c:2014", "sys/vfs/hammer/hammer_vnops.c:3324", "sys/vfs/hammer/hammer_btree.c:216", "sys/vfs/hammer/hammer_btree.c:277", "sys/vfs/hammer/hammer_object.c:705", "sys/vfs/hammer/hammer_inode.c:858" ], "poc_changes": "seed concept was image-crafted namekeys at wrap; actual unprivileged vector required (1) a deterministic identical-key PAIR solver (4-byte CRC-32 GF(2) inversion for the segment-sum + 3 alphabet brute bytes for the whole-name crc wrap zone; an append-4-bytes-to-state inversion is rank-deficient and was discarded), (2) create-order discipline (nameA keeps slot K occupied so nameB is bumped to K+1), (3) remount between create and probe to flush dirents to disk and cold the positive namecache that otherwise masks stat/open; trigger+recheck run as uid/gid 65534", "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", "runtime_sec": 2100, "guest_dirty": 0, "build_cmd": "cc -O2 -o /root/trig /root/trig.c && cc -O2 -o /root/recheck /root/recheck.c", "run_cmd": "sh /root/run.sh (root for fs setup; PoC stages drop to uid 65534)", "code_hash": "73ab06608146d97b", "notes": "hammer1 v7 image via newfs_hammer -f (v>=6 selects DIRHASH_ALG1 for new dirs, hammer_inode.c:858-862); plain libkern crc32 (not hammer_datacrc) used by namekey, verified vs solver; kernel built with -fno-strict-overflow so the wrap is deterministic; create-side ++key walk past INT64_MAX (hammer_object.c:708 KKASSERT) noted but impractical (needs ~16M successive occupied keys); fix validated on rebuilt kernel #1", "recommended_fix": "clamp *max_iterationsp in hammer_direntry_namekey() so key + max_iterations <= INT64_MAX", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Sep 6 12:02:37 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to /usr/src in guest, make nativekernel + installkernel + reboot (kernel #1); exact PoC re-run: stage1 unlink nameB : OK + readdir cnt 0; focused durable check (pair created, remounted): persist stat nameB : OK, persist unlink nameB : OK, no leak after 5 re-creates \u2014 poisoning gone", "fix_evidence": [ "fix_run.log", "fix_run_stage1.log", "fix.diff" ] } |