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

Unvalidated on-disk blockref type byte: any media array slot with type outside 1..6 selected by lookup reaches hammer2_chain_alloc's raw panic() β€” deterministic kernel panic (mount + one directory read) on production kernels

Summary

base_find selects any non-EMPTY slot regardless of the type byte and chain_lookup passes it to chain_alloc whose switch PANICS on unrecognized types (a raw panic call, not an INVARIANTS KASSERT): a crafted image with one dirent bref type 0xAA mounts cleanly, then the first ls panics with 'unrecognized blockref type: 170' and the exact trace chain_alloc<-chain_get<-chain_lookup<-xop_readdir. Stock-kernel-reproduced (ssh dies, serial panic captured); bref_sane type-byte rejection fix validated (ls returns total 0, stat ENOENT, clean umount). Same class as DF-2618/2632.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5785 Β· 15 files
FileTypeDescriptionSize
README.md β€” 1.5 KB ↓ raw
VERDICT.md β€” 2.9 KB ↓ raw
run_df5785.sh β€” 918 B view raw
craftA.img β€” 64.0 MB ↓ download
run.log β€” 352 B view raw
run_fix.log β€” 421 B view raw
panic.txt β€” 688 B view raw
build.log β€” 151 B view raw
fix.diff β€” 2.0 KB view raw
fix_build.log β€” 291 B view raw
env.txt β€” 418 B view raw
env_fixkernel.txt β€” 375 B view raw
code_hashes.txt β€” 171 B view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 3.0 KB view raw

DF-5785 β€” unvalidated on-disk blockref type byte β†’ chain_alloc panic (pass 3)

Finding (short)

The type byte of every hammer2 blockref array element read from media is attacker-controlled and never validated on the read path. Any lookup that selects a slot with a type outside 1..6 (e.g. 0xAA) drives hammer2_chain_lookup β†’ hammer2_chain_get β†’ hammer2_chain_alloc (sys/vfs/hammer2/hammer2_chain.c:209-210) into a raw panic() β€” present in production kernels, INVARIANTS not required. Deterministic: mount the crafted image, read the directory once β†’ panic.

Files

  • craftA.img β€” forged from ../DF-5784/base.img (f1 DIRENT bref type β†’ 0xAA; ancestors methods=CHECK_NONE; volhdr CRC32Cs recomputed) by ../DF-5784/forge_df5784.py variant A
  • run_df5785.sh β€” guest runner (control + crafted)
  • panic.txt β€” captured kernel panic (exact cited path)
  • run.log / run_fix.log β€” baseline (panic) and patched (clean) runs
  • fix.diff β€” validated fix (shared with DF-5784)

Build / Run

See ../DF-5784/build.sh (produces craftA.img) then on the guest:

sh /root/poc/df5784/run_df5785.sh

