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

ffs_vget publishes the inode into the ihash before validating it: bread-failure branch hands concurrent ufs_ihashget waiters a VBAD zeroed-inode vnode as a successful VFS_VGET result (DF-3208 ufs twin)

Summary

ffs_vget inserts the fresh inode via ufs_ihashins (ffs_vfsops.c:1120) before bread()ing the dinode; on read error (:1134-1145) it marks the vnode VBAD and vx_puts it while it is still hashed. A concurrent ufs_ihashget that blocked in vget wakes and receives the VBAD vnode with an all-zero dinode (i_mode 0, i_devvp NULL) as a SUCCESSFUL lookup. Crafted-image reachable: ufs_lookup passes dirent ino values to VFS_VGET with no range check, so a crafted ino whose inode block lands beyond the device end makes bread fail deterministically; the post-publish window is the disk-read time. Contrast: the EBUSY handler is SAFE by ordering (v_data assigned after insert, reclaim sees NULL) - the difference that makes the ext2 twin DF-3250 a real bug. No memory-safety sink found (mode-0 short-circuits inactive, reclaim tolerates NULL devvp) - VFS_VGET contract violation, hence Low. Not executed. Fix: ufs_ihashrem before the VBAD vx_put on the error branch.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3252 Β· 2 files
FileTypeDescriptionSize
README.md file 1.9 KB ↓ raw
verdict.json file 2.0 KB view raw
README.md file
↓ download raw

DF-3252 β€” ffs_vget publish-before-validate: bread-failure branch hands waiters a VBAD zeroed-inode vnode (DF-3208 ufs twin)

Status: untested (Low severity; outside the mandatory Phase V buckets).

What

ffs_vget() (sys/vfs/ufs/ffs_vfsops.c:1066) inserts the fresh inode into the per-mount ihash (ufs_ihashins, :1120) before reading the dinode from disk. If the read fails (:1134-1145) the creator sets vp->v_type = VBAD and vx_puts the still-hashed, still-uninitialized (M_ZERO) inode. A concurrent ufs_ihashget() waiter (ufs_ihash.c:105 vget() β€” it blocked on the creator's VX lock holding a reference) wakes and receives the VBAD vnode with an all-zero dinode (i_mode 0, i_devvp NULL, i_effnlink 0) as a successful VFS_VGET result.

Reachability from crafted images: ufs_lookup() passes the directory entry's dp->i_ino to VFS_VGET with no range validation (ufs_lookup.c:480,527,567,588), so a crafted dirent whose ino_to_fsba lands beyond the device end makes bread() fail deterministically (verified reachable in principle; the DF-3190 crafted-FFS harness is the basis for a PoC).

Why only Low (materially weaker than the ext2 twin DF-3251):

  • ufs_vinit returns 0 unconditionally (ufs_vnops.c:1954) β€” its error branch at ffs_vfsops.c:1158 is dead code, so no fully-initialized escaped inode.
  • The zeroed inode short-circuits ufs_inactive (i_mode == 0, ufs_inode.c:75) and ufs_reclaim tolerates the never-assigned i_devvp == NULL (all if (ip) guards, ufs_inode.c:126-163).
  • No memory-safety sink identified; the defect is a VFS-contract violation (VBAD vnode escaping a successful lookup into namei/NFS).
  • DF-0928 already covers the unlocked hash; this finding is the publish-before-validate ordering on top of it.

Fix sketch

Insert into the ihash only after the dinode bread succeeds (hash a VX-locked placeholder, complete in place), or ufs_ihashrem + VBAD before vx_put on the error branch.

Fix verification

not_testable
per-fix-DF-3252

Confirmed kernel references

Detail

Evidence (decisive lines)

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

Verified recommended fix

Move ufs_ihashins after the dinode bread succeeds (hash a VX-locked placeholder and remove it on error), or ufs_ihashrem + VBAD before vx_put on the error branch.

Verdict

Structural finding only (Low, DF-3208 ufs twin): ffs_vget inserts the inode into the unlocked per-mount ihash at ffs_vfsops.c:1120 before reading the dinode; on the bread error branch (:1134-1145, reachable from crafted images - ufs_lookup.c:480 passes directory-entry ino values to VFS_VGET with no range check, so ino_to_fsba can address past the device end) the creator marks the vnode VBAD and vx_puts it, and a concurrent ufs_ihashget waiter that blocked in vget while holding a reference receives the VBAD vnode with an all-zero dinode (i_mode 0, i_devvp NULL) as a successful VFS_VGET result. Materially weaker than the ext2 twin (DF-3251): ufs_vinit's error branch is dead code (ufs_vinit returns 0 unconditionally - ufs_vnops.c:1954), the zeroed inode short-circuits ufs_inactive (ufs_inode.c:75 i_mode==0) and ufs_reclaim tolerates NULL i_devvp, so no memory-safety sink was identified; the escaped VBAD vnode violates the VFS_VGET contract and is cached/handed to namei callers. Note DF-0928 already covers the unlocked hash itself; this finding is the publish-before-validate ordering, not the missing lock.