[PATCH v8 0/4] ext4: fix xattr iput deadlock with s_writepages_rwsem

From: Yun Zhou

Date: Fri Jun 19 2026 - 21:41:18 EST


This series fixes a circular lock dependency reported by syzbot:

s_writepages_rwsem --> jbd2_handle --> xattr_sem --> s_writepages_rwsem

The deadlock occurs when iput() on an EA inode triggers write_inode_now()
while xattr_sem and a jbd2 handle are held. The triggering path is
during mount-time orphan cleanup (!SB_ACTIVE) where iput_final() calls
write_inode_now() synchronously.

Patch 1 blocks the deadlock by skipping extra isize expansion when
!SB_ACTIVE -- this prevents the xattr manipulation path from being
entered during mount.

Patch 2 is a belt-and-suspenders semantic improvement: an inode under
eviction never needs extra isize expansion.

Patches 3-4 are a structural improvement using a per-sb delayed workqueue:

Patch 3 introduces ext4_put_ea_inode(), which does direct iput() when
SB_ACTIVE (zero overhead) and defers to a delayed worker (1 jiffie)
when !SB_ACTIVE. The delay allows multiple EA inodes to accumulate
before the worker runs, reducing context switches. It also converts
the first call site (ext4_xattr_block_set release path).

Patch 4 converts all remaining EA inode iput() calls that execute
under xattr_sem or a jbd2 handle. Direct iput() in pure lookup paths
(ext4_xattr_inode_get, ext4_xattr_inode_cache_find, tmp_inode) is
left unchanged since these do not hold locks.

Link: https://syzkaller.appspot.com/bug?extid=5d19358d7eb30ffb0cc5

v8:
- Use delayed_work with 1 jiffie delay instead of immediate work,
allowing EA inodes to batch before processing (per reviewer suggestion).
- Move flush_delayed_work() in ext4_put_super() before ext4_quotas_off()
to ensure deferred iputs complete while quota is still active.
- Convert ext4_xattr_inode_inc_ref_all() main loop iput calls and
ext4_xattr_inode_create() error path to ext4_put_ea_inode() for
completeness -- eliminates all direct iput of EA inodes under locks.
- Convert ext4_xattr_inode_dec_ref_all() ENOMEM fallback iput which
is reachable during !SB_ACTIVE via ext4_evict_inode ->
ext4_xattr_delete_inode -> ext4_xattr_release_block.
- Add flush_delayed_work() in __ext4_fill_super() failed_mount_wq
error path to prevent use-after-free if mount fails.

v7:
- Replaced the deferred-iput array threading approach (v4-v6) with a
simpler per-sb workqueue + lock-free llist design. No function
signature changes needed. ext4_put_ea_inode() does direct iput when
SB_ACTIVE (zero overhead in normal operation) and defers to the
workqueue only during mount (!SB_ACTIVE).
- Converted the iput in ext4_xattr_delete_inode()'s quota accounting
loop to ext4_put_ea_inode() to eliminate a lockdep-reportable lock
ordering violation (jbd2_handle -> iput -> s_writepages_rwsem).
- Moved flush_work() before the if (sbi->s_journal) check in
ext4_put_super() to cover nojournal mode.
- Split patch 3 into two for easier review: infrastructure + first
conversion, then remaining mechanical conversions.

v6:
- ext4_inline_data_truncate(): use local ea_inode_array instead of
passing NULL, freed after ext4_journal_stop().

v5:
- Split into 3 patches for easier review.
- Add explicit !SB_ACTIVE early-return in ext4_try_to_expand_extra_isize()
to block ALL mount-time paths.

v4:
- Comprehensive rewrite: thread ea_inode_array through all xattr
functions, use __GFP_NOFAIL, set EXT4_STATE_NO_EXPAND in evict.

v3:
- Make ext4_xattr_set_handle() take ea_inode_array output parameter.

v2:
- Defer iput() in ext4_xattr_block_set() via ea_inode_array.

v1:
- Set EXT4_STATE_NO_EXPAND in ext4_evict_inode().

Yun Zhou (4):
ext4: skip extra isize expansion during mount to prevent deadlock
ext4: set EXT4_STATE_NO_EXPAND in ext4_evict_inode
ext4: introduce ext4_put_ea_inode() for safe deferred iput
ext4: convert remaining EA inode iput() calls to ext4_put_ea_inode()

fs/ext4/ext4.h | 5 +++
fs/ext4/inode.c | 11 +++++
fs/ext4/super.c | 6 +++
fs/ext4/xattr.c | 114 ++++++++++++++++++++++++++++++++++++++++++------
fs/ext4/xattr.h | 2 +
5 files changed, 125 insertions(+), 13 deletions(-)

--
2.43.0