Expected

  • control base.img: mounts, ls -l shows f1, rc=0
  • craftA.img: mounts, then ls β†’ connection death; serial console shows panic: hammer2_chain_alloc: unrecognized blockref type: 170 with the trace chain_alloc ← chain_get ← chain_lookup ← xop_readdir (panic.txt).
  • on the patched kernel (#1, fix.diff): ls returns total 0 cleanly, stat β†’ ENOENT, umount OK.
VERDICT.md
↓ download raw

DF-5785 β€” VERDICT

Status: reproduced (impact: panic β€” deterministic kernel panic from a mounted crafted hammer2 image; raw panic(), so it fires on production kernels too, INVARIANTS not required). Fix validated.

Root cause

hammer2_chain_alloc() (sys/vfs/hammer2/hammer2_chain.c:177-237) switches on bref->type and panic()s on any unrecognized value (hammer2_chain.c:207-210):

case HAMMER2_BREF_TYPE_EMPTY:
default:
        panic("hammer2_chain_alloc: unrecognized blockref type: %d",
              bref->type);

The type byte of every blockref read from media is attacker-controlled and unvalidated on the read path: hammer2_base_find() selects any non-EMPTY slot (hammer2_chain.c:4975-4986), hammer2_combined_find returns its bref, and hammer2_chain_lookup (hammer2_chain.c:2606-2615) / hammer2_chain_create_indirect (:3942) pass it straight into hammer2_chain_get β†’ hammer2_chain_alloc. Valid media types are 1..6; bytes 7..253 (e.g. 0xAA=170) panic instantly. (Types 254/255 are pseudo-types that never appear in arrays but are also accepted by the alloc switch β€” they take a different bad path in hammer2_chain_load_data :1086 β€” same missing-validation class.)

Reproduction (stock INVARIANTS kernel #0 Jul 2 2026)

craftA.img = base image with f1's DIRENT bref type byte set to 0xAA (ancestors methods=CHECK_NONE so the edited blocks load; volhdr CRCs recomputed). Mount succeeds (label lookup does not touch the dirent); the first directory read panics:

panic: hammer2_chain_alloc: unrecognized blockref type: 170
hammer2_chain_alloc() at hammer2_chain_alloc+0x1ca
hammer2_chain_get() at hammer2_chain_get+0xbd
hammer2_chain_lookup() at hammer2_chain_lookup+0x615
hammer2_xop_readdir() at hammer2_xop_readdir+0x151
hammer2_primary_xops_thread() at hammer2_primary_xops_thread+0x280

(panic.txt; matches the cited path exactly.) Control base.img mounts and lists f1 normally.

Fix validation

Same fix.diff as DF-5784 (validated together, kernel #1 Oct 12 15:27): hammer2_bref_sane() rejects non-media types and hammer2_base_find() skips such elements like EMPTY slots. On the patched kernel craftA mounts, ls returns cleanly (total 0 β€” poisoned entry invisible), stat returns ENOENT, umount OK β€” panic gone (run_fix.log).

Impact

Local DoS (panic) under the mounted-crafted-image threat model (root mount, or unprivileged with vfs.usermount=1 + owned vnode device) β€” same class/severity as DF-2618/DF-2632 panics. No memory-corruption primitive: the panic is a fail-stop before any allocation-sized operation.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (shared with DF-5784) applied, kernel rebuilt (#1 Oct 12 15:27:54 2026), rebooted, exact PoC re-run: craftA mounts, ls returns total 0, stat ENOENT, umount clean (run_fix.log). Panic gone.

run_fix.log; fix_build.log; env_fixkernel.txt
↓ fix.diffper-fix-DF-5785

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt (panic + full backtrace matching hammer2_chain.c:209 path); run.log (baseline: control lists f1, crafted ls kills the session); run_fix.log (patched kernel: SURVIVED_LS rc=0, DF5785_DONE)

PoC changes

Forger written fresh from the DF-2616 rig technique; type-byte patch needed no check-code recompute for the child bref itself, only methods=CHECK_NONE on ancestors + volhdr CRC32C recompute

Verified recommended fix

Reject/skip media blockrefs with non-media type bytes at array-scan time (hammer2_bref_sane() + base_find skip); keep chain_alloc's panic as defense-in-depth

Verdict

The blockref type byte from media is unvalidated: any array slot with type outside 1..6 selected by hammer2_base_find()/combined_find reaches hammer2_chain_get -> hammer2_chain_alloc, whose switch panics (hammer2_chain.c:207-210) - a raw panic() active in production kernels. Reproduced on the stock INVARIANTS guest: crafted image (f1 DIRENT bref type=0xAA) mounts cleanly, the first directory read (ls) panics with 'hammer2_chain_alloc: unrecognized blockref type: 170' and the exact cited trace chain_alloc <- chain_get <- chain_lookup <- xop_readdir (panic.txt). Control image behaves normally. Fix (hammer2_bref_sane() type check + base_find skip, shared with DF-5784) validated on a rebuilt kernel: mount + ls + stat + umount all clean, no panic.