# DF-3122 VERDICT

## Bottom line

**REPRODUCED (panic / crafted-image DoS + demonstrated attacker-driven walk;
4-byte OOB heap read source-certain, runtime-silent in 3/3 probes).**

On the stock INVARIANTS kernel (#0, `6.5-DEVELOPMENT`), an unprivileged user
(`maxx`) issuing **one `stat`** of an absent name inside an htree-indexed
directory of a crafted ext2 image (RW-mounted by root) panicked the kernel:

```
panic: ext2_dirbad: : bad dir ino 12 at offset 7168: mangled entry
ext2_dirbad() at ext2_dirbad+0x32
ext2_search_dirblock() at ext2_search_dirblock+0x194
ext2_htree_lookup() at ext2_htree_lookup+0x12e
ext2_lookup() at ext2_lookup+0x28d
vop_old_lookup() at vop_old_lookup+0x50
```

## Why (code trace, exact chain)

1. `ext2_lookup_ino()` dispatches to `ext2_htree_lookup()` because the dir has
   `IN_E3INDEX` (`sys/vfs/ext2fs/ext2_lookup.c:377-382`).
2. `ext2_htree_find_leaf()` walks the crafted root (limit==124 passes the
   `:308-310` root-limit check, count=3, ind_levels=1), binary search selects
   entry 1 → descends into **B1, a fully honest interior node**
   (`limit=127 == ext2_htree_node_limit()`, count=1) → returns entry 0 (the
   node's count/limit overlay entry, whose `h_blk` legitimately points at the
   leftmost child — this is the DF-3085 "underflow" case and it is
   *format-correct*, see notes-df3085.md).
3. `ext2_htree_lookup()` searches leaf block 3 (real dirent data): miss.
4. `ext2_htree_check_next()` (`ext2_htree.c:113-155`): level-1 entry exhausted
   → advances the **root** entry pointer, parity gate passes (`next_hash ==
   H|1`), then `:144-151` `blkatoff()`s the next root entry's block — the
   crafted node B2 — and installs it as `h_levels[1]` with **zero validation**
   (no count/limit/capacity checks; contrast `find_leaf`'s `:313-315`).
5. The do-while loop (`:381-415`) then consumes B2's entries: `h_entry` walks
   0,1,2,…,126 while `h_entry < h_entries + get_count(h_entries)` with
   `count = 0xFFFF` (`:127-129`) — every entry's `h_blk` (attacker bytes)
   choosing the next `blkatoff()` target (`:383-384`). Entry 126's block
   pointer = 7 → offset 7168 = a hole → zero-filled buffer →
   `ext2_check_direntry` rejects `rec_len == 0` → `ext2_dirbad()` → **panic**
   (writable mount) at *exactly the chosen offset*.
6. **OOB read**: had entry 126 pointed at a valid miss-leaf (a2.img), the walk
   reaches entry 127 at node offset `8+8*127 = 1024` — the first byte past the
   1 KiB buffer. `ext2_htree_get_hash()` (`:136`) then reads kernel heap at
   `b_data+1024..1027`; passing the parity gate, `ext2_htree_get_block()`
   (`:383`) reads `b_data+1028..1031` and feeds the garbage to `blkatoff()`.
   In 3/3 fresh-boot runs the gate rejected the garbage → clean ENOENT, guest
   survived (silent OOB read). Source-certain regardless: `count` is never
   bounded by the node capacity in this path.

## Distinctness from DF-0850

DF-0850 = missing interior-node *limit* validation in `find_leaf`'s descent
(`:335-339`). This finding = missing *everything* validation in
`check_next`'s reload (`:144-151`) — a different function, different missing
check, reached only via the miss-walk. The stage-1 PoC's descent node (B1) is
honest (`limit == node_limit == 127`), so DF-0850's recommended fix does not
prevent this chain; the panic offset (7168) is chosen by the *reloaded* node's
entry 126, which `find_leaf` never touches.

## Baseline (stock kernel #0)

- stage 1 (a1.img): panic above (run.log) — 1/1 attempts.
- stage 2 (a2.img): silent ENOENT, guest up — 3/3 fresh boots (run2.log,
  run3.log) — OOB read occurs invisibly; gate rejects heap garbage.

## Fix validation (kernel #1, built in-guest)

`fix.diff` validates the reloaded node in `ext2_htree_check_next` (count != 0,
count <= limit, limit == `ext2_htree_node_limit()`); on failure it brelse's
the new buffer and returns 0 (scan stops → ENOENT).

Built with `make -j6 nativekernel KERNCONF=X86_64_GENERIC` in the guest
(build.log, rc=0), `make installkernel`, reboot (uname in env.txt):

- stage 1 (a1.img): **no panic**; `stat /mnt/e2/d/zzqq01` → clean ENOENT;
  guest up (fix_run1.log).
- stage 2 (a2.img): **no panic**; clean ENOENT; guest up (fix_run2.log).
- regression — valid e2fsck -fyD image: `[ -e payloadname001..100 ]` sweep
  **ok=100 miss=0**, `rm -v payloadname050` removed cleanly (100→99),
  open/lstat/unlink via C tester all succeed (fix_valid.log). No behavioral
  regression on legitimate indexed directories.

## Impact ceiling

Crafted-image class (root mount or `vfs.usermount=1`), then any user:
deterministic kernel panic (DoS) and an OOB-read primitive (4–8 bytes per
walk step past a 1 KiB buffer-cache object, extendable with heap grooming).
No direct disclosure channel demonstrated; same bar as DF-0850 → Medium.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
