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

Crafted image triggers kernel panic via unimplemented directory hash algorithm (ALG2/ALG3)

Summary

hammer_subs.c:953 switch(dip->ino_data.cap_flags & HAMMER_INODE_CAP_DIRHASH_MASK=0x03). ALG0(0x00) ALG1(0x01) implemented. ALG2(0x02) ALG3(0x03) default :1026-1032 hpanic("bad algorithm %p",dip) = kernel panic. cap_flags read verbatim from disk at hammer_inode.c:525 no semantic validation. CRC32 checked but trivially recomputable by attacker. Trigger: crafted HAMMER1 image directory inode cap_flags bits[1:0]=0x02 mount then any filename operation (touch/ls/create/rename). Fix: fall back to ALG0 + kprintf warning instead of panic.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0838 Β· 13 files
FileTypeDescriptionSize
image_patcher.c trigger-source in-guest tool that flips directory-inode cap_flags to ALG2 on a HAMMER1 v6 image and refreshes the leaf data_crc + B-Tree node CRC 7.1 KB view raw
repro.sh repro-script end-to-end driver: newfs_hammer -> mkdir -> umount -> patch -> mount -> ls (panics on unpatched) 2.4 KB view raw
build.sh build-script cc image_patcher.c -lz 231 B view raw
run.sh run-script alias of repro.sh chain 1.6 KB view raw
panic.txt panic-signature baseline panic in hammer_direntry_namekey via hammer_vop_nresolve on unpatched #0 632 B view raw
run.log run-log PATCHED #1 kernel: ls/cat succeed, no panic, fallback warning in dmesg 992 B view raw
fix_build.log build-log full nativekernel build of the single-fix kernel (#1), NK_DONE rc=0 5.6 MB ↓ download
env.txt environment uname, cc version, vfs.hammer.supported_version, hammer utility paths 799 B view raw
fix.diff suggested-fix two-file fix: add HAMMER_INODE_BADDIRHASH flag; replace hpanic in default case with ALG0 fallback + once-per-inode kprintf warning 1.7 KB view raw
README.md readme human-facing build/run/expected docs 3.1 KB ↓ raw
VERDICT.md verdict full mechanism walkthrough, evidence, fix validation 6.7 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 build/run/expected docs
↓ download raw

DF-0838 β€” build & run

Build (the in-guest image-patcher)

cc -O2 -Wall -o image_patcher image_patcher.c -lz

The C source uses only zlib's crc32() (matches HAMMER v≀6 inode/leaf CRC). Run as root on the DragonFlyBSD guest.

Reproduce the panic (full chain)

Driver script repro.sh (run as root inside the guest) does everything:

  1. cc the patcher
  2. truncate -s 2G /root/df0838.img
  3. vnconfig vn /root/df0838.img β†’ e.g. vn4
  4. newfs_hammer -L df0838 -V 6 -f /dev/vn4 (forces vol_version=6 so leaf CRC is plain crc32, matching the patcher)
  5. mount_hammer + mkdir testdir + populate testdir/file1 + umount
  6. Run image_patcher against the raw image file: * finds every directory inode (ver=1, mode=0755, obj_type=1) * flips cap_flags bits[1:0] to 0x02 (ALG2 β€” unimplemented) * recomputes the per-leaf data_crc32 * recomputes the parent B-Tree node's CRC (4 KiB node, CRC over bytes [4,4096))
  7. vnconfig vn /root/df0838.img again
  8. mount_hammer /dev/vnN /mnt/df0838
  9. ls -la /mnt/df0838/testdir/file1 β†’ panics the unpatched kernel

Expected on the UNPATCHED kernel (#0 baseline)

panic: hammer_direntry_namekey: bad algorithm 0xfffff80116ed4640
cpuid = 3
Trace beginning at frame 0xfffff80118b07418
hammer_direntry_namekey() at hammer_direntry_namekey+0x1ae
hammer_vop_nresolve() at hammer_vop_nresolve+0x20f
vop_nresolve() at vop_nresolve+0x53
cache_resolve() at cache_resolve+0x61e
nlookup() at nlookup+0x8e1
Debugger("panic")
Stopped at      Debugger+0x7c:  movb    $0,0xbdaf09(%rip)
db>

The trace points exactly at hammer_subs.c:1031 (hpanic("bad algorithm")) reached via hammer_vnops.c:1220 (hammer_vop_nresolve).

Expected on the PATCHED kernel (#1, with fix.diff applied)

mount rc=0
-rw-r--r--  1 root  wheel  6 ... /mnt/df0838/testdir/file1
ls rc=0
hello
# dmesg:
hammer_direntry_namekey: dir ino 0000000000000001 has unknown dirhash alg 2, falling back to ALG0
hammer_direntry_namekey: dir ino 0000000100000615 has unknown dirhash alg 2, falling back to ALG0

No panic, file accessible, fallback to ALG0.

Realism / reachability (Phase 6)

Triggering requires mount_hammer of the attacker's crafted image. On a real system an attacker:

  • Crafts the HAMMER1 image offline (their own machine) β€” every step in image_patcher.c works just as well on a copy of hammer_disk.h plus zlib.
  • Ships it to a victim (USB / download / mail attachment).
  • Victim mounts the image β€” either as root, or, with vfs.usermount=1 and ownership of the image+mountpoint, as any unprivileged user.
  • Any subsequent filename op (ls /mnt/foo, cat /mnt/x, touch, rename) under the corrupted directory panics the kernel.

This is a kernel DoS via a crafted filesystem image β€” a classic attacker- supplied-parses-data threat. No escalation chain applies (the panic kills the kernel before any primitive can be derived); impact is panic / DoS.

cap_flags is read verbatim from disk at hammer_inode.c:525 with no semantic validation of the DIRHASH bits β€” only the leaf data_crc32 (which the attacker can recompute) is checked.

VERDICT.md verdict full mechanism walkthrough, evidence, fix validation
↓ download raw

DF-0838 β€” VERDICT

Finding: Crafted HAMMER1 image triggers kernel panic via unimplemented directory-hash algorithm (ALG2/ALG3 bits).

Severity: Low (local DoS via crafted filesystem image β€” requires the victim to mount an attacker-supplied image).

Verdict: REPRODUCED. Fix validated.

Mechanism

hammer_direntry_namekey(dip, name, len, max_iter) at sys/vfs/hammer/hammer_subs.c:944 is the directory-name hash function. It switches on the low 2 bits of dip->ino_data.cap_flags:

case HAMMER_INODE_CAP_DIRHASH_ALG0: ... break;          /* 0x00 β€” crc32 */
case HAMMER_INODE_CAP_DIRHASH_ALG1: ... break;          /* 0x01 β€” domain hash */
case HAMMER_INODE_CAP_DIRHASH_ALG2:
case HAMMER_INODE_CAP_DIRHASH_ALG3:
default:
    key = 0;
    *max_iterationsp = 1;
    hpanic("bad algorithm %p", dip);                    /* <-- hammer_subs.c:1031 */
    break;

ALG0 and ALG1 are the only implemented algorithms; ALG2/ALG3 are reserved and unimplemented. The default case calls hpanic (an always-panic wrapper).

dip->ino_data is the in-memory copy of the on-disk struct hammer_inode_data (hammer_disk.h:880). It is loaded verbatim from disk at hammer_inode.c:525:

ip->ino_data = cursor.data->inode;

with no semantic validation of the cap_flags byte (offset 65 of the struct). The only on-disk integrity check is the leaf data_crc32 (hammer_crc.h:265-289), which the attacker can freely recompute after modifying cap_flags. The CRC is crc32 for vol_version ≀6, iscsi_crc32 for β‰₯7.

The directory-hash function is reached from every directory-modifying / lookup VOP β€” hammer_vop_nresolve (path lookup, hammer_vnops.c:1220), hammer_vop_ncreate (hammer_vnops.c:3310), hammer_vop_nlink (hammer_vnops.c:2000), and hammer_ip_create (hammer_object.c:676) β€” so any filename operation under the corrupted directory triggers the panic.

Trigger / PoC

End-to-end reproduction driver is in repro.sh (run as root inside the guest). The image-corruption tool is image_patcher.c.

  1. Build a HAMMER1 v6 image (newfs_hammer -V 6).
  2. Populate it (mkdir testdir; echo hello > testdir/file1).
  3. Run image_patcher on the raw image: * Locates every directory inode by signature (ver=1, mode=0o755, obj_type=HAMMER_OBJTYPE_DIRECTORY=1). * Sets cap_flags bits[1:0] to 0x02 (ALG2) on every directory. * Recomputes the per-leaf data_crc32 (over the inode's first 112 bytes = HAMMER_INODE_CRCSIZE). * Recomputes the parent B-Tree node's own CRC (hammer_crc.h:227: crc32(&node->crc+1, sizeof(node)-4)).
  4. Mount the modified image and do any filename operation under it.

The patcher modifies three directory inodes by default (root + an intermediate dir + testdir), so even a ls /mnt/somefile lookup (which goes through the root directory's hash) hits the panic β€” there's no need to navigate into testdir specifically.

Evidence

Baseline panic (unpatched kernel 6.5-DEVELOPMENT #0, after ls -la /mnt/df0838/testdir/file1):

panic: hammer_direntry_namekey: bad algorithm 0xfffff80116ed4640
cpuid = 3
Trace beginning at frame 0xfffff80118b07418
hammer_direntry_namekey() at hammer_direntry_namekey+0x1ae 0xffffffff80951f5e
hammer_direntry_namekey() at hammer_direntry_namekey+0x1ae 0xffffffff80951f5e
hammer_vop_nresolve() at hammer_vop_nresolve+0x20f 0xffffffff8095743f
vop_nresolve() at vop_nresolve+0x53 0xffffffff8070c3c3
cache_resolve() at cache_resolve+0x61e 0xffffffff806e06fe
nlookup() at nlookup+0x8e1 0xffffffff806ef621
Debugger("panic")
Stopped at      Debugger+0x7c:  movb    $0,0xbdaf09(%rip)
db>

The stack pins the bug exactly: hammer_direntry_namekey+0x1ae is the hpanic call site at hammer_subs.c:1031.

Reproduced twice from fresh vm.sh reset with-src snapshots; deterministic.

Impact

  • Confidentiality: none.
  • Integrity: none (the panic is reached before any state change).
  • Availability: kernel panic β‡’ full system crash β‡’ local DoS.
  • Preconditions: the victim must mount_hammer an attacker-supplied HAMMER1 image. Root can do this directly; an unprivileged user can do this when vfs.usermount=1 and the image+mountpoint are owned by them (a normal configuration for "mount this USB / image" workflows).

No escalation chain applies β€” the panic fires synchronously inside the syscall path before any corruption primitive can be derived. Impact is DoS only, matching the original Low severity.

Fix

Authored in fix.diff. The change is two-file:

  1. sys/vfs/hammer/hammer.h β€” add a per-inode flag HAMMER_INODE_BADDIRHASH (0x10000000) to log the warning exactly once per affected directory inode.

  2. sys/vfs/hammer/hammer_subs.c:1026 β€” in the default case, instead of hpanic("bad algorithm %p", dip), fall back to the ALG0 computation (identical code to the ALG0 case) and emit one hdkprintf warning.

This is the minimal, root-cause fix: an unimplemented value in attacker-controlled on-disk metadata should never be a fatal assertion. The filesystem continues to operate (the ALG0 hash is correct for any directory, so existing entries β€” which the attacker has not moved β€” remain findable).

Fix validated on a single-fix kernel

  • Unpatched kernel #0 (6.5-DEVELOPMENT #0, Thu Jul 2 06:02:54 UTC 2026): the PoC panics, signature above.
  • Single-fix kernel #1 (built from with-src + fix.diff, 6.5-DEVELOPMENT #1, Tue Jul 14 01:05:39 UTC 2026, sha256 b726a337…855602924): mount_hammer succeeds, ls /mnt/df0838/testdir/file1 returns the file, cat returns hello, no panic, dmesg logs hammer_direntry_namekey: dir ino … has unknown dirhash alg 2, falling back to ALG0.
  • Repeated 3Γ— to confirm determinism.

The fix supersedes the finding markdown's proposal ("fall back to ALG0 + kprintf warning") by implementing exactly that, plus a once-per-inode log guard to avoid log spam.

Citations

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline panic at hammer_direntry_namekey+0x1ae; patched ls rc=0, cat=hello, dmesg 'falling back to ALG0'.

BEFORE: panic hammer_direntry_namekey bad algorithm. AFTER: ls rc=0, hello, dmesg fallback. x3 runs.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 01:05:39 UTC 2026 (sha256 b726a337...)

Confirmed kernel references

Detail

Exploit chain

none -- hpanic fires synchronously in VOP path. DoS only. No write.

Evidence (decisive lines)

BASELINE: panic hammer_direntry_namekey+0x1ae. PATCHED: ls rc=0, cat=hello, dmesg 'unknown dirhash alg 2, falling back to ALG0'.

PoC changes

Authored from scratch: image_patcher.c (flips cap_flags to ALG2 + recomputes leaf+BTree CRC), run.sh, fix.diff (fall back to ALG0 + once-per-inode kprintf), VERDICT.md, manifest.json.

Verified recommended fix

Replace hpanic in default case at hammer_subs.c:1031 with ALG0 hash + once-per-inode kprintf warning (HAMMER_INODE_BADDIRHASH guard). Full git-apply-able diff in findings/poc/DF-0838/fix.diff.

Verdict

REPRODUCED. hammer_direntry_namekey switch default hpanic(bad algorithm) for any cap_flags DIRHASH bits != 0 or 1. Crafted HAMMER v6 image with cap_flags ALG2 -> mount + ls panic. Deterministic 2/2.