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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0867 Β· 15 files
FileTypeDescriptionSize
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
README.md readme claim, threat model, reproduce instructions
↓ download 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_WRITE ioctl is guarded by caps_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-write on 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_type
  • setup.sh β€” create+newfs+mount a HAMMER image (root)
  • build.sh / run.sh
  • fix.diff β€” validate rec_type in hammer_ioc_mirror_write_rec
VERDICT.md verdict full mechanism walkthrough + fix validation
↓ download raw

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:

  1. HAMMERIOC_MIRROR_WRITE ioctl reaches hammer_ioctl() (sys/vfs/hammer/hammer_ioctl.c:72 β€” guarded by caps_priv_check(cred, SYSCAP_NOVFS_IOCTL), i.e. root locally; :169 dispatches HAMMERIOC_MIRROR_WRITE).
  2. hammer_ioc_mirror_write() (sys/vfs/hammer/hammer_mirror.c:330) reads mirror records from the user buffer, checks only signature (:418) and rec_size bounds (:422-427), then dispatches HAMMER_MREC_TYPE_REC to hammer_ioc_mirror_write_rec() (:436-440).
  3. hammer_ioc_mirror_write_rec() (:566) validates data_len (:575-579) but never mrec->leaf.base.rec_type. When the record does not exist (B-tree lookup β†’ ENOENT, :627) and create_tid is in window (:628), it calls hammer_create_at_cursor() (:629).
  4. hammer_create_at_cursor() (sys/vfs/hammer/hammer_object.c:2274) calls hammer_alloc_data(trans, leaf->data_len, leaf->base.rec_type, …) at :2299 whenever leaf->data_len != 0 (:2298) β€” before the copyin/CRC checks at :2312-2314.
  5. hammer_alloc_data() (sys/vfs/hammer/hammer_ondisk.c:1634) has a switch(rec_type) (:1646) that handles only INODE/DIRENTRY/EXT/FIX/PFS/SNAPSHOT/CONFIG/DATA/DB. The default case (:1669) does hpanic("rec_type %04x unknown", rec_type), which expands to panic().

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, built make -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)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Jul 11 18:29:09 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (sha256 /boot/kernel/kernel = 500dc238d46912fd3a4df10ba6d94a13a6aef225593115a34b721242194e97cc)

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.