]> 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, 16 Sep 2026 21:40:00 +0000 (17:40 -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 fac0bad4b88ab8293451acc533d4e5f73fa5c864..1176b007b92c1f3d99659a7c607f4b579a4de37b 100644 (file)
@@ -3200,6 +3200,21 @@ int btrfs_read_block_groups(struct btrfs_fs_info *info)
                        inc_block_group_ro(cache, 1);
        }
 
+       /*
+        * 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:
@@ -3783,6 +3798,22 @@ static int cache_save_setup(struct btrfs_block_group *block_group,
        if (!btrfs_test_opt(fs_info, SPACE_CACHE))
                return 0;
 
+       /*
+        * 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 0;
+       }
+
        /*
         * If this block group is smaller than 100 megs don't bother caching the
         * block group.
index 2a6768c8b825e1d9a02d2d09e744f55cd328a26e..8d22d98e144294e5c9478ea351ecf78ef960a8dd 100644 (file)
@@ -4269,6 +4269,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 afe31b5e730e45e8671d33762c9d62b15f89a875..0e3e770847c1c7ec7dd94c1be277de046044323d 100644 (file)
@@ -170,6 +170,7 @@ int btrfs_trim_block_group_bitmaps(struct btrfs_block_group *block_group,
                                   u64 maxlen, bool async);
 
 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 c3d8f53e51d0edf2a7d27c767c6b3cea1ab3cfa7..0a6513638727ea66b9d2dfdf35c43d5dbae624b5 100644 (file)
@@ -718,9 +718,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_is_zoned(info)) {
@@ -1576,6 +1585,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);