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

ext2_valloc EEXIST path leaks the ext2_ihashget() vnode lock+vref β€” crafted bitmap makes the mount un-unmountable with uninterruptible D-state processes

Summary

ext2_ihashget() returns the vnode vget()'d (vref + LK_EXCLUSIVE, recursive against the create path's parent lock). When a crafted inode bitmap (live root inode marked free, nifree=1) makes the allocator hand out in-use inode 2, ext2_valloc prints 'ext2_valloc: vp %p exists for inode 2' and returns EEXIST without vput() - the lock recursion and vref leak permanently. VERIFIED: touch fails EEXIST; afterwards ls and umount wedge uninterruptibly (D-state PIDs observed, timeout cannot kill); recovery requires reboot; each trigger also permanently consumes an allocated inode. Fix: vput(vp) before the return + roll back the bitmap consumption when the hash has the inode.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3064 Β· 6 files
FileTypeDescriptionSize
craft3064.py β€” 1.9 KB view raw
vntool.c β€” 1.1 KB view raw
fix.diff β€” 515 B view raw
run.log β€” 784 B view raw
console.txt β€” 214 B view raw
VERDICT.md β€” 1.8 KB ↓ raw
VERDICT.md
↓ download raw

DF-3064 VERDICT

Reproduced? YES β€” single decisive run

  • Guest: DragonFly 6.5-DEVELOPMENT #0 Jul 2 2026, stock ext2fs.ko.
  • Image: craft3064.py β€” plain rev1 ext2 (no features, so no gd checksums); group-0 inode bitmap bit 1 (inode 2, the LIVE root) cleared; gd nifree=1; s_free_inodes_count=1.
  • Trigger: touch /mnt/e4/x
  • returned File exists (TOUCH-RC=1)
  • console: ext2_valloc: vp 0xfffff80118106c00 exists for inode 2
  • Aftermath: ls /mnt/e4 and umount /mnt/e4 wedge in uninterruptible D state (ps: PIDs 982, 1001 ls, 1022 umount; timeout 15 could not kill them). Only namecache-served reads still work. Reboot required.

Mechanism (path:line)

  • ext2_nodealloccg (sys/vfs/ext2fs/ext2_alloc.c:1358 memcchr scan / 1395 return) hands out the first clear bitmap bit β€” the crafted-clear root bit β€” and returns ino 2 with nifree/ficount consumed.
  • ext2_valloc (sys/vfs/ext2fs/ext2_alloc.c:423-426) then calls ext2_ihashget() which returns the root vnode vget()'d (sys/vfs/ext2fs/ext2_ihash.c:100, vget LK_EXCLUSIVE + vref; recursive against the create path's own parent lock) β€” and the EEXIST return path never vput()s it. The vnode's lock recursion and vref are permanently leaked: the mount cannot be locked for directory ops nor flushed by umount; every triggering create also permanently consumes an inode.

Impact

Local DoS (crafted image β†’ mount-wide uninterruptible wedge, un-unmountable mount, inode leak). Not a privilege escalation.

Fix

One line (fix.diff): vput(vp); before return (EEXIST);. Applies clean; not rebuilt in-guest (DoS class β€” fix-build budget spent on DF-3062's mandatory memcorrupt cycle). Companion hardening: check the inode hash before consuming the bitmap bit so the allocation itself is rolled back.

impact=dos; confidence=certain; attempts=1.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

one-line vput(vp) fix authored, applies cleanly; not rebuilt in-guest (budget spent on mandatory DF-3062 fix cycle)

fix.diff
↓ fix.diffper-fix-DF-3064

Confirmed kernel references

Detail

Exploit chain

crafted image (root-inode bit cleared in inode bitmap, nifree=1, ficount=1) -> RW mount -> touch -> ext2_valloc -> ext2_nodealloccg returns live ino 2 -> ext2_ihashget vget -> return EEXIST leaking lock+vref -> D-state wedge of mount + umount

Evidence (decisive lines)

['run.log (touch EEXIST; D-state ps output for ls/umount)', "console.txt ('ext2_valloc: vp 0xfffff80118106c00 exists for inode 2')"]

PoC changes

none; single decisive run

Verified recommended fix

vput(vp) before return (EEXIST) in ext2_valloc; companion: verify allocated ino against the inode hash BEFORE consuming the bitmap bit

Verdict

ext2_valloc's inode-collision path (ext2_alloc.c:423-426) returns EEXIST without vput() on the vnode returned by ext2_ihashget() (vget: vref + LK_EXCLUSIVE, recursive vs the create path's parent lock). A crafted bitmap marking the live root inode free triggers it on the first create: touch fails EEXIST with console 'ext2_valloc: vp … exists for inode 2'; afterwards ls and umount block forever in uninterruptible D state (PIDs 982/1001/1022 observed) and each trigger permanently leaks an allocated inode. Mount becomes un-unmountable until reboot.