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)
PoC verification
Evidence pack
findings/poc/DF-3109 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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
- sys/vfs/hammer/hammer_cursor.c:202
- sys/vfs/hammer/hammer_cursor.c:117-130
- sys/vfs/hammer/hammer_ondisk.c:1315-1333
- sys/vfs/hammer/hammer_inode.c:499
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_testableConfirmed 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.
No comments yet.