# DF-2616 — Verdict

**REPRODUCED** — Missing geometry validation of on-disk `bref.data_off` in
hammer2 chain loading. Impact demonstrated end-to-end:

1. **Stock kernel (X86_64_GENERIC, INVARIANTS ON): deterministic panic**
   (mount-time and file-read-time) at the `KKASSERT` in `hammer2_io_alloc`
   (`hammer2_io.c:126`) — reliable local DoS from a crafted image.
2. **Non-INVARIANTS (release-style) kernel: kernel heap OOB read AND write
   past the 64KB DIO buffer**, plus a kernel-mode page fault (DoS) when the
   adjacent KVA slot is unmapped.

uid=0 escalation was NOT achieved within this run; the write primitive was
characterized (below) but the adjacent-object placement could not be
controlled deterministically on this guest (see "Exploit chain").

## The bug, confirmed in source

- `hammer2_chain_alloc` (`hammer2_chain.c:189-192`) derives `chain->bytes`
  purely from the low 6 radix bits of the attacker-controlled
  `bref.data_off` (`HAMMER2_OFF_MASK_RADIX = 0x3F`, `hammer2_disk.h:461`).
  **No geometry check anywhere on the load path.**
- `hammer2_chain_load_data` (`hammer2_chain.c:919-1102`) only checks the
  embedded case (`data_off & ~MASK == 0`, line 938-939), then issues I/O
  (994-1000) and installs `chain->data = hammer2_io_data(dio, data_off)`
  (line 1048, 1100) = `bp->b_data + (lbase & 0xFFFF)`.
- The only geometry guard is `hammer2_io_alloc` `hammer2_io.c:122-126`:
  an **unconditional** `kprintf("Illegal: ...")` (so the pass-through is
  visible on release kernels) followed by a `KKASSERT` that is compiled out
  without INVARIANTS. On non-INVARIANTS builds execution continues and
  `chain->data + chain->bytes` runs past the end of the 64KB DIO buffer.
- Distinct from DF-0763/DF-2605 (radix magnitude 17-63): DF-2616 fires with
  a **perfectly valid radix** (10 or 16) — the violation is the *geometry*
  (offset not contained in one 64KB window). Note one refinement to the
  finding text discovered during verification: hammer2's freemap allocates
  at 1KB granularity, so a radix-N block at a merely-1KB-aligned offset is
  **legal on real filesystems** (the over-strict alignment check in fix v1
  broke the root fs boot — see below). The actual unchecked invariant is
  exactly what `hammer2_io.c:126` asserts: `pbase != 0` and
  `[lbase, lbase+lsize)` within one 64KB window (plus radix ≤ 16).

## Demonstrated sinks

- **OOB read to userspace:** `hammer2_strategy_read_completion`
  (`hammer2_strategy.c:487`) `bcopy(data, bp->b_data, focus->bytes)` with
  `data = chain->data` crossed; `read(2)` of the crafted file returns bytes
  from beyond the DIO buffer.
- **OOB write:** `hammer2_write_bp` (`hammer2_strategy.c:1310-1356`):
  `hammer2_io_newnz(... data_off ...)` → `hammer2_io_data()` →
  `bcopy(data, bdata, chain->bytes)` writes attacker file data through the
  crossed pointer — in-window part lands at the crossed media offset
  (forensically recovered), past-end part lands in the adjacent kernel
  buffer. The overwrite-in-place path (`hammer2_chain_modify`,
  `chain.c:1504-1516`, CHECK_NONE + modify_tid > pfs_lsnap_tid) keeps the
  crossed `data_off` instead of COWing away from it.

## Reproduction summary (full logs in this pack)

- **panic_A.txt** — variant A (sroot bref `0x180fd0a`: radix 10 VALID,
  lbase `0x180fd00`, misaligned + crossing), stock kernel:
  `Illegal: 0000000001800000 000000000180fd00+00000400` →
  `panic: assertion ... hammer2_io_alloc at hammer2_io.c:126` at MOUNT time.
- **panic_B.txt** — variant B (file f1 DATA bref `0x1c0ff0a`): mount OK,
  first read of `/mnt/h2/f1` → same panic via `hammer2_chain_load_data`.
- **fault_B_noinv.txt** — same variant B on kernel #1 (INVARIANTS off):
  `Fatal trap 12: page fault while in kernel mode ... memmove+0x28
  movq (%rsi),%rdx`, fault VA `0xfffff8006be66000` = `bp->b_data + 0x10000`
  — the OOB bcopy source ran off the end of the 64KB buffer onto an
  unmapped page. Kernel stopped in DDB; ssh dead (DoS).
- **run_E_noinv.log + forensic_E.txt** — variant E on kernel #1 with 32
  marker files groomed into their own windows (one live DIO buffer each):
  the crossed read **survived** (adjacent KVA slot mapped — returned its
  content, zeros in this run, to userspace: the OOB read primitive), and
  the crossed write planted the attacker pattern through the crossed
  pointer — recovered from the flushed image at media offset `0x200ff05`
  (inside the crossed window tail, exactly where `chain->data` pointed).
  The `Illegal:` console marker fired on every crossed access (proving the
  kernel sailed past the compiled-out guard).

## Exploit chain (what was and was not achieved)

Demonstrated: crafted image → mount → `read(2)` returns out-of-bounds
kernel-buffer memory; `write(2)` plants attacker-controlled (LZ4-wrapped)
content at a chosen out-of-bounds offset relative to a 64KB kernel buffer
(0x300 bytes past its end in the radix-10 forge; up to 0xFF00 with a
radix-16 chain — that variant faulted on an unmapped intermediate slot).

NOT achieved: deterministic placement of a chosen victim object in the
adjacent KVA slot (so no uid=0). The adjacent slot identity is set by the
fixed per-header KVA scheme (`vfs_bio.c:638`, `b_kvabase =
vm_map_min(buffer_map) + MAXBSIZE*n`) and the per-CPU buffer queues, which
the attacker cannot steer precisely from a mounted filesystem in this
setup; in the successful run the slot held a clean, zero-filled buffer
(mapped but never flushed), so the OOB write could not be recovered from
media. The primitives (chosen-offset read/write beyond a kernel buffer
object with attacker content) are in place; weaponization requires a
victim-object grooming strategy against the buffer cache arena.

## Fix validation

- **fix v1 (alignment-strict) was WRONG**: it rejected legitimate radix-N
  blocks at 1KB-aligned offsets that real filesystems contain (root fs boot
  logged dozens of `illegal data_off geometry 0x...0b/0c/0d` rejections —
  radix 11/12/13 blocks at 1KB granularity — and degraded the boot). This
  is itself a verification artifact: the check hooks every chain load.
- **fix v2 (fix.diff in this pack)** enforces exactly the io.c invariant:
  radix 0-with-offset / radix > 16 / `lbase < 64KB` / window-crossing
  rejected in `hammer2_chain_load_data` (sets `chain->error =
  HAMMER2_ERROR_CHECK`, cleanly failing reads via `strategy.c:350` and
  writes via `strategy.c:809`), the same check for OPTDATA modify
  (`chain.c:1525+`) before `hammer2_write_bp` can issue I/O, a guard on
  `dedup_off` installation, and a widened diagnostic condition in
  `hammer2_io_alloc`. See `fix_run_*.log` for the before/after.
