]> git.hungrycats.org Git - linux/commit
btrfs: fix flushoncommit deadlock when cloning an inline extent inside i_size
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Sun, 16 Aug 2026 02:21:10 +0000 (22:21 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 18 Sep 2026 21:36:16 +0000 (17:36 -0400)
commitbba771b2e5e39200a8f66e719fe86cb2dab06b86
treeb5a2f09c8e8d89318f69d39bd9ab7c38aa310fdd
parent1397021ba66f3c04ce85760af47d0cd7f52f7d4b
btrfs: fix flushoncommit deadlock when cloning an inline extent inside i_size

Commit b48c980b6a7e ("btrfs: fix deadlock between reflink and transaction
commit when using flushoncommit") fixed a deadlock between a transaction
commit and a reflink that copied an inline extent's data to a folio beyond
the destination's i_size: commit-time delalloc flushing tries to invalidate
the beyond-EOF folio and blocks on the extent range lock held by the
reflink task, which itself waits for the commit when starting a transaction
to update the inode item.

The same cycle still triggers when the destination offset is inside i_size
(e.g. deduplicating a small file into a larger one, where the destination
inode's i_size exceeds the inline extent's length):
clone_copy_inline_extent() copies the inline data to a folio - dirtying it
inside the range that stays locked in the io tree for the whole clone - and
only then starts a transaction. If a commit has reached
TRANS_STATE_COMMIT_START by that point, the reflink task blocks in
wait_current_trans() while holding the range lock. With flushoncommit the
committing task flushes dirty inodes through the generic writeback path,
which knows nothing about BTRFS_INODE_NO_DELALLOC_FLUSH, and the flusher
blocks forever in find_lock_delalloc_range() on the locked range - writing
the folio back rather than invalidating it, since it is inside i_size:

  reflink:   holds the dst range lock, blocked in wait_current_trans()
  committer: btrfs_commit_transaction() -> btrfs_start_delalloc_flush()
             -> try_to_writeback_inodes_sb(), waiting for the flusher
  flusher:   writepage_delalloc() -> find_lock_delalloc_range(), blocked
             on the dst range lock

Reproduced on a plain single-device filesystem mounted with
-o flushoncommit,compress=zstd in under a minute by running concurrently:
truncating rewrites of small compressible files with fsync, FIDEDUPERANGE
over the same files (inline source extents are the essential ingredient)
and a "btrfs filesystem sync" loop.

Fix it by reserving space and starting the transaction before copying the
inline data into the folio. While the locked range is still clean the
flusher has no reason to touch it, so blocking on the transaction start is
safe, and once the handle is held the reflink task can no longer block
waiting for a commit. The space reservation moves out of
copy_inline_to_page() so it stays ordered before the transaction start,
since reserving space may itself flush and wait for a commit. The i_size
update from commit b48c980b6a7e is kept right after the copy so a folio
dirtied beyond EOF is still written back instead of being discarded by
folio invalidation.

Fixes: 05a5a7621ce6 ("Btrfs: implement full reflink support for inline extents")
Assisted-by: Claude:claude-fable-5
fs/btrfs/reflink.c