# DF-0850 — Missing interior-node limit validation in ext2_htree_find_leaf

## Verdict: REPRODUCED → FIXED

**Status:** reproduced (panic / DoS)
**Impact:** panic — kernel panic from crafted ext2 image mount + lookup
**Confidence:** certain
**Fix:** validated (single-fix ext2fs.ko module, panic gone)

---

## Mechanism (line-by-line trace)

The ext2 htree directory-index lookup `ext2_htree_find_leaf()` in
`sys/vfs/ext2fs/ext2_htree.c:257-347` walks htree interior nodes without
fully validating on-disk metadata:

1. **Root level — limit validated** (`ext2_htree.c:308-310`):
   ```c
   if (ext2_htree_get_limit(entp) != ext2_htree_root_limit(ip, rootp->h_info.h_info_len))
       goto error;
   ```
   The root's limit is checked against the filesystem-computed value. ✓

2. **Root level — count/limit guard** (`ext2_htree.c:313-315`):
   ```c
   cnt = ext2_htree_get_count(entp);
   if (cnt == 0 || cnt > ext2_htree_get_limit(entp))
       goto error;
   ```
   Count is bounded by the validated root limit. ✓

3. **Descent into interior node** (`ext2_htree.c:335-339`):
   ```c
   if (ext2_blkatoff(vp, ext2_htree_get_block(found) * m_fs->e2fs_bsize,
       NULL, &bp) != 0)
       goto error;
   entp = ((struct ext2fs_htree_node *)bp->b_data)->h_entries;
   ```
   The code descends into the block pointed to by `found`. **No validation
   of `entp`'s limit against `ext2_htree_node_limit(ip)`.** ✗

4. **Next iteration — count/limit guard uses attacker data** (`ext2_htree.c:313-315`):
   On the next loop iteration, `get_limit(entp)` reads from the interior
   node's on-disk header — **NOT** validated against `ext2_htree_node_limit()`.
   An attacker sets both `count=0xFFFF` and `limit=0xFFFF`:
   - `cnt (0xFFFF) > get_limit (0xFFFF)` → **false** → passes the check
   - `end = entp + cnt - 1 = entp + 65534` → **~512KB past the 1KB `bp->b_data`**

5. **OOB binary-search read** (`ext2_htree.c:319-325`):
   ```c
   while (start <= end) {
       middle = start + (end - start) / 2;
       if (ext2_htree_get_hash(middle) > hash_major)  // OOB READ
   ```
   Each binary-search probe reads 8 bytes from an `ext2fs_htree_entry` up to
   ~512KB past the buffer. The OOB data feeds `found = start - 1` →
   `ext2_htree_get_block(found)` → `ext2_blkatoff()` with a garbage block
   number, causing secondary faults.

### Two crash manifestations observed:

- **V1 image** (header block = 0): Root binary search with count=1 produces
  `found = header entry` whose `h_blk = 0`. `ext2_blkatoff(vp, 0, ...)` tries
  to re-lock the root buffer (already locked at line 281) → **panic:
  "lockmgr: locking against myself"**. Reproduces deterministically.

- **V3 image** (header block = 573): Root binary search correctly descends
  into block 573 (interior node). Interior node has `count=limit=0xFFFF`.
  Binary search reads ~512KB of OOB kernel heap. The OOB block number
  produced is non-deterministic — may cause secondary panic or silent OOB
  read (kernel heap info leak via hash comparison side channel).

---

## PoC

### Trigger images

Crafted ext2 images (`craft_htree.py`) with:
- `EXT2F_COMPAT_DIRHASHINDEX` feature enabled (superblock)
- `IN_E3INDEX` (0x1000) flag on directory inode
- htree root: `h_ind_levels=1`, root limit matching `ext2_htree_root_limit`
- Interior node block: `count=0xFFFF`, `limit=0xFFFF`

### Build & Run

```sh
# On host: craft the image
python3 craft_htree.py    # creates evil_ext2.img

# On guest (as root — mount threat model):
kldload ext2fs
vnconfig vn0 /root/evil_ext2.img
mount -t ext2fs -o ro /dev/vn0 /mnt/df0850
stat /mnt/df0850/testdir/f0000_longname_to_force_htree_index_split
# → panic on unpatched, clean ENOENT on patched
```

### Threat model

Mount-time / lookup parsing of an attacker-controlled ext2 filesystem image.
Requires root to mount (`vfs.usermount=0`) or an admin who mounts a
user-supplied image. This is a realistic threat for removable media,
downloaded VM images, or filesystem fuzzing.

---

## Exploit chain

This is a **DoS / OOB read** primitive, not a write primitive. No escalation
chain is applicable:
- The OOB read leaks kernel heap data via hash comparison side channels
- The garbage block number from OOB can trigger secondary panics
- No write capability is derived from this bug

Impact ceiling: **local DoS (kernel panic)** from a crafted ext2 image, plus
potential kernel heap info leak.

---

## Fix (fix.diff)

Two targeted checks in `ext2_htree_find_leaf`:

1. **Block 0 re-lock prevention** (before descent):
   ```c
   if (ext2_htree_get_block(found) == 0)
       goto error;
   ```
   Prevents crafted images from causing `ext2_blkatoff(vp, 0, ...)` to
   re-lock the root buffer.

2. **Interior node limit validation** (after descent, mirroring root check):
   ```c
   if (ext2_htree_get_limit(entp) != ext2_htree_node_limit(ip))
       goto error;
   ```
   Validates the interior node's on-disk limit against the filesystem-computed
   value, preventing the `count=limit=0xFFFF` attack that defeats the
   `cnt > get_limit()` guard and causes the OOB binary-search read.

---

## Fix validation

| Test | Unpatched (#0 + orig ext2fs.ko) | Patched (fix.diff ext2fs.ko) |
|------|--------------------------------|------------------------------|
| V1 image stat | **panic**: lockmgr locking against myself | **clean**: ENOENT (linear fallback) |
| V3 image stat | OOB read (silent or secondary panic) | **clean**: ENOENT (linear fallback) |
| Guest survives | ❌ (panic, DDB) | ✅ (up, responsive) |

**Module SHA256 (patched):** `5c7a91ab9ec68fae49e3fc21c5cdea919de7669fcf8eea56b8184ebac6d29674`

The fix closes both crash paths. The htree lookup returns -1 on detecting
the corrupt image, `ext2_htree_lookup` returns -1, and the caller falls back
to a linear directory scan (which succeeds for valid files, returns ENOENT
for missing ones).
