# DF-3167 — VERDICT

**Status: reproduced** (stock kernel) — **fix validated** (fixed kernel).

## What was audited

Pass-2 (GLM 5.3) audit of `sys/vfs/hammer/hammer_reblock.c` (683 LOC): ioctl
entry `hammer_ioc_reblock`, scan loop, `hammer_reblock_helper` /
`hammer_reblock_data` / `hammer_reblock_leaf_node` / `hammer_reblock_int_node`
/ `hammer_move_node`, cursor invalidation protocol, freemap interplay,
ioctl arg validation.

## The bug (root cause, line-accurate)

`hammer_reblock_data()` consumes the **on-disk** `elm->leaf.data_len` /
`elm->leaf.data_offset` of every record it moves with **no production
validation**:

1. `hammer_reblock.c:464` — `hammer_btree_extract_data(cursor)` loads
   `cursor->data = buffer->ondisk + (data_offset & 0x3FFF)`; the only bound
   is the INVARIANTS-only `KKASSERT(data_len >= 0 && data_len <=
   HAMMER_XBUFSIZE)` at `hammer_btree.c:736`. The data CRC gate
   (`hammer_btree.c:764`) is satisfiable by the image author because the
   pointed-to bytes are his own.
2. `hammer_reblock.c:467` — `hammer_alloc_data(trans, data_len, rec_type,…)`
   (`hammer_ondisk.c:1634`): for non-DATA records the zone is META **without
   any 16KB rounding** (:1646-1655), and `hammer_blockmap_alloc()` only
   enforces the *big-block* boundary for requests > 16KB
   (`hammer_blockmap.c:183-188`) — the allocation may land at any 16-byte
   aligned append cursor `xoff` inside a 16KB buffer (the META cursor is
   *naturally* misaligned; :372 even special-cases `(next_offset &
   HAMMER_BUFMASK) != 0`).
3. `hammer_reblock.c:487` — `bcopy(cursor->data, ndata, elm->leaf.data_len)`
   writes `[xoff, xoff+data_len)` into a buffer whose kernel data area is
   exactly `DOALIGN16K(data_len)` bytes (`hammer_bread_ext` →
   `_hammer_bread` `hammer_ondisk.c:1116-1143`, `buffer->io.bytes = bytes`,
   `ondisk = bp->b_data` :715-722/922).

→ For any `data_len > 16384` with `xoff + data_len > DOALIGN16K(data_len)`
  the copy runs **up to 16368 bytes past the end of a kernel buffer**, with
  fully attacker-controlled bytes (the source record data). The symmetric
  misaligned-**source** variant is an OOB **read** of adjacent kernel buffer
  memory, CRC'd and persisted into the record's new data block.

## Reproduction (baseline, stock kernel #0 Jul 2 06:02:54)

Crafted in-guest, no kernel changes:

1. `mkimage.sh`: 12G file-backed vn, `newfs_hammer` (v7), 1MB of `'A'` in
   `/d/bigfile`, marker dirent `/d/PWNMARKER_TARGET`, 8 long-named dirents
   (they keep the META append cursor misaligned — it sits at
   `0x9000000021800f90`, i.e. `xoff=0xf90`).
2. `patch_leaf.c`: forges elm 8 of leaf node `0x8000000021000000` (the
   PWNMARKER dirent, located/verified via `hammer -f /dev/vn0 show`):
   `data_offset=0xa000000022000000` (bigfile's all-'A' large-data extent,
   buffer-aligned), `data_len=65536` (≤ HAMMER_XBUFSIZE → **passes every
   KKASSERT even on INVARIANTS kernels**), `data_crc=crc32c('A'×65536)`
   (v7 iscsi CRC), node CRC recomputed.
3. `trigger.sh`: mount rw, `vfs.hammer.debug_general=0x4000`, `hammer
   reblock /mnt` (plain reblock: `free_level=0` from the default 100% fill
   → every record qualifies).

Observed (serial console, run.log / panic.txt):

