]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: refuse space_cache=v1
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 5 Aug 2026 03:33:32 +0000 (23:33 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 5 Aug 2026 03:33:32 +0000 (23:33 -0400)
The v1 free space cache inode is nodatacow, preallocated and nodatasum,
so the cache is overwritten in place during commit: a sub-stripe write
into a data block group, landing in whatever committed stripes the cache
occupies, with no csum that could reveal the damage afterwards.  That is
precisely the write this series exists to prevent.

The free space tree is not what stripe_alloc needs -- nothing in it reads
the tree, and the by-size index it allocates from is the in-memory free
space, which exists whatever the on-disk format is.  Having no cache at
all is fine too.  Only v1 has to be kept away, so say that instead of
demanding v2.

Three places, because the cache format can only be converted at mount and
never at remount, so the option alone is not the whole story:

 - btrfs_check_mountopts() rejects space_cache=v1 with stripe_alloc.

 - btrfs_reconfigure() rewrites the cache options after that validation,
   to restore what is on disk.  It has to, given the above.  Re-check
   afterwards rather than assume the options still mean what they did.

 - block group read time refuses to mount when v1 cache inodes are
   present on disk at all.  cache_generation only records whether the
   last mount wrote the cache; the inodes are the durable evidence, so
   btrfs_free_space_cache_v1_present() looks for one under
   BTRFS_FREE_SPACE_OBJECTID in the tree root.  Clearing them is one
   mount away, and the error message says so.

cache_save_setup() also declines to set the cache up while stripe_alloc
is on.  The checks above should make that unreachable, but it is the
point where the in-place write would actually be issued.

Assisted-by: Claude:claude-fable-5
fs/btrfs/block-group.c
fs/btrfs/free-space-cache.c
fs/btrfs/free-space-cache.h
fs/btrfs/super.c

index 18c3fa772ec6f7016274921e2a150a26a18b2cda..81792aa971533a88f2f72cc6e57e44964655a356 100644 (file)
@@ -3328,6 +3328,21 @@ int btrfs_read_block_groups(struct btrfs_fs_info *info)
                        inc_block_group_ro(cache, true);
        }
 
+       /*
+        * The mount options rule out space_cache=v1, but the cache format can
+        * only be changed at mount, so a filesystem can arrive here with cache
+        * inodes still on disk while the options say otherwise.  Those inodes
+        * are nodatacow and preallocated: whatever writes them next writes in
+        * place.  Refuse rather than hope nothing does.
+        */
+       if (btrfs_test_opt(info, STRIPE_ALLOC) &&
+           btrfs_free_space_cache_v1_present(info)) {
+               btrfs_err(info,
+"stripe_alloc is not supported while a v1 space cache is present on disk; mount once without stripe_alloc to clear it (-o clear_cache, or space_cache=v2)");
+               ret = -EINVAL;
+               goto error;
+       }
+
        btrfs_init_global_block_rsv(info);
        ret = check_chunk_block_group_mappings(info);
 error:
