From: Jeff Layton <jlayton@kernel.org>
To: 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: Wed, 19 Aug 2026 08:28:00 -0400 [thread overview]
Message-ID: <fdb9d5bb8a87126399f328717cd358351ed0f4cd.camel@kernel.org> (raw)
In-Reply-To: <8e4f535ac093c7085ab0641114e2f082788585f5.1786576348.git.wqu@suse.com>
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?
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?
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.
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>
next prev parent reply other threads:[~2026-08-19 12:28 UTC|newest]
Thread overview: 4+ 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 [this message]
2026-08-19 22:14 ` Qu Wenruo
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=fdb9d5bb8a87126399f328717cd358351ed0f4cd.camel@kernel.org \
--to=jlayton@kernel.org \
--cc=linux-btrfs@vger.kernel.org \
--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