# DF-3251 — ext2 publish-before-validate: concurrent ext2_ihashget waiters receive half/fully-initialized VBAD/VNON inodes (DF-3208 twin)

**Status: untested** (Medium severity; outside the mandatory Phase V buckets;
structural analysis only — the twin pattern was verified as
Medium/not_reproduced for cd9660 as DF-3208).

## What

`ext2_vget()` publishes the new inode into the global ext2 ihash at
`ext2_alloc_vnode()` → `ext2_ihashins()` (sys/vfs/ext2fs/ext2_vfsops.c:1313)
and only then validates/initializes it. A concurrent `ext2_ihashget()`
(ext2_ihash.c:100 `vget()`) blocks on the creator's VX lock holding a
reference, so the creator's post-publish error branches hand the waiter a
"successfully looked up" vnode that is **VBAD with a zeroed dinode** or
**VNON with attacker-specified dinode contents**:

* ext2_vfsops.c:1358-1371 (bread error) — sets VBAD; inode all-zero
  (M_ZERO), `i_devvp` (= `i_ump->um_devvp` macro) never vref'd.
* ext2_vfsops.c:1373-1380 (`ext2_ei2i` EINVAL) — **v_type not set**;
  half-converted inode. Deterministic crafted-image triggers:
  `ext2_inode_cnv.c` ino-range check and bad `e2di_extra_isize` — both run
  after `i_nlink`/`i_mode` were already stored, so the escaped inode has a
  nonzero attacker-controlled mode with zeroed `i_db[]`.
* ext2_vfsops.c:1406-1410 (`ext2_vinit` EINVAL: `i_mode != 0 &&
  v_type == VNON`) — fully-converted attacker dinode that just FAILED the
  "only unallocated inodes should be VNON" validation; the validation is
  bypassed by handing the vnode out before it runs.

Single-threaded, the same paths self-clean via `vx_put` → VREF_FINALIZE →
synchronous reclaim (verified during the DF-3250 work: vfs_lock.c:763,
ext2_inode.c:527 `vrecycle`). Only the concurrent-waiter handoff escapes.
`ext2_inactive()` short-circuits on `i_mode == 0` (ext2_inode.c:527) but the
ei2i/vinit-failure variants have nonzero modes.

Impact ceiling: a VNON/VBAD vnode with crafted uid/mode/size escapes into
the VFS layer as a successful VFS_VGET result (namei/NFS), violating the
invariant `ext2_vinit` exists to enforce; no direct memory-safety sink was
identified (block pointers are only used for device-bounded I/O), matching
DF-3208's Medium assessment.

## Fix sketch

Do not insert into the ihash until the dinode has been read and validated
(move `ext2_ihashins` after `ext2_ei2i`/`ext2_vinit` — requires switching
the concurrency protocol to the ffs-style "hash a locked placeholder then
complete in place" or vhold'ing); at minimum, remove the inode from the
hash before `vx_put` on every post-publish error branch and set
`vp->v_type = VBAD` on the ei2i branch.
