From: Deepanshu Kartikey <kartikey406@gmail.com>
To: syzbot+63056bf627663701bbbf@syzkaller.appspotmail.com
Cc: Deepanshu Kartikey <Kartikey406@gmail.com>,
stable@vger.kernel.org,
Deepanshu Kartikey <kartikey406@gmail.com>
Subject: [PATCH] btrfs: fix hung task when cloning inline extent races with writeback
Date: Thu, 26 Mar 2026 07:19:53 +0530 [thread overview]
Message-ID: <20260326014953.16727-1-kartikey406@gmail.com> (raw)
From: Deepanshu Kartikey <Kartikey406@gmail.com>
#syz test: git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git master
When cloning an inline extent, clone_copy_inline_extent() calls
copy_inline_to_page() which locks an extent range in the destination
inode's io_tree, dirties a page with the inline data, and sets
BTRFS_INODE_NO_DELALLOC_FLUSH on the inode. At this point i_size is
still 0 since clone_finish_inode_update() has not been called yet.
Then clone_copy_inline_extent() calls start_transaction() which may
block waiting for the current transaction to commit. While blocked,
the transaction commit calls btrfs_start_delalloc_flush() which calls
try_to_writeback_inodes_sb(), queuing a kworker to flush the clone
destination inode.
The kworker calls btrfs_writepages() -> extent_writepage() and since
i_size is still 0, the dirty page appears to be beyond EOF. This
causes extent_writepage() to call folio_invalidate() ->
btrfs_invalidate_folio() -> btrfs_lock_extent() which blocks forever
because the clone operation holds that lock, creating a circular
deadlock:
clone -> waits for transaction commit to finish
commit -> waits for kworker writeback to finish
kworker -> waits for extent lock held by clone
Additionally any periodic background writeback that races with the
clone operation before i_size is updated will also block on the same
extent lock causing a hung task warning.
The flag BTRFS_INODE_NO_DELALLOC_FLUSH was introduced by commit
3d45f221ce62 to prevent this deadlock but was only checked inside
start_delalloc_inodes(), which is only reached through the btrfs
metadata reclaim path. The transaction commit path goes through
try_to_writeback_inodes_sb() which is a VFS function that bypasses
start_delalloc_inodes() entirely, so the flag was never checked there.
Fix this by checking BTRFS_INODE_NO_DELALLOC_FLUSH at the top of
btrfs_writepages() and returning early if set. This catches all
writeback paths since every writeback on a btrfs inode eventually
calls btrfs_writepages(). The inode will be safely written after the
clone operation finishes and clears the flag, at which point all
locks are released and i_size is properly updated.
Also change the local variable type from 'struct inode *' to
'struct btrfs_inode *' to avoid the double BTRFS_I() conversion.
Fixes: 3d45f221ce62 ("btrfs: fix deadlock when cloning inline extent and low on free metadata space")
CC: stable@vger.kernel.org
Reported-by: syzbot+63056bf627663701bbbf@syzkaller.appspotmail.com
Signed-off-by: Deepanshu Kartikey <kartikey406@gmail.com>
---
fs/btrfs/extent_io.c | 39 ++++++++++++++++++++++++++++++++++++---
1 file changed, 36 insertions(+), 3 deletions(-)
diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
index 5f97a3d2a8d7..f7df7c0c8955 100644
--- a/fs/btrfs/extent_io.c
+++ b/fs/btrfs/extent_io.c
@@ -2698,21 +2698,54 @@ void extent_write_locked_range(struct inode *inode, const struct folio *locked_f
int btrfs_writepages(struct address_space *mapping, struct writeback_control *wbc)
{
- struct inode *inode = mapping->host;
+ struct btrfs_inode *inode = BTRFS_I(mapping->host);
int ret = 0;
struct btrfs_bio_ctrl bio_ctrl = {
.wbc = wbc,
.opf = REQ_OP_WRITE | wbc_to_write_flags(wbc),
};
+ /*
+ * If this inode is being used for a clone/reflink operation that
+ * copied an inline extent into a page of the destination inode, skip
+ * writeback to avoid a deadlock or a long blocked task.
+ *
+ * The clone operation holds the extent range locked in the inode's
+ * io_tree for its entire duration. Any writeback attempt on this
+ * inode will block trying to lock that same extent range inside
+ * writepage_delalloc() or btrfs_invalidate_folio(), causing a
+ * hung task.
+ *
+ * When writeback is triggered from the transaction commit path via
+ * btrfs_start_delalloc_flush() -> try_to_writeback_inodes_sb(),
+ * this becomes a true circular deadlock:
+ *
+ * clone -> waits for transaction commit to finish
+ * commit -> waits for kworker writeback to finish
+ * kworker -> waits for extent lock held by clone
+ *
+ * The flag BTRFS_INODE_NO_DELALLOC_FLUSH was already checked in
+ * start_delalloc_inodes() but only for the btrfs metadata reclaim
+ * path. The transaction commit path goes through
+ * try_to_writeback_inodes_sb() which bypasses that check entirely
+ * and calls btrfs_writepages() directly.
+ *
+ * By checking the flag here we catch all writeback paths. The inode
+ * will be safely written after the clone operation finishes and
+ * clears BTRFS_INODE_NO_DELALLOC_FLUSH, at which point all locks
+ * are released and writeback can proceed normally.
+ */
+ if (test_bit(BTRFS_INODE_NO_DELALLOC_FLUSH, &inode->runtime_flags))
+ return 0;
+
/*
* Allow only a single thread to do the reloc work in zoned mode to
* protect the write pointer updates.
*/
- btrfs_zoned_data_reloc_lock(BTRFS_I(inode));
+ btrfs_zoned_data_reloc_lock(inode);
ret = extent_write_cache_pages(mapping, &bio_ctrl);
submit_write_bio(&bio_ctrl, ret);
- btrfs_zoned_data_reloc_unlock(BTRFS_I(inode));
+ btrfs_zoned_data_reloc_unlock(inode);
return ret;
}
--
2.43.0
next reply other threads:[~2026-03-26 1:50 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-26 1:49 Deepanshu Kartikey [this message]
2026-03-26 2:46 ` [syzbot] [btrfs?] INFO: task hung in btrfs_invalidate_folio (3) syzbot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260326014953.16727-1-kartikey406@gmail.com \
--to=kartikey406@gmail.com \
--cc=stable@vger.kernel.org \
--cc=syzbot+63056bf627663701bbbf@syzkaller.appspotmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox