[PATCH] ocfs2: fix deadlock in inline-data truncate transactions

ZhengYuan Huang <[email protected]> Mon, 3 Aug 2026 11:23:46 +0800
Newsgroups dev.linux.lists.ocfs2-devel,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
[BUG]
Shrinking an inline-data file can trigger a real ABBA deadlock cycle and
lockdep warning:

WARNING: possible circular locking dependency detected
------------------------------------------------------
syz.0.6/353 is trying to acquire lock:
ffff888015936610 (sb_internal#2){.+.+}-{0:0}, at: ocfs2_truncate_inline+0x389/0xbf0 fs/ocfs2/alloc.c:7436

but task is already holding lock:
ffff888019156a20 (&ocfs2_file_ip_alloc_sem_key){++++}-{4:4}, at: ocfs2_truncate_file+0x23a/0x13c0 fs/ocfs2/file.c:471

which lock already depends on the new lock.

the existing dependency chain (in reverse order) is:

-> #3 (&ocfs2_file_ip_alloc_sem_key){++++}-{4:4}:
       down_write+0x8f/0x200 kernel/locking/rwsem.c:1590
       ocfs2_xattr_ibody_set+0x119/0xc50 fs/ocfs2/xattr.c:2783
       __ocfs2_xattr_set_handle+0xdb/0xdb0 fs/ocfs2/xattr.c:3322
       ocfs2_xattr_set+0x1447/0x2610 fs/ocfs2/xattr.c:3650
       ocfs2_xattr_security_set+0x37/0x50 fs/ocfs2/xattr.c:7241
       __vfs_setxattr+0x14f/0x1c0 fs/xattr.c:200
       __vfs_setxattr_noperm+0x10b/0x5c0 fs/xattr.c:234
       __vfs_setxattr_locked+0x172/0x240 fs/xattr.c:295
       vfs_setxattr+0x167/0x390 fs/xattr.c:321
       do_setxattr+0x13c/0x180 fs/xattr.c:636
       filename_setxattr+0x16b/0x1c0 fs/xattr.c:665
       path_setxattrat+0x1d8/0x280 fs/xattr.c:713
       __do_sys_setxattr fs/xattr.c:747 [inline]
       __se_sys_setxattr fs/xattr.c:743 [inline]
       __x64_sys_setxattr+0xcd/0x150 fs/xattr.c:743
       x64_sys_call+0x148c/0x26a0 arch/x86/include/generated/asm/syscalls_64.h:189
       do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
       do_syscall_64+0x93/0xf80 arch/x86/entry/syscall_64.c:94
       entry_SYSCALL_64_after_hwframe+0x76/0x7e

-> #2 (jbd2_handle){++++}-{0:0}:
       start_this_handle+0x5c7/0x13c0 fs/jbd2/transaction.c:444
       jbd2__journal_start+0x397/0x690 fs/jbd2/transaction.c:501
       jbd2_journal_start+0x31/0x50 fs/jbd2/transaction.c:540
       ocfs2_start_trans+0x39b/0x870 fs/ocfs2/journal.c:374
       ocfs2_xattr_set+0x1401/0x2610 fs/ocfs2/xattr.c:3643
       ocfs2_xattr_security_set+0x37/0x50 fs/ocfs2/xattr.c:7241
       __vfs_setxattr+0x14f/0x1c0 fs/xattr.c:200
       __vfs_setxattr_noperm+0x10b/0x5c0 fs/xattr.c:234
       __vfs_setxattr_locked+0x172/0x240 fs/xattr.c:295
       vfs_setxattr+0x167/0x390 fs/xattr.c:321
       do_setxattr+0x13c/0x180 fs/xattr.c:636
       filename_setxattr+0x16b/0x1c0 fs/xattr.c:665
       path_setxattrat+0x1d8/0x280 fs/xattr.c:713
       __do_sys_setxattr fs/xattr.c:747 [inline]
       __se_sys_setxattr fs/xattr.c:743 [inline]
       __x64_sys_setxattr+0xcd/0x150 fs/xattr.c:743
       x64_sys_call+0x148c/0x26a0 arch/x86/include/generated/asm/syscalls_64.h:189
       do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
       do_syscall_64+0x93/0xf80 arch/x86/entry/syscall_64.c:94
       entry_SYSCALL_64_after_hwframe+0x76/0x7e

-> #1 (&journal->j_trans_barrier){.+.+}-{4:4}:
       down_read+0x9c/0x4a0 kernel/locking/rwsem.c:1537
       ocfs2_start_trans+0x390/0x870 fs/ocfs2/journal.c:372
       ocfs2_xattr_set+0x1401/0x2610 fs/ocfs2/xattr.c:3643
       ocfs2_xattr_security_set+0x37/0x50 fs/ocfs2/xattr.c:7241
       __vfs_setxattr+0x14f/0x1c0 fs/xattr.c:200
       __vfs_setxattr_noperm+0x10b/0x5c0 fs/xattr.c:234
       __vfs_setxattr_locked+0x172/0x240 fs/xattr.c:295
       vfs_setxattr+0x167/0x390 fs/xattr.c:321
       do_setxattr+0x13c/0x180 fs/xattr.c:636
       filename_setxattr+0x16b/0x1c0 fs/xattr.c:665
       path_setxattrat+0x1d8/0x280 fs/xattr.c:713
       __do_sys_setxattr fs/xattr.c:747 [inline]
       __se_sys_setxattr fs/xattr.c:743 [inline]
       __x64_sys_setxattr+0xcd/0x150 fs/xattr.c:743
       x64_sys_call+0x148c/0x26a0 arch/x86/include/generated/asm/syscalls_64.h:189
       do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
       do_syscall_64+0x93/0xf80 arch/x86/entry/syscall_64.c:94
       entry_SYSCALL_64_after_hwframe+0x76/0x7e

-> #0 (sb_internal#2){.+.+}-{0:0}:
       check_prev_add kernel/locking/lockdep.c:3165 [inline]
       check_prevs_add kernel/locking/lockdep.c:3284 [inline]
       validate_chain kernel/locking/lockdep.c:3908 [inline]
       __lock_acquire+0x14ae/0x21e0 kernel/locking/lockdep.c:5237
       lock_acquire kernel/locking/lockdep.c:5868 [inline]
       lock_acquire+0x169/0x2f0 kernel/locking/lockdep.c:5825
       percpu_down_read_internal include/linux/percpu-rwsem.h:53 [inline]
       percpu_down_read_freezable include/linux/percpu-rwsem.h:83 [inline]
       __sb_start_write include/linux/fs.h:1916 [inline]
       sb_start_intwrite include/linux/fs.h:2099 [inline]
       ocfs2_start_trans+0x2a8/0x870 fs/ocfs2/journal.c:370
       ocfs2_truncate_inline+0x389/0xbf0 fs/ocfs2/alloc.c:7436
       ocfs2_truncate_file+0x6d3/0x13c0 fs/ocfs2/file.c:488
       ocfs2_setattr+0xa6d/0x1fd0 fs/ocfs2/file.c:1212
       notify_change+0x4b5/0x1030 fs/attr.c:546
       do_truncate+0x1d2/0x230 fs/open.c:68
       vfs_truncate+0x56c/0x670 fs/open.c:118
       do_sys_truncate fs/open.c:141 [inline]
       __do_sys_truncate fs/open.c:153 [inline]
       __se_sys_truncate fs/open.c:151 [inline]
       __x64_sys_truncate+0x165/0x1d0 fs/open.c:151
       x64_sys_call+0xbc8/0x26a0 arch/x86/include/generated/asm/syscalls_64.h:77
       do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
       do_syscall_64+0x93/0xf80 arch/x86/entry/syscall_64.c:94
       entry_SYSCALL_64_after_hwframe+0x76/0x7e

other info that might help us debug this:

Chain exists of:
  sb_internal#2 --> jbd2_handle --> &ocfs2_file_ip_alloc_sem_key

 Possible unsafe locking scenario:

       CPU0                    CPU1
       ----                    ----
  lock(&ocfs2_file_ip_alloc_sem_key);
                               lock(jbd2_handle);
                               lock(&ocfs2_file_ip_alloc_sem_key);
  rlock(sb_internal#2);

 *** DEADLOCK ***

3 locks held by syz.0.6/353:
 #0: ffff888015936420 (sb_writers#12){.+.+}-{0:0}, at: vfs_truncate+0x30c/0x670 fs/open.c:96
 #1: ffff888019156d80 (&sb->s_type->i_mutex_key#18){+.+.}-{4:4}, at: inode_lock_killable include/linux/fs.h:985 [inline]
 #1: ffff888019156d80 (&sb->s_type->i_mutex_key#18){+.+.}-{4:4}, at: do_truncate+0x139/0x230 fs/open.c:63
 #2: ffff888019156a20 (&ocfs2_file_ip_alloc_sem_key){++++}-{4:4}, at: ocfs2_truncate_file+0x23a/0x13c0 fs/ocfs2/file.c:471

Call Trace:
 ...
 lock_acquire+0x169/0x2f0 kernel/locking/lockdep.c:5825
 percpu_down_read_internal include/linux/percpu-rwsem.h:53 [inline]
 percpu_down_read_freezable include/linux/percpu-rwsem.h:83 [inline]
 __sb_start_write include/linux/fs.h:1916 [inline]
 sb_start_intwrite include/linux/fs.h:2099 [inline]
 ocfs2_start_trans+0x2a8/0x870 fs/ocfs2/journal.c:370
 ocfs2_truncate_inline+0x389/0xbf0 fs/ocfs2/alloc.c:7436
 ocfs2_truncate_file+0x6d3/0x13c0 fs/ocfs2/file.c:488
 ocfs2_setattr+0xa6d/0x1fd0 fs/ocfs2/file.c:1212
 notify_change+0x4b5/0x1030 fs/attr.c:546
 do_truncate+0x1d2/0x230 fs/open.c:68
 vfs_truncate+0x56c/0x670 fs/open.c:118
 do_sys_truncate fs/open.c:141 [inline]
 __do_sys_truncate fs/open.c:153 [inline]
 __se_sys_truncate fs/open.c:151 [inline]
 __x64_sys_truncate+0x165/0x1d0 fs/open.c:151
 ...

[CAUSE]
ocfs2_truncate_file() and inline range removal hold ip_alloc_sem when
calling ocfs2_truncate_inline().  The helper starts a transaction, which
adds an ip_alloc_sem-to-transaction dependency opposite to the existing
transaction-to-ip_alloc_sem order in the xattr path.

[FIX]
Make ocfs2_truncate_inline() use a caller-owned transaction handle. Start
the inline update transaction before taking ip_alloc_sem in the truncate
and range-removal callers, and release an unused handle if inline data was
converted to extents while waiting for the semaphore. This keeps the
inline update serialized by ip_alloc_sem while preserving a single
transaction-before-ip_alloc_sem order.

Fixes: 85db90e77806 ("ocfs2/xattr: Merge xattr set transaction.")
Signed-off-by: ZhengYuan Huang <[email protected]>
---
 fs/ocfs2/alloc.c        | 18 ++-------
 fs/ocfs2/alloc.h        |  5 ++-
 fs/ocfs2/file.c         | 83 +++++++++++++++++++++++++++++++++++------
 fs/ocfs2/file.h         |  2 +-
 fs/ocfs2/refcounttree.c |  2 +-
 5 files changed, 81 insertions(+), 29 deletions(-)

diff --git a/fs/ocfs2/alloc.c b/fs/ocfs2/alloc.c
index be09e766ac1f..2f00ab2e2b76 100644
--- a/fs/ocfs2/alloc.c
+++ b/fs/ocfs2/alloc.c
@@ -7412,12 +7412,12 @@ int ocfs2_commit_truncate(struct ocfs2_super *osb,
 /*
  * 'start' is inclusive, 'end' is not.
  */
-int ocfs2_truncate_inline(struct inode *inode, struct buffer_head *di_bh,
-			  unsigned int start, unsigned int end, int trunc)
+int ocfs2_truncate_inline(handle_t *handle, struct inode *inode,
+			  struct buffer_head *di_bh, unsigned int start,
+			  unsigned int end, int trunc)
 {
 	int ret;
 	unsigned int numbytes;
-	handle_t *handle;
 	struct ocfs2_super *osb = OCFS2_SB(inode->i_sb);
 	struct ocfs2_dinode *di = (struct ocfs2_dinode *)di_bh->b_data;
 	struct ocfs2_inline_data *idata = &di->id2.i_data;
@@ -7444,18 +7444,11 @@ int ocfs2_truncate_inline(struct inode *inode, struct buffer_head *di_bh,
 		goto out;
 	}
 
-	handle = ocfs2_start_trans(osb, OCFS2_INODE_UPDATE_CREDITS);
-	if (IS_ERR(handle)) {
-		ret = PTR_ERR(handle);
-		mlog_errno(ret);
-		goto out;
-	}
-
 	ret = ocfs2_journal_access_di(handle, INODE_CACHE(inode), di_bh,
 				      OCFS2_JOURNAL_ACCESS_WRITE);
 	if (ret) {
 		mlog_errno(ret);
-		goto out_commit;
+		goto out;
 	}
 
 	numbytes = end - start;
@@ -7481,9 +7474,6 @@ int ocfs2_truncate_inline(struct inode *inode, struct buffer_head *di_bh,
 	ocfs2_update_inode_fsync_trans(handle, inode, 1);
 	ocfs2_journal_dirty(handle, di_bh);
 
-out_commit:
-	ocfs2_commit_trans(osb, handle);
-
 out:
 	return ret;
 }
diff --git a/fs/ocfs2/alloc.h b/fs/ocfs2/alloc.h
index 1c0c83362904..95e7f1a7a365 100644
--- a/fs/ocfs2/alloc.h
+++ b/fs/ocfs2/alloc.h
@@ -217,8 +217,9 @@ int ocfs2_zero_range_for_truncate(struct inode *inode, handle_t *handle,
 int ocfs2_commit_truncate(struct ocfs2_super *osb,
 			  struct inode *inode,
 			  struct buffer_head *di_bh);
-int ocfs2_truncate_inline(struct inode *inode, struct buffer_head *di_bh,
-			  unsigned int start, unsigned int end, int trunc);
+int ocfs2_truncate_inline(handle_t *handle, struct inode *inode,
+			  struct buffer_head *di_bh, unsigned int start,
+			  unsigned int end, int trunc);
 
 int ocfs2_find_leaf(struct ocfs2_caching_info *ci,
 		    struct ocfs2_extent_list *root_el, u32 cpos,
diff --git a/fs/ocfs2/file.c b/fs/ocfs2/file.c
index d6e977ba6565..78b039b1a67d 100644
--- a/fs/ocfs2/file.c
+++ b/fs/ocfs2/file.c
@@ -441,6 +441,7 @@ int ocfs2_truncate_file(struct inode *inode,
 			       u64 new_i_size)
 {
 	int status = 0;
+	handle_t *handle = NULL;
 	struct ocfs2_dinode *fe = NULL;
 	struct ocfs2_super *osb = OCFS2_SB(inode->i_sb);
 
@@ -474,7 +475,23 @@ int ocfs2_truncate_file(struct inode *inode,
 		goto bail;
 	}
 
+	if ((OCFS2_I(inode)->ip_dyn_features & OCFS2_INLINE_DATA_FL) &&
+	    new_i_size < i_size_read(inode)) {
+		handle = ocfs2_start_trans(osb, OCFS2_INODE_UPDATE_CREDITS);
+		if (IS_ERR(handle)) {
+			status = PTR_ERR(handle);
+			handle = NULL;
+			mlog_errno(status);
+			goto bail;
+		}
+	}
+
 	down_write(&OCFS2_I(inode)->ip_alloc_sem);
+	if (!(OCFS2_I(inode)->ip_dyn_features & OCFS2_INLINE_DATA_FL) &&
+	    handle) {
+		ocfs2_commit_trans(osb, handle);
+		handle = NULL;
+	}
 
 	ocfs2_resv_discard(&osb->osb_la_resmap,
 			   &OCFS2_I(inode)->ip_la_data_resv);
@@ -491,10 +508,19 @@ int ocfs2_truncate_file(struct inode *inode,
 		unmap_mapping_range(inode->i_mapping,
 				    new_i_size + PAGE_SIZE - 1, 0, 1);
 		truncate_inode_pages(inode->i_mapping, new_i_size);
-		status = ocfs2_truncate_inline(inode, di_bh, new_i_size,
-					       i_size_read(inode), 1);
-		if (status)
-			mlog_errno(status);
+		if (new_i_size < i_size_read(inode)) {
+			if (unlikely(!handle)) {
+				status = -EIO;
+				mlog_errno(status);
+				goto bail_unlock_sem;
+			}
+
+			status = ocfs2_truncate_inline(handle, inode, di_bh,
+						       new_i_size,
+						       i_size_read(inode), 1);
+			if (status)
+				mlog_errno(status);
+		}
 
 		goto bail_unlock_sem;
 	}
@@ -521,6 +547,8 @@ int ocfs2_truncate_file(struct inode *inode,
 	/* TODO: orphan dir cleanup here. */
 bail_unlock_sem:
 	up_write(&OCFS2_I(inode)->ip_alloc_sem);
+	if (handle)
+		ocfs2_commit_trans(osb, handle);
 
 bail:
 	if (!status && OCFS2_I(inode)->ip_clusters == 0)
@@ -1772,7 +1800,7 @@ static void ocfs2_calc_trunc_pos(struct inode *inode,
 
 int ocfs2_remove_inode_range(struct inode *inode,
 			     struct buffer_head *di_bh, u64 byte_start,
-			     u64 byte_len)
+			     u64 byte_len, handle_t *inline_handle)
 {
 	int ret = 0, flags = 0, done = 0, i;
 	u32 trunc_start, trunc_len, trunc_end, trunc_cpos, phys_cpos;
@@ -1807,11 +1835,20 @@ int ocfs2_remove_inode_range(struct inode *inode,
 			goto out;
 		}
 
-		ret = ocfs2_truncate_inline(inode, di_bh, byte_start,
-					    byte_start + byte_len, 0);
-		if (ret) {
-			mlog_errno(ret);
-			goto out;
+		if (byte_start < i_size_read(inode)) {
+			if (!inline_handle) {
+				ret = -EINVAL;
+				mlog_errno(ret);
+				goto out;
+			}
+
+			ret = ocfs2_truncate_inline(inline_handle, inode,
+						    di_bh, byte_start,
+						    byte_start + byte_len, 0);
+			if (ret) {
+				mlog_errno(ret);
+				goto out;
+			}
 		}
 		/*
 		 * There's no need to get fancy with the page cache
@@ -1953,6 +1990,7 @@ static int __ocfs2_change_file_space(struct file *file, struct inode *inode,
 	struct ocfs2_super *osb = OCFS2_SB(inode->i_sb);
 	struct buffer_head *di_bh = NULL;
 	handle_t *handle;
+	handle_t *inline_handle = NULL;
 	unsigned long long max_off = inode->i_sb->s_maxbytes;
 
 	if (unlikely(ocfs2_emergency_state(osb)))
@@ -2024,7 +2062,26 @@ static int __ocfs2_change_file_space(struct file *file, struct inode *inode,
 		}
 	}
 
+	if ((cmd == OCFS2_IOC_UNRESVSP || cmd == OCFS2_IOC_UNRESVSP64) &&
+	    (OCFS2_I(inode)->ip_dyn_features & OCFS2_INLINE_DATA_FL) &&
+	    sr->l_start < i_size_read(inode)) {
+		inline_handle = ocfs2_start_trans(osb,
+						  OCFS2_INODE_UPDATE_CREDITS);
+		if (IS_ERR(inline_handle)) {
+			ret = PTR_ERR(inline_handle);
+			inline_handle = NULL;
+			mlog_errno(ret);
+			goto out_inode_unlock;
+		}
+	}
+
 	down_write(&OCFS2_I(inode)->ip_alloc_sem);
+	if (!(OCFS2_I(inode)->ip_dyn_features & OCFS2_INLINE_DATA_FL) &&
+	    inline_handle) {
+		ocfs2_commit_trans(osb, inline_handle);
+		inline_handle = NULL;
+	}
+
 	switch (cmd) {
 	case OCFS2_IOC_RESVSP:
 	case OCFS2_IOC_RESVSP64:
@@ -2038,7 +2095,7 @@ static int __ocfs2_change_file_space(struct file *file, struct inode *inode,
 	case OCFS2_IOC_UNRESVSP:
 	case OCFS2_IOC_UNRESVSP64:
 		ret = ocfs2_remove_inode_range(inode, di_bh, sr->l_start,
-					       sr->l_len);
+					       sr->l_len, inline_handle);
 		break;
 	default:
 		ret = -EINVAL;
@@ -2053,6 +2110,10 @@ static int __ocfs2_change_file_space(struct file *file, struct inode *inode,
 			i_size_write(inode, size);
 	}
 	up_write(&OCFS2_I(inode)->ip_alloc_sem);
+	if (inline_handle) {
+		ocfs2_commit_trans(osb, inline_handle);
+		inline_handle = NULL;
+	}
 	if (ret) {
 		mlog_errno(ret);
 		goto out_inode_unlock;
diff --git a/fs/ocfs2/file.h b/fs/ocfs2/file.h
index 41e65e45a9f3..de6d56aadd2a 100644
--- a/fs/ocfs2/file.h
+++ b/fs/ocfs2/file.h
@@ -70,5 +70,5 @@ int ocfs2_check_range_for_refcount(struct inode *inode, loff_t pos,
 				   size_t count);
 int ocfs2_remove_inode_range(struct inode *inode,
 			     struct buffer_head *di_bh, u64 byte_start,
-			     u64 byte_len);
+			     u64 byte_len, handle_t *inline_handle);
 #endif /* OCFS2_FILE_H */
diff --git a/fs/ocfs2/refcounttree.c b/fs/ocfs2/refcounttree.c
index d9f22b4a2654..e191a62db91d 100644
--- a/fs/ocfs2/refcounttree.c
+++ b/fs/ocfs2/refcounttree.c
@@ -4560,7 +4560,7 @@ static loff_t ocfs2_reflink_remap_extent(struct inode *s_inode,
 		/* Punch out the dest range. */
 		pstart = ocfs2_clusters_to_bytes(t_inode->i_sb, tpos);
 		plen = ocfs2_clusters_to_bytes(t_inode->i_sb, num_clusters);
-		ret = ocfs2_remove_inode_range(t_inode, t_bh, pstart, plen);
+		ret = ocfs2_remove_inode_range(t_inode, t_bh, pstart, plen, NULL);
 		if (ret) {
 			mlog_errno(ret);
 			goto out;
-- 
2.43.0