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

NULL deref in direct I/O paths when hammer_get_volume fails β€” hammer_rel_volume(NULL) unconditional

Summary

hammer_io.c:1487,1561,1726 hammer_get_volume(hmp,vol_no,&error) may return NULL (ENOENT RB_LOOKUP hammer_ondesk.c:430 or hammer_load_volume I/O error :441). :1501,1600,1760 hammer_rel_volume(volume,0) UNCONDITIONAL β€” no NULL check. hammer_rel_volume (hammer_ondisk.c:533) immediately derefs volume->io.lock = NULL deref panic. if(error==0) guards correctly short-circuit volume->maxbuf_off/ondisk/devvp access but trailing release is unconditional. hammer_blockmap_lookup (hammer.h:1507) skips validation when hammer_verify_zone==0 (default) passes invalid data_offset vol_no through verbatim. Correct NULL-check pattern at hammer_recover.c:1104-1108. Trigger: crafted HAMMER image B-Tree leaf data_offset vol_no >= mounted volume count mount then cat file. DoS only not memory corruption. Fix: if(volume) hammer_rel_volume(volume,0) at 3 call sites + NULL guard in hammer_rel_volume itself.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0792 Β· 12 files
FileTypeDescriptionSize
corrupt_hammer.c trigger-source offline image corruptor: flips vol_no byte of zone-10 leaf entries, recomputes btree node CRC 4.8 KB view raw
build.sh build-script cc -O2 -o corrupt_hammer corrupt_hammer.c 206 B view raw
run.sh run-script end-to-end: newfs image, write file, corrupt, remount, cat -> panic (unpatched) or ENOENT (patched) 2.5 KB view raw
fix.diff suggested-fix NULL guards at 3 io.c call sites + defense-in-depth NULL check in hammer_rel_volume 1.2 KB view raw
panic.txt panic-signature Fatal trap 12 in hammer_rel_interlock+0x20 (movl (%r12),%ebx), fault va=0x0, from cat reading corrupted testfile 2.2 KB view raw
fix_build.log build-log full make nativekernel output for the patched kernel; NK_DONE rc=0 5.6 MB ↓ download
fix_run.log run-log full run.sh output on the patched #1 kernel; cat returns ENOENT, no panic 2.1 KB view raw
env.txt environment uname, kern.version (#1), kernel sha256, vfs.usermount=0, verify_zone=0 511 B view raw
README.md readme human-facing reproduction and expected-behaviour table 3.9 KB ↓ raw
VERDICT.md verdict root-cause narrative with path:line citations 6.1 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-facing reproduction and expected-behaviour table
↓ download raw

DF-0792 β€” NULL deref in HAMMER direct I/O when hammer_get_volume fails

Verdict

REPRODUCED β€” Low-severity local DoS (kernel panic via NULL-pointer fetch).

The bug is a real, reachable NULL-pointer dereference in three HAMMER direct-I/O paths. Triggering it requires an admin to mount a maliciously crafted HAMMER v1 filesystem image and then read any file whose B-Tree leaf entries reference a volume number that is not present in the mount. No memory corruption, no info leak, no privilege escalation β€” pure DoS (kernel panic). Validated end-to-end: panic on the unpatched #0 kernel, clean EIO/ENOENT error return + guest stays up on the single-fix #1 kernel.

How to reproduce

# As root on the DragonFlyBSD guest (vfs.usermount=0 by default).
scp corrupt_hammer.c run.sh build.sh dfbsd:/root/htest/
ssh dfbsd
sh /root/htest/run.sh

run.sh does everything end-to-end: 1. Creates a 2 GiB sparse image and newfs_hammer -V 6 on it (vol_version 6 so the B-Tree node CRC is plain crc32, which the corruptor can recompute locally). 2. Mounts it, writes a 256 KiB testfile (forces the zone-10 large-data direct-I/O path), syncs, unmounts. 3. Runs corrupt_hammer, which scans the offline image for B-Tree leaf nodes (validated by recomputing the node CRC), finds every zone-10 leaf entry whose data_len >= 16 KiB, and flips the vol_no byte of data_offset from 0 to 7 (any value not in the mount). The B-Tree node CRC is then recomputed so the kernel's hammer_crc_test_btree() check (hammer_ondisk.c:1315) does not reject the node before reaching the buggy call site. 4. Re-mounts the now-malicious image and cats testfile.

Expected

Kernel Behaviour
Unpatched #0 (audit base) Kernel panic; guest dies in DDB.
Stopped at hammer_rel_interlock+0x20: movl (%r12),%ebx
Patched #1 (this fix.diff) cat: ...: No such file or directory (ENOENT from the
missing volume). Guest stays up. dmesg shows repeated
hammer_io_direct_read: failed @ 2070000022000000.

