β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0772

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0772 Β· 14 files
FileTypeDescriptionSize
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
README.md readme Reproduce instructions
↓ download 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

  1. Create a valid HAMMER2 image: dd if=/dev/zero ... && newfs_hammer2.
  2. Corrupt one child blockref of the super-root inode: change its type byte from INODE (1) to DATA (3) (or any non-INODE, non-EMPTY value), and set the super-root blockref's check method to CHECK_NONE so the modified inode data passes CRC validation during volume-header loading.
  3. vnconfig -c vn0 image.img && mount_hammer2 /dev/vn0 /mnt/x (RW).
  4. 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
VERDICT.md verdict Full narrative analysis
↓ download raw

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 global hammer2_mntlk lockmgr 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

  1. hammer2_vfs_mount (line ~1056): acquires hammer2_mntlk.
  2. 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.
  3. Super-root inode loaded via hammer2_chain_lookup on hmp->vchain (line ~1273). Our tool sets the blockref check method to CHECK_NONE so the modified inode data passes CRC.
  4. hammer2_recovery(hmp) returns 0 ("no recovery needed" β€” confirmed in serial log).
  5. 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 uptime times 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, mount all 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 + copy kernel.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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 9 20:29:43 UTC 2026

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.