hammer2_get_volume unconditional panic on out-of-range data_off from crafted filesystem image
Summary
hammer2_ondisk.c:731-732 if(!ret) panic(no volume for offset). offset from blockref data_off on-disk field. Flow: mount loads sroot_blockset from voldata -> hammer2_chain_load_data -> hammer2_io_bread -> _hammer2_io_getblk -> hammer2_io_alloc -> hammer2_get_volume. KKASSERT at io.c:126 only catches pbase==0 any non-zero OOB offset passes. Crafted image with valid-CRC volume header but sroot_blockset data_off outside volume range = panic on mount. Fix: return NULL caller checks.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0875 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| patch_image.c | trigger-source | CRC32C-correct image patcher: corrupts sroot_blockset.blockref[0].data_off, recomputes ICRC0/ICRC1/ICRCVH | 4.7 KB | view raw |
| verify_image.c | trigger-source | Validates all three volume-header CRC32C values (sanity-checks CRC impl vs kernel) | 2.6 KB | view raw |
| build.sh | build-script | cc -O2 -o patch_image patch_image.c ; cc -O2 -o verify_image verify_image.c | 275 B | view raw |
| run.sh | run-script | Full reproduction: create image, newfs, corrupt data_off, fix CRCs, mount | 1.4 KB | view raw |
| fix.diff | suggested-fix | Validated fix: return NULL/EIO instead of panic + full NULL-propagation chain across hammer2_io.c | 1.7 KB | view raw |
| VERDICT.md | verdict | Full analysis: mechanism trace, reachability, fix design, Phase 8 validation | 6.9 KB | β raw |
| README.md | readme | Human-facing summary + reproduce instructions | 1.9 KB | β raw |
| run.log | run-log | Unpatched #0 reproduction: full output + panic signature | 1.9 KB | view raw |
| fix_run.log | fix-run-log | Patched #1 validation: clean EINVAL, before/after contrast, regression check | 2.4 KB | view raw |
| fix_build.log | build-log | Full single-fix kernel build log (make nativekernel, rc=0) | 5.6 MB | β download |
| panic.txt | panic-signature | panic: no volume for offset 0x0040000000000000 + stack trace from boot.log | 592 B | view raw |
| boot_panic_raw.log | panic-signature | Raw boot.log tail capturing the full panic sequence | 3.0 KB | view raw |
| env.txt | environment | uname, cc version, sysctl vfs.usermount | 307 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-0875 β hammer2_get_volume unconditional panic on out-of-range data_off
Bug: hammer2_get_volume() (sys/vfs/hammer2/hammer2_ondisk.c:731-732)
unconditionally panic()s when a blockref's data_off does not fall within
any mounted volume. A crafted HAMMER2 image with a valid-CRC volume header but
an out-of-range sroot_blockset.blockref[0].data_off triggers the panic on
mount. Impact: kernel DoS (panic). No write primitive β no escalation.
Fix: return NULL / propagate EIO instead of panicking. Validated on a built and booted single-fix kernel.
Reproduce
On the DragonFly guest as root:
./build.sh # cc -O2 -o patch_image patch_image.c ; cc -O2 -o verify_image verify_image.c
./run.sh # create image, newfs, corrupt data_off, fix CRCs, mount
Expected on unpatched kernel: panic β panic: no volume for offset 0x0040000000000000,
guest dead in DDB (check dfbsd-qemu/boot.log).
Expected on fixed kernel: mount: Invalid argument (EINVAL), guest stays
up. Dmesg shows hammer2_get_volume: no volume for offset ... (diagnostic)
followed by hammer2_mount: error I/O Error reading super-root.
Files
| File | Description |
|---|---|
patch_image.c |
Image patcher: corrupts data_off, recomputes CRC32C (ICRC0/ICRC1/ICRCVH) |
verify_image.c |
Validates all three volume-header CRC32C values |
build.sh |
Builds the two tools |
run.sh |
Full reproduction: image β newfs β corrupt β mount |
fix.diff |
Validated fix (supersedes finding proposal) |
VERDICT.md |
Full analysis: mechanism, trace, fix, validation |
run.log |
Unpatched-kernel reproduction (panic signature) |
fix_run.log |
Patched-kernel validation (clean EINVAL, before/after contrast) |
fix_build.log |
Full single-fix kernel build log |
panic.txt |
Panic signature excerpt from boot.log |
env.txt |
Guest environment |
manifest.json |
Machine-readable artifact catalog |
DF-0875 β hammer2_get_volume unconditional panic on out-of-range data_off
Verdict: REPRODUCED (panic / kernel DoS via crafted image); FIX VALIDATED
Severity: Medium (CVSS 3.1: AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H β local DoS
via a crafted filesystem image that an admin or auto-mounter mounts).
The bug
hammer2_get_volume() in sys/vfs/hammer2/hammer2_ondisk.c unconditionally
calls panic() when a blockref's data_off does not fall within any mounted
volume's offset range:
// sys/vfs/hammer2/hammer2_ondisk.c:731-732
if (!ret)
panic("no volume for offset 0x%016jx", (intmax_t)offset);
ret is only set if the linear scan over hmp->volumes[] finds a volume whose
[offset, offset+size) range contains the queried offset. If none matches (an
out-of-range data_off), ret stays NULL and the kernel panics.
Reachability (mount path, traced line-by-line)
- A crafted HAMMER2 image has a volume header with valid magic + all three
CRC32C sectors (ICRC0/ICRC1/ICRCVH) correct, but
sroot_blockset.blockref[0].data_offset to an offset outside every volume (e.g.0x004000000000000eβ 1 PB, radix 14). - On
mount -t hammer2, the kernel reads the volume header and validates the CRCs (hammer2_read_volume_header,hammer2_ondisk.c:528-558). All pass. - Mount sets up
hmp->vchainwithbref.type = HAMMER2_BREF_TYPE_VOLUMEandvchain.data = &hmp->voldata(hammer2_vfsops.c:1189-1192). - Mount locates the super-root inode:
hammer2_chain_lookup()on keyHAMMER2_SROOT_KEYstarting fromvchain(hammer2_vfsops.c:1273-1276). - The lookup finds
voldata.sroot_blockset.blockref[0](the corrupted entry,hammer2_chain.c:1249-1252) and tries to load its on-disk data. hammer2_chain_load_data()callshammer2_io_bread()with the corrupteddata_off(hammer2_chain.c:998-1000)._hammer2_io_bread()β_hammer2_io_getblk()βhammer2_io_alloc()(hammer2_io.c:201).hammer2_io_alloc()computespbase = lbase & pmask. The corrupted offset is non-zero and page-aligned, so theKKASSERT(pbase != 0 && ...)athammer2_io.c:126passes.hammer2_io_alloc()callshammer2_get_volume(hmp, pbase)(hammer2_io.c:142). No volume matches βpanic("no volume for offset ...")athammer2_ondisk.c:732.
The KKASSERT at hammer2_io.c:126 only catches pbase == 0 or a
cross-page-boundary block; any non-zero, page-aligned, out-of-range offset
passes straight through to the unconditional panic.
Impact
Kernel panic / denial of service. A local user who can supply a crafted
HAMMER2 filesystem image (USB media, downloaded disk image, forensic analysis,
auto-mount) and have it mounted (root, or vfs.usermount=1) triggers an
immediate kernel panic. This is a DoS, not a memory-corruption / write
primitive β no escalation chain is possible (the panic is an immediate halt,
not a corruptible write). Phase 6 (escalation) does not apply.
Reproduction
Trigger PoC (patch_image.c + verify_image.c + run.sh):
- Create a 256 MB image,
vnconfig,newfs_hammer2 -L testfs. - Patch all present volume-header copies: set
sroot_blockset.blockref[0].data_offto0x004000000000000e(offset 1 PB, radix 14 = 16 KB block). - Recompute the three CRC32C values (ICRC1 sector-1 β ICRC0 sector-0 β ICRCVH whole-block, in dependency order) so the kernel accepts the header.
mount -t hammer2 /dev/vn0@testfs /mntβ panic.
The CRC32C implementation in patch_image.c / verify_image.c uses the
standard Castagnoli polynomial (reflected 0x82F63B78, init 0xFFFFFFFF,
final XOR) matching the kernel's iscsi_crc32() (sys/libkern/icrc32.c:777).
It was validated against a known-good newfs_hammer2 image before use β
all three CRCs matched on the original (uncorrupted) header.
Panic signature (decisive evidence β from boot.log)
panic: no volume for offset 0x0040000000000000 hammer2_get_volume() at hammer2_get_volume+0x4b 0xffffffff8098726b _hammer2_io_getblk() at _hammer2_io_getblk+0x477 0xffffffff809650f7 _hammer2_io_bread() at _hammer2_io_bread+0x17 0xffffffff80965397 hammer2_chain_load_data() at hammer2_chain_load_data+0x2a5 0xffffffff8096f4a5 hammer2_chain_lock() at hammer2_chain_lock+0xde 0xffffffff8096fa4e
Fix
fix.diff (git-apply-able, validated on a built+booted single-fix kernel).
Supersedes the finding markdown's one-line proposal ("return NULL, caller
checks") by adding the full NULL-propagation chain the caller graph requires:
hammer2_ondisk.c:731βhammer2_get_volume(): replacepanic()with a diagnostickprintf+return NULL; drop the now-redundantKKASSERT(ret).hammer2_io.c:142βhammer2_io_alloc(): ifhammer2_get_volume()returns NULL, return NULL instead of creating a dio with a NULLvol->dev->devvp.hammer2_io.c:205β_hammer2_io_getblk(): in thecreateitpath, ifhammer2_io_alloc()returns NULL, return NULL.hammer2_io.c:399β_hammer2_io_putblk(): guard against*diop == NULL(the error-cleanup path inhammer2_chain_load_datacallshammer2_io_bqrelse(&chain->dio)with a NULL dio; without this guard the fix's first iteration NULL-dereferenced here βFatal trap 12).hammer2_io.c:579,589,612βhammer2_io_new(),hammer2_io_newnz(),_hammer2_io_bread(): if*diop == NULL, returnEIOinstead of dereferencing(*diop)->error.
The error then propagates through the existing clean path:
hammer2_chain_load_data() sees error != 0 β sets chain->error =
HAMMER2_ERROR_EIO β hammer2_mount() sees schain->error β returns EINVAL
(hammer2_vfsops.c:1285-1294).
Fix validation (Phase 8)
- Baseline (unpatched
#0): PoC panics βpanic: no volume for offset 0x0040000000000000, guest dead in DDB. (fix_baseline_reproduced = true) - Patched (
#1, sha25687ad15e5...): PoC returnsmount: Invalid argument(EINVAL), guest stays up. Dmesg shows the clean diagnostic chain:hammer2_get_volume: no volume for offset ...βhammer2_chain_load_data: I/O error ...: 5βhammer2_mount: error I/O Error reading super-root. (fix_patched_reproduced = false= bug does NOT reproduce) - Determinism: ran twice, both clean EINVAL, no panic.
- Regression check: an uncorrupted
newfs_hammer2image still mounts and is writable on the patched kernel (MOUNT_RC=0,WRITE_OK). No regression.
PoC changes
Authored from scratch (the finding had no pre-existing PoC folder):
- patch_image.c β CRC32C-correct image patcher (corrupts data_off,
recomputes ICRC0/ICRC1/ICRCVH in dependency order).
- verify_image.c β validates all three volume-header CRCs (used to confirm
the CRC implementation matches the kernel before mounting).
- build.sh / run.sh β exact reproducible build and run commands.
- fix.diff β the validated fix (supersedes the finding's proposal).
Fix verification
fixedVALIDATED. The same PoC (crafted image with out-of-range data_off) panics on the unpatched #0 baseline (panic: no volume for offset 0x0040000000000000, guest dead in DDB) and does NOT panic on the single-fix #1 kernel -- instead mount returns 'Invalid argument' (EINVAL) cleanly, guest stays up, dmesg shows the diagnostic chain (hammer2_get_volume: no volume / I/O error 5 / error reading super-root). Ran twice on patched kernel for determinism (both clean EINVAL). Regression check: an uncorrupted newfs_hammer2 image still mounts and is writable on the patched kernel. The first fix iteration missed the _hammer2_io_putblk NULL guard (causing a Fatal trap 12 in the error-cleanup path); the second iteration added it and the fix is complete.
BEFORE (unpatched #0): panic: no volume for offset 0x0040000000000000 / hammer2_get_volume() at +0x4b / guest dead in DDB. AFTER (patched #1): hammer2_get_volume: no volume for offset 0x0040000000000000 (diagnostic) / hammer2_chain_load_data: I/O error 004000000000000e: 5 / hammer2_mount: error I/O Error reading super-root / mount: Invalid argument (MOUNT_RC=1) / uptime: up 49 secs, guest healthy. REGRESSION: uncorrupted image MOUNT_RC=0, WRITE_OK.
Confirmed kernel references
Detail
Exploit chain
none -- this is a pure panic/DoS finding (CWE-754 improper check for unusual conditions), not a memory-corruption primitive. hammer2_get_volume calls panic() immediately on the out-of-range offset; there is no write, no UAF, no type confusion. No escalation chain is possible or needed. The realistic impact ceiling is a local/kernel DoS via a crafted filesystem image mounted by an admin or via vfs.usermount=1.
Evidence (decisive lines)
UNPATCHED #0: panic: no volume for offset 0x0040000000000000 / hammer2_get_volume() at hammer2_get_volume+0x4b / _hammer2_io_getblk() at +0x477 / _hammer2_io_bread() at +0x17 / hammer2_chain_load_data() at +0x2a5 / hammer2_chain_lock() at +0xde / Debugger("panic") / Stopped at Debugger+0x7c / db>. Guest dead in DDB on mount of the crafted image.
PoC changes
Authored the entire evidence pack from scratch (the finding had no pre-existing PoC folder): patch_image.c (CRC32C-correct image patcher that corrupts sroot_blockset.blockref[0].data_off and recomputes ICRC0/ICRC1/ICRCVH in dependency order), verify_image.c (validates all three volume-header CRCs against the kernel's iscsi_crc32 to prove the CRC implementation is correct before mounting), build.sh/run.sh reproducible scripts, VERDICT.md with the full line-by-line trace, and fix.diff (the validated fix).
Verified recommended fix
Replace the unconditional panic() in hammer2_get_volume (ondisk.c:731) with return NULL + a diagnostic kprintf, then propagate the NULL through the caller chain: hammer2_io_alloc returns NULL on vol==NULL (io.c:142), _hammer2_io_getblk returns NULL on dio==NULL (io.c:205), _hammer2_io_putblk guards diop==NULL (io.c:399, critical -- without this the error-cleanup path NULL-derefs), and hammer2_io_new/newnz/_hammer2_io_bread return EIO on diop==NULL. The existing error path in hammer2_chain_load_data (chain.c:1003) then sets HAMMER2_ERROR_EIO and mount returns EINVAL cleanly. Supersedes the finding's one-line proposal ('return NULL, caller checks') by adding the full NULL-propagation chain the caller graph requires. Full git-apply-able diff in findings/poc/DF-0875/fix.diff.
Verdict
REPRODUCED. hammer2_get_volume() at sys/vfs/hammer2/hammer2_ondisk.c:731-732 unconditionally panic()s when a blockref data_off falls outside all mounted volumes. Traced the full mount path: hammer2_mount (vfsops.c:1273) does hammer2_chain_lookup on vchain -> finds voldata.sroot_blockset.blockref[0] with a corrupted data_off -> hammer2_chain_load_data (chain.c:998) -> _hammer2_io_bread -> _hammer2_io_getblk -> hammer2_io_alloc (io.c:142) -> hammer2_get_volume. The KKASSERT at io.c:126 only catches pbase==0, so any non-zero page-aligned out-of-range offset passes through to the unconditional panic. Confirmed by crafting a HAMMER2 image with valid-CRC volume header but sroot_blockset.blockref[0].data_off=0x004000000000000e (1 PB offset, radix 14): mount panics 'no volume for offset 0x0040000000000000'. This is a pure DoS/panic (no write primitive).
No comments yet.