# DF-3122 — ext2_htree_check_next() reloads dx nodes with zero validation → attacker-driven walk + OOB heap read past the directory-index buffer

## What (severity: Medium, bucket: memcorrupt, crafted ext2 image + RW mount)

`ext2_htree_check_next()` (`sys/vfs/ext2fs/ext2_htree.c:142-152`) loads the
*next* index node during a lookup miss-walk and applies **no validation at
all** — not the count/limit cross-check `ext2_htree_find_leaf()` applies
(`:313-315`), not the node-capacity check (`ext2_htree_node_limit()`), nothing.
Two consequences on a crafted htree index:

1. **Attacker-driven walk**: the reload trusts the node's count (u16 from
   disk), so `level->h_entry` advances through up to `count` entries — every
   entry's `h_blk` directly selects which directory block `blkatoff()` reads
   next. A count of `0xFFFF` in a 1 KiB node (capacity: 127 entries) walks
   `h_entry` past the end of the buffer.
2. **OOB heap read**: entry index 127 lives at node offset `8 + 8*127 = 1024`
   == the first byte past the 1 KiB buffer; `ext2_htree_get_hash(h_entry)`
   (`:136`) reads 4 kernel-heap bytes past `bp->b_data`, and if the parity
   gate passes, `ext2_htree_get_block()` (`:383`) reads 4 more OOB bytes and
   feeds them to `blkatoff()` as a file offset.

Distinct from DF-0850 (missing interior-node limit validation in
`find_leaf`'s `:335` descent): the stage-1 PoC's descent node B1 is *fully
honest* (`limit == ext2_htree_node_limit() == 127`, count=1), so a kernel
carrying DF-0850's recommended fix still takes this path. DF-0850's fix does
not touch `check_next`.

## Reproduce

Host (needs mke2fs/debugfs/e2fsck — e2fsprogs 1.47.0 used):

```
./craft.sh                 # builds valid.img, a1.img (panic), a2.img (OOB probe)
```

Guest (root): `sh trigger-root.sh /root/a1.img` (vntool attach + RW mount + chmod)
Guest (maxx): `stat /mnt/e2/d/zzqq01`

Expected on stock kernel:

```
panic: ext2_dirbad: : bad dir ino 12 at offset 7168: mangled entry
ext2_dirbad() / ext2_search_dirblock() / ext2_htree_lookup() / ext2_lookup()
```

offset 7168 = the 126th crafted entry's block pointer (blk 7) — proof that the
unvalidated reload happened and the walk consumed 126 attacker-crafted entries
before reading a hole block chosen by the attacker.

`a2.img` variant: entry 126 points at a valid miss-leaf, so the walk reaches
entry 127 — entirely past the 1 KiB buffer: the 4-byte OOB read is
source-certain (`h_levels[].h_entry < h_entries + 0xFFFF` at `:127-129`, gate
value read at `:136` from `b_data+1024..1027`). Runtime outcome is
heap-dependent: in 3/3 fresh-boot runs the garbage failed the parity gate and
the lookup returned clean ENOENT (guest survived) — the OOB read itself is
invisible without heap grooming.

## Preconditions

- crafted ext2 image with `dir_index`, mounted read-write (mount needs root or
  `vfs.usermount=1` — same precondition class as DF-0850/DF-0877/DF-3083)
- any user can then trigger with a single `stat` of an absent name

## Fix

`fix.diff` — validate the reloaded node in `check_next` (count != 0,
count <= limit, limit == `ext2_htree_node_limit()`); on failure stop the scan
(brelse the new buffer, return 0 → ENOENT, linear fallback preserved upstream).
Validated: patched kernel survives both PoC images, valid-image lookups still
work 100/100 (see VERDICT.md).
