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

Panic / NULL deref via crafted HAMMER2 image in strategy_read_completion β€” unknown bref.type/methods from media

Summary

hammer2_strategy_read_completion :441 switch(focus->bref.type). INODE branch :452 bcopy from data unconditionally but if bref.data_off==0 chain_load_data returns chain->data NULL hammer2_xop_gdata NULL deref page fault. DATA branch :474 switch(HAMMER2_DEC_COMP(bref.methods)) only handles LZ4(2)/ZLIB(3)/NONE(0) any other value (AUTOZERO(1)/4-15 crafted image can set freely) falls through to panic(unknown compression type) :496. If bref.type anything other than INODE/DATA (INDIRECT/DIRENT/FREEMAP_LEAF encodable in crafted blockref table) panic(unknown bref type) :500. chain_lookup hammer2_chain.c:2387 loads blockrefs verbatim from media no value-range validation. Trigger: crafted HAMMER2 image mount then read file with bogus bref.type/methods/data_off=0 = instant panic. Fix: validate type/methods before switch + NULL check focus->data + replace panic with EIO+bzero.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0804 Β· 13 files
FileTypeDescriptionSize
corrupt_image.py trigger-source pure-python image corruptor: flips DATA bref comp nibble + re-weaves XXH64/CRC32C cascade (file->BOOT->SUPROOT->volhdr) 6.4 KB view raw
trigger.sh trigger-source full sequence: newfs_hammer2, write file, corrupt, mount, read 1.9 KB view raw
build.sh build-script no-op (image trigger; nothing to compile) 515 B view raw
run.sh run-script invokes trigger.sh 441 B view raw
fix.diff suggested-fix git-apply-able: replaces panic() with EIO, adds AUTOZERO case + NULL-data guard 2.0 KB view raw
panic.txt panic-signature serial-console panic excerpt from unpatched #0 run 923 B view raw
fix_run.log run-log patched #1 run: graceful EIO, guest alive 678 B view raw
fix_build.log build-log full single-fix kernel build (rc=0) 5.6 MB ↓ download
env.txt environment uname, threat model, hardening notes 625 B view raw
VERDICT.md verdict root-cause + before/after analysis 4.1 KB ↓ raw
README.md readme human-readable reproduction guide 4.2 KB ↓ 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 human-readable reproduction guide
↓ download raw

DF-0804 β€” Panic / NULL deref via crafted HAMMER2 image in strategy_read_completion

Severity: Medium Class: CWE-754 (Improper Check for Exceptional Conditions) / DoS File: sys/vfs/hammer2/hammer2_strategy.c:441-502 CVSS: 3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Verdict

