β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3251

ext2 publish-before-validate (DF-3208 twin): all three post-ext2_ihashins error branches hand concurrent ext2_ihashget waiters half- or fully-initialized VBAD/VNON inodes, bypassing ext2_vinit's 'only unallocated inodes should be VNON' validation

Summary

ext2_vget publishes the inode at :1313 and only then initializes/validates. Three error branches then discard the still-hashed vnode while a concurrent ext2_ihashget waiter (blocked in vget, holding a reference so no reclaim runs) receives it as a successful VFS_VGET: (1) bread error :1358 -> VBAD with all-zero dinode; (2) ext2_ei2i EINVAL :1373 -> v_type stays VNON, inode HALF-converted with attacker-controlled i_mode/i_nlink/i_size already stored but i_db[] zero - deterministic crafted-image triggers (bad e2di_extra_isize, ino-range); (3) ext2_vinit EINVAL :1406 (crafted typeless mode) -> VNON vnode with a FULLY-converted attacker dinode that just FAILED the VNON validation. Single-threaded the same paths self-clean via VREF_FINALIZE; only the concurrent handoff escapes. No direct memory-safety sink identified - VFS-layer invariant break, limited-integrity/DoS ceiling. Not executed (Medium, outside mandatory buckets); harness reusable from DF-3250. Fix: hash only after validation (or ihashrem+VBAD on every post-publish error branch).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3251 Β· 2 files
FileTypeDescriptionSize
README.md file 2.6 KB ↓ raw
verdict.json file 1.7 KB view raw
README.md file
↓ download raw

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.

Fix verification

not_testable
per-fix-DF-3251

Confirmed kernel references

Detail

Evidence (decisive lines)

['README.md (path:line analysis)']

Verified recommended fix

Hash the inode only after ext2_ei2i + ext2_vinit succeed (or remove it from the hash and set VBAD on every post-publish error branch before vx_put).

Verdict

Structural finding only (Medium, DF-3208 cd9660 twin): ext2_vget publishes the inode via ext2_ihashins (ext2_vfsops.c:1313) before reading/validating the dinode, and all three post-publish error branches (bread :1358, ext2_ei2i EINVAL :1373 with deterministic crafted-image triggers, ext2_vinit VNON EINVAL :1406) hand a concurrent ext2_ihashget waiter a VBAD/VNON half- or fully-initialized vnode as a successful VFS_VGET result, bypassing ext2_vinit's 'only unallocated inodes should be VNON' validation. Not PoC-verified in this run (outside the mandatory verify buckets); single-threaded self-cleanup via VREF_FINALIZE synchronous reclaim was verified during DF-3250 work.