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