]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
btrfs: scrub: cancel the run if the process or fs is being frozen
authorQu Wenruo <wqu@suse.com>
Sun, 19 Oct 2025 00:45:27 +0000 (11:15 +1030)
committerDavid Sterba <dsterba@suse.com>
Mon, 24 Nov 2025 21:24:52 +0000 (22:24 +0100)
It's a known bug that btrfs scrub/dev-replace can prevent the system
from suspending.

There are at least two factors involved:

- Holding super_block::s_writers for the whole scrub/dev-replace duration
  We hold that percpu rw semaphore through mnt_want_write_file() for the
  whole scrub/dev-replace duration.

  That will prevent the fs being frozen, which can be initiated by
  either the user (e.g. fsfreeze) or power management suspend/hibernate.

- Stuck in the kernel space for a long time
  During suspend all user processes (and some kernel threads) will
  be frozen.
  But if a user space progress has fallen into kernel (scrub ioctl) and
  do not return for a long time, it will make process freezing time out.

  Unfortunately scrub/dev-replace is a long running ioctl, and it will
  prevent the btrfs process from returning to the user space, thus make PM
  suspend/hibernate time out.

Address them in one go:

- Introduce a new helper should_cancel_scrub()
  Which includes the existing cancel request and new fs/process freezing
  checks.

  Here we have to check both fs and process freezing for PM
  suspend/hibernate.

  PM can be configured to freeze filesystems before processes.
  (The current default is not to freeze filesystems, but planned to
  freeze the filesystems as the new default.)

  Checking only fs freezing will fail PM without fs freezing, as the
  process freezing will time out.

  Checking only process freezing will fail PM with fs freezing since the
  fs freezing happens before process freezing.

  And the return value will indicate the reason, -ECANCLED for the
  explicitly canceled runs, and -EINTR for fs freeze or PM reasons.

- Cancel the run if should_cancel_scrub() is true
  Unfortunately canceling is the only feasible solution here, pausing is
  not possible as we will still stay in the kernel space thus will still
  prevent the process from being frozen.

This will cause a user impacting behavior change:

  Dev-replace can be interrupted by PM, and there is no way to resume
  but start from the beginning again.

This means dev-replace may fail on newer kernels, and end users will
need extra steps like using systemd-inhibit to prevent
suspend/hibernate, to get back the old uninterrupted behavior.

This behavior change will need extra documentation updates and
communication with projects involving scrub/dev-replace including
btrfs-progs.

Reviewed-by: Filipe Manana <fdmanana@suse.com>
Link: https://lore.kernel.org/linux-btrfs/d93b2a2d-6ad9-4c49-809f-11d769a6f30a@app.fastmail.com/
Reported-by: Chris Murphy <lists@colorremedies.com>
Signed-off-by: Qu Wenruo <wqu@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
fs/btrfs/scrub.c

index 00e42a7f52afca0d8fe9044d83030c89d2432336..9738caa355c4247e748b8d1cc813c367b74e7fe0 100644 (file)
@@ -2069,6 +2069,44 @@ static int queue_scrub_stripe(struct scrub_ctx *sctx, struct btrfs_block_group *
        return 0;
 }
 
+/*
+ * Return 0 if we should not cancel the scrub.
+ * Return <0 if we need to cancel the scrub, returned value will
+ * indicate the reason:
+ * - -ECANCELED - Being explicitly canceled through ioctl.
+ * - -EINTR     - Being interrupted by fs/process freezing.
+ */
+static int should_cancel_scrub(const struct scrub_ctx *sctx)
+{
+       struct btrfs_fs_info *fs_info = sctx->fs_info;
+
+       if (atomic_read(&fs_info->scrub_cancel_req) ||
+           atomic_read(&sctx->cancel_req))
+               return -ECANCELED;
+
+       /*
+        * The user (e.g. fsfreeze command) or power management (PM)
+        * suspend/hibernate can freeze the fs.  And PM suspend/hibernate will
+        * also freeze all user processes.
+        *
+        * A user process can only be frozen when it is in user space, thus we
+        * have to cancel the run so that the process can return to the user
+        * space.
+        *
+        * Furthermore we have to check both filesystem and process freezing,
+        * as PM can be configured to freeze the filesystems before processes.
+        *
+        * If we only check fs freezing, then suspend without fs freezing
+        * will timeout, as the process is still in kernel space.
+        *
+        * If we only check process freezing, then suspend with fs freezing
+        * will timeout, as the running scrub will prevent the fs from being frozen.
+        */
+       if (fs_info->sb->s_writers.frozen > SB_UNFROZEN || freezing(current))
+               return -EINTR;
+       return 0;
+}
+
 static int scrub_raid56_parity_stripe(struct scrub_ctx *sctx,
                                      struct btrfs_device *scrub_dev,
                                      struct btrfs_block_group *bg,
@@ -2091,9 +2129,9 @@ static int scrub_raid56_parity_stripe(struct scrub_ctx *sctx,
 
        ASSERT(sctx->raid56_data_stripes);
 
-       if (atomic_read(&fs_info->scrub_cancel_req) ||
-           atomic_read(&sctx->cancel_req))
-               return -ECANCELED;
+       ret = should_cancel_scrub(sctx);
+       if (ret < 0)
+               return ret;
 
        if (atomic_read(&fs_info->scrub_pause_req))
                scrub_blocked_if_needed(fs_info);
@@ -2277,11 +2315,9 @@ static int scrub_simple_mirror(struct scrub_ctx *sctx,
                u64 found_logical = U64_MAX;
                u64 cur_physical = physical + cur_logical - logical_start;
 
-               if (atomic_read(&fs_info->scrub_cancel_req) ||
-                   atomic_read(&sctx->cancel_req)) {
-                       ret = -ECANCELED;
+               ret = should_cancel_scrub(sctx);
+               if (ret < 0)
                        break;
-               }
 
                if (atomic_read(&fs_info->scrub_pause_req))
                        scrub_blocked_if_needed(fs_info);