```
hammer_reblock_data: 00000001 9000000021800450 -> 9000000021801330   <- meta cursor drift
hammer_blockmap_getfree: a000000022000000 -> 7340032                 <- forged record
hammer_reblock_helper: D 7340032/0                                   <- move starts
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff8005272a000
fault code           = supervisor write data, page not present
instruction pointer  = 0x8:0xffffffff80bcaa0a
current process      = 1388 (hammer reblock)
Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi)               <- the bcopy
```

Geometry check: the last meta move allocated at `0x…01330`, so the forged
record's 65536-byte allocation landed at `xoff = 0x1390` (4912) into the
64KB destination buffer → 4912 bytes of `'A'` written past its end before
the first unmapped page faulted the kernel. Guest dead at `db>`.

**Primitive**: controlled-content, controlled-length (≤16368B) OOB write
past a getblk'd kernel buffer, from a privileged-mount image via a standard
maintenance command. When the adjacent KVA is mapped (other buffers), it is
*silent* kernel-memory corruption; here the first unmapped page made it a
fatal trap. Not an unpriv→root path (mount+ioctl are root), impact ceiling
= kernel memory corruption / host crash from a malicious filesystem image.

## Fix validation

`fix.diff` (two hunks, generated against the read-only tree):

* `hammer_reblock.c` — `hammer_reblock_data()` rejects any record whose
  `(data_offset, data_len)` is not fully contained in one 16KB buffer or
  has `data_len <= 0` (legit records never cross a 16KB buffer; large-data
  is written one record per 16KB buffer) → `EINVAL`, logged.
* `hammer_blockmap.c` — `hammer_blockmap_alloc()` buffer-aligns
  allocations > 16KB (defense-in-depth for every other alloc_data caller).

Build: `make -j6 nativekernel` in-guest (BUILD_RC=0), installkernel, reboot
into `#1: Mon Sep 7 12:28:26 UTC 2026`.

Re-run of the **exact** PoC on the fixed kernel:

```
hammer_blockmap_getfree: a000000022000000 -> 7340032
hammer_reblock_helper: D 7340032/0
HAMMER: reblock a000000022000000/65536: illegal data range      <- guard fires
Reblock /mnt failed: Invalid argument
TRIGGER-DONE   (no trap; guest up; unmount clean; 0 "Fatal trap" in messages)
```

An intermediate fix-v1 run (bounds-check only, no containment check) is also
documented: it stopped the OOB write (the forged record moved to a
buffer-aligned offset) but the *source-side* 64KB `getblk` then tripped a
separate `panic: bgetvp - overlapping buffer` — the same unvalidated
`data_len` class surfacing at a second sink, which motivated the final
16KB-containment guard. fix-v2 kills both at this call site.

## Negative results (classes hunted, killed)

* ioctl arg validation (hammer_reblock.c:70-103): localization PSEUDOFS
  bits, obj_id range, `free_level` bounds all checked; `vol_no` used only
  for comparison (no indexing); whole ioctl privileged
  (hammer_ioctl.c:72 `caps_priv_check(SYSCAP_NOVFS_IOCTL)`).
* alloc/free balance: no error path between a successful
  `hammer_alloc_data`/`hammer_alloc_btree` and the corresponding
  free/delete (helper :467-513, :534-565, :594-627).
* scan-loop progress: each iteration advances or exits; EDEADLK retry
  re-seeds from `key_cur` (:148-149, :241); EINTR escape (:162, :243-246);
  checkspace bounded at 10 → ENOSPC (:176-179).
* cursor protocol: the checkspace (:180-186) and bd_heatup (:223-227)
  unlock windows `goto skip`/iterate without using the stale `elm`; the
  uncache window (:344-367) reloads `elm` and revalidates
  (RETEST/type/index/bcmp) before use; `hammer_init_cursor`'s error IS
  checked here (:108-112) unlike DF-3109's broken callers; DF-3110's
  moved-element defect lives in hammer_btree.c (not re-reported).
* node moves: fixed-size `bcopy` (:641), children-lock and parent/root
  update ordering match the btree code; only an unreachable
  fixup `hpanic` (:680).
* known findings DF-3033 (direct_uncache getblk entry :342-346) and
  DF-3076 (blockmap_free bytes) not re-reported; the >64KB `data_len`
  getblk-panic variant at this call site is covered by the same guard.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