@@ -3956,6 +3971,22 @@ static void cache_save_setup(struct btrfs_block_group *block_group,
        if (!btrfs_test_opt(fs_info, SPACE_CACHE))
                return;
 
+       /*
+        * The v1 cache inode is nodatacow and preallocated: its blocks are
+        * overwritten in place during commit.  That is exactly the write this
+        * allocator exists to prevent, and the cache carries no csums, so a
+        * torn stripe would be undetectable afterwards.  stripe_alloc requires
+        * the free space tree at mount, so this should be unreachable -- but
+        * remount rewrites the cache options after validating them (see
+        * btrfs_reconfigure()), so refuse here as well rather than trust that.
+        */
+       if (btrfs_test_opt(fs_info, STRIPE_ALLOC)) {
+               btrfs_warn_rl(fs_info,
+"not writing the v1 space cache for block group %llu: stripe_alloc is enabled",
+                             block_group->start);
+               return;
+       }
+
        /*
         * If this block group is smaller than 100 megs don't bother caching the
         * block group.
index 9686f4da770de6243ba50da7462d294beb9d8659..b92f8bc0e6ef10ae197619a04beaa612bb2e3587 100644 (file)
@@ -4308,6 +4308,51 @@ bool btrfs_free_space_cache_v1_active(struct btrfs_fs_info *fs_info)
        return btrfs_super_cache_generation(fs_info->super_copy);
 }
 
+/*
+ * Is a v1 free space cache present on disk?
+ *
+ * btrfs_free_space_cache_v1_active() reads the superblock's
+ * cache_generation, which says whether the last mount wrote the cache, not
+ * whether the cache is there.  The cache inodes themselves are the durable
+ * evidence, so look for one: they live in the tree root under
+ * BTRFS_FREE_SPACE_OBJECTID, keyed by block group start.
+ *
+ * Returns false if the tree cannot be searched; callers use this to refuse
+ * an option, and failing a mount because the answer was unavailable would
+ * be worse than the risk it guards against.
+ */
+bool btrfs_free_space_cache_v1_present(struct btrfs_fs_info *fs_info)
+{
+       struct btrfs_root *root = fs_info->tree_root;
+       struct btrfs_path *path;
+       struct btrfs_key key;
+       struct btrfs_key found_key;
+       bool present = false;
+       int ret;
+
+       path = btrfs_alloc_path();
+       if (!path)
+               return false;
+
+       key.objectid = BTRFS_FREE_SPACE_OBJECTID;
+       key.type = 0;
+       key.offset = 0;
+
+       ret = btrfs_search_slot(NULL, root, &key, path, 0, 0);
+       if (ret < 0)
+               goto out;
+       if (path->slots[0] >= btrfs_header_nritems(path->nodes[0])) {
+               ret = btrfs_next_leaf(root, path);
+               if (ret)
+                       goto out;
+       }
+       btrfs_item_key_to_cpu(path->nodes[0], &found_key, path->slots[0]);
+       present = (found_key.objectid == BTRFS_FREE_SPACE_OBJECTID);
+out:
+       btrfs_free_path(path);
+       return present;
+}
+
 static int cleanup_free_space_cache_v1(struct btrfs_fs_info *fs_info,
                                       struct btrfs_trans_handle *trans)
 {
index 70700067f10a53b8e3148f6cf0803d66e111049a..6bb88b82d113e5bb4e1b4343dcbdbdbcd3aefa25 100644 (file)
@@ -163,6 +163,7 @@ int btrfs_trim_block_group_bitmaps(struct btrfs_block_group *block_group,
 void btrfs_trim_fully_remapped_block_group(struct btrfs_block_group *bg);
 
 bool btrfs_free_space_cache_v1_active(struct btrfs_fs_info *fs_info);
+bool btrfs_free_space_cache_v1_present(struct btrfs_fs_info *fs_info);
 int btrfs_set_free_space_cache_v1_active(struct btrfs_fs_info *fs_info, bool active);
 /* Support functions for running our sanity tests */
 #ifdef CONFIG_BTRFS_FS_RUN_SANITY_TESTS
index 6db7ec0cf145ff2500714a05e4e20ee6ca2322c0..7dbbc921615faef2cc2c2938d12d1644706836ba 100644 (file)
@@ -720,9 +720,18 @@ bool btrfs_check_options(const struct btrfs_fs_info *info,
        }
 
        if (btrfs_raw_test_opt(*mount_opt, STRIPE_ALLOC)) {
-               if (!btrfs_raw_test_opt(*mount_opt, FREE_SPACE_TREE)) {
+               /*
+                * Only v1 is a problem: its cache inode is nodatacow and
+                * preallocated, so the cache is overwritten in place during
+                * commit -- a sub-stripe write into a data block group, into
+                * whatever committed stripes the cache occupies, and nodatasum
+                * so nothing would show the damage afterwards.  The free space
+                * tree and no cache at all are both fine; nothing here needs
+                * either one.
+                */
+               if (btrfs_raw_test_opt(*mount_opt, SPACE_CACHE)) {
                        btrfs_err(info,
-       "stripe_alloc requires the free space tree (space_cache=v2)");
+       "stripe_alloc is not supported with space_cache=v1");
                        ret = false;
                }
                if (btrfs_fs_incompat(info, REMAP_TREE)) {
@@ -1583,6 +1592,21 @@ static int btrfs_reconfigure(struct fs_context *fc)
                }
        }
 
+       /*
+        * The block above rewrites the cache options to match the disk after
+        * btrfs_check_options() has already had its say -- it has to, because
+        * the cache format can only be converted at mount, never at remount.
+        * So whatever it decides is what we run with, and nothing re-examines
+        * stripe_alloc against it.  Re-check what we actually ended up with.
+        */
+       if (btrfs_test_opt(fs_info, STRIPE_ALLOC) &&
+           btrfs_test_opt(fs_info, SPACE_CACHE)) {
+               btrfs_err(fs_info,
+"stripe_alloc is not supported with space_cache=v1, which this filesystem is still using");
+               ret = -EINVAL;
+               goto restore;
+       }
+
        ret = 0;
        if (!sb_rdonly(sb) && (fc->sb_flags & SB_RDONLY))
                ret = btrfs_remount_ro(fs_info);