# DF-3040 VERDICT — unbounded HAMMER_UNDO_INDEX → vol0_undo_array OOB kernel heap read

**Status: REPRODUCED (baseline, twice, fresh boots) · FIXED (fix.diff validated on a rebuilt kernel).**

## What was claimed vs what happened

The pass-2 audit of `sys/vfs/hammer/hammer_ondisk.c` found that the zone-3
(undo) dispatch at `hammer_ondisk.c:688-689` reaches
`hammer_undo_lookup()` (hammer_undo.c:58-76), whose only offset bound —
`KKASSERT(zone3_off < undomap->alloc_offset)` — references the same
attacker-crafted `vol0_blockmap[3]` that supplies the offset. The final
translation `hammer_xlate_to_undo` (hammer_disk.h:827-829) indexes
`vol0_undo_array[HAMMER_UNDO_INDEX(zone3)]` with
`HAMMER_UNDO_INDEX = short(zone3)/8MB`; the array holds 128 entries
(`HAMMER_MAX_UNDO_BIGBLOCKS`, hammer_disk.h:512) and **no code in sys/
compares the index against 128**. The mount path reaches this with a fully
crafted offset: `hammer_recover_stage1` (hammer_recover.c:232) only checks
first/next against the attacker's own `alloc_offset`, then the version-4
backscan (`recover.c:257-263 → :798`) issues `hammer_bread()` on the crafted
zone-3 offset.

Two baseline runs on the stock INVARIANTS kernel (#0, Jul 2 2026), each after
a fresh `vm.sh reset with-src`, mounting a `newfs_hammer` image whose
`vol0_blockmap[3]` was patched by `undo_craft`:

| run | short(zone3) | idx | load address in bp | result |
|-----|--------------|-----------|--------------------|--------|
| 1 | 0x400000000 (2^34) | 2048 | 904+16384 = bp+17288 (904B past end) | Fatal trap 12, `hammer_undo_lookup+0x84`, fault VA 0xfffff80060cea380 |
| 2 | 0x4000000000 (2^38) | 32768 | 904+262144 = bp+263048 (~240KB past end) | Fatal trap 12, same instruction, fault VA 0xfffff800641f6380 |

The faulting instruction `addq 0x388(%rcx,%rdx,8),%rbx` **is** the
`vol0_undo_array[idx]` load (`0x388`=904=`offsetof(vol0_undo_array)`); the
fault VAs are past the 16KB rootvol bp mapping, at distances that track the
attacker-chosen index exactly. The index is image-controlled up to 2^29
(~4GB reach). Both runs wedged the guest at `db>` (kernel panic — guest
down), i.e. a deterministic DoS on mount of the crafted image.

## Why not higher impact

The OOB *value* is consumed as a zone-2 offset (`result_offset`), not copied
to userspace. On INVARIANTS kernels the subsequent
`KKASSERT(hammer_is_zone_raw_buffer(...))` (hammer_ondisk.c:703) or — as
observed — the unmapped OOB load itself panics first. On production
(non-INVARIANTS) kernels the garbage zone-2 offset leads to
`hammer_get_volume()` ENOENT or an EIO device read in the common case; there
is no demonstrated exfiltration channel for the OOB-read bytes. The primitive
is an attacker-aimed kernel read (CWE-125) whose practical impact on this
guest is the panic. Note the in-bounds half of the same bug: for idx in
[128,1792] the load stays inside the 16KB bp but past the on-disk struct —
the attacker then *knows* the value (it is their own image bytes at LBA1+),
giving a fully controlled zone-2 offset bypassing the 128-entry map.

Severity Medium: mount-time (root/usermount preconditions, same as DF-0797 /
DF-0798), memory-safety proven, disclosure not demonstrated, deterministic
panic on default INVARIANTS builds.

## Fix validation (mandatory: memory-corruption class reproduced)

`fix.diff` bounds `zone3_off` to the on-disk format's supported undo range in
`hammer_undo_lookup()` (EIO before `hammer_xlate_to_undo`); authored against
the read-only audit tree (`git apply --check` clean), applied only to the
guest's `/usr/src` copy, `make nativekernel KERNCONF=X86_64_GENERIC` → OK,
rebooted into kernel #1 (Sat Sep 5 16:07:13 UTC 2026):

* **Baseline reproduced**: 2/2 panics on stock kernel #0 (run.log,
  panic_idx*.txt).
* **Patched kernel**: same PoC (`run.sh 0x4000000000`) → `mount: Input/output
  error`, console `Unable to read UNDO TAIL … recovery failure during seqno
  backscan / Failed to recover HAMMER filesystem on mount`, guest up (fix_run.log).
* **Regression**: stock newfs_hammer mount + create/rm/sync/umount clean on
  the fixed kernel (freemap probe in run.log).

## Environmental check performed (priority 7 of this audit)

DF-3011's note about `KKASSERT(layer2->zone==zone)` (hammer_blockmap.c:832)
and a suspected "off-by-8 between the on-disk layer1 table and the blockmap
root" is **not reproducible on stock images**: a plain `newfs_hammer` image
goes through mount + btree-node alloc/free (file create, delete, sync)
without tripping any freemap KKASSERT, on both the stock and fixed kernels.
The artifact seen in DF-3011 came from the deepforge grafting (nodes
allocated at `vol0_blockmap[8].next_offset` without matching freemap layer2
zone bits), not from a kernel decode bug. No finding filed.

## Related-but-distinct (not re-reported)

DF-0797 (vol_no → volume_map OOB write) and DF-0798 (node in-buffer offset
OOB read during CRC) remain unfixed in-tree, as does DF-0776's proposed
count/type validation (hammer_load_node/hammer_btree.c carry only KKASSERTs).