Trigger realism

vfs.usermount=0 on this guest, so mounting a HAMMER image requires root. The realistic threat is "admin mounts a HAMMER image obtained from an untrusted source" (USB stick, downloaded snapshot, restore-from-backup of unknown provenance). HAMMER is no longer the default filesystem (HAMMER2 is), but HAMMER v1 is still supported, so an admin can be presented with one. The default vfs.hammer.verify_zone=0 means the blockmap lookup does not validate the zone-2 vol_no before it reaches hammer_get_volume, so the malicious value is delivered verbatim.

Files in this folder

File What
corrupt_hammer.c offline image corruptor: flips vol_no byte + rebuilds btree node CRC
build.sh cc -O2 -o corrupt_hammer corrupt_hammer.c
run.sh end-to-end trigger (newfs, mount, write, corrupt, remount, cat)
fix.diff git-apply-able unified diff: NULL guards at 3 io.c call sites + defense-in-depth NULL check in hammer_rel_volume itself
panic.txt serial-console panic signature from the unpatched run
fix_build.log full make nativekernel output for the patched kernel
fix_run.log full run.sh output on the patched kernel (no panic)
env.txt guest uname, kern.version, kernel sha256, sysctl state
VERDICT.md human-readable root-cause narrative
manifest.json machine-readable artifact catalog
VERDICT.md verdict root-cause narrative with path:line citations
↓ download raw

DF-0792 β€” VERDICT

