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)
PoC verification
Evidence pack
findings/poc/DF-3064 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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/e4andumount /mnt/e4wedge in uninterruptible D state (ps: PIDs 982, 1001ls, 1022umount;timeout 15could 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_testableone-line vput(vp) fix authored, applies cleanly; not rebuilt in-guest (budget spent on mandatory DF-3062 fix cycle)
fix.diff
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.
No comments yet.