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

hammer_init_cursor error contract is broken: unconditional KKASSERT(error==0), removed cleanup, and 12 callers ignore the error β€” INVARIANTS panic or production NULL-node cursor dereference (btree_search reads NULL left_bound) on any root-node load failure

Summary

hammer_init_cursor can only fail when the root volume or the vol0_btree_root node fails to load (I/O error, or EIO/EDOM from a CRC-bad root node at first load - hammer_load_node, hammer_ondisk.c:1327-1332). On that path the function has no cleanup: the commented-out hammer_done_cursor at :203 was never replaced, and :202 asserts success unconditionally. On INVARIANTS kernels every cursor initialization panics at the KKASSERT. On production kernels the error is returned with cursor->node==NULL and cursor->left_bound==NULL while ip->lock was already taken and cursor_ip_refs incremented (:117-130); twelve callers ignore the return entirely (hammer_vop_readdir vnops.c:1705, hammer_vop_readlink :1854, hammer_vop_nrename :2003, hammer_vop_strategy_read :2703, hammer_vop_bmap :3000, hammer_dounlink :3313, hammer_ip_add_direntry object.c:701, hammer_ip_check_directory_empty :2614, hammer_get_inode inode.c:499, hammer_load_pseudofs :1008, hammer_save_pseudofs :1070) and feed the NULL-node cursor straight into hammer_ip_lookup/hammer_btree_lookup, whose btree_search immediately dereferences cursor->left_bound (hammer_btree.c:1009) -> NULL-deref kernel panic on ordinary unprivileged VOPs. Callers that bail without hammer_done_cursor additionally leak the inode sh-lock permanently (unkillable hang on that inode). Unpriv user on a fs whose root B-Tree node fails its first load - bitrot/failing media on any mounted HAMMER1 fs, or deterministically via crafted image with CRC-corrupted vol0_btree_root mounted by root. Availability only; NULL-pointer read, no user->root route. Fix: unwind acquired state before returning the error (row diff) + fix the 12 ignoring callers to propagate. [VERIFIED pass2: reproduced - CRC-corrupted root btree node mount panics at KKASSERT error-eq-0 hammer_cursor.c 202 via hammer_get_inode; row-fix-alone A/B itself moved the panic to the first ignoring caller live-proving the 12-caller claim; full fix mounts same image clean EIO]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3109 Β· 13 files
FileTypeDescriptionSize
corrupt_root.c β€” 3.3 KB view raw
panic.txt β€” 859 B view raw
README.md β€” 720 B ↓ raw
VERDICT.md β€” 2.3 KB ↓ raw
build.sh β€” 258 B view raw
run.sh β€” 153 B view raw
manifest.json β€” 939 B view raw
verdict.json β€” 2.6 KB view raw
fix.diff β€” 542 B view raw
env.txt β€” 272 B view raw
ab_fixed.log β€” 219 B view raw
ab_fixed_panic2.txt β€” 305 B view raw
compile_validation.txt β€” 1.8 KB view raw

DF-3109 -- verification pack

Finding: hammer_init_cursor error contract broken: KKASSERT + no cleanup on root-node load failure

Status: reproduced / impact panic / confidence certain

What was run

guest: cc -O -o corrupt_root corrupt_root.c; image: vnconfig -c -S 2g /dev/vn0 h1.img && newfs_hammer -f /dev/vn0 && vnconfig -u vn0 && ./corrupt_root h1.img

How to reproduce

guest: vnconfig -c -S 2g /dev/vn0 h1.img && mount_hammer /dev/vn0 /mnt/h1

Expected

panic: assertion error == 0 failed in hammer_init_cursor at hammer_cursor.c:202 (see panic.txt)

See VERDICT.md for the full narrative and verdict.json for the persisted machine verdict. Artifacts: corrupt_root.c, panic.txt, run.log.

VERDICT.md
↓ download raw

DF-3109 verification

Kernel: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Bottom line

Crafted HAMMER1 image with a CRC-corrupted root B-Tree node panics the stock INVARIANTS kernel at mount: panic assertion error == 0 failed in hammer_init_cursor at hammer_cursor.c:202, via sys_mount -> hammer_vfs_mount -> hammer_vfs_vget -> hammer_get_inode -> hammer_init_cursor. The commented-out cleanup line sits directly beneath the assertion, so on production kernels the same trigger returns with cursor->node NULL and cursor->ip refs and the inode sh-lock held, feeding the NULL-node cursor into btree_search (left_bound deref) for the twelve ignoring callers. Root-gated (mount of crafted image); availability only, no privilege gain.

Kernel references

Exploit chain

(none: availability/correctness finding; no privilege chain)

Fix

hammer_init_cursor now unwinds via hammer_done_cursor and returns the error instead of asserting; combined-kernel A/B: mount of the same image returns clean EIO with no panic

See fix.diff in this directory (git-apply-able against the read-only sys/ tree; applied only inside the disposable verification guest).

Reproduction

build: guest: cc -O -o corrupt_root corrupt_root.c; image: vnconfig -c -S 2g /dev/vn0 h1.img && newfs_hammer -f /dev/vn0 && vnconfig -u vn0 && ./corrupt_root h1.img

run: guest: vnconfig -c -S 2g /dev/vn0 h1.img && mount_hammer /dev/vn0 /mnt/h1

expected: panic: assertion error == 0 failed in hammer_init_cursor at hammer_cursor.c:202 (see panic.txt)

Notes on the PoC (what changed vs the finding sketch)

corrupt_root.c written from scratch: locates the root node by scanning for CRC-valid btree nodes whose parent offset is zero, then flips one byte so the stored CRC no longer matches

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

not_testable
↓ fix.diffper-fix-DF-3109

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt (full dmesg panic + backtrace), run.log (craft + mount sequence), corrupt_root.c (scans CRC-valid btree nodes, flips a byte in the parent==0 root node)

PoC changes

corrupt_root.c written from scratch: locates the root node by scanning for CRC-valid btree nodes whose parent offset is zero, then flips one byte so the stored CRC no longer matches

Verified recommended fix

hammer_init_cursor now unwinds via hammer_done_cursor and returns the error instead of asserting; combined-kernel A/B: mount of the same image returns clean EIO with no panic

Verdict

Crafted HAMMER1 image with a CRC-corrupted root B-Tree node panics the stock INVARIANTS kernel at mount: panic assertion error == 0 failed in hammer_init_cursor at hammer_cursor.c:202, via sys_mount -> hammer_vfs_mount -> hammer_vfs_vget -> hammer_get_inode -> hammer_init_cursor. The commented-out cleanup line sits directly beneath the assertion, so on production kernels the same trigger returns with cursor->node NULL and cursor->ip refs and the inode sh-lock held, feeding the NULL-node cursor into btree_search (left_bound deref) for the twelve ignoring callers. Root-gated (mount of crafted image); availability only, no privilege gain.