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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0875 Β· 15 files
FileTypeDescriptionSize
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
README.md readme Human-facing summary + reproduce instructions
↓ download 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
VERDICT.md verdict Full analysis: mechanism trace, reachability, fix design, Phase 8 validation
↓ download raw

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)

  1. 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_off set to an offset outside every volume (e.g. 0x004000000000000e β€” 1 PB, radix 14).
  2. 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.
  3. Mount sets up hmp->vchain with bref.type = HAMMER2_BREF_TYPE_VOLUME and vchain.data = &hmp->voldata (hammer2_vfsops.c:1189-1192).
  4. Mount locates the super-root inode: hammer2_chain_lookup() on key HAMMER2_SROOT_KEY starting from vchain (hammer2_vfsops.c:1273-1276).
  5. The lookup finds voldata.sroot_blockset.blockref[0] (the corrupted entry, hammer2_chain.c:1249-1252) and tries to load its on-disk data.
  6. hammer2_chain_load_data() calls hammer2_io_bread() with the corrupted data_off (hammer2_chain.c:998-1000).
  7. _hammer2_io_bread() β†’ _hammer2_io_getblk() β†’ hammer2_io_alloc() (hammer2_io.c:201).
  8. hammer2_io_alloc() computes pbase = lbase & pmask. The corrupted offset is non-zero and page-aligned, so the KKASSERT(pbase != 0 && ...) at hammer2_io.c:126 passes.
  9. hammer2_io_alloc() calls hammer2_get_volume(hmp, pbase) (hammer2_io.c:142). No volume matches β†’ panic("no volume for offset ...") at hammer2_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):

  1. Create a 256 MB image, vnconfig, newfs_hammer2 -L testfs.
  2. Patch all present volume-header copies: set sroot_blockset.blockref[0].data_off to 0x004000000000000e (offset 1 PB, radix 14 = 16 KB block).
  3. Recompute the three CRC32C values (ICRC1 sector-1 β†’ ICRC0 sector-0 β†’ ICRCVH whole-block, in dependency order) so the kernel accepts the header.
  4. 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:

  1. hammer2_ondisk.c:731 β€” hammer2_get_volume(): replace panic() with a diagnostic kprintf + return NULL; drop the now-redundant KKASSERT(ret).
  2. hammer2_io.c:142 β€” hammer2_io_alloc(): if hammer2_get_volume() returns NULL, return NULL instead of creating a dio with a NULL vol->dev->devvp.
  3. hammer2_io.c:205 β€” _hammer2_io_getblk(): in the createit path, if hammer2_io_alloc() returns NULL, return NULL.
  4. hammer2_io.c:399 β€” _hammer2_io_putblk(): guard against *diop == NULL (the error-cleanup path in hammer2_chain_load_data calls hammer2_io_bqrelse(&chain->dio) with a NULL dio; without this guard the fix's first iteration NULL-dereferenced here β€” Fatal trap 12).
  5. hammer2_io.c:579,589,612 β€” hammer2_io_new(), hammer2_io_newnz(), _hammer2_io_bread(): if *diop == NULL, return EIO instead 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, sha256 87ad15e5...): PoC returns mount: 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_hammer2 image 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 11 20:48:08 UTC 2026 (sha256 87ad15e5ea59faa0aec00ace3681bcd92b2c5b19c9b0ff7090db75be657d30a1)

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).