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)
PoC verification
Evidence pack
findings/poc/DF-0804 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 anyHAMMER2_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 anybref.typeother 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:
newfs_hammer2a fresh image, write one 64 KiB file (forces a DATA bref).- Flip the DATA bref
methodslow nibble: NONE(0) β 4 (undefined). - 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 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.)
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)
-
Trigger. A crafted HAMMER2 image is mounted and a file is read. The image's DATA blockref for that file carries
bref.methodswith low nibble4(undefined compression). The blockref passes integrity verification because the attacker (who controls the whole image) recomputes the covering XXH64/CRC32 cascade β seecorrupt_image.py. -
Primitive. The read reaches
hammer2_strategy_read_completion()(sys/vfs/hammer2/hammer2_strategy.c:441). The DATA branch (:458β:474) doesswitch (HAMMER2_DEC_COMP(focus->bref.methods)). The value4matches none of NONE/LZ4/ZLIB and falls through to thedefault:at:495:
c
default:
panic("hammer2_strategy_read_completion: "
"unknown compression type");
- 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) callshammer2_chain_lookup()(:344) withHAMMER2_LOOKUP_ALWAYSβ forces data resolution.hammer2_chain_lookup()(:2387) scans the parent's blockref array viahammer2_combined_find()(:2599) which returns the on-diskbrefwithout validatingtype/methods/data_offranges.hammer2_chain_get()builds a chain copyingbsave = *brefverbatim (:2599);hammer2_chain_load_data()(:920) honoursdata_offand, if(data_off & ~HAMMER2_OFF_MASK_RADIX) == 0, returns at:938withchain->datastill NULL andchain->errorstill 0.- Frontend (
:401-410):error == 0βdata = hammer2_xop_gdata()->bufβhammer2_strategy_read_completion(focus, data, bio). hammer2_strategy_read_completion():474switch β:495default β panic.
Fix (fix.diff)
Three changes, all in hammer2_strategy_read_completion:
- NULL-data guard in the INODE branch (
:452): ifdata == NULL, setB_ERROR/EIOand return (closes thedata_off==0NULL-deref). HAMMER2_COMP_AUTOZEROcase: zero-fill the buffer (comp=1 is a valid "logically zero, not stored" method that previously fell through to panic).- Replace both
panic()calls (:496,:500) withkprintf+B_ERROR/EIO+bp->b_resid = bp->b_bcount. A corrupted/crafted image now yields a per-readEIO(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
fixedVALIDATED. 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).
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.
No comments yet.