# DF-2594 — Unvalidated redo_data_bytes in hammer REDO recovery → OOB kernel read / panic on mount

## Verdict
**REPRODUCED** (panic on mount of a crafted HAMMER image). Fix **VALIDATED** (single-fix kernel rejects the record, no panic).

## Mechanism (trigger → primitive → effect)

HAMMER Stage-2 REDO recovery replays `HAMMER_REDO_WRITE` records found in the
UNDO/REDO FIFO on a read-write (v4+) mount.  `hammer_recover_redo_exec()`
hands the on-disk `redo->redo_data_bytes` **directly** to `vn_rdwr()` as the
copy length, reading from the in-kernel FIFO buffer at `(redo + 1)`:

- `sys/vfs/hammer/hammer_recover.c:1332` —
  `vn_rdwr(UIO_WRITE, vp, (void *)(redo + 1), redo->redo_data_bytes, ...)`

There is **no** validation that `redo_data_bytes` fits inside the FIFO record
(`hdr_size - sizeof(*redo) - sizeof(tail)`).  This is in stark contrast to the
sibling UNDO path, `hammer_recover_undo()`, which **does** validate at
`hammer_recover.c:1053-1060`:

```c
bytes = undo->head.hdr_size - sizeof(*undo) - sizeof(struct hammer_fifo_tail);
if (bytes < 0 || undo->undo_data_bytes < 0 || undo->undo_data_bytes > bytes) {
    ... return(EIO);
}
```

The only checks a REDO record must pass before reaching the vulnerable line are
`_hammer_check_signature()` (`hammer_recover.c:930-942`): a CRC over `hdr_size`
bytes (which an attacker recomputes) and a minimum `hdr_size >=
sizeof(head)+sizeof(tail)` = **24 bytes**.  A 64-byte REDO record (56-byte
`hammer_fifo_redo` + 8-byte tail, **zero payload**) therefore sails past every
gate while declaring `redo_data_bytes = 16384` (or up to `0x7FFFFFFF`).  When
`vn_rdwr` then copies `redo_data_bytes` bytes starting at `(redo + 1)` it reads
**past the 16 KB hammer buffer into adjacent kernel memory**.

### Trigger path (confirmed on the running kernel)
1. `newfs_hammer` a v7 image, create a victim regular file (so the recovery
   write has a writable, non-directory inode to target — the root dir objid 1
   rejects `VOP_WRITE` with `EINVAL`).
2. Forge the image (`forge.c`, links the kernel's own `icrc32.c`/`crc32.c`):
   inject three valid-CRC FIFO records at the start of the UNDO zone —
   `REDO_WRITE` (seqno S, `redo_data_bytes = 0x4000`, objid = victim file),
   `REDO_SYNC` (seqno S+1, `redo_offset` = extended-range start), and a
   `DUMMY` sentinel (mismatched seqno so Stage-1's forward seqno scan
   terminates via the discontinuity branch).  Patch the UNDO blockmap
   `first_offset`/`next_offset` and recompute the blockmap `entry_crc` and
   volume-header `vol_crc`.
3. `mount_hammer` RW.  Stage-1 finds `REDO_SYNC` → sets `REDO_RECOVERY_REQ`.
   Stage-2 scans the extended range, finds the `REDO_WRITE` (unfiltered),
   calls `hammer_recover_redo_exec` → `vn_rdwr(..., redo_data_bytes=0x4000)`.
4. `uiomove`/`memmove` reads 16384 bytes from `(redo+1)` (buffer offset 56) →
   runs off the 16 KB buffer → **page fault on the next unmapped kernel page**.

### Observed crash (unpatched `#0` baseline)
```
HAMMER(TEST) Found REDO_SYNC 3000000000000000
HAMMER(TEST) recovery redo  3000000000000040-3000000000000080 (64 bytes)(RW)
HAMMER(TEST) Find extended redo  3000000000000000, 64 extbytes
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0xfffff8007522a000   (page just past the hammer buffer)
fault code             = supervisor read data, page not present
instruction pointer    = 0x8:0xffffffff80bcaa0a
Stopped at  memmove+0x10a:  repe movsq  (%rsi),%es:(%rdi)
```

## Impact ceiling

Mount requires root (or an auto-mount / `vfs.usermount` threat model), so this
is **not** an unprivileged→root escalation.  It is a **root-mountable-image
kernel OOB read / DoS**:

- With a *huge* `redo_data_bytes` (e.g. `0x7FFFF000`): the recovery write is
  attempted but returns `EINVAL` (the root dir is not writable / length cap) —
  the vulnerable line is still *reached and executed* with the attacker length,
  but no memory is copied in that degenerate config (confirmed: "write ...
  returned 22").
- With a *moderate* `redo_data_bytes` (e.g. `0x4000` = 16384) targeting a real
  regular-file inode: the `vn_rdwr` actually performs the read past the buffer
  → reliable **panic** (page fault), or, with a smaller over-read that stays
  within mapped pages, kernel-heap bytes written into the recovered file
  (**kernel memory disclosure** readable after mount).

The two confirmed effects: **panic (DoS)** and **OOB kernel-memory read**.

## Exploit chain

Not an unprivileged-corruption class — this is a filesystem-parsing bug
triggered by mounting a crafted image (root/auto-mount precondition).  No
uid0 chain applies (Phase 6 N/A).  The primitive is characterized above; the
deliverable is the panic + the disclosed-kernel-memory ceiling.

## PoC changes
Authored the full image-forger (`forge.c`) plus `build.sh`/`run.sh` and
`icrc32.c`/`crc32.c` (kernel CRC sources, userspace-buildable, so forged
CRCs are byte-identical to the kernel's).  No prior PoC scaffolding existed.

## Fix (`fix.diff`)
Adds the missing validation to `hammer_recover_redo_run()`, mirroring the UNDO
path: require `hdr_size >= sizeof(*redo)+sizeof(tail)` (so the REDO struct
fields are in-record), and for `REDO_WRITE` require
`0 <= redo_data_bytes <= hdr_size - sizeof(*redo) - sizeof(tail)`.  A crafted
record now returns `EIO` and recovery ends cleanly instead of feeding an
attacker length to `vn_rdwr`.  **Supersedes** any pre-verification proposal
(this is the verified, line-accurate fix).

## Fix validation
- Baseline (`#0`, unpatched): PoC → `Fatal trap 12` page fault in `memmove`
  during the recovery write.  Guest down.
- Single-fix kernel (`#1`, only this `fix.diff` applied, rebuilt
  `nativekernel`): same PoC → `HAMMER: Corrupt REDO record, redo_data_bytes
  16384/0` → `End redo recovery` → mount fails with `EIO`, **no panic**, guest
  stays up.  `fix_status = fixed`.