REPRODUCED on the default GENERIC kernel (6.5-DEVELOPMENT #0), then FIX-VALIDATED on a single-fix #1 kernel. Impact: panic / DoS (kernel halt from a crafted filesystem image). No privilege escalation β€” this is a pure denial-of-service at a fixed code offset.

The bug

hammer2_strategy_read_completion() is the read-strategy completion callback for HAMMER2. It dispatches on focus->bref.type and, for DATA blocks, on the compression nibble of focus->bref.methods. Both dispatch tables terminate in unconditional panic() instead of returning EIO:

  • hammer2_strategy.c:496 β€” panic("unknown compression type") for any HAMMER2_DEC_COMP(methods) value other than NONE(0)/LZ4(2)/ZLIB(3). HAMMER2_COMP_AUTOZERO (1) and crafted values 4–15 reach here.
  • hammer2_strategy.c:500 β€” panic("unknown bref type") for any bref.type other than INODE(1)/DATA(3).

Additionally the INODE branch at :452 does an unconditional bcopy from data; if bref.data_off == 0, hammer2_chain_load_data() (hammer2_chain.c:938) returns early leaving chain->data == NULL and chain->error == 0, so the NULL pointer reaches bcopy β†’ page fault.

The type/methods/data_off fields are loaded verbatim from media by hammer2_chain_lookup() / hammer2_chain_get() (hammer2_chain.c:2599) with no value-range validation β€” a crafted image controls them freely.

Threat model

Filesystem-image parsing: an unprivileged actor can supply a HAMMER2 image (USB media, downloaded image, multi-tenant storage). When an admin (or an automated mount) mounts it and a file is read, the kernel panics. This is a local denial-of-service with no privilege boundary to cross beyond "image is mountable."

Reproduction (live image, full CRC re-weave)

The non-trivial part is that HAMMER2 verifies per-blockref check codes (XXH64) and the volume header's iSCSI-CRC32 trio. A naive byte-flip of the methods field fails the parent inode's CRC and the mount rejects the image. The trigger therefore:

  1. newfs_hammer2 a fresh image, write one 64 KiB file (forces a DATA bref).
  2. Flip the DATA bref methods low nibble: NONE(0) β†’ 4 (undefined).
  3. Walk the check cascade and recompute every covering digest so the image still passes integrity verification: * file-inode XXH64 β†’ BOOT-inode bref .check * BOOT-inode XXH64 β†’ SUPROOT-inode bref .check * SUPROOT-inode XXH64 β†’ volume-header sroot bref .check * volume-header ICRC1 (sroot sector) β†’ icrc_sects[6] * volume-header ICRC0 (sector 0) β†’ icrc_sects[7] * volume-header ICRCVH (whole 64 KiB) β†’ icrc_volheader
  4. mount_hammer2 + cat target.bin β†’ on the unpatched kernel: panic: hammer2_strategy_read_completion: unknown compression type.

The re-weave is implemented in pure Python (corrupt_image.py) β€” XXH64 (validated against the reference xxhash package for β‰₯32-byte payloads) and CRC32C/Castagnoli (validated against the 0xE3069283 test vector).

Files

file purpose
corrupt_image.py pure-python image corruptor (XXH64 + CRC32C, no deps)
trigger.sh full sequence: newfs β†’ write file β†’ corrupt β†’ mount β†’ read
fix.diff git-apply-able fix (replaces panic with EIO, adds AUTOZERO + NULL check)
panic.txt serial-console panic signature from the unpatched run
fix_run.log patched-kernel run output (graceful EIO, no panic)
fix_build.log full single-fix kernel build log (rc=0)
VERDICT.md detailed root-cause + before/after analysis
env.txt guest environment

Quick reproduce

On a DragonFlyBSD master DEV guest with hammer2 in the kernel:

python3 corrupt_image.py h2.img <file_off> <boot_off> <suproot_off>
vnconfig -c -S labels -T vn1 h2.img
mount_hammer2 /dev/vn1@BOOT /mnt
cat /mnt/target.bin > /dev/null      # panics on unpatched, EIO on fixed

(offsets come from hammer2 show /dev/vn1 | grep inode.0.)

VERDICT.md verdict root-cause + before/after analysis
↓ download raw

DF-0804 β€” VERDICT

Verdict: REPRODUCED on default GENERIC #0; FIX-VALIDATED on single-fix #1. Impact: panic / DoS (local, via crafted HAMMER2 image). No escalation path (NULL deref / panic at a fixed offset β€” pure availability impact).

Mechanism (trigger β†’ primitive β†’ effect)

  1. Trigger. A crafted HAMMER2 image is mounted and a file is read. The image's DATA blockref for that file carries bref.methods with low nibble 4 (undefined compression). The blockref passes integrity verification because the attacker (who controls the whole image) recomputes the covering XXH64/CRC32 cascade β€” see corrupt_image.py.

  2. Primitive. The read reaches hammer2_strategy_read_completion() (sys/vfs/hammer2/hammer2_strategy.c:441). The DATA branch (:458 β†’ :474) does switch (HAMMER2_DEC_COMP(focus->bref.methods)). The value 4 matches none of NONE/LZ4/ZLIB and falls through to the default: at :495:

c default: panic("hammer2_strategy_read_completion: " "unknown compression type");

  1. Effect. panic() β†’ Debugger("panic") β†’ all CPUs stopped, DDB prompt, system halted. Serial console:

panic: hammer2_strategy_read_completion: unknown compression type hammer2_xop_strategy_read() at hammer2_xop_strategy_read+0x606 Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip) db>

