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>
prev parent 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox