# DF-3040 — Unbounded `HAMMER_UNDO_INDEX` indexes `vol0_undo_array[128]` out of bounds: kernel heap OOB read at attacker-chosen offset on mount of a crafted HAMMER1 image

Kernel: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), sys/vfs/hammer
File under audit: `sys/vfs/hammer/hammer_ondisk.c` (pass 2); root cause sink in
`hammer_undo_lookup()` / `hammer_xlate_to_undo` (hammer_disk.h).

## Reproduce

```
# guest (root), after pushing undo_craft.c/build.sh/run.sh:
sh build.sh                       # cc undo_craft + newfs_hammer 12G base.img
sh run.sh 0x400000000             # idx 2048  -> read at bp+17288
sh run.sh 0x4000000000            # idx 32768 -> read at bp+263048
# expected (stock INVARIANTS kernel):
#   Fatal trap 12: page fault, supervisor read data, page not present
#   Stopped at hammer_undo_lookup+0x84: addq 0x388(%rcx,%rdx,8),%rbx
#   (guest wedged at db>; the mount never completes)
```

The faulting instruction is the `vol0_undo_array[idx]` load itself:
`0x388` = 904 = `offsetof(struct hammer_volume_ondisk, vol0_undo_array)`,
`%rdx` = attacker-chosen index (`short(zone3)/HAMMER_BIGBLOCK_SIZE`), and the
fault VA is 904 + idx*8 bytes into the 16KB rootvol bp mapping — i.e. past
its end.

## Root cause

* `hammer_get_buffer()` (hammer_ondisk.c:688-689) dispatches zone-3 offsets to
  `hammer_undo_lookup()` (hammer_undo.c:58-76).
* The only bounds applied anywhere on the undo path are:
  - `hammer_recover_stage1` (hammer_recover.c:232): `first_offset > alloc_offset
    || next_offset > alloc_offset → EIO` — but `alloc_offset` itself comes from
    the attacker-crafted `vol0_blockmap[3]` in the root volume header;
  - `KKASSERT(zone3_off < undomap->alloc_offset)` (hammer_undo.c:70) — same
    attacker-controlled reference, and INVARIANTS-only.
* `hammer_xlate_to_undo()` (hammer_disk.h:827-829) then computes
  `vol0_undo_array[HAMMER_UNDO_INDEX(zone3)]` where
  `HAMMER_UNDO_INDEX = short(zone3) / HAMMER_BIGBLOCK_SIZE` (8MB). The array
  has exactly `HAMMER_MAX_UNDO_BIGBLOCKS = 128` entries (hammer_disk.h:512,789);
  the kernel never checks the index against it (the constant is not referenced
  anywhere in sys/). short(zone3) spans 2^52, so idx spans 2^29 → the load can
  be aimed up to ~4GB past the 16KB header buffer.
* Trigger path: `mount_hammer` → `hammer_mount` (vfsops.c:671 copies the
  crafted blockmap) → `hammer_recover_stage1` version-4 backscan
  (recover.c:257-263) → `hammer_recover_scan_rev` → `hammer_bread(zone3)`
  (recover.c:798) → `hammer_get_buffer` → `hammer_undo_lookup` → OOB load.

Impact: crafted-image mount-time kernel OOB read (CWE-125) at an
attacker-chosen offset up to ~4GB from the header bp. On INVARIANTS guests
this manifests as the observed page-fault panic (deterministic local DoS from
a root-mounted image; `vfs.usermount=1` makes it unprivileged). On production
(non-INVARIANTS) kernels the OOB value is used as a zone-2 offset; the read
itself stays in-kernel (no demonstrated disclosure of the OOB bytes to
userspace — the value only steers which device block is subsequently read).
Mount of a crafted image requires root (or usermount), consistent with the
DF-0797/DF-0798 threat model.

## Fix validation

* `fix.diff` (authored against the read-only audit tree; applied only to the
  guest's /usr/src copy): reject `zone3_off >= HAMMER_ENCODE_UNDO(128 * 8MB)`
  in `hammer_undo_lookup()` with EIO before `hammer_xlate_to_undo()`.
* `make nativekernel KERNCONF=X86_64_GENERIC` → OK (fix_build.log); rebooted
  into kernel #1 (Sat Sep 5 16:07:13 UTC 2026).
* Re-ran the exact PoC (`run.sh 0x4000000000`) on the fixed kernel:
  `mount: Input/output error`; console shows
  `Unable to read UNDO TAIL at 3000003ffffffff8 / recovery failure during
  seqno backscan / Failed to recover HAMMER filesystem on mount`;
  **no panic, guest stays up** (fix_run.log).
* Regression on the fixed kernel: stock newfs_hammer image mount + create +
  rm + sync + umount cycle is clean (freemap_probe.sh, also refutes the
  DF-3011 "off-by-8" environmental artifact — see run.log).

## Artifacts

* `undo_craft.c` — image forger (patches vol0_blockmap[3] only; no CRC fixup
  needed because the kernel never verifies vol_crc — see DF-3042)
* `build.sh` / `run.sh` / `freemap_probe.sh`
* `run.log` — both baseline runs + console transcripts (full console in
  `panic_idx2048.txt`, `panic_idx32768.txt`)
* `fix.diff`, `fix_build.log`, `fix_run.log`
* `manifest.json`, `verdict.json`
