# DF-2605 — hammer2 `hammer2_chain_alloc` unvalidated on-disk radix → kernel panic / OOB (DoS on default GENERIC; OOB-write primitive on noinv)

## Verdict: REPRODUCED (panic / DoS on default GENERIC; write-primitive characterised on noinv), FIX VALIDATED

The bug is **real and confirmed**. `hammer2_chain_alloc()` at
`sys/vfs/hammer2/hammer2_chain.c:189-190` derives `chain->bytes` from the
low 6 bits of `bref->data_off` (the "radix") with no range check:

```c
if ((int)(bref->data_off & HAMMER2_OFF_MASK_RADIX))
    bytes = 1U << (int)(bref->data_off & HAMMER2_OFF_MASK_RADIX);
else
    bytes = 0;
```

A malicious hammer2 filesystem image can set that radix to any of 0..63.
`HAMMER2_RADIX_MAX` is **16** (`HAMMER2_PBUFSIZE` = 64 KB) but is **never
enforced** at this derivation site. Two failure classes:

1. **radix 17..31** — `bytes` is well-defined (128 KB..2 GB) but exceeds
   `HAMMER2_PBUFSIZE`. The unprivileged readdir path drives the kernel into
   `hammer2_io_alloc()` (`hammer2_io.c:126`) where the
   `KKASSERT(pbase != 0 && ((lbase + lsize - 1) & pmask) == pbase)` trips
   because the oversized request crosses a 64 KB page. **Panic on default
   GENERIC (INVARIANTS ON)** — reproduced.

2. **radix >= 32** — `1U << radix` is **undefined behaviour** (shift count >=
   type width). On gcc 8.3 x86-64 `-O2` the `shl` masks the count to 5 bits,
   so e.g. `1U << 32` evaluates to `1`, producing a **bogus `bytes`** and a
   mis-sized 1-byte I/O. Confirmed: no panic, but `chain->bytes` is wrong
   (implementation-defined) — exactly the latent UB the finding flags.

On a **non-INVARIANTS** kernel the `hammer2_io.c:126` KKASSERT is skipped and
the oversized `chain->bytes` later drives the OOB blockref-array walk in
`hammer2_flush_core()` (`hammer2_flush.c:1094`,
`count = parent->bytes / sizeof(hammer2_blockref_t)`) — the same write-capable
primitive characterised in the DF-2583 sibling (same root-cause class). On the
default GENERIC kernel that primitive is masked by the earlier KKASSERT and
manifests purely as a DoS panic; per the bright-line rule this is reported as
`panic` on GENERIC (the realistic target), with the noinv OOB-write noted as a
non-default-kernel characterisation.

**The fix is validated on a built-and-booted single-fix kernel (`#1`):** the
same forged image (radix 17 — which panicked the baseline `#0`) now mounts and
readdir returns `EDOM` (errno 33) instead of panicking; the guest stays up; the
warnonce `kprintf` from the new alloc-site guard fires. radix 32 (UB path) is
also cleanly rejected. A clean (unforged) image still readdirs all 13 files —
**no regression**.

## Mechanism (trigger → primitive → effect)

1. **Attacker input.** A hammer2 image has an INDIRECT blockref whose
   `bref.data_off` low 6 bits are forged to 17 (`data_off = 0x1c01011`).
   The full hammer2 CRC chain (XXH64 over inode/indirect blocks with seed
   `0x4d617474446c6c6e`, CRC32C over the volume header) is recomputed by
   `forge.c` so the kernel accepts the forged blockref.

2. **Unvalidated derivation** at `hammer2_chain.c:189-190` (the DF-2605 root
   cause): `bytes = 1U << 17 = 131072`, no bound check. `HAMMER2_RADIX_MAX`
   (= 16) is defined in `hammer2_disk.h:89` but never enforced here.

3. **Reachable from unprivileged readdir.** `getdents(2)` on the directory
   whose inode blockset holds the forged INDIRECT bref descends:
   `hammer2_chain_get` → `hammer2_chain_lock` → `hammer2_chain_load_data`
   (`hammer2_chain.c:940`) → `hammer2_io_bread(data_off, bytes=131072)`
   → `_hammer2_io_getblk` → `hammer2_io_alloc`.

4. **Default GENERIC (INVARIANTS ON):** the KKASSERT at `hammer2_io.c:126`
   fails (`lsize=131072 > HAMMER2_PBUFSIZE=65536` crosses a page boundary).
   `Illegal: 0000000001c00000 0000000001c01000+00020000 / ffffffffffff0000`
   then `panic: assertion ... failed in hammer2_io_alloc`. Guest → `db>`,
   ssh dies. **Reproduced** (see `panic.txt`).

