Kernel panic via unvalidated rec_type in mirror_write_rec path
Summary
hammer_mirror.c:575-577 validates data_len only never rec_type. :629 hammer_create_at_cursor passes to hammer_alloc_data (hammer_ondisk.c:1645-1672) switch handles only INODE/DATA/DIRENTRY/DB/EXT/FIX/PFS/SNAPSHOT/CONFIG. Default case :1669 hpanic(rec_type %04x unknown) = panic(). mrec with data_len>0 and rec_type=0x0002 (or any unhandled) = instant kernel panic. LOCAL: root/jail calls HAMMERIOC_MIRROR_WRITE. REMOTE: malicious mirror source feeds crafted stream to victim hammer mirror-write = target panic without target credentials. Fix: validate rec_type in mirror_write_rec.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0867 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | HAMMERIOC_MIRROR_WRITE with unhandled rec_type=0x0002, data_len=16 | 4.3 KB | view raw |
| setup.sh | setup-script | create 2G image, vnconfig, newfs_hammer -f, mount_hammer, touch target | 1.1 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o trigger trigger.c | 157 B | view raw |
| run.sh | run-script | setup + run trigger against mounted HAMMER fs | 558 B | view raw |
| README.md | readme | claim, threat model, reproduce instructions | 1.9 KB | β raw |
| VERDICT.md | verdict | full mechanism walkthrough + fix validation | 5.3 KB | β raw |
| build.log | build-log | trigger compile output (clean) | 295 B | view raw |
| run.log | run-log | baseline panic run + serial panic signature | 2.3 KB | view raw |
| fix_run.log | run-log | patched-kernel runs (clean EINVAL, no panic) x2 | 1.4 KB | view raw |
| fix_build.log | build-log | full nativekernel build of single-fix kernel (rc=0) | 5.6 MB | β download |
| panic.txt | panic-signature | panic: hammer_alloc_data: rec_type 0002 unknown + stack | 780 B | view raw |
| env.txt | environment | uname, cc 8.3, vfs.usermount=0, hammer module loaded | 269 B | view raw |
| fix.diff | suggested-fix | git-apply-able: validate rec_type in hammer_ioc_mirror_write_rec | 851 B | view 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 |
DF-0867 β PoC: kernel panic via unvalidated rec_type in mirror_write_rec
Claim (finding)
The HAMMER mirror-write record path accepts an on-disk rec_type without
validating it against the valid record-type set. A crafted mirror stream whose
record has data_len > 0 and an unhandled rec_type reaches
hammer_alloc_data() (sys/vfs/hammer/hammer_ondisk.c:1634), whose switch()
only handles INODE/DIRENTRY/EXT/FIX/PFS/SNAPSHOT/CONFIG/DATA/DB; the default
case (:1669) calls hpanic("rec_type %04x unknown", rec_type) β kernel
panic.
Reachability / threat model
HAMMERIOC_MIRROR_WRITEioctl is guarded bycaps_priv_check(cred, SYSCAP_NOVFS_IOCTL)(hammer_ioctl.c:72) β root locally.- Realistic remote vector: a malicious mirror source feeds a crafted
mirror stream to a victim running
hammer mirror-writeon its HAMMER slave. The victim kernel parses the attacker-controlled stream and panics. No credentials on the target needed beyond what mirror-write already grants (a configured mirror relationship). - This is a memory-safe DoS (the unhandled case panics before any write occurs; there is no write primitive). No escalation path.
How to reproduce
# as root on the DragonFly guest:
./build.sh # cc -o trigger trigger.c
./run.sh # creates a HAMMER image, mounts it, fires the ioctl
Expected on the unpatched kernel: guest dies; dfbsd-qemu/boot.log shows
panic: rec_type 0002 unknown
(with Stopped at hammer_alloc_data+... / db>).
Expected on a fixed kernel: trigger returns cleanly, prints
ioctl returned (no panic): head.error=... (EINVAL), guest stays up.
Files
trigger.cβ minimal mirror-write mrec with bad rec_typesetup.shβ create+newfs+mount a HAMMER image (root)build.sh/run.shfix.diffβ validate rec_type in hammer_ioc_mirror_write_rec
DF-0867 β VERDICT
Verdict
REPRODUCED β kernel panic (DoS). Fix VALIDATED on a single-fix kernel.
Mechanism (trigger β primitive β effect)
The HAMMER mirror-write path accepts an attacker-controlled on-disk
rec_type without validating it. The full chain, every hop cited:
HAMMERIOC_MIRROR_WRITEioctl reacheshammer_ioctl()(sys/vfs/hammer/hammer_ioctl.c:72β guarded bycaps_priv_check(cred, SYSCAP_NOVFS_IOCTL), i.e. root locally;:169dispatchesHAMMERIOC_MIRROR_WRITE).hammer_ioc_mirror_write()(sys/vfs/hammer/hammer_mirror.c:330) reads mirror records from the user buffer, checks onlysignature(:418) andrec_sizebounds (:422-427), then dispatchesHAMMER_MREC_TYPE_RECtohammer_ioc_mirror_write_rec()(:436-440).hammer_ioc_mirror_write_rec()(:566) validatesdata_len(:575-579) but nevermrec->leaf.base.rec_type. When the record does not exist (B-tree lookup βENOENT,:627) andcreate_tidis in window (:628), it callshammer_create_at_cursor()(:629).hammer_create_at_cursor()(sys/vfs/hammer/hammer_object.c:2274) callshammer_alloc_data(trans, leaf->data_len, leaf->base.rec_type, β¦)at:2299wheneverleaf->data_len != 0(:2298) β before thecopyin/CRC checks at:2312-2314.hammer_alloc_data()(sys/vfs/hammer/hammer_ondisk.c:1634) has aswitch(rec_type)(:1646) that handles onlyINODE/DIRENTRY/EXT/FIX/PFS/SNAPSHOT/CONFIG/DATA/DB. Thedefaultcase (:1669) doeshpanic("rec_type %04x unknown", rec_type), which expands topanic().
A single mirror record with data_len>0 and an unhandled rec_type
(e.g. 0x0002, used here) reaches step 5 and panics the kernel. Confirmed
serial-log signature:
panic: hammer_alloc_data: rec_type 0002 unknown hammer_alloc_data() at hammer_alloc_data+0x184 hammer_create_at_cursor() at hammer_create_at_cursor+0xf0 hammer_ioc_mirror_write() at hammer_ioc_mirror_write+0x3f8 hammer_ioctl() at hammer_ioctl+0xebe hammer_vop_ioctl() at hammer_vop_ioctl+0x48
Exploit chain / impact
This is a memory-safe DoS β hammer_alloc_data()'s default branch is a
panic, not a write. There is no memory-corruption primitive, so there is
no escalation chain to develop (exploit_chain: none, non-corruption class).
Impact = dos.
Threat model / reachability:
- Local: requires SYSCAP_NOVFS_IOCTL (root or jail-with-cap), so a
local unprivileged user cannot reach it directly.
- Remote (realistic): a malicious HAMMER mirror source feeds a crafted
mirror stream to a victim that runs hammer mirror-write on its slave.
The victim kernel parses the attacker-controlled stream (copyin of the
mrec at hammer_mirror.c:415) and panics β a remote, pre-auth (within the
configured mirror relationship) kernel DoS of the mirror target.
PoC
trigger.c issues HAMMERIOC_MIRROR_WRITE with one HAMMER_MREC_TYPE_REC
mrec whose leaf.base.rec_type = 0x0002 and leaf.data_len = 16. setup.sh
creates a 2 GiB HAMMER image (newfs_hammer -f), vnconfigs a free vn*,
mount_hammers it, and touches a target. run.sh ties it together.
PoC changes from the seed
The finding shipped no seed PoC (only poc_path was set). This runner
authored the complete evidence pack from scratch: trigger.c, setup.sh,
build.sh, run.sh, README.md, fix.diff, and all logs. The struct
layouts and constants were taken from the audited sys/vfs/hammer/hammer_*.h
and the installed /usr/include/vfs/hammer/ headers (the guest has them).
Fix (fix.diff)
Validate rec_type at the mirror trust boundary in
hammer_ioc_mirror_write_rec() (right after the existing data_len
validation, hammer_mirror.c:579): a switch allows only the nine types
hammer_alloc_data() can handle and returns EINVAL for anything else.
Minimal, targeted at the root cause (untrusted rec_type entering the
allocator). Supersedes/matches the finding's "validate rec_type in
mirror_write_rec" recommendation β same intent, implemented as a concrete
patch.
Fix validation (Phase 8)
- Baseline (
#0, unpatched): trigger βpanic: hammer_alloc_data: rec_type 0002 unknown, guest dies. (fix_baseline_reproduced = 1.) - **Single-fix kernel (
#1, only this diff applied, builtmake -j6 nativekernel, installed over/boot/kernel/kernel):sysctl kern.versionβ6.5-DEVELOPMENT #1: Sat Jul 11 18:29:09 UTC 2026; same trigger βioctl returned (no panic): head.error=22(EINVAL), guest stays up. Deterministic across 2 runs. (fix_patched_reproduced = 0;fix_status = fixed.)
Kernel refs (confirmed during verification)
sys/vfs/hammer/hammer_ioctl.c:72βSYSCAP_NOVFS_IOCTLprivilege gatesys/vfs/hammer/hammer_ioctl.c:169βHAMMERIOC_MIRROR_WRITEdispatchsys/vfs/hammer/hammer_mirror.c:566βhammer_ioc_mirror_write_recsys/vfs/hammer/hammer_mirror.c:575-579βdata_lenvalidated,rec_typenotsys/vfs/hammer/hammer_mirror.c:629β callshammer_create_at_cursorsys/vfs/hammer/hammer_object.c:2299βhammer_alloc_data(..., leaf->base.rec_type, ...)sys/vfs/hammer/hammer_ondisk.c:1646-1672βswitch(rec_type), only 9 types handledsys/vfs/hammer/hammer_ondisk.c:1669βhpanic("rec_type %04x unknown")β panic
Fix verification
fixedVALIDATED. Built a single-fix kernel (make -j6 nativekernel, only findings/poc/DF-0867/fix.diff applied over the with-src baseline; rc=0), installed over /boot/kernel/kernel, rebooted to #1 (kern.version ts Sat Jul 11 18:29:09). The SAME trigger that panicked the unpatched #0 baseline now returns head.error=22 (EINVAL) cleanly and the guest stays up, over 2 deterministic runs. The unhandled rec_type is now rejected at the mirror boundary before reaching hammer_alloc_data(), so the hpanic default case is no longer reachable from HAMMERIOC_MIRROR_WRITE. Fix closes the bug.
BEFORE (#0 unpatched): panic: hammer_alloc_data: rec_type 0002 unknown (guest dies, db> prompt on serial). AFTER (#1 single-fix): [!] ioctl returned (no panic): head.error=22 ; RC=0 ; vm.sh status => up (x2 runs, deterministic).
Confirmed kernel references
Detail
Exploit chain
none -- non-corruption class (memory-safe panic via hpanic default case; hammer_alloc_data never writes on the unhandled path, it panics before any allocation/use). No write/UAF/double-free/type-confusion primitive exists, so there is no slab-grooming/uid0 chain to develop. Realistic impact ceiling is a kernel DoS (local root/jail, or remote via malicious mirror source).
Evidence (decisive lines)
BASELINE (#0 unpatched): ./trigger /mnt/df0867/target -> ssh dies (timeout), boot.log shows: panic: hammer_alloc_data: rec_type 0002 unknown hammer_alloc_data() at hammer_alloc_data+0x184 hammer_create_at_cursor() at hammer_create_at_cursor+0xf0 hammer_ioc_mirror_write() at hammer_ioc_mirror_write+0x3f8 hammer_ioctl() at hammer_ioctl+0xebe hammer_vop_ioctl() at hammer_vop_ioctl+0x48 Stopped at Debugger+0x7c ; db> PATCHED (#1, fix.diff applied): same trigger -> '[!] ioctl returned (no panic): head.error=22' (EINVAL), RC=0, guest stays up. Deterministic over 2 runs.
PoC changes
Authored the entire evidence pack from scratch (the finding shipped poc_path only, no seed sources). trigger.c issues HAMMERIOC_MIRROR_WRITE (struct layouts/constants from sys/vfs/hammer/hammer_.h and the installed /usr/include/vfs/hammer headers) with one HAMMER_MREC_TYPE_REC carrying rec_type=0x0002 + data_len=16. setup.sh creates a 2G sparse HAMMER image, vnconfigs a free vn, newfs_hammer -f, mount_hammer, touches a target. build.sh/run.sh tie it together. Iterated on setup: vn4 (no node) -> auto-pick free vn0-3; newfs needs -f for <10G. fix.diff adds a rec_type validation switch to hammer_ioc_mirror_write_rec returning EINVAL for unhandled types.
Verified recommended fix
In hammer_ioc_mirror_write_rec() (sys/vfs/hammer/hammer_mirror.c, immediately after the data_len validation at :579), add a switch on mrec->leaf.base.rec_type that allows only the nine types hammer_alloc_data() handles (INODE/DATA/DIRENTRY/DB/EXT/FIX/PFS/SNAPSHOT/CONFIG) and returns EINVAL for any other -- rejecting attacker-controlled rec_types at the mirror trust boundary before they reach the allocator's hpanic default case. Matches the finding's 'validate rec_type in mirror_write_rec' recommendation; concrete git-apply-able diff in findings/poc/DF-0867/fix.diff (applies clean).
Verdict
REPRODUCED as a memory-safe kernel panic (DoS). The HAMMER mirror-write path validates mrec->leaf.data_len but never mrec->leaf.base.rec_type (hammer_mirror.c:575-579). A HAMMER_MREC_TYPE_REC mirror record with data_len>0 and an unhandled rec_type (0x0002) flows hammer_ioc_mirror_write_rec -> hammer_create_at_cursor (hammer_object.c:2299) -> hammer_alloc_data, whose switch (hammer_ondisk.c:1646) only handles INODE/DATA/DIRENTRY/DB/EXT/FIX/PFS/SNAPSHOT/CONFIG; the default case (:1669) calls hpanic('rec_type %04x unknown') -> panic. Confirmed on the unpatched #0 kernel by the exact serial signature 'panic: hammer_alloc_data: rec_type 0002 unknown' with stack hammer_vop_ioctl->hammer_ioctl->hammer_ioc_mirror_write->hammer_create_at_cursor->hammer_alloc_data. The ioctl is gated by caps_priv_check(SYSCAP_NOVFS_IOCTL) (hammer_ioctl.c:72), so locally it needs root; the realistic remote vector is a malicious HAMMER mirror SOURCE feeding a crafted stream to a victim running 'hammer mirror-write' on its slave, panicking the target kernel without target credentials. This is a panic (no write primitive), so impact=dos and there is no escalation chain.
No comments yet.