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)
PoC verification
Evidence pack
findings/poc/DF-5785 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.pyvariant Arun_df5785.shβ guest runner (control + crafted)panic.txtβ captured kernel panic (exact cited path)run.log/run_fix.logβ baseline (panic) and patched (clean) runsfix.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 -lshowsf1, rc=0 craftA.img: mounts, thenlsβ connection death; serial console showspanic: hammer2_chain_alloc: unrecognized blockref type: 170with the tracechain_alloc β chain_get β chain_lookup β xop_readdir(panic.txt).- on the patched kernel (#1, fix.diff):
lsreturnstotal 0cleanly, stat β ENOENT, umount OK.
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
fixedfix.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
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.
No comments yet.