5. **radix >= 32 (UB):** `1U << radix` is UB; on this gcc `bytes` wraps to a
   tiny value, so the I/O KKASSERT does not fire, but the chain is mis-sized
   and readdir returns `EDOM`. The behaviour is implementation-defined — a
   different compiler/flags could produce anything (including a huge `bytes`
   that does panic). The finding's UB concern is real.

6. **noinv (non-default):** KKASSERT skipped → the oversized `parent->bytes`
   drives `count = parent->bytes / 128` at `hammer2_flush.c:1094`, iterating
   the blockref array (`base[]`) far past its actual size → OOB read/write of
   kernel heap with attacker-influenced blockref data on `sync`. This is the
   DF-2583 write primitive (same root cause). Not escalated to `uid=0` here:
   on the realistic target (default GENERIC) the KKASSERT converts this to a
   pure DoS panic before any OOB write lands, which is the valid hard blocker
   for a default-kernel escalation.

## Exploit chain / impact

- **Impact class:** memory-corruption-primitive (oversized/UB radix →
  oversized `chain->bytes`) surfaced as a **DoS panic** on the default
  GENERIC kernel.
- **Bucket / victim object:** N/A for the default-kernel demonstration — the
  KKASSERT at `hammer2_io.c:126` fires before any heap victim is touched. The
  write primitive (noinv only) targets the INDIRECT block's `npdata[]`
  blockref array at `hammer2_flush.c:1094`; characterised in DF-2583.
- **Unprivileged reachability:** yes — `maxx` (uid 1001, not in wheel) reads
  the mounted directory; the forged INDIRECT chain is loaded during
  `getdents`. Realistic precondition: an admin mounts (or makes mountable via
  `vfs.usermount`) a hammer2 image and lets the user read it.
- **Why not `uid=0`:** on the default GENERIC kernel (INVARIANTS ON — the
  realistic target) the `hammer2_io.c:126` KKASSERT catches the oversized I/O
  *before* any OOB write can land, so the manifestation is a DoS panic, not an
  escalation. The write-capable OOB only exists on the non-default `noinv`
  kernel (characterised in DF-2583; per the bright-line rule a `noinv`-only
  escalation is not a default-GENERIC `uid0`). This is a valid hard blocker
  (the primitive is masked by INVARIANTS on the default kernel).

## The fix (validated)

`fix.diff` makes two minimal, targeted changes in
`sys/vfs/hammer2/hammer2_chain.c`:

1. **`hammer2_chain_alloc` (root cause, DF-2605):** compute `radix` once;
   if `radix > HAMMER2_RADIX_MAX`, do **not** execute the UB/oversized
   `1U << radix` — instead cap `bytes = 1U << HAMMER2_RADIX_MAX` and mark the
   chain `HAMMER2_ERROR_CHECK` (with a warnonce `kprintf`). This closes both
   the radix-17..31 oversized-I/O path *and* the radix>=32 UB at the
   derivation site.

2. **`hammer2_chain_load_data` (companion, = the DF-2583 guard):** reject a
   chain already marked errored (or with `bytes > HAMMER2_PBUFSIZE`) before
   calling `hammer2_io_bread()`. This is what actually prevents the I/O-layer
   KKASSERT: the errored chain short-circuits, callers return `EDOM`/`EIO`,
   no panic.

The pair fully closes DF-2605 (and subsumes DF-2583's downstream guard).

### Before / after (decisive)

| kernel | PoC (radix=17 readdir) | result |
|---|---|---|
| `#0` unpatched baseline | `su maxx ./poc /mnt/h2test/testdir` | **panic** `hammer2_io_alloc:126` KKASSERT → `db>`, ssh dies |
| `#1` single-fix | same | **EDOM** (errno 33), guest UP, warnonce kprintf fires |
| `#1` single-fix | radix=32 (UB path) | **EDOM**, guest UP, no UB executed |
| `#1` single-fix | clean (unforged) image | readdirs 13 files normally — **no regression** |

## PoC changes

Authored from scratch (the finding shipped no PoC). Reused the CRC-chain
forger pattern proven on the DF-2583 sibling (XXH64 seed
`0x4d617474446c6c6e`, CRC32C volume header) — DF-2605 is the same
unvalidated-radix root-cause class, re-targeted at the upstream
`hammer2_chain_alloc` site. Files: `forge.c` (image forger), `poc.c`
(getdents trigger), `setup_image.sh` (creates a base hammer2 image with
enough entries to force an INDIRECT block), `build.sh` / `run.sh`,
`fix.diff`, full logs.
