]> git.hungrycats.org Git - linux/commit
btrfs: reflink: never block on space reservations while the locked range has delalloc
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Sun, 16 Aug 2026 07:31:14 +0000 (03:31 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 18 Sep 2026 21:36:16 +0000 (17:36 -0400)
commit18304b687ebe66d820c8ed0cc331b81c7816f879
tree3ac767e8cb50dad48100c169d6e9dfebffb524f1
parentbba771b2e5e39200a8f66e719fe86cb2dab06b86
btrfs: reflink: never block on space reservations while the locked range has delalloc

The previous commit made clone_copy_inline_extent() take its transaction
handle before dirtying a folio in the locked destination range, closing a
flushoncommit deadlock for the single-extent inline clone.  Two gaps
remained, and one of them was captured live on a test box within hours:

1) A source file can have an inline extent at offset 0 followed by more
   items (e.g. created small, then extended by an append).  After the
   inline extent's data is copied into a folio - leaving delalloc inside
   the locked destination range - the clone loop continues to the next
   item and btrfs_replace_file_extents() starts more transactions under
   the lock.  The captured deadlock (drgn against a live hung kernel):

     dedupe:    btrfs_replace_file_extents() at [4096,40959], blocked in
                __reserve_bytes() on a metadata reservation ticket, while
                holding the dst range lock with extent_state [0,4095] =
                EXTENT_LOCKED | EXTENT_DELALLOC (dirtied by the inline
                copy of the source's first item)
     reclaim:   flush_space() -> btrfs_commit_current_transaction() ->
                btrfs_start_delalloc_flush() -> try_to_writeback_inodes_sb()
     flusher:   find_lock_delalloc_range() on that inode's [0,4095],
                blocked on the dedupe's extent lock

   Everything else on the filesystem then queues behind the starved
   ticket.  Note the blocking point here is a reservation ticket, not
   TRANS_STATE_COMMIT_START: ticket servicing commits the transaction,
   so with flushoncommit ANY blocking reservation made while the locked
   range has delalloc can deadlock.

   Fix: once the inline copy's inode update is committed, unlock the
   dirtied block before the clone continues.  Every byte still locked is
   clean, so the commit-time flusher never needs our lock, and i_rwsem
   (held for the whole remap) keeps writers away from the unlocked block
   until the clone finishes.

2) Delalloc may already exist inside the range when it is first locked:
   the pre-lock flush waits for ordered extents, but compressed writeback
   is asynchronous and may not have created them yet.  Check for
   EXTENT_DELALLOC after locking; if present, unlock, flush and retry,
   giving up with -EAGAIN after a few attempts.

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