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

HAMMER1 mirror-write: unvalidated wire leaf.base.btype panics hammer_btree_insert (and defers panics to every later delete-through)

Summary

hammer_ioc_mirror_write_rec validates only data_len (:575-579); the rest of the wire leaf - including base.btype - is stored verbatim. A single HAMMERIOC_MIRROR_WRITE mrec with btype==0 and data_len==0 (which skips the whole alloc/copyin/CRC block at hammer_object.c:2298-2339) reaches hammer_btree_insert's always-on KKASSERT(elm->base.btype != 0) (hammer_btree.c:816) and panics INVARIANTS kernels; a nonzero non-'R' btype is stored and instead panics any later delete-through (hammer_mirror.c:737 / hammer_object.c:2448). Non-INVARIANTS kernels store the bogus btype on disk. Malicious/compromised mirror master feeds one crafted record into the slave kernel via hammer mirror-stream (untrusted wire data behind a privileged local ioctl) -> deterministic slave kernel panic/DoS. Distinct from DF-0867 (rec_type -> hammer_alloc_data hpanic, data_len>0 path). VERIFIED stock INVARIANTS: one ioctl carrying one REC mrec {btype=0, data_len=0, rec_type=DB, create_tid=0x1000} -> 'panic: assertion "elm->base.btype != 0" failed in hammer_btree_insert at :816'. No uid0 route (DoS primitive). Fix validated (reject btype != HAMMER_BTREE_TYPE_RECORD -> EINVAL): panic gone.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3148 Β· 12 files
FileTypeDescriptionSize
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 3.2 KB ↓ raw
mirror_poc.c β€” 6.6 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 152 B view raw
run.log β€” 835 B view raw
run.fixed.log β€” 230 B view raw
panic.txt β€” 727 B view raw
env.txt β€” 651 B view raw
fix.diff β€” 1.2 KB view raw
verdict.json β€” 3.3 KB view raw
manifest.json β€” 684 B view raw

DF-3148 β€” HAMMER1 mirror-write: unvalidated wire btype panics hammer_btree_insert

What

A single HAMMERIOC_MIRROR_WRITE ioctl containing one HAMMER_MREC_TYPE_REC mrec whose leaf.base.btype == 0 (and data_len == 0) reaches hammer_btree_insert() which executes the always-compiled-under-INVARIANTS

KKASSERT(elm->base.btype != 0);            /* hammer_btree.c:816 */

and panics the kernel. A nonzero-but-not-'R' btype is instead stored verbatim into the B-Tree leaf and later trips KKASSERT(elm->base.btype == HAMMER_BTREE_TYPE_RECORD) at hammer_mirror.c:737 / hammer_object.c:2448 on any subsequent delete-through (deferred panic).

Threat model

HAMMERIOC_MIRROR_WRITE is issued by the local root (caps_priv_check( SYSCAP_NOVFS_IOCTL), hammer_ioctl.c:72,169-176) but the mrec stream itself is the untrusted master's wire output β€” a malicious/compromised mirror master panics the slave kernel with one record. Non-INVARIANTS production kernels store the bogus btype instead (on-disk invariant violation).

Reproduce (baseline, stock INVARIANTS kernel)

# on the guest, as root (any DragonFly with hammer(4) available)
dd if=/dev/zero of=/build/h.img bs=1m count=2048
vnconfig -c /dev/vn0 /build/h.img
newfs_hammer -f -L MIRRORTEST /dev/vn0
mkdir -p /mnt && mount_hammer /dev/vn0 /mnt && touch /mnt/f1
cc -O -o /tmp/mirror_poc mirror_poc.c
/tmp/mirror_poc /mnt/f1 btype0
# -> kernel panic (console): assertion "elm->base.btype != 0" failed
#    in hammer_btree_insert at hammer_btree.c:816

Fix validation

Apply fix.diff in /usr/src, make nativekernel && make installkernel, reboot, re-run the same command: no panic; head.error=EINVAL (stream rejected). See VERDICT.md.

VERDICT.md
↓ download raw

VERDICT β€” DF-3148

Status: REPRODUCED (panic) / fix VALIDATED