Status: REPRODUCED (Low-severity local DoS / kernel panic via NULL-pointer fetch) Fix: VALIDATED (panic on #0, clean error return + guest stays up on #1) Class: NULL-pointer dereference (CWE-476). No memory corruption, no info leak, no privesc.

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

Three direct-I/O paths in sys/vfs/hammer/hammer_io.c follow the same shape: look up the volume by vol_no, do the I/O if no error, and unconditionally release the volume reference afterwards β€” even when the lookup just returned NULL with an error set.

Call site 1 β€” hammer_io_direct_read (sys/vfs/hammer/hammer_io.c:1487-1501)

vol_no = HAMMER_VOL_DECODE(zone2_offset);
volume = hammer_get_volume(hmp, vol_no, &error);     /* may return NULL */
if (error == 0 && zone2_offset >= volume->maxbuf_off) /* guarded */
    error = EIO;
if (error == 0) {
    /* ...uses volume... */                            /* guarded */
}
hammer_rel_volume(volume, 0);                         /* BUG: unconditional */

Call site 2 β€” hammer_io_indirect_read (sys/vfs/hammer/hammer_io.c:1561-1600)

Same shape; same trailing hammer_rel_volume(volume, 0) at line 1600.

Call site 3 β€” hammer_io_direct_write (sys/vfs/hammer/hammer_io.c:1726-1760)

Same shape; trailing release at line 1760.

Why NULL is reachable

hammer_get_volume (sys/vfs/hammer/hammer_ondisk.c:421-448) returns NULL with *errorp = ENOENT whenever vol_no is not in the in-memory volume RB tree:

volume = RB_LOOKUP(hammer_vol_rb_tree, &hmp->rb_vols_root, vol_no);
if (volume == NULL) {
    *errorp = ENOENT;
    return(NULL);
}

It also returns NULL when hammer_load_volume fails (*errorp set, volume = NULL at line 442).

vol_no is decoded from a zone-2 offset (HAMMER_VOL_DECODE extracts bits 52-59) that came from hammer_blockmap_lookup (sys/vfs/hammer/hammer.h:1508-1526). The default vfs.hammer.verify_zone == 0 (verified on this guest) skips hammer_blockmap_lookup_verify and just translates the zone-X offset to zone-2 verbatim, so an attacker-controlled data_offset in a B-Tree leaf entry delivers an arbitrary vol_no straight into hammer_get_volume. The leaf entry's data_offset lives on disk and is attacker-controllable in a crafted image.

Why the deref crashes

hammer_rel_volume (sys/vfs/hammer/hammer_ondesk.c:528-542) immediately dereferences volume:

void
hammer_rel_volume(hammer_volume_t volume, int locked)
{
    struct buf *bp;

    if (hammer_rel_interlock(&volume->io.lock, locked)) {   /* NULL deref */
        ...
    }
}

The first instruction of hammer_rel_interlock is a load from &volume->io.lock, which at volume == NULL is a small positive offset from zero. The kernel faults; on this build it lands in DDB:

Fatal user address access from kernel mode from cat at ffffffff80951940
Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x0
Stopped at      hammer_rel_interlock+0x20:      movl    (%r12),%ebx

The if (error == 0) guards are correct but incomplete

The reviewer's claim that the volume is dereferenced via volume->maxbuf_off / volume->ondisk / volume->devvp only inside if (error == 0) blocks is correct β€” those are properly guarded. The bug is only the trailing, unconditional release.

Confirmed reproduction

run.sh builds a 2 GiB HAMMER v1 (-V 6) image, writes a 256 KiB file, takes it offline, flips the vol_no byte of every zone-10 leaf entry from 0 to 7 (recomputing the B-Tree node CRC so hammer_crc_test_btree at hammer_ondisk.c:1315 doesn't reject the node), re-mounts, and cats the file.

  • Unpatched #0 kernel: kernel panic, guest dies in DDB. See panic.txt.
  • Patched #1 kernel (with fix.diff): cat returns No such file or directory (the ENOENT from the missing volume, propagated up), guest stays up. dmesg shows hammer_io_direct_read: failed @ 2070000022000000 (the hdkprintf("failed @ %016jx\n", ...) at hammer_io.c:1504, now reached only after the guarded release is skipped). See fix_run.log.

Exploit chain

Not applicable. This is a NULL-pointer fetch β€” it crashes the kernel cleanly with no write primitive, no info leak, no privilege change. There is no escalation chain to develop. The realistic impact ceiling is local DoS of an admin who mounts an untrusted HAMMER image.

Fix

fix.diff makes the release conditional at all three call sites and also adds a defense-in-depth NULL check inside hammer_rel_volume itself (so any other caller that ever passes NULL cannot reintroduce the panic). This matches the existing correct pattern in hammer_recover.c:1082 (if (volume == NULL) ... break;).

The fix is minimal and targeted at the root cause; it does not change the error handling or volume lookup, only the unconditional release.

PoC changes

There was no pre-existing PoC source for DF-0792 in the repo, so I authored corrupt_hammer.c, build.sh, and run.sh from scratch. The corruptor recomputes the B-Tree node CRC (crc32 for vol_version <= 6) so the kernel does not reject the node before reaching the buggy call site β€” without this step the kernel's hammer_crc_test_btree check at hammer_ondisk.c:1315 would return EIO before the bug could fire, masking the bug entirely.

Notes for the maintainer

  • The trigger requires root to mount the image (vfs.usermount=0). This is consistent with the Low severity rating. The realistic threat is an admin mounting an untrusted HAMMER image (USB, downloaded snapshot, restore of unknown provenance).
  • HAMMER is no longer the default filesystem (HAMMER2 is), but HAMMER v1 is still fully supported, so admins can still be presented with such images.
  • The same defensive NULL check is independently useful in hammer_rel_volume itself β€” it converts any future bug of this shape into a silent no-op rather than a panic, at zero cost.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline NULL deref panic at hammer_rel_interlock+0x20; patched ENOENT, guest up.

BEFORE: Fatal trap 12 at hammer_rel_interlock+0x20, va=0x0, guest down. AFTER: cat ENOENT, guest up, dmesg 'hammer_io_direct_read: failed'.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Jul 13 19:10:59 UTC 2026 (sha256 70b537b6...)

Confirmed kernel references

Detail

Exploit chain

none. Pure NULL-pointer read fetch (CWE-476). No write, no info leak. Local DoS.

Evidence (decisive lines)

BASELINE: Fatal trap 12 at hammer_rel_interlock+0x20 (movl (%r12),%ebx), fault va=0x0, guest down. PATCHED: cat returns ENOENT, guest up, dmesg shows 'hammer_io_direct_read: failed' (error propagated, no panic).

PoC changes

Authored corrupt_hammer.c (offline image corruptor: flips vol_no byte of zone-10 B-Tree leaves + recomputes node CRC), run.sh (end-to-end: newfs_hammer + mount + write + unmount + corrupt + remount + cat). Added fix.diff, build.sh, VERDICT.md, manifest.json.

Verified recommended fix

Add if(volume!=NULL) guards before hammer_rel_volume at hammer_io.c:1501/1600/1760, plus defense-in-depth NULL check in hammer_rel_volume itself at hammer_ondisk.c:533. Full git-apply-able diff in findings/poc/DF-0792/fix.diff.

Verdict

REPRODUCED. hammer_io_direct_read/indirect_read/direct_write call hammer_rel_volume(volume,0) unconditionally after hammer_get_volume may return NULL (vol_no from crafted image). hammer_rel_volume derefs &volume->io.lock -> page fault at va=0x0. Crafted HAMMER v1 image, root-only mount.