# DF-3123 — Latent memory-safety landmines in the disabled (`#if 0`) ext2 htree write path

## Status

**Info (latent / dead code).** No Phase V verification performed — none of this
code is reachable in the stock kernel: `ext2_direnter()` compiles the entire
htree write path out (`sys/vfs/ext2fs/ext2_lookup.c:920-941`, `#if 0`), which
gates the only callers of `ext2_htree_add_entry()`, `ext2_htree_create_index()`
and (transitively) `ext2_htree_writebuf()`, `ext2_htree_split_dirblock()`,
`ext2_htree_insert_entry*()`, `ext2_append_entry()`.

This pack documents three latent defects for whoever re-enables that path
(FreeBSD has it enabled; a dfly re-enable without these fixes walks straight
into a heap write overflow):

## 1. Heap buffer overflow in the index-node split (ext2_htree.c:841-877)

`ext2_htree_add_entry()` splits a full interior node:

```c
816: entries = info.h_levels[info.h_levels_num - 1].h_entries;   /* interior node */
817: ent_num = ext2_htree_get_count(entries);                    /* from disk */
818: if (ent_num == ext2_htree_get_limit(entries)) {             /* limit from disk */
...
852: src_ent_num = ent_num / 2;
853: dst_ent_num = ent_num - src_ent_num;
857: memcpy(dst_entries, entries + src_ent_num,
858:     dst_ent_num * sizeof(struct ext2fs_htree_entry));       /* dst = blksize malloc */
```

The only validation `ext2_htree_find_leaf()` performs on an interior node is
`cnt <= on-disk limit` (`ext2_htree.c:313-315`); the limit is never checked
against the buffer capacity (`ext2_htree_node_limit()`) — that missing check is
DF-0850. On a crafted image with interior `count = limit = 0xFFFF`, a file
create inside the directory overflows the fresh `blksize` index block by
`dst_ent_num*8 - (blksize-8)` bytes (up to ~256 KiB past a 1 KiB allocation)
with heap-adjacent content, plus the same OOB *source* read. The follow-up
`ext2_htree_insert_entry()` (`:928` → `:472-479`) `memmove`s with the same
unchecked `count`, an intra-buffer OOB write of one entry past the limit.

Fixed automatically by DF-0850's recommended fix (validate interior limit ==
`ext2_htree_node_limit()` after the `:335` descent), but should be stated.

## 2. Double-brelse of buffers already consumed by bwrite() (error paths)

DragonFly's `bwrite()` releases the buffer unconditionally, error or not
(`sys/kern/vfs_bio.c:1000`). Four error paths here `brelse()` the same buffer
again → buf refcount underflow / use-after-free of `struct buf`:

- `ext2_htree_writebuf()` `:454-459`: returns after a failed `bwrite(bp)`;
  caller `ext2_htree_add_entry()` then takes `finish:` with `write_info == 0`
  and `ext2_htree_release()` (`:958-959`) brelse's *all* level buffers again —
  including every buffer `bwrite()` already consumed.
- `ext2_htree_add_entry()` `:882-885`: `bwrite(dst_bp)` error → `finish:`
  `:950-951` brelse's `dst_bp` again.
- `ext2_htree_add_entry()` `:938-941`: `bwrite(bp)` error → `finish:`
  `:952-953` brelse's `bp` again.
- `ext2_htree_create_index()` `:742-746`: `bwrite(bp)` error → `out:`
  `:765-767` brelse's `bp` again.

Trigger precondition (if ever enabled): any I/O error writing the index
(e.g. vn-backed image whose backing store fills / errors).

Fix sketch: on `bwrite()` error do not touch the buffer again; have
`ext2_htree_writebuf()` mark which levels were consumed, or `brelse()` before
returning only the *not-yet-written* ones.

## 3. u16 reclen truncation with 64 KiB blocks (ext2_htree.c:527-528, 644-645)

```c
527: new_entry->e2d_reclen = htole16(block + blksize - (char *)last_entry - csum_size);
644: last->e2d_reclen = htole16(block1 + blksize - (char *)last - csum_size);
```

With `e2fs_bsize == 65536` and the appended entry at block start, the value
65536 does not fit `uint16_t` and wraps to 0 — downstream walkers then see
`reclen == 0` (infinite-skip / `ext2_dirbad` panic family, cf. DF-3083).
Clamp or refuse `blksize > 32768` in this path.

## Artifacts

analysis-only pack: no trigger, no guest run (dead code). See
`fix.diff` for the illustrative combined patch.

## 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.
