Infinite loop in hammer2_fixup_pfses on non-INODE blockref under super-root β system-wide HAMMER2 mount/unmount hang
Summary
hammer2_fixup_pfses :2389 while(chain) { :2391 if(chain->bref.type != HAMMER2_BREF_TYPE_INODE) continue; β bypasses hammer2_chain_next() advance at :2415-2417. chain unchanged on continue re-evaluates identical type check identical continue forever. Crafted image: single type=DATA(3) blockref in super-root inode u.blockset. hammer2_chain_get (chain.c:2051) + hammer2_base_find (chain.c:4921) do not reject non-INODE types under inode parent. Compare sibling hammer2_update_pmps :1553-1566 correctly always calls chain_next. hammer2_fixup_pfses called from hammer2_vfs_mount :1337 (if(!hmp->ronly)) and hammer2_remount :1605. Global hammer2_mntlk held :1056->:1486 spmp->iroot locked :2384->:2423 both past hang = ALL hammer2 mount/unmount operations block forever. Trigger: crafted image RW mount. RO mount unaffected (gated !ronly). Fix: restructure type check into if/else-if chain so chain_next always reached.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0772 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| corrupt_h2.c | trigger-source | HAMMER2 image corruption + CRC recomputation tool (compiles with kernel icrc32.c) | 7.6 KB | view raw |
| trigger.sh | trigger-source | End-to-end trigger: newfs_hammer2 β corrupt β vnconfig β RW mount | 1.1 KB | view raw |
| build.sh | build-script | Compile the corruption tool | 171 B | view raw |
| run.sh | run-script | Run the end-to-end trigger | 110 B | view raw |
| fix.diff | suggested-fix | git-apply-able fix: restructure type check into if/else-if so chain_next() always reached | 719 B | view raw |
| serial_log_baseline.txt | panic-signature | Unpatched #0 serial log: stops at 'no recovery needed' (infinite loop) | 13.2 KB | view raw |
| serial_log_patched.txt | dmesg | Patched #1 serial log: shows fix kprintf + INITIATE SPANs (loop eliminated) | 13.3 KB | view raw |
| fix_build.log | build-log | Full make nativekernel output (rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | Patched kernel test: before/after contrast | 1.2 KB | view raw |
| env.txt | environment | uname, cc version, sysctl state | 252 B | view raw |
| VERDICT.md | verdict | Full narrative analysis | 6.1 KB | β raw |
| README.md | readme | Reproduce instructions | 3.1 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-0772 β Infinite loop in hammer2_fixup_pfses on non-INODE blockref
Summary
hammer2_fixup_pfses() in sys/vfs/hammer2/hammer2_vfsops.c enters an
infinite loop when it encounters a non-INODE blockref under the super-root
inode during a read-write mount. The loop body checks
chain->bref.type != HAMMER2_BREF_TYPE_INODE and executes continue,
bypassing the hammer2_chain_next() advance at the bottom of the loop. Since
chain is unchanged, the loop re-evaluates the identical type check and
continues forever β a tight kernel busy-loop.
The spin holds the global hammer2_mntlk lockmgr lock and the spmp->iroot
inode lock, blocking all subsequent HAMMER2 mount/unmount operations
system-wide (including root-FS sync). Impact: denial of service triggered by
mounting a crafted HAMMER2 filesystem image read-write (root-mount threat
model, CVSS PR:H).
The bug (line-by-line)
// sys/vfs/hammer2/hammer2_vfsops.c:2389-2417 (VULNERABLE)
while (chain) {
if (chain->bref.type != HAMMER2_BREF_TYPE_INODE)
continue; // <-- BUG: chain NOT advanced
if (chain->error) {
...
} else if (...) {
...
}
chain = hammer2_chain_next(...); // <-- NEVER REACHED for non-INODE
}
The sibling function hammer2_update_pmps() (same file, lines 1553-1566)
shows the correct pattern: the type check is inside an if/else-if chain so
hammer2_chain_next() at the bottom is always executed.
Trigger
- Create a valid HAMMER2 image:
dd if=/dev/zero ... && newfs_hammer2. - Corrupt one child blockref of the super-root inode: change its
typebyte fromINODE (1)toDATA (3)(or any non-INODE, non-EMPTY value), and set the super-root blockref's check method toCHECK_NONEso the modified inode data passes CRC validation during volume-header loading. vnconfig -c vn0 image.img && mount_hammer2 /dev/vn0 /mnt/x(RW).- The mount syscall enters
hammer2_vfs_mount β hammer2_fixup_pfses, hits the non-INODE child, and spins forever.
Fix
Restructure the type check into an if/else-if chain (mirroring
hammer2_update_pmps) so hammer2_chain_next() is always reached. See
fix.diff.
Files
| File | Description |
|---|---|
corrupt_h2.c |
Image-corruption + CRC-recomputation tool (compiles with kernel icrc32.c) |
trigger.sh |
End-to-end trigger: newfs β corrupt β vnconfig β mount (RW) |
fix.diff |
git-apply-able one-hunk fix |
serial_log_baseline.txt |
Unpatched #0 kernel: serial log stops at "no recovery needed" (infinite loop) |
serial_log_patched.txt |
Patched #1 kernel: shows "Non-inode chain type 3, skipping" + "INITIATE SPANs" (fixed) |
fix_build.log |
Full make nativekernel output (rc=0) |
VERDICT.md |
Detailed analysis |
Reproduce
# On the DragonFlyBSD guest as root:
dd if=/dev/zero of=/tmp/h2.img bs=1m count=512
newfs_hammer2 /tmp/h2.img
cc -o /tmp/corrupt_h2 corrupt_h2.c /usr/src/sys/libkern/icrc32.c
/tmp/corrupt_h2 /tmp/h2.img corrupt # flip child[0] type INODEβDATA
vnconfig -c vn0 /tmp/h2.img
mkdir -p /mnt/h2test
mount_hammer2 /dev/vn0 /mnt/h2test # RW mount β HANGS on unpatched kernel
DF-0772 β VERDICT
Verdict: REPRODUCED β system-wide HAMMER2 DoS via infinite loop
Status: reproduced (DoS / infinite loop)
Impact: dos β kernel busy-loop holding global hammer2_mntlk, blocking all HAMMER2 mount/unmount/sync
Confidence: certain
Fix status: fixed β validated on single-fix kernel
Mechanism
hammer2_fixup_pfses() (sys/vfs/hammer2/hammer2_vfsops.c:2366-2425) is called
during a read-write HAMMER2 mount (after hammer2_recovery() succeeds) to fix
mis-flagged PFS inodes. It scans the super-root inode's children:
// VULNERABLE (lines 2389-2417)
chain = hammer2_chain_lookup(&parent, &key_next, KEY_MIN, KEY_MAX, &error, 0);
while (chain) {
if (chain->bref.type != HAMMER2_BREF_TYPE_INODE)
continue; // BUG: chain is NOT advanced
if (chain->error) { ... }
else if (...) { ... }
chain = hammer2_chain_next(&parent, chain, &key_next, ...); // SKIPPED
}
When chain->bref.type != HAMMER2_BREF_TYPE_INODE (e.g., a crafted image has
a DATA type=3 blockref as a child of the super-root), continue jumps back
to while (chain) without calling hammer2_chain_next(). Since chain is
unchanged, the loop re-evaluates the same type check β continue β spin
forever.
Why the loop holds the system hostage
hammer2_vfs_mount()acquires the globalhammer2_mntlklockmgr lock early (line ~1056, held through line ~1486). The infinite loop is inside this critical section.hammer2_inode_lock(spmp->iroot, 0)is held (line 2384 β 2423).- ALL subsequent HAMMER2 mount/unmount operations block on
hammer2_mntlk. The root filesystem is on HAMMER2 (vbd0s1d on / (hammer2)), so sync/unmount of the root FS is also blocked.
Correct sibling: hammer2_update_pmps (lines 1553-1566)
while (chain) {
if (chain->error) { ... }
else if (chain->bref.type != HAMMER2_BREF_TYPE_INODE) {
kprintf("Non inode chain type %d under super-root\n", ...);
} else { ... }
chain = hammer2_chain_next(...); // ALWAYS reached
}
The fix restructures hammer2_fixup_pfses to match this pattern.
Reachability analysis
hammer2_vfs_mount(line ~1056): acquireshammer2_mntlk.- Volume header loaded and CRC-validated (hammer2_ondisk.c:485-560). Our corruption tool recomputes the 3 volume-header CRCs (iscsi_crc32) so this passes.
- Super-root inode loaded via
hammer2_chain_lookuponhmp->vchain(line ~1273). Our tool sets the blockref check method toCHECK_NONEso the modified inode data passes CRC. hammer2_recovery(hmp)returns 0 ("no recovery needed" β confirmed in serial log).hammer2_fixup_pfses(hmp)enters the infinite loop.
The trigger requires a crafted HAMMER2 image mounted RW by root
(!hmp->ronly gate at line 1334). RO mounts skip the vulnerable path.
Reproduction evidence
Baseline (unpatched #0 kernel)
Serial log (serial_log_baseline.txt) after RW mount of corrupted image:
hammer2_mount: device="/dev/vn0" label="DATA" rdonly=0 hammer2_ondisk: "/dev/vn0" zone=0 id=0 offset=0x0 size=0x20000000 hammer2_mount: "/dev/vn0": no recovery needed [SILENCE β kernel spinning in hammer2_fixup_pfses]
- ssh
uptimetimes out (guest unresponsive, all CPUs eventually starved by the spinning thread holding mntlk). - Load average on an earlier run: 1.59 (one CPU fully consumed).
- A second HAMMER2 mount (clean image) also blocks on
hammer2_mntlk.
Patched (#1 kernel, fix applied)
Serial log (serial_log_patched.txt) after RW mount of the SAME corrupted image:
hammer2_mount: device="/dev/vn0" label="DATA" rdonly=0 hammer2_mount: "/dev/vn0": no recovery needed hammer2: Non-inode chain type 3 under super-root, skipping β FIX FIRED Non inode chain type 3 under super-root β hammer2_update_pmps HAMMER2: VOLDATA DUMP HAMMER2: INITIATE SPANs β mount PROCEEDED PAST fixup
- Load average: 0.01 (CPU NOT spinning β no infinite loop).
- ssh responsive;
echo,uptime,mountall return immediately.
The before/after contrast is unambiguous: the infinite loop is eliminated.
The corruption tool (corrupt_h2.c)
A userspace C tool that:
1. Reads the volume header (first 64KB of the image).
2. Follows sroot_blockset.blockref[0].data_off to the super-root inode data.
3. Changes the first child blockref's type byte from INODE(1) to DATA(3).
4. Sets the super-root blockref's check method to CHECK_NONE (so the modified
inode data isn't rejected by the CRC check during chain loading).
5. Recomputes the 3 volume-header CRCs (iscsi_crc32 from kernel libkern).
Compiled with: cc -o corrupt_h2 corrupt_h2.c /usr/src/sys/libkern/icrc32.c
Fix validation
fix.diff
Restructures the if (... != INODE) continue; + if (chain->error) into an
if (... != INODE) { kprintf(...); } else if (chain->error) chain, so
hammer2_chain_next() at the bottom of the loop is always reached. Adds a
diagnostic kprintf matching hammer2_update_pmps's style.
Build + install
make -j6 quickkernel KERNCONF=X86_64_GENERICβ rc=0 (warm obj, ~90s).make installkernel+ copykernel.strippedβ/boot/kernel/kernel.- Booted:
DragonFly 6.5-DEVELOPMENT #1: Thu Jul 9 20:29:43 UTC 2026.
Before/after
| Kernel | Serial log after RW mount | Load avg | ssh responsive |
|---|---|---|---|
| #0 (unpatched) | stops at "no recovery needed" | 1.59+ | NO (times out) |
| #1 (patched) | "Non-inode type 3, skipping" β "INITIATE SPANs" | 0.01 | YES |
Conclusion: fix closes the bug. The infinite loop in hammer2_fixup_pfses
is eliminated; the mount proceeds past the fixup function on the patched kernel.
Notes
- The mount may still be slow on the patched kernel for reasons unrelated to this bug (HAMMER2 cluster/iocom messaging setup on a vn-backed image with no cluster peers). This is a separate behavior, not the infinite loop.
- The trigger requires root to mount (PR:H). A real-world scenario: an admin mounts a malicious HAMMER2 image (e.g., from an untrusted USB device or a downloaded filesystem image).
- RO mounts are unaffected (gated by
!hmp->ronly).
Fix verification
fixedVALIDATED the fix: RW mount of the corrupted HAMMER2 image on the unpatched #0 baseline (6.5-DEVELOPMENT #0) enters an infinite loop in hammer2_fixup_pfses (serial log stops at 'no recovery needed', load avg 1.59, ssh unresponsive). On the single-fix kernel (#1), the same mount proceeds past fixup_pfses -- the fix's kprintf 'Non-inode chain type 3 under super-root, skipping' fires, the loop advances via hammer2_chain_next(), and the mount continues to 'INITIATE SPANs' (load avg 0.01, ssh responsive). The fix closes the bug.
BASELINE (#0): serial stops at 'hammer2_mount: no recovery needed' -> SILENCE (infinite loop); load avg 1.59; ssh times out. || PATCHED (#1): serial shows 'hammer2: Non-inode chain type 3 under super-root, skipping' -> 'INITIATE SPANs' (fixup completed); load avg 0.01; ssh responsive. The before/after contrast is unambiguous.
Confirmed kernel references
Detail
Exploit chain
none (pure DoS -- infinite loop / hang, no memory corruption). An infinite loop in kernel context holding global hammer2_mntlk constitutes a system-wide HAMMER2 denial of service: all mount/unmount/sync operations block forever. No escalation chain applicable.
Evidence (decisive lines)
BASELINE (#0) serial after RW mount of corrupted image: 'hammer2_mount: rdonly=0' -> 'no recovery needed' -> [SILENCE -- infinite loop]. ssh 'uptime' times out; load avg 1.59 (one CPU spinning). || PATCHED (#1) serial: 'no recovery needed' -> 'hammer2: Non-inode chain type 3 under super-root, skipping' (FIX FIRED, loop advanced) -> 'INITIATE SPANs' (mount proceeded past fixup). ssh responsive, load avg 0.01 (no spinning).
PoC changes
Authored corrupt_h2.c (image-corruption tool that flips a super-root child blockref type INODE->DATA and recomputes volume-header CRCs via kernel iscsi_crc32), trigger.sh (end-to-end: newfs_hammer2 + corrupt + vnconfig + RW mount), build.sh, run.sh, VERDICT.md, manifest.json, fix.diff, and saved serial logs for both baseline and patched kernels. No PoC sources existed prior (finding had no poc/ directory).
Verified recommended fix
Restructure the type check in hammer2_fixup_pfses (sys/vfs/hammer2/hammer2_vfsops.c:2390-2392) from if (... != INODE) continue; if (chain->error) { into if (... != INODE) { kprintf(...); } else if (chain->error) { so hammer2_chain_next() at line 2415 is always reached -- mirroring the correct pattern in hammer2_update_pmps (lines 1553-1566). supersedes finding proposal (the finding also suggested restructuring into if/else-if). Full diff in findings/poc/DF-0772/fix.diff.
Verdict
REPRODUCED. hammer2_fixup_pfses (sys/vfs/hammer2/hammer2_vfsops.c:2389-2391) has if (chain->bref.type != HAMMER2_BREF_TYPE_INODE) continue; which skips the hammer2_chain_next() advance at line 2415, causing an infinite loop on any non-INODE blockref child of the super-root inode. Triggered by RW-mounting a crafted HAMMER2 image (root-mount threat model, PR:H). The loop holds global hammer2_mntlk + spmp->iroot lock, blocking ALL hammer2 mount/unmount/sync system-wide. Confirmed on unpatched #0 kernel: serial log stops at 'no recovery needed' (silent infinite loop), load avg 1.59, ssh completely unresponsive. The sibling hammer2_update_pmps (lines 1553-1566) uses the correct if/else-if pattern where chain_next() is always reached -- this is the model for the fix.
No comments yet.