Baseline (stock INVARIANTS kernel #0, 2026-07-02)

Single HAMMERIOC_MIRROR_WRITE ioctl, one REC mrec: data_len=0, btype=0, create_tid=0x1000, rec_type=DB(0x12), PFS0.

Result: kernel panic, exact predicted sink and chain (serial console):

panic: assertion "elm->base.btype != 0" failed in hammer_btree_insert
       at /usr/src/sys/vfs/hammer/hammer_btree.c:816
hammer_btree_insert() at hammer_btree_insert+0x2d9
hammer_create_at_cursor() at hammer_create_at_cursor+0x1cf
hammer_ioc_mirror_write() at hammer_ioc_mirror_write+0x3f8
hammer_ioctl() at hammer_ioctl+0xebe
hammer_vop_ioctl() at hammer_vop_ioctl+0x48

Root cause chain (line-accurate): - hammer_ioc_mirror_write_rec() validates ONLY data_len (sys/vfs/hammer/hammer_mirror.c:575-579). The rest of the wire leaf β€” including base.btype β€” is passed through unvalidated. - hammer_create_at_cursor() (sys/vfs/hammer/hammer_object.c:2274) stores the leaf verbatim: with data_len == 0 the whole alloc/copyin/CRC block is skipped (:2298-2339), so no other validation happens either; then hammer_btree_insert(). - hammer_btree_insert() KKASSERTs elm->base.btype != 0 (sys/vfs/hammer/hammer_btree.c:816). btype is a full wire-controlled byte. - Nonzero-but-wrong btype (not 'R') passes :816 and is stored; any later mirror delete-through hits KKASSERT(elm->base.btype == HAMMER_BTREE_TYPE_RECORD) at hammer_mirror.c:737 / hammer_object.c:2448 (deferred panic), so BOTH btype=0 and btype=junk panic INVARIANTS kernels.

Reachability: ioctl gated by caps_priv_check(SYSCAP_NOVFS_IOCTL) (hammer_ioctl.c:72,169-176) → local root, or the untrusted mirror-master wire stream (the real threat: hammer mirror-stream pipes remote records into this ioctl; a malicious master kills the slave kernel with one record). Not DF-0867: that finding is rec_type→hammer_alloc_data hpanic (data_len>0 path); this is btype→B-Tree insert/delete assertions.

Non-INVARIANTS production kernels: the bogus btype is stored on disk (invariant violation) instead of panicking.

Fix validation (patched kernel #1, 2026-09-07, fix.diff applied)

fix.diff rejects btype != HAMMER_BTREE_TYPE_RECORD in hammer_ioc_mirror_write_rec (hammer_mirror.c:580-586 after patch).

Re-run of the identical PoC on a fresh fs:

=== btype0 on patched kernel ===
MIRROR_WRITE rc=0 head.error=22 head.flags=00008000 count=0
=> kernel REJECTED the stream (error 22)

No panic, guest stayed up, stream rejected with EINVAL (22). Build: make -j6 nativekernel RC=0, make installkernel RC=0 (same X86_64_GENERIC INVARIANTS config).

Bottom line

Certain. Single-ioctl kernel panic from wire data on INVARIANTS kernels; on-disk B-Tree invariant violation on production kernels; malicious-master remote DoS of HAMMER1 mirror targets. Fix proven.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

make -j6 nativekernel RC=0 + make installkernel RC=0 with fix.diff; identical PoC on fresh fs: head.error=22 (EINVAL), count=0, no panic, guest stayed up. Baseline panic gone.

run.fixed.log; patched-kernel uname #1 (2026-09-07)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Sep 7 05:10:35 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

gated ioctl (SYSCAP_NOVFS_IOCTL) or malicious mirror-master stream -> HAMMERIOC_MIRROR_WRITE -> hammer_ioc_mirror_write_rec (only data_len validated) -> hammer_create_at_cursor (data_len==0 skips all alloc/CRC checks) -> hammer_btree_insert -> KKASSERT(btype != 0) panic

Evidence (decisive lines)

['panic.txt: exact console panic + backtrace hammer_btree_insert<-hammer_create_at_cursor<-hammer_ioc_mirror_write', 'run.log: baseline trigger (ssh dies, guest down)', 'run.fixed.log: patched kernel rejects with error 22, no panic', 'fix.diff: validated kernel fix']

PoC changes

Hand-rolled wire structs (hammer_ioc_mirror_rw=200B, mrecord_rec=80B, base_elm=40B with btype at +0x23) with _Static_asserts; no kernel headers needed; data_len=0 avoids the DF-0867 rec_type hpanic so the btype assert is the only sink

Verified recommended fix

In hammer_ioc_mirror_write_rec reject mrec->leaf.base.btype != HAMMER_BTREE_TYPE_RECORD (see fix.diff).

Verdict

Single HAMMERIOC_MIRROR_WRITE ioctl with one REC mrec whose wire leaf.base.btype==0 (data_len=0) panics the stock INVARIANTS kernel at hammer_btree_insert() KKASSERT(elm->base.btype != 0) (hammer_btree.c:816) via hammer_create_at_cursor (the wire leaf is stored verbatim; only data_len is validated at hammer_mirror.c:575-579). Nonzero-but-wrong btype is stored and panics later at hammer_mirror.c:737 / hammer_object.c:2448 on delete-through. Malicious mirror master -> slave kernel DoS; local root can trigger directly. Not DF-0867 (that is rec_type->hammer_alloc_data hpanic on the data_len>0 path). fix.diff (reject btype != HAMMER_BTREE_TYPE_RECORD in write_rec) validated on a rebuilt kernel: no panic, stream rejected EINVAL.