# DF-3076 VERDICT

**REPRODUCED (stock INVARIANTS kernel #0, deterministic, unprivileged
trigger): kernel panic in the audited function itself.**

```
panic: assertion "bytes <= HAMMER_XBUFSIZE" failed in hammer_blockmap_free
       at /usr/src/sys/vfs/hammer/hammer_blockmap.c:786
hammer_blockmap_free() at hammer_blockmap_free+0x39a
hammer_delete_at_cursor() at hammer_delete_at_cursor+0x6b9
hammer_ip_delete_range() at hammer_ip_delete_range+0x18f
hammer_sync_inode() at hammer_sync_inode+0x3c7
hammer_flusher_slave_thread()
```

Guest wedged in DDB; reproduction required a `vm.sh reset with-src`.

## Root cause (line-accurate)

`hammer_blockmap_free()` takes `bytes` from its caller with no
production validation (`sys/vfs/hammer/hammer_blockmap.c:762`):

* `:785` `bytes = HAMMER_DATA_DOALIGN(bytes);` — 16-byte alignment only;
  data_len up to `0x7FFFFFF0` passes through unchanged (and DOALIGN of
  `0x7FFFFFF1..0x7FFFFFFF` wraps negative).
* `:786` `KKASSERT(bytes <= HAMMER_XBUFSIZE);` — **INVARIANTS-only**;
  this is the *only* size bound on the whole path.
* The value originates on media: `hammer_delete_at_cursor()` copies
  `data_len = elm->leaf.data_len` straight from the on-disk B-Tree
  element (`hammer_object.c:2512-2513`) and hands it to
  `hammer_blockmap_free()` for zone 9/10/11 records
  (`hammer_object.c:2538-2543`). The leaf's node CRC is forgeable by the
  image author (CRC32C is not a security barrier — DF-3011 precedent).
* Same pattern in `hammer_blockmap_dedup()` (`:909-910`,
  `KKASSERT(bytes <= HAMMER_BIGBLOCK_SIZE)`, reached via the
  SYSCAP-gated dedup ioctl, `hammer_dedup.c:134`) and
  `hammer_blockmap_finalize()` (`:1009-1010`, frontend-bounded in
  practice) — hardened in the same fix.diff.

## Reachability (unprivileged post-mount)

`rm -f /mnt/target.bin` as uid 1001 on a **nohistory** mount → inode
teardown in the flusher slave (`hammer_sync_inode` →
`hammer_ip_delete_range`) → `hammer_ip_delete_record` passes
`HAMMER_DELETE_DESTROY` because `hammer_nohistory(ip)`
(`hammer_object.c:2234-2239`) → `hammer_delete_at_cursor:2542`. No
ioctl, no capability — just file deletion on the (crafted) filesystem.
Mounting the crafted image itself needs root or `vfs.usermount=1`
(guest default 0) — identical preconditions to the DF-3003/3011/3040
crafted-media family.

## What the PoC had to thread (why 0x80000)

`hammer_ip_delete_range` ratchets `ran_beg = off` per deleted record
(`hammer_object.c:2054`) and panics on the "left edge case"
(`:1997-2000`) when `key - data_len < ran_beg` — a shield that sits
*upstream* of the audited sink and fires for aggressively-large lengths:

* Dense 1MB file + data_len=0x7FFFFFF0 → panic `hammer left edge case
  0000000000000020 2147483632` (demonstrated, pre-fix run 1).
* Dense 1MB file, last record + data_len=0x80000 → still left-edge
  (`ran_beg` ratcheted to 0xF8000; demonstrated, run 2).
* **Sparse file** (single zone-10 record key=0x110000, scan begins at
  ran_beg=0) + data_len=0x80000 ≤ key → passes both edge checks
  (`:1997`, `:2016`) → reaches `hammer_blockmap_free` → `:786` panic.
  **This is the recorded reproduction.**

## Impact

* **INVARIANTS kernels (recorded):** deterministic local kernel panic /
  DoS from an unprivileged file deletion on the crafted fs. The guest is
  single-user here, but the same code runs on any debug-built
  production DragonFly box that mounts untrusted media.
* **Production kernels (code-trace, not run — no production kernel
  built):** the assert is compiled out. `layer2->bytes_free += bytes`
  (`:833`) is int32 arithmetic; with attacker-chosen initial
  `bytes_free` (freemap layer2 CRCs forgeable, e.g.
  `bytes_free=0x80800010` + `bytes=0x7FFFFFF0` → exactly
  `HAMMER_BIGBLOCK_SIZE` mod 2^32) the force-free branch at `:853-874`
  fires: `layer2->zone = 0; layer2->append_off = 0; ++blocks_free;
  ++vol0_stat_freebigblocks` — releasing a big-block **still referenced
  by other records** (the crafted data_len is fictional). Consequence:
  silent freemap corruption and stale/cross-record data on the mounted
  filesystem; the reservation machinery that normally prevents reuse
  (`hammer_reserve_setdelay_offset`, `:857`) is engaged but the block
  still returns to the allocator pool. No kernel memory unsafety was
  found on this path — `bytes` never sizes a copy in this file; every
  layer1/layer2 access is in-bounds of a 16K buffer-cache buffer.
  Severity ceiling therefore Low/Medium (DoS on debug kernels,
  own-fs integrity corruption on production), consistent with the
  DF-3033 calibration.

## Fix validation (fix.diff, kernel #1 Sun Sep 6 00:57:37 UTC 2026)

`fix.diff` adds production bounds checks to all three consumers
(free: warn+return; dedup/finalize: EINVAL):

* `make nativekernel KERNCONF=X86_64_GENERIC` → NK_RC=0,
  `make installkernel` → IK_RC=0 (`fix_build.log`).
* Baseline (stock #0): panic at `:786`, guest wedged (`run.log`).
* Patched #1, same crafted image, same unprivileged rm:
  `hammer_blockmap_free: blockmap_free: bad bytes 524288 for
  a000000022000000` on console, rm rc=0, clean unmount, guest stays up
  (`fix_run.log`).
* Control on patched kernel (unpatched image): rm clean, no warning
  beyond the crafted record, clean unmount.

## Artifacts

* `lenforge.c` / `icrc32.c` / `build.sh` — image forger (B-Tree walk,
  max-key DATA record selection, data_len patch, node CRC32C recompute)
* `setup.sh` — base image (sparse target file, nohistory clean umount)
* `run.sh A|B|keep` — forge+mount runner
* `run.log` — baseline reproduction transcript + panic
* `panic.txt` — full serial console capture (panic at hammer_blockmap.c:786)
* `fix_build.log`, `fix_run.log` — fix kernel build + patched-behavior run
* `env.txt` — guest environment (uname, cc, vfs.usermount=0)
* `fix.diff` — the validated fix (git-apply-able; never applied to sys/)
