All of lore.kernel.org
 help / color / mirror / Atom feed
From: Jeff Layton <jlayton@kernel.org>
To: Qu Wenruo <quwenruo.btrfs@gmx.com>, Qu Wenruo <wqu@suse.com>,
	 linux-btrfs@vger.kernel.org
Subject: Re: [PATCH v2] btrfs: properly cleanup replace_task when the replace failed to start
Date: Thu, 20 Aug 2026 07:28:32 -0400	[thread overview]
Message-ID: <db623f9dde2aa2979e39e84c457ee95e51a8a63a.camel@kernel.org> (raw)
In-Reply-To: <77d934f5-0dd0-4607-84f9-a95ddf052cf1@gmx.com>

On Thu, 2026-08-20 at 07:44 +0930, Qu Wenruo wrote:
> 
> 在 2026/8/19 21:58, Jeff Layton 写道:
> > Claude had some more comments on this one:
> > 
> > This moves the dev_replace->replace_task assignment in
> > btrfs_dev_replace_start() below the replace_state switch, clears it when
> > btrfs_start_transaction() fails there, and clears it at the top of
> > btrfs_dev_replace_finishing() so every exit from that function drops it.
> > 
> > > In the function btrfs_dev_replace_start(), we have several error paths
> > > that assigns replace_task without reverting it back to NULL.
> > > 
> > > There are two involved error paths:
> > > 
> > > - There is already a running dev-replace
> > >    Then replace_task is over-written to the current task.
> > >    This is the one with long running effect.
> > > 
> > > - The btrfs_start_transaction() call failed
> > >    This is much harder to hit though.
> > 
> > [ ... ]
> > 
> > > Thankfully this bug is very hard to hit.
> > > 
> > > As dev-replace is an exclusive operation, thus if there is already
> > > a running replace, a new one will be rejected early without reaching
> > > btrfs_dev_replace_start().
> > > 
> > > The only remaining case is a suspended replace, which is much harder to
> > > hit, e.g. requiring async dev-replace conflicting with another exclusive
> > > operation, then a new replace is started.
> > 
> > Is "very hard to hit" still accurate now that btrfs_dev_replace_finishing()
> > is fixed too?
> 
> That "very hard to hit" was for the old code before the fix, but indeed 
> Claude found an extra path that cancel can always leave that stale 
> replace_task.
> 
> 
> > 
> > A plain "btrfs replace cancel" leaves replace_task set every time.
> > btrfs_dev_replace_cancel() calls btrfs_scrub_cancel() while replace_state is
> > BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED, so btrfs_scrub_dev() returns
> > -ECANCELED to btrfs_dev_replace_start(), which passes it straight into
> > btrfs_dev_replace_finishing():
> > 
> > fs/btrfs/dev-replace.c:btrfs_dev_replace_start() {
> > 	...
> > 	ret = btrfs_scrub_dev(fs_info, src_device->devid, 0, ...);
> > 
> > 	ret = btrfs_dev_replace_finishing(fs_info, ret);
> > 	...
> > }
> > 
> > fs/btrfs/dev-replace.c:btrfs_dev_replace_finishing() {
> > 	...
> > 	} else {
> > 		if (scrub_ret != -ECANCELED)
> > 			btrfs_err(fs_info, ...);
> > error:
> > 		up_write(&dev_replace->rwsem);
> > 		...
> > 		return scrub_ret;
> > 	}
> > 	...
> > }
> > 
> > That exit never reaches the replace_task = NULL at the end of the function,
> > so the ioctl returns to userspace with replace_task still pointing at the
> > task that ran it.  Any scrub failure, not only -ECANCELED, lands there as
> > well.
> > 
> > The subject line says "when the replace failed to start", which does not
> > cover the cancel or scrub-error case either.  Could the subject and the
> > changelog describe those paths too?
> > 
> > > diff --git a/fs/btrfs/dev-replace.c b/fs/btrfs/dev-replace.c
> > > index 72cba7fed942..c5e67524b417 100644
> > > --- a/fs/btrfs/dev-replace.c
> > > +++ b/fs/btrfs/dev-replace.c
> > 
> > [ ... ]
> > 
> > > @@ -874,18 +875,20 @@ static int btrfs_dev_replace_finishing(struct btrfs_fs_info *fs_info,
> > >   	/* don't allow cancel or unmount to disturb the finishing procedure */
> > >   	mutex_lock(&dev_replace->lock_finishing_cancel_unmount);
> > > 
> > > -	down_read(&dev_replace->rwsem);
> > > +	down_write(&dev_replace->rwsem);
> > > +	dev_replace->replace_task = NULL;
> > > +
> > >   	/* was the operation canceled, or is it finished? */
> > >   	if (dev_replace->replace_state !=
> > >   	    BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED) {
> > > -		up_read(&dev_replace->rwsem);
> > > +		up_write(&dev_replace->rwsem);
> > >   		mutex_unlock(&dev_replace->lock_finishing_cancel_unmount);
> > >   		return 0;
> > >   	}
> > > 
> > >   	tgt_device = dev_replace->tgtdev;
> > >   	src_device = dev_replace->srcdev;
> > > -	up_read(&dev_replace->rwsem);
> > > +	up_write(&dev_replace->rwsem);
> > 
> > Is clearing replace_task this early intentional?
> 
> Yes.
> 
> The real work of dev-replace has all finished.
> > 
> > At this point replace_state is still BTRFS_IOCTL_DEV_REPLACE_STATE_STARTED,
> > and btrfs_start_delalloc_roots() plus the btrfs_start_transaction() /
> > btrfs_commit_transaction() loop are still ahead.  For all of that window the
> > replace task goes back to taking the rwsem in btrfs_map_block():
> > 
> > fs/btrfs/volumes.c:btrfs_map_block() {
> > 	...
> > 	if (dev_replace->replace_task != current)
> > 		down_read(&dev_replace->rwsem);
> > 
> > 	dev_replace_is_ongoing = btrfs_dev_replace_is_ongoing(dev_replace);
> > 	...
> > }
> > 
> > which is the exemption 8cca35cb29f8 ("btrfs: don't take dev_replace rwsem on
> > task already holding it") added.
> > 
> > The down_write() further down already covers both the success exit and the
> > error: exit, so clearing replace_task next to the replace_state update there,
> > plus the three early returns, would leave the ongoing window unchanged.
> 
> Alright, I guess the model you're using is digging deeper than opus.
> 

Actually, this was Opus 5. I find that there is a fair amount of
randomness with these LLMs. Sometimes it just gets lucky!

> It looks like we should not reset replace_task until the replace_state 
> is also updated.
> 
> Or during the transaction commit, it will lead to the same problem.
> 
> We rely on btrfs_dev_replace_is_ongoing() to return false to release 
> rwsem early and avoid the deadlock.
> 
> So the early reset leaves a window we can deadlock again.
> 
> Will send an update to this patch.
> 

Sounds good.

> 
> > 
> > The changelog says only:
> > 
> > >    - Reset replace_task to NULL for all paths of
> > >      btrfs_dev_replace_finishing()
> > 
> > Could it also mention that the read lock at the head of the function becomes
> > a write lock?
> > 
> > 
> 

-- 
Jeff Layton <jlayton@kernel.org>

      reply	other threads:[~2026-08-20 11:28 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-12 23:14 [PATCH v2] btrfs: properly cleanup replace_task when the replace failed to start Qu Wenruo
2026-08-14 10:16 ` Johannes Thumshirn
2026-08-19 12:28 ` Jeff Layton
2026-08-19 22:14   ` Qu Wenruo
2026-08-20 11:28     ` Jeff Layton [this message]

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=db623f9dde2aa2979e39e84c457ee95e51a8a63a.camel@kernel.org \
    --to=jlayton@kernel.org \
    --cc=linux-btrfs@vger.kernel.org \
    --cc=quwenruo.btrfs@gmx.com \
    --cc=wqu@suse.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.