The same function has a second unconditional panic("unknown bref type") at :500 for any bref.type other than INODE(1)/DATA(3), and a latent NULL deref in the INODE branch (:452) when bref.data_off == 0 (which makes hammer2_chain_load_data return early at hammer2_chain.c:938 leaving chain->data == NULL, chain->error == 0).

Why no escalation

This is a panic() / NULL-deref at a fixed code offset with no attacker-controlled write primitive. The crash terminates the kernel; there is no corrupted object to groom, no function pointer to redirect. It is a pure denial-of-service. (The guest has SMAP/SMEP/KASLR off, but those only matter for a write/control primitive β€” a panic is game-over-by-definition with no shell to land.)

Reachability confirmation (line-by-line)

  • hammer2_xop_strategy_read() (:308) calls hammer2_chain_lookup() (:344) with HAMMER2_LOOKUP_ALWAYS β€” forces data resolution.
  • hammer2_chain_lookup() (:2387) scans the parent's blockref array via hammer2_combined_find() (:2599) which returns the on-disk bref without validating type/methods/data_off ranges.
  • hammer2_chain_get() builds a chain copying bsave = *bref verbatim (:2599); hammer2_chain_load_data() (:920) honours data_off and, if (data_off & ~HAMMER2_OFF_MASK_RADIX) == 0, returns at :938 with chain->data still NULL and chain->error still 0.
  • Frontend (:401-410): error == 0 β†’ data = hammer2_xop_gdata()->buf β†’ hammer2_strategy_read_completion(focus, data, bio).
  • hammer2_strategy_read_completion() :474 switch β†’ :495 default β†’ panic.

Fix (fix.diff)

Three changes, all in hammer2_strategy_read_completion:

  1. NULL-data guard in the INODE branch (:452): if data == NULL, set B_ERROR/EIO and return (closes the data_off==0 NULL-deref).
  2. HAMMER2_COMP_AUTOZERO case: zero-fill the buffer (comp=1 is a valid "logically zero, not stored" method that previously fell through to panic).
  3. Replace both panic() calls (:496, :500) with kprintf + B_ERROR/ EIO + bp->b_resid = bp->b_bcount. A corrupted/crafted image now yields a per-read EIO (and a diagnostic dmesg line) instead of halting the kernel.

Before / after

kernel trigger result
#0 unpatched baseline mount crafted image, cat target.bin panic unknown compression type, system halted, DDB db>
#1 single-fix same image, same command EIO (cat exit 1), dmesg: unknown compression type 4, guest stays up

Reproduced deterministically (Γ—2 on the patched kernel). The build compiled cleanly (make -j6 nativekernel rc=0) and the #1 kernel boots and operates normally.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. fix.diff applies cleanly (3 hunks), compiles (make -j6 nativekernel rc=0), boots (#1). The SAME crafted image that panicked the unpatched #0 baseline (panic 'unknown compression type', system halted at DDB db>) now produces a graceful per-read EIO on the patched #1 kernel: cat exit 1, dmesg 'hammer2_strategy_read_completion: unknown compression type 4', guest stays up and responsive (uptime confirms). Deterministic across 2 runs. The fix closes the bug.

BASELINE (#0): cat /mnt/h2/target.bin -> panic: hammer2_strategy_read_completion: unknown compression type / Stopped at Debugger+0x7c / db> (guest DEAD). PATCHED (#1): cat /mnt/h2/target.bin -> READ_EXIT=1 (EIO) / dmesg: hammer2_strategy_read_completion: unknown compression type 4 / uptime: up 2 mins (guest ALIVE).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Fri Jul 10 17:57:41 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (sha256 kernel=2ff97b5e8409f7bd31cfeea72a0befc5486000c1473561b60ed47e58ce52d0c4)

Confirmed kernel references

Detail

Exploit chain

none -- this is a pure panic/DoS (CWE-754) at a fixed code offset with no attacker-controlled write primitive; panic terminates the kernel so there is no object to groom, no pointer to redirect, no shell to land. A crafted HAMMER2 image mounted by root (filesystem-image-parsing threat model) instantly halts the system. No escalation chain exists or is attempted (NULL deref / panic is game-over-by-definition).

Evidence (decisive lines)

UNPATCHED #0: mount_hammer2 /dev/vn1@BOOT /mnt/h2 -> MOUNT_OK; cat /mnt/h2/target.bin -> panic: hammer2_strategy_read_completion: unknown compression type / hammer2_xop_strategy_read() at +0x606 / Stopped at Debugger+0x7c / db>  (guest DEAD). PATCHED #1 same image+cmd: cat -> READ_EXIT=1 (EIO), dmesg: 'hammer2_strategy_read_completion: unknown compression type 4', uptime up 2 mins (guest ALIVE). Deterministic x2.

PoC changes

Authored the full PoC from scratch (no prior poc folder existed). corrupt_image.py: pure-python (no deps) image corruptor implementing XXH64 (seed 0x4d617474446c6c6e, validated vs xxhash pkg for >=32B payloads) and CRC32C/Castagnoli (validated vs 0xE3069283 test vector); flips the DATA bref methods low nibble NONE(0)->4 and re-weaves the 3-level XXH64 check cascade + the volume-header ICRC0/ICRC1/ICRCVH iSCSI-CRC32 trio so the crafted image passes kernel integrity verification. trigger.sh: newfs_hammer2 + write 64KB file + corrupt + mount + read. README.md, VERDICT.md, build.sh, run.sh, env.txt written.

Verified recommended fix

In hammer2_strategy_read_completion (hammer2_strategy.c): (1) add a NULL-data guard in the INODE branch before the bcopy at :452 (closes the data_off==0 NULL deref); (2) add a HAMMER2_COMP_AUTOZERO(1) case that zero-fills the buffer (a valid 'logically zero' method that previously fell through to panic); (3) replace panic('unknown compression type') at :496 and panic('unknown bref type') at :500 with kprintf + bp->b_flags|=B_ERROR + bp->b_error=EIO + bp->b_resid=bp->b_bcount so a corrupted/crafted image yields a per-read EIO instead of halting the kernel. This is a new proposal (no prior finding markdown existed); supersedes the DB summary's sketch by also handling AUTOZERO and the data_off==0 NULL deref. Full git-apply-able diff in findings/poc/DF-0804/fix.diff.

Verdict

REPRODUCED on default GENERIC #0, then FIX-VALIDATED on single-fix #1. hammer2_strategy_read_completion() (hammer2_strategy.c:441) terminates its DATA-bref compression dispatch (switch on HAMMER2_DEC_COMP(bref.methods) at :474) with an unconditional panic('unknown compression type') at :496, and its bref.type dispatch with panic('unknown bref type') at :500. blockref type/methods/data_off are loaded verbatim from media by hammer2_chain_lookup/chain_get (hammer2_chain.c:2599) with no value-range validation; chain_load_data returns early at :938 on data_off==0 leaving chain->data NULL with error==0 (latent NULL deref in the INODE bcopy at :452). I crafted a HAMMER2 image whose DATA blockref carries comp nibble 4 (undefined), re-wove the full integrity cascade (file-inode XXH64 -> BOOT bref check -> SUPROOT bref check -> volume sroot bref check -> volume ICRC0/ICRC1/ICRCVH, pure-python XXH64+CRC32C in corrupt_image.py) so the image passes the kernel's verification, then mounted it and read the file: panic 'hammer2_strategy_read_completion: unknown compression type', system halted at DDB. Confirmed via serial console boot.log.