hammer2_chain_cmp key-range end overflow: crafted bref with key + (1<<keybits) > 2^64 wraps the comparator range, mis-ordering the in-memory rbtree (pruned by RB_SCAN, duplicated by every lookup) and regressing iteration keys β unkillable getdents live-lock, hung umount, and unbounded kernel chain leak from a mounted crafted image
Summary
Nothing validates that a media blockref's key range fits the keyspace: with key=0xC000000000000001 and keybits=63 (defined shifts, not the DF-2606 UB class) the range end wraps below the key, hammer2_chain_cmp report the chain fully-left of everything (including its own left neighbors), RB_SCAN prunes it, and every lookup re-creates a duplicate chain - one leaked hammer2_chain per iteration until unmount - while key_next regresses so directory iteration can NEVER advance: getdents live-locks unkillably (kill -9 -> EPERM), umount hangs, interrupt-livelock console spam, chain_allocs monotone creep. Stock-kernel-reproduced with a full causality control (same key, keybits=0: 200k entries, delta 0, clean umount - the wrap is the trigger). bref_sane+skip fix validated on a rebuilt kernel (1M entries, delta 0, clean umount). No corruption primitive - exhaustion/wedge only, but the system does not recover without hard reset.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5784 Β· 21 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.2 KB | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| forge_df5784.py | β | 8.5 KB | view raw | |
| leakloop.c | β | 1.5 KB | view raw | |
| build.sh | β | 222 B | view raw | |
| run_df5784.sh | β | 1.4 KB | view raw | |
| base.img | β | 64.0 MB | β download | |
| craftA.img | β | 64.0 MB | β download | |
| craftB.img | β | 64.0 MB | β download | |
| craftC.img | β | 64.0 MB | β download | |
| run.log | β | 890 B | view raw | |
| run_wedge_transcript.txt | β | 1.5 KB | view raw | |
| run_control_craftC.log | β | 250 B | view raw | |
| run_fix.log | β | 637 B | view raw | |
| build.log | β | 242 B | view raw | |
| fix_build.log | β | 291 B | view raw | |
| fix.diff | β | 2.0 KB | view raw | |
| env.txt | β | 375 B | view raw | |
| code_hashes.txt | β | 157 B | view raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-5784 β hammer2 chain comparator key-range overflow (pass 3)
Finding (short)
hammer2_chain_cmp() (sys/vfs/hammer2/hammer2_chain.c:97-118) computes a
chain's key range as end = key + ((hammer2_key_t)1 << keybits) - 1 with
no validation that the range wraps the 64-bit keyspace. A crafted
filesystem image can carry a blockref whose key + (1 << keybits) > 2^64
(e.g. key=0xC000000000000001, keybits=63 β both fully defined shifts, no
UB). The in-memory chain for that bref then compares as "fully to the left"
(end < other.key) of every other chain β including chains to its own
left β so:
- the chain is mis-placed in the parent's in-memory rbtree, and
hammer2_chain_find()'sRB_SCAN(tree.h:comp < 0descends RIGHT only) prunes it, so every lookup re-creates a duplicate chain from the same media bref (hammer2_chain_lookupβhammer2_chain_getβhammer2_chain_alloc, hammer2_chain.c:2610-2614), and - iteration keys regress:
key_next = key + (1 << keybits)wraps belowkey_beg(hammer2_chain.c:2748-2749, 2594-2595), so directory iteration can never advance past the element β the readdir xop backend/frontend (hammer2_xop_readdir,hammer2_xop_collect'scollect_keyadvance, hammer2_admin.c:1000-1019) live-lock.
Observed on the guest (stock INVARIANTS kernel): mounting the crafted image
and issuing one directory read leaves the reading process stuck in
getdents forever, unkillable (kill -9 β Operation not
permitted), umount hangs, a CPU pegged with kernel interrupt-livelock
messages (intr 16 on cpu1 at 40001/40000 hz, livelock limit engaged!),
and the live chain counter vfs.hammer2.chain_allocs creeps upward
(monotone leak; every lookup allocates a new duplicate chain that is
dropped to 0 refs but remains in the rbtree until unmount β which can no
longer happen). Threat model identical to DF-2616/DF-2617/DF-2618:
crafted hammer2 image, mounted by root (or unprivileged user with
vfs.usermount=1 and an owned vnode device).
Distinct from the known findings: DF-2618 is about overlapping ranges (cmp()==0 collisions/panics); DF-2606 is about UB shifts (keybits β₯ 64). This is a fully-defined-shift integer wrap of the range end that breaks the strict-weak ordering silently β no assert fires, no panic, the system just wedges and leaks.
Files
forge_df5784.pyβ host-side forger (builds craftA/craftB from base.img)craftCvariant: same patched key withkeybits=0(no wrap) β causality control, built with an inline python snippet (see build.sh)leakloop.cβ drains a directory via readdir(3), prints livevfs.hammer2.chain_allocsrun_df5784.shβ guest runner (control + crafted)run_wedge_transcript.txtβ captured wedged-state evidencerun_control_craftC.logβ causality control output (200000 entries, CRAFTC_DELTA=0, umount OK)fix.diffβ validated fix (skip insane media brefs inhammer2_base_find()'s forward scan +hammer2_bref_sane()helper)
Build
- guest (root):
sh mkbase.sh-equivalent (see build.sh) βbase.img - host:
python3 forge_df5784.py base.img craftA.img craftB.img - host: inline python for
craftC.img(keybits=0 variant) β see build.sh - push to guest
/root/poc/df5784/
Run (guest, root)
sh /root/poc/df5784/run_df5784.sh
Expected
- control (base.img): leakloop completes 1,000,000 entries quickly, chain_allocs delta β 0, umount OK.
- crafted (craftB.img): leakloop never completes; the ssh session must
be abandoned; from a second shell:
psshows the loopR2,kill -9βOperation not permitted,umounthangs,chain_allocscreeps up. - causality control (craftC.img, same key, keybits=0): 200,000 entries complete, CRAFTC_DELTA=0, umount rc=0 β proves the wrap (not merely the odd key) is the trigger.
Fix validation
fix.diff makes hammer2_base_find() skip media elements that fail
hammer2_bref_sane() (unknown type or wrapped key range), exactly as it
skips EMPTY slots β lookup/scan/readdir then never select the poisoned
element. Validated by rebuilding the kernel in the guest and re-running:
crafted image mounts, ls returns (entry invisible), leakloop completes
with delta 0, umount OK, no wedge. See VERDICT.md.
DF-5784 β VERDICT
Status: reproduced (impact: dos β unkillable kernel-side live-lock + unbounded kernel memory growth from a mounted crafted hammer2 image; the mount cannot be unmounted afterwards). Fix validated (kernel rebuilt in guest: wedge gone, chain counter flat, umount clean).
Root cause (code)
hammer2_chain_cmp() (sys/vfs/hammer2/hammer2_chain.c:97-118) computes
c1_end = c1_beg + ((hammer2_key_t)1 << chain1->bref.keybits) - 1; /* :109 */
Nothing between media ingest (hammer2_chain_get β hammer2_chain_alloc,
hammer2_chain.c:2050-2098 / 177-237) and this comparator validates that
key + (1 << keybits) does not exceed 2^64. A crafted image bref with
key = 0xC000000000000001, keybits = 63 (all shifts fully defined) has
end = 0xC000000000000001 + 0x7FFFFFFFFFFFFFFF (mod 2^64) = 0x4000000000000000 < key
so cmp(wrapped, X) returns -1 ("fully to the left") for every X β and
cmp(X, wrapped) also returns -1 whenever X.end < wrapped.key. Two
consequences, both demonstrated on the guest:
-
rbtree mis-ordering + duplicate chains. The wrapped chain is inserted as a left child of everything (RB_INSERT uses cmp==0 only for true overlap β none here, so no assert fires even on INVARIANTS).
hammer2_chain_find()(hammer2_chain.c:1931) runsRB_SCANwithhammer2_chain_find_cmp(hammer2_chain.c:1955-1969); DragonFly's RB_SCAN descends RIGHT-only whenscancmp < 0(sys/sys/tree.h,_##name##_RB_SCAN), so the scan prunes the mis-placed chain.hammer2_combined_findstill selects its media bref viahammer2_base_find(hammer2_chain.c:4975-4986 forward scan breaks onscan->key > key_beg), sohammer2_chain_lookup(:2610-2614) callshammer2_chain_getβ a fresh duplicate chain is allocated, inserted, returned, dropped to 0 refs β and never found again. Live countervfs.hammer2.chain_allocscreeps upward monotonically; the duplicates can only be reclaimed at unmount β which hangs (see 2). -
Iteration regression + unkillable getdents live-lock. Iteration advance computes
key_beg = key + (1 << keybits)(hammer2_chain.c:2748-2749 inhammer2_chain_next, :2594-2595 inhammer2_chain_lookup, andhammer2_base_find'sscan_end:4992) β for the wrapped bref this is below the current position, so neither the chain iterator norhammer2_xop_collect'scollect_key = lokey+1advance (hammer2_admin.c:1000-1019) can move the scan past the element. The readdir xop backend/frontend ping-pong (h2colltsleep vs. FIFO stall wakeups) and the userspacegetdentsnever returns.
Reproduction (stock INVARIANTS kernel #0 Jul 2 2026)
- control
base.img: 1M-entry readdir drain completes instantly, chain delta 0, umount OK. craftB.img(f1 dirent bref key=0xC000000000000001 keybits=63; ancestors methods=CHECK_NONE; volhdr CRCs recomputed): singleleakloop(readdir loop) never completes; processR2;kill -9βOperation not permitted;umountβ hangs (rc=124); serial console showsintr 16 on cpu1 ... livelock limit engaged!; chain_allocs creeps (818β830 over minutes). Transcript:run_wedge_transcript.txt.- causality control
craftC.img(SAME patched key, keybits=0 β no wrap): 200,000 entries complete in 66,666 passes,CRAFTC_DELTA=0, umount rc=0 (run_control_craftC.log). The wrap β not merely the odd key β is the trigger.
Fix validation
fix.diff: hammer2_bref_sane() helper (reject unknown blockref types
and wrapped key ranges) + hammer2_base_find() forward scan skips insane
media elements exactly like EMPTY slots. Applied to guest /usr/src,
kernel #1 (Oct 12 15:27:54 2026) built+installed, rebooted:
- craftB on patched kernel: 1,000,000 entries complete (499,999 passes β
the poisoned entry is simply not returned),
CRAFTED_DELTA=0, umount OK (AFTER_UMOUNT=821). No wedge, no leak. (run_fix.log) - DF-5785's craftA also validated on the same kernel (no panic).
Impact ceiling
DoS (wedge + resource exhaustion) β a memory-corruption escalation from
this primitive was not identified: duplicates are properly allocated
hammer2_chain objects; the rbtree remains structurally valid (RB_REMOVE
is linkage-based, no comparator use); no OOB access occurs inside the
loop. The system requires a reboot to recover the mount.
Not-a-duplicate note
DF-2618 = overlapping ranges (cmp==0 collisions β panic/tree-wipe); DF-2606 = keybits β₯ 64 UB shifts. This finding is a defined-behavior end-of-range wrap that silently breaks strict-weak ordering and iteration advancement β different trigger, different failure face (no panic at all, even on INVARIANTS).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.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.
run_fix.log; fix_build.log; env.txt (fix kernel uname)
Confirmed kernel references
- 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
Detail
Evidence (decisive lines)
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
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
Verified 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
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<
No comments yet.