Re: [PATCH v2] btrfs: properly cleanup replace_task when the replace failed to start

Jeff Layton <[email protected]>
Newsgroups org.kernel.vger.linux-btrfs
Message-ID <[email protected]>
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 <[email protected]>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.