ext4_evict_ea_inode() calls mb_cache_entry_wait_unused() to wait for the EA inode's mb_cache entry refcount to drop before deleting it. This can deadlock against ext4_xattr_inode_cache_find() which holds an mb_cache entry reference while calling ext4_iget(): Thread A (evicting EA inode X): ext4_evict_ea_inode -> mb_cache_entry_wait_unused [waits for refcnt] Thread B (setxattr looking for dedup): ext4_xattr_inode_cache_find -> mb_cache_entry_find [holds refcnt] -> ext4_iget(X) -> __wait_on_freeing_inode [waits on I_FREEING] Thread A's eviction sets I_FREEING and then waits for the mb_cache refcount to drop. Thread B holds that refcount and waits for I_FREEING to clear -- circular dependency. Fix by making ext4_evict_ea_inode() non-blocking: try to delete the entry once, and if it's currently in use, clear MBE_REUSABLE_B and release it. Clearing the reusable bit prevents future cache lookups from finding this entry (mb_cache_entry_find skips non-reusable entries), avoiding stale hits that could trigger spurious filesystem errors when the inode number is later reused for a non-EA inode. Fixes: 458aee4a6e5b ("ext4: remove EA inode entry from mbcache on inode eviction") Reported-by: syzbot+fd5533bcd0f7343bb8ca@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=fd5533bcd0f7343bb8ca Signed-off-by: Yun Zhou --- v2: - Clear MBE_REUSABLE_B on the in-use entry instead of just leaving it stale. This prevents future cache lookups from finding the entry, avoiding spurious ext4_error_inode() if the inode number is later reallocated to a non-EA inode. [1] [1] https://sashiko.dev/#/patchset/20260629052732.1442618-1-yun.zhou@windriver.com?part=1 fs/ext4/xattr.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c index 85ad2561474b..3588fe933d5e 100644 --- a/fs/ext4/xattr.c +++ b/fs/ext4/xattr.c @@ -471,10 +471,17 @@ void ext4_evict_ea_inode(struct inode *inode) if (!EA_INODE_CACHE(inode)) return; - /* Wait for entry to get unused so that we can remove it */ - while ((oe = mb_cache_entry_delete_or_get(EA_INODE_CACHE(inode), - ext4_xattr_inode_get_hash(inode), inode->i_ino))) { - mb_cache_entry_wait_unused(oe); + /* + * Try to delete the cache entry. If it's currently in use by + * another thread (e.g. ext4_xattr_inode_cache_find), mark it + * non-reusable so future lookups won't find it. Waiting here + * would deadlock if the other thread's iget is blocked on this + * inode's I_FREEING. + */ + oe = mb_cache_entry_delete_or_get(EA_INODE_CACHE(inode), + ext4_xattr_inode_get_hash(inode), inode->i_ino); + if (oe) { + clear_bit(MBE_REUSABLE_B, &oe->e_flags); mb_cache_entry_put(EA_INODE_CACHE(inode), oe); } } -- 2.43.0