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

sub_key-derived bitmap index overflows the 64-bit element in hammer2_bmap_alloc() for non-radix-aligned crafted DATA bref keys β€” KKASSERT panic (INVARIANTS) / truncated mask with overlapping 64K allocations (noinv)

Field Value
ID DF-2653
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-20 Improper Input Validation
File sys/vfs/hammer2/hammer2_freemap.c
Lines 663-679
Area vfs
Confidence certain
Discovered 2026-08-29
Pass 2 (GLM 5.3 second pass)
Bucket hammer2
Reported pending
Known CVE none
CVE match novel

Summary

hammer2_bmap_alloc()'s DATA fast path (:660-687) derives bitmap indexes i/j from the chain bref's own key (sub_key) with only an after-the-fact KKASSERT that j+bmradix<=64 at :679. The kernel only ever creates radix-16 DATA chains at 64K-aligned keys (hammer2_calc_logical, subr.c:267-281), but a crafted image can supply a DATA bref with a 16K-aligned key (0x7C000): j becomes 62 while bmradix=8 (radix 16), so j+bmradix=70 β€” KKASSERT panic on INVARIANTS kernels; on production kernels bmmask=(0xFF)<<62 truncates to 2 bits, the 64K allocation marks only one 16K pair, avail -= 64K, and the returned data_off (element base + 62*8K, 64K long) crosses into bitmapq element i+1 whose chunks stay 00 β€” subsequent allocations overlap this one (DF-2652 class).

Threat model & preconditions

Crafted hammer2 image: a DATA bref {key=0x7C000, keybits=16, radix=16} planted in a file's indirect block (covering a hole) is found by the write path for lbase 0x80000 and COW-modified. Any user of the mounted fs writing into that range triggers the panic/corruption. DoS on default kernels; allocation-overlap corruption on production noinv kernels.

Proof of concept

VERIFIED on the stock guest (findings/poc/DF-2653/): forge_2653.py inserts the poison bref at ascending-key slot 4 of f2653's indirect block (methods=0x30 iscsi32 β€” 0x00/CHECK_NONE takes the overwrite-in-place path and never reallocates, discovered in iteration 1). Mount, then dd if=<64K urandom> of=/mnt/h2t/f2653 bs=64k seek=8 conv=notrunc β†’ async panic: assertion "j + bmradix <= HAMMER2_BMAP_BITS_PER_ELEMENT" failed in hammer2_bmap_alloc at hammer2_freemap.c:679 with backtrace hammer2_bmap_alloc ← hammer2_freemap_alloc ← hammer2_chain_modify ← hammer2_assign_physical ← hammer2_xop_strategy_write; guest down. Fixed kernel #1: same write completes, readback md5 matches, guest up.

Turn the assertion into a gate and fall back to the (safe, self-bounded) general element scan:

--- a/sys/vfs/hammer2/hammer2_freemap.c
+++ b/sys/vfs/hammer2/hammer2_freemap.c
@@ -676,7 +676,11 @@
        default:
            break;
        }
-       if (i >= 0) {
+       /*
+        * DF-2653: reject sub_key-derived indexes that do not
+        * leave room for bmradix bits inside the 64-bit element
+        * (crafted DATA bref key that is not radix-aligned gives
+        * j=62 with bmradix=8).  Fall back to the general scan.
+        */
+       if (i >= 0 && j + bmradix <= HAMMER2_BMAP_BITS_PER_ELEMENT) {
            KKASSERT(i < HAMMER2_BMAP_ELEMENTS &&
                 j < 2 * HAMMER2_BMAP_BLOCKS_PER_ELEMENT);
-           KKASSERT(j + bmradix <= HAMMER2_BMAP_BITS_PER_ELEMENT);

References

  • DF-2652 (the sibling mask defect in adjust), DF-2616 family

Timeline

  • 2026-08-29 Discovered during pass-2 audit of hammer2_freemap.c (GLM 5.3); verified reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2653 Β· 15 files
FileTypeDescriptionSize
forge_2653.py β€” 4.5 KB view raw
h2common.py β€” 3.8 KB view raw
forge2653b.img.gz β€” 2.1 MB ↓ download
f3lo.bin.gz β€” 256.1 KB ↓ download
f3hi.bin.gz β€” 256.1 KB ↓ download
panic.txt β€” 831 B view raw
panic2653_full.txt β€” 3.1 KB view raw
run.log β€” 2.5 KB view raw
env.txt β€” 195 B view raw
fix.diff β€” 747 B view raw
fix_build.log β€” 254 B view raw
fix_run.log β€” 572 B view raw
build.sh β€” 907 B view raw
run.sh β€” 623 B view raw
verdict.json β€” 4.5 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff gates the sub_key fast path with a real (j + bmradix <= 64) check and falls back to the general element scan: on kernel #1 the same forged image + 64K write at f2653+0x80000 completes, readback md5 matches the written bytes, guest stays up; on #0 the same operation panicked at freemap.c:679 in hammer2_xop_strategy_write. Baseline panic gone.

fix_run.log (MOUNT_OK, md5 match, guest alive on #1); panic.txt for the #0 baseline assertion + backtrace
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Aug 30 01:17:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

crafted hammer2 image (misaligned-key DATA bref in indirect block) -> mount -> unprivileged write into the covered hole range -> chain found + COW modify -> freemap_alloc -> bmap_alloc j+bmradix=70>64 -> KKASSERT panic (INVARIANTS) / truncated mask + overlapping allocation (noinv)

Evidence (decisive lines)

['panic.txt: exact assertion text + backtrace through hammer2_xop_strategy_write', 'run.log iteration 2: MOUNT_OK, WRITE_DONE, then guest down; serial console panic block', 'run.log fix validation: patched kernel MOUNT_OK, readback md5 f1fdf21f00ddb16c35b1e77fac5eaeac == written data, guest alive', 'forge_2653.py: poison insertion with ascending-key slot shifting']

PoC changes

Two iterations: first attempt used CHECK_NONE (0x00) methods on the poison bref and did not panic - hammer2's overwrite-in-place optimization (chain.c:1495-1521) skipped freemap_alloc entirely; fixed by using methods=0x30 (iscsi32) to force the COW realloc. Panic fires asynchronously in the strategy xop, so success must be checked via vm.sh status/log, not the dd exit.

Verified recommended fix

Gate the sub_key fast path with 'if (i >= 0 && j + bmradix <= HAMMER2_BMAP_BITS_PER_ELEMENT)' and fall back to the general element scan otherwise.

Verdict

REPRODUCED on stock INVARIANTS kernel #0: a crafted DATA bref with a 16K-aligned key (0x7C000) and radix 16, planted in a file's indirect block covering a hole, is found by the write path for lbase 0x80000 (hammer2_assign_physical, strategy.c:733) and COW-modified; hammer2_chain_modify -> hammer2_freemap_alloc -> hammer2_bmap_alloc computes j=(0x7C000&0x7FFFF)/163842=62 with bmradix=8, and the KKASSERT at freemap.c:679 fires: 'panic: assertion "j + bmradix <= HAMMER2_BMAP_BITS_PER_ELEMENT" failed in hammer2_bmap_alloc at hammer2_freemap.c:679' with backtrace hammer2_bmap_alloc <- hammer2_freemap_alloc <- hammer2_chain_modify <- hammer2_assign_physical <- hammer2_xop_strategy_write. Panic is async (strategy xop thread). On no-INVARIANTS production kernels the same arithmetic truncates bmmask=(0xFF<<62) to 2 bits: the 64K allocation marks one 16K pair, avail -= 64K, and data_off (element base + 628K, 64K long) crosses into the next bitmapq element whose chunks stay 00 -> subsequent allocations overlap (same corruption class as DF-2652). Fixed by turning the KKASSERT into a real gate on the fast path (misaligned sub_key falls back to the general element scan); validated on kernel #1: same forged image + write completes, readback md5 matches, guest stays up.