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)
PoC verification
Evidence pack
findings/poc/DF-3148 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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
fixedmake -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)
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.
No comments yet.