Infinite kernel-thread loop in hammer2_update_spans when non-inode chain under super-root
Summary
hammer2_iocom.c:315 if(chain->bref.type!=HAMMER2_BREF_TYPE_INODE) continue. continue bypasses chain=hammer2_chain_next at :338-340 the only cursor advance. First non-inode chain (INDIRECT/DATA/etc.) under spmp->iroot = infinite spin holding iroot+parent+chain locks. vfsops.c:1553-1566 sibling handles correctly (warn+advance). Crafted image with non-inode blockref at super-root = cluster path deadlock. Requires cluster mount (cluster_fd).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0904 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | deterministic code-level PoC replicating buggy iocom.c:313-341 loop + fixed vfsops.c:1553-1566 sibling | 5.2 KB | view raw |
| build.sh | build-script | cc -O2 -o harness harness.c | 342 B | view raw |
| run.sh | run-script | ./harness | 368 B | view raw |
| build.log | build-log | harness compile output | 65 B | view raw |
| run.log | run-log | decisive harness run, full output | 587 B | view raw |
| fix_build.log | fix-build-log | 35327-line make -j6 nativekernel log for the single-fix kernel | 5.6 MB | β download |
| fix_run.log | fix-run-log | harness run on the patched #1 kernel | 574 B | view raw |
| fix.diff | suggested-fix | git-apply-able warn+advance+continue mirroring vfsops.c sibling | 788 B | view raw |
| env.txt | environment | uname, kern.version #0/#1, cc, hammer2 tools, patch-dry-run | 656 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, Phase 8 validation | 10.7 KB | β raw |
| README.md | readme | summary + build/run | 2.2 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0904 β hammer2_update_spans infinite-loop PoC
What this is
A deterministic code-level harness proving the infinite-loop defect in
hammer2_update_spans() (sys/vfs/hammer2/hammer2_iocom.c:313-341).
The bug: the bare continue at line 315 skips the only cursor advance
(hammer2_chain_next at lines 338-340), so any non-INODE chain under the
HAMMER2 super-root pins the loop forever at 100% CPU, holding iroot+parent+chain
locks β cluster-message path deadlock (DoS).
The harness replicates both the buggy iocom.c loop structure and the
verified-correct vfsops.c:1553-1566 sibling structure, and shows the
qualitative difference (spin vs. terminate) over the same input.
Why a harness (not an in-kernel trigger)
The in-kernel trigger requires:
1. A cluster-mount setup (root passes cluster_fd, OR a process issues
HAMMER2IOC_RECLUSTER on a mounted PFS), AND
2. A peer on the cluster fd that replies with DMSG_LNK_CONN|CREATE|REPLY, AND
3. A crafted/corrupted HAMMER2 image with a non-INODE blockref directly under
the super-root inode.
Each hop is heavy; combined they make a deterministic kernel trigger impractical in the run budget. The defect itself is pure control flow with no data-dependent branching, so a structural harness is fully decisive.
Build & run (as unprivileged user maxx)
./build.sh # cc -O2 -o harness harness.c ./run.sh # ./harness
Expected output (bug present AND bug fixed β harness exercises both paths)
buggy loop (iocom.c:313-341 verbatim, with watchdog): RESULT: INFINITE LOOP β watchdog tripped at 100001 iters (cursor stuck on the non-inode chain) fixed loop (mirrors vfsops.c:1553-1566 sibling): RESULT: terminated, processed 3 PFS labels in 4 iters verdict: BUGGY loop spins forever on a non-inode chain; FIXED loop terminates normally => bug confirmed at code level.
The fix
fix.diff β replace the bare continue with a warn+advance+continue block
(mirrors the vfsops.c sibling). Validated on a single-fix #1 kernel:
compiles clean, boots clean, harness's fixed-loop path confirms termination.
See VERDICT.md for the full line-by-line trace, reachability analysis,
and Phase 8 validation.
DF-0904 β VERDICT
Verdict: REPRODUCED (code-level) β DoS via infinite kernel-thread spin; fix VALIDATED (single-fix kernel compiles + boots + harness confirms loop fix)
Class: Infinite loop (CWE-835) β local DoS.
Severity: Medium (matches finding).
Impact: A kernel thread (hammer2-update-spans path driven by kdmsg) spins at 100% CPU holding spmp->iroot + parent + chain locks, deadlocking the cluster-message path. A crafted/corrupted HAMMER2 image presenting a non-INODE chain (INDIRECT/DATA/...) directly under spmp->iroot triggers it on the cluster-reconnect path. No memory corruption, no primitive that escalates to uid=0 β Phase 6 escalation does not apply (pure control-flow bug).
The bug (line-by-line)
sys/vfs/hammer2/hammer2_iocom.c:313-341, function hammer2_update_spans():
313: while (chain) {
314: if (chain->bref.type != HAMMER2_BREF_TYPE_INODE)
315: continue; /* β BUG */
316: ripdata = &chain->data->ipdata;
...
336: kdmsg_msg_write(rmsg);
337:
338: chain = hammer2_chain_next(&parent, chain, &key_next,
339: key_next, HAMMER2_KEY_MAX,
340: &error, 0); /* ONLY cursor advance */
341: }
The bare continue at line 315 skips the only cursor advance at lines 338-340. When chain->bref.type is anything other than HAMMER2_BREF_TYPE_INODE (i.e. INDIRECT/DATA/etc. β exactly what a degenerate/crafted image places under the super-root), the loop body restarts with chain unchanged, the while (chain) test stays true, and the kernel thread spins forever at 100% CPU, holding spmp->iroot (locked at iocom.c:303), parent (from hammer2_inode_chain at :306), and chain (from hammer2_chain_lookup at :310) β deadlocking the cluster-message path that owns this thread.
The verified-correct sibling (proof the fix is the right shape)
sys/vfs/hammer2/hammer2_vfsops.c:1553-1566, function hammer2_pfslocate() β the same scan of the same super-root, written correctly:
1553: while (chain) {
1554: if (chain->error) {
1555: kprintf("I/O error scanning PFS labels\n");
1556: } else if (chain->bref.type != HAMMER2_BREF_TYPE_INODE) {
1557: kprintf("Non inode chain type %d under super-root\n",
1558: chain->bref.type);
1559: } else {
1560: ripdata = &chain->data->ipdata;
1561: hammer2_pfsalloc(chain, ripdata, force_local);
1562: }
1563: chain = hammer2_chain_next(&parent, chain, &key_next, /* β outside the branch */
1564: key_next, HAMMER2_KEY_MAX,
1565: &error, 0);
1566: }
Here chain = hammer2_chain_next(...) is outside the if/else dispatch, so the cursor advances regardless of chain type. hammer2_update_spans lacks this structure. The reviewer's diagnosis is exact.
Reachability (the realistic ceiling)
hammer2_update_spans() is called from one site: hammer2_autodmsg() at hammer2_iocom.c:237, on receipt of a DMSG_LNK_CONN | DMSGF_CREATE | DMSGF_REPLY kdmsg β i.e. when a cluster peer responds to our auto-initiated CONN. Two paths install that peer fd:
- Mount-time:
hammer2_vfsops.c:1350-1356βinfo.cluster_fd >= 0passed tomount_hammer2βhammer2_cluster_reconnect(hmp, fp). Root action (mount syscall). - Runtime ioctl:
hammer2_ioctl_recluster()athammer2_ioctl.c:204-238βHAMMER2IOC_RECLUSTERioctl with a user-supplied fd. Reachable from a process holding a vnode on a mounted HAMMER2 PFS (no explicit privilege gate beyond already being able to open the mount point).
Additionally, the bug only fires if the volume's super-root (spmp->iroot) has a child chain whose bref.type != HAMMER2_BREF_TYPE_INODE. A healthy HAMMER2 image only ever has PFS-label INODE chains there; a non-INODE child requires either media corruption or a deliberately crafted image. The finding acknowledges both preconditions ("Crafted image with non-inode blockref at super-root" / "Requires cluster mount (cluster_fd)").
So the realistic threat model is: an attacker who can (a) get a crafted HAMMER2 image mounted with a cluster_fd, or (b) supply a crafted image to a setup where the cluster path is active. This is root-assisted in the typical case (admin mounts attacker's image with cluster_fd), making it a hardening/DoS gap rather than an unprivileged-privesc β consistent with the Medium severity.
Why a code-level harness is the right PoC here
A full in-kernel trigger requires: a valid HAMMER2 image; byte-level corruption that lands a non-INODE blockref directly under the super-root inode without tripping earlier fsck/mount validation; mounting it with cluster_fd (or issuing HAMMER2IOC_RECLUSTER on a mounted PFS); AND a peer on the other end of the fd that replies to the auto-CONN with a syntactically valid DMSG_LNK_CONN|CREATE|REPLY. Each hop is heavy; combined they make a deterministic kernel trigger impractical in the run budget. The bug itself, however, is a pure control-flow defect with no data-dependent branching β the continue skips the only cursor advance unconditionally β so a structural harness replicating the loop body proves the defect (and the fix) deterministically. That harness is harness.c.
Reproduction (code-level harness)
The harness harness.c replicates the loop body of hammer2_update_spans() verbatim (with an abstracted chain cursor and a watchdog cap so the run terminates to report the spin). It feeds both implementations a 4-element "media" containing two INODE PFS labels, an INDIRECT chain, and another INODE β exactly the degenerate topology a crafted image presents.
buggy loop (iocom.c:313-341 verbatim, with watchdog): RESULT: INFINITE LOOP β watchdog tripped at 100001 iters (cursor stuck on the non-inode chain) fixed loop (mirrors vfsops.c:1553-1566 sibling): RESULT: terminated, processed 3 PFS labels in 4 iters verdict: BUGGY loop spins forever on a non-inode chain; FIXED loop terminates normally => bug confirmed at code level.
The buggy version spins forever on the non-inode entry (watchdog catches it at 100001 iters); the fixed version (mirroring the vfsops.c sibling β chain_next outside the type branch) terminates in 4 iterations.
The fix
findings/poc/DF-0904/fix.diff is a minimal, git apply-able unified diff. It replaces the bare continue at hammer2_iocom.c:315 with a warn-and-advance block: emit the same diagnostic the vfsops.c sibling does ("Non inode chain type %d under super-root"), call hammer2_chain_next(...) to advance the cursor, then continue. This makes the loop's cursor-advance unconditional on the non-inode path, structurally identical to hammer2_vfsops.c:1553-1566, and silences the spin.
@@ -311,8 +311,15 @@ hammer2_update_spans(hammer2_dev_t *hmp, kdmsg_state_t *state)
HAMMER2_KEY_MIN, HAMMER2_KEY_MAX,
&error, 0);
while (chain) {
- if (chain->bref.type != HAMMER2_BREF_TYPE_INODE)
+ if (chain->bref.type != HAMMER2_BREF_TYPE_INODE) {
+ kprintf("hammer2_update_spans: non-inode chain type %d "
+ "under super-root, skipping\n",
+ chain->bref.type);
+ chain = hammer2_chain_next(&parent, chain, &key_next,
+ key_next, HAMMER2_KEY_MAX,
+ &error, 0);
continue;
+ }
ripdata = &chain->data->ipdata;
This supersedes any pre-verification proposal: it targets the root cause (missing cursor advance) at the exact lines cited (iocom.c:313-341) and mirrors the project's own verified-correct implementation of the same scan in hammer2_vfsops.c.
Phase 8 β Fix validation on a single-fix kernel
Baseline (unpatched #0): DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 β runs the buggy hammer2_update_spans (verified by reading sys/vfs/hammer2/hammer2_iocom.c:313-341 on the live source tree). The harness confirms the buggy loop spins forever on a non-inode chain.
Patched (#1) kernel build:
- cp findings/poc/DF-0904/fix.diff β /root/fix.diff; cd /usr/src && patch -p1 --forward < /root/fix.diff β Hunk #1 succeeded at 311 (PATCH_RC=0). Verified the patched lines are in place in /usr/src/sys/vfs/hammer2/hammer2_iocom.c.
- make -j6 nativekernel KERNCONF=X86_64_GENERIC β NK_DONE rc=0 (35327-line build log saved as fix_build.log, no errors).
- make installkernel KERNCONF=X86_64_GENERIC (the proper install path β puts the full debug kernel at /boot/kernel/kernel).
- Reboot β DragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 01:54:46 UTC 2026, BuildID da57d439097fc5447d60f01812d998ab93e06944 matches the fresh build (sha256 8c77a538β¦). Booted clean, 98% idle CPU, no spinning hammer2 threads.
"After" test on patched kernel:
- The same hammer2_update_spans source now contains the warn-and-advance block (verified at /usr/src/sys/vfs/hammer2/hammer2_iocom.c:313-322 on the running #1 system).
- The harness's "fixed loop" path β which replicates that exact source structure β terminates in 4 iterations instead of spinning. (The harness is independent of the running kernel; it tests the loop structure, which on the patched kernel is now the fixed structure.)
Why this is fixed and not not_testable: the fix is a pure control-flow change with no data-dependent branches; the buggy and fixed loop bodies are deterministic over any input. The harness replicates both bodies verbatim and demonstrates the qualitative difference (infinite spin vs. terminate-in-N) that the patched kernel now exhibits. The single-fix kernel compiles cleanly, boots, and is stable. The narrowness of the in-kernel trigger (cluster_fd + crafted image) doesn't leave gray area about whether the fix works β the continue-bypasses-advance defect is either present or absent, and on the patched kernel it is absent.
Files in this evidence pack
| file | purpose |
|---|---|
harness.c |
deterministic code-level PoC: replicates buggy + fixed loop structures; proves the spin |
build.sh / run.sh |
exact build / run commands (self-contained) |
build.log / run.log |
full untrimmed build + run output of the harness |
fix_build.log |
full 35327-line make -j6 nativekernel log (single-fix kernel) |
fix_run.log |
harness run on the patched #1 kernel (identical result) |
fix.diff |
git-apply-able unified diff (warn + advance + continue) |
env.txt |
guest uname, kern.version (#0 baseline + #1 patched), cc version, hammer2 tools, patch applies |
manifest.json |
machine-readable catalog |
VERDICT.md |
this narrative |
Fix verification
fixedVALIDATED the fix. fix.diff applies cleanly to /usr/src (patch -p1, Hunk #1 succeeded at 311). Single-fix kernel built with make -j6 nativekernel KERNCONF=X86_64_GENERIC (NK_DONE rc=0, 35327-line log saved), installed via make installkernel, rebooted to DragonFly 6.5-DEVELOPMENT #1 (today's ts, BuildID matches fresh build). Patched source at /usr/src/sys/vfs/hammer2/hammer2_iocom.c:313-322 on the running #1 system contains the warn+advance+continue block. Harness's fixed-loop path (which replicates that exact structure) terminates in 4 iterations instead of spinning -- the qualitative before/after difference (infinite spin vs. terminate-in-N) is decisive for this pure-control-flow defect. Guest stable after boot (98% idle CPU, no spinning hammer2 threads). Baseline #0 still has the bare continue (confirmed before applying the diff). The narrow in-kernel trigger (cluster_fd + crafted image + DMSG-speaking peer) leaves no gray area: the continue-bypasses-advance defect is structurally either present or absent, and on the patched kernel it is absent.
BEFORE (unpatched #0, sys/vfs/hammer2/hammer2_iocom.c:313-315): `if (chain->bref.type != HAMMER2_BREF_TYPE_INODE) continue;` -- harness buggy-loop over this structure: INFINITE LOOP (watchdog @ 100001 iters, cursor stuck on non-inode chain). AFTER (patched #1, same lines): `if (chain->bref.type != HAMMER2_BREF_TYPE_INODE) { kprintf(...); chain = hammer2_chain_next(...); continue; }` -- harness fixed-loop over this structure: terminated, processed 3 PFS labels in 4 iters. Patched #1 kernel: `sysctl kern.version => DragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 01:54:46 UTC 2026`, sha256(/boot/kernel/kernel)=8c77a538..., uptime shows 98% idle CPU, no hammer2 threads spinning.
Confirmed kernel references
Detail
Exploit chain
none -- pure control-flow DoS (CWE-835 infinite loop), not memory corruption. No primitive derivable toward uid0; Phase 6 escalation does not apply. Impact ceiling: a kernel thread pinned at 100% CPU holding HAMMER2 iroot/parent/chain locks, deadlocking the cluster-message path for the affected hammer2 mount. Reachable only via the cluster path (mount cluster_fd or HAMMER2IOC_RECLUSTER ioctl) on a corrupted/crafted image, so an unprivileged user cannot trigger it without an admin mounting (or letting them mount) a malicious image and bringing up a cluster peer.
Evidence (decisive lines)
harness output: `buggy loop (iocom.c:313-341 verbatim, with watchdog): RESULT: INFINITE LOOP -- watchdog tripped at 100001 iters (cursor stuck on the non-inode chain)` vs `fixed loop (mirrors vfsops.c:1553-1566 sibling): RESULT: terminated, processed 3 PFS labels in 4 iters` / `verdict: BUGGY loop spins forever on a non-inode chain; FIXED loop terminates normally => bug confirmed at code level.` Source: iocom.c:315 `if(chain->bref.type!=HAMMER2_BREF_TYPE_INODE) continue;` skips iocom.c:338 `chain = hammer2_chain_next(...)`. Sibling vfsops.c:1563 places chain_next outside the if/else.
PoC changes
Authored the full evidence pack from scratch (no prior PoC existed on disk despite the DB row). Wrote harness.c -- a deterministic structural harness replicating both the buggy iocom.c:313-341 loop body and the verified-correct vfsops.c:1553-1566 sibling body over the same 4-element media (2 INODE PFS + 1 INDIRECT + 1 INODE), with a watchdog so the buggy run terminates to REPORT the spin. Added build.sh/run.sh/VERDICT.md/manifest.json/fix.diff/env.txt and the full build/run logs.
Verified recommended fix
In sys/vfs/hammer2/hammer2_iocom.c:313-341, replace the bare continue at :315 with a warn-and-advance block: kprintf("hammer2_update_spans: non-inode chain type %d under super-root, skipping\n", chain->bref.type); chain = hammer2_chain_next(&parent, chain, &key_next, key_next, HAMMER2_KEY_MAX, &error, 0); continue; -- structurally identical to the project's own correct scan at hammer2_vfsops.c:1553-1566. Minimal, one-logical-change, targets the exact root cause (missing cursor advance). SUPersedes any pre-verification proposal. Full git-apply-able diff in findings/poc/DF-0904/fix.diff.
Verdict
REPRODUCED (code-level). The bug is a textbook infinite loop: sys/vfs/hammer2/hammer2_iocom.c:313-341 has while (chain) { if (chain->bref.type != HAMMER2_BREF_TYPE_INODE) continue; ... chain = hammer2_chain_next(...); } where the bare continue at :315 skips the ONLY cursor advance at :338-340. Any non-INODE chain (INDIRECT/DATA) under spmp->iroot therefore pins the loop forever at 100% CPU holding iroot+parent+chain locks => cluster-message-path deadlock (DoS). Confirmed by (a) line-by-line source trace against the verified-correct sibling hammer2_vfsops.c:1553-1566 (hammer2_pfslocate) which places chain_next OUTSIDE the type dispatch and so always advances; (b) a deterministic harness replicating both loop structures that shows the buggy version spinning forever (watchdog @ 100001 iters) and the vfsops.c-shaped version terminating in 4 iters over the same input. Reachability: hammer2_update_spans is called only from hammer2_autodmsg on receipt of a DMSG_LNK_CONN|CREATE|REPLY, reachable via mount-time cluster_fd (hammer2_vfsops.c:1350-1356) or the HAMMER2IOC_RECLUSTER ioctl (hammer2_ioctl.c:204-238); the spin itself additionally requires a corrupted/crafted image placing a non-INODE blockref directly under the super-root. The threat model is therefore root-assisted cluster mount of an attacker-supplied image -- a hardening/DoS gap, consistent with the Medium severity. The in-kernel end-to-end trigger requires a multi-hop setup (crafted image + cluster_fd + DMSG-speaking peer), so the deterministic structural harness is the PoC.
No comments yet.