]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: allow-rmw policy property and mount option
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Thu, 30 Jul 2026 21:52:30 +0000 (17:52 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Mon, 3 Aug 2026 07:21:55 +0000 (03:21 -0400)
Replace the provisional stripe_alloc_nocow flag with the settled
interface: a word-list policy naming the cases in which stripe_alloc
may permit the legacy unsafe RMW, each independently:

  nodatacow  in-place writes for nodatacow files' extents
  prealloc   in-place writes into preallocated extents
  fsync      waive the close-at-log-commit guarantee: no settling, no
             per-inode LOG steering, no carry-forward; logged stripes
             may be extended and RMWed as before those patches

It is "allow_rmw", not "allow_overwrite": the fsync case overwrites
nothing, but all three permit read-modify-write of stripes that a
degraded crash can then tear.  The nodatacow and prealloc cases still
require per-extent stripe isolation (the blast radius stays confined to
the writing file); fsync restores the 3a-era exposure where a degraded
crash may cost just-fsynced data, detectably, in exchange for none of
the log-commit costs.

The policy is persistent as the btrfs.stripe_alloc_allow_rmw property
on the top-level root directory, following stripe_alloc's precedent
(applied when the root inode loads during mount, before any user IO),
with a mount option of the same name as a non-persistent override; the
effective policy is their union.  Words are separated by comma, space
or colon -- the mount option form must use colon, since mount splits
options at commas.  Everything defaults off: plain stripe_alloc keeps
forcing COW and keeps the full fsync guarantee.

Assisted-by: Claude:claude-fable-5
fs/btrfs/block-group.c
fs/btrfs/block-group.h
fs/btrfs/defrag.c
fs/btrfs/file.c
fs/btrfs/fs.h
fs/btrfs/inode.c
fs/btrfs/props.c
fs/btrfs/super.c

index d3535d0ec67568669cb1f0bea95c2ece523651a1..bd55c1ba4f72920217d43a94734102f8e5cfb844 100644 (file)
@@ -919,6 +919,58 @@ bool btrfs_stripe_run_range_usable(struct btrfs_block_group *bg,
        return true;
 }
 
+/*
+ * Parse a stripe_alloc_allow_rmw word list ("nodatacow",
+ * "prealloc", "fsync") into its mask.  Words are separated by comma,
+ * space or colon; the mount option form must use colon, since mount
+ * itself splits options at commas.  An empty value yields an empty
+ * mask.  Returns -EINVAL on any unknown word.  Shared by the mount
+ * option and the filesystem property.
+ */
+int btrfs_parse_stripe_rmw(const char *value, size_t len, u32 *mask)
+{
+       u32 m = 0;
+       size_t i = 0;
+
+       while (i < len) {
+               size_t j = i;
+
+               while (j < len && value[j] != ',' && value[j] != ' ' &&
+                      value[j] != ':')
+                       j++;
+               if (j - i == strlen("nodatacow") &&
+                   !strncmp(value + i, "nodatacow", j - i))
+                       m |= BTRFS_STRIPE_RMW_NODATACOW;
+               else if (j - i == strlen("prealloc") &&
+                        !strncmp(value + i, "prealloc", j - i))
+                       m |= BTRFS_STRIPE_RMW_PREALLOC;
+               else if (j - i == strlen("fsync") &&
+                        !strncmp(value + i, "fsync", j - i))
+                       m |= BTRFS_STRIPE_RMW_FSYNC;
+               else if (j != i)
+                       return -EINVAL;
+               i = j + 1;
+       }
+       *mask = m;
+       return 0;
+}
+
+void btrfs_show_stripe_rmw(struct seq_file *seq, u32 mask)
+{
+       const char *sep = "";
+
+       if (mask & BTRFS_STRIPE_RMW_NODATACOW) {
+               seq_printf(seq, "%snodatacow", sep);
+               sep = ":";
+       }
+       if (mask & BTRFS_STRIPE_RMW_PREALLOC) {
+               seq_printf(seq, "%sprealloc", sep);
+               sep = ":";
+       }
+       if (mask & BTRFS_STRIPE_RMW_FSYNC)
+               seq_printf(seq, "%sfsync", sep);
+}
+
 /*
  * Does every committed extent in [start, start + len) belong solely to
  * @ino in @root_id -- exactly one reference, a plain data ref, count 1?
@@ -1052,7 +1104,7 @@ out:
  * data passes.
  */
 bool btrfs_stripe_nocow_writable(struct btrfs_inode *inode, u64 start,
-                                u64 num_bytes)
+                                u64 num_bytes, u32 which)
 {
        struct btrfs_fs_info *fs_info = inode->root->fs_info;
        struct btrfs_block_group *bg;
@@ -1063,7 +1115,7 @@ bool btrfs_stripe_nocow_writable(struct btrfs_inode *inode, u64 start,
        u64 end;
        bool ret = true;
 
-       if (!btrfs_test_opt(fs_info, STRIPE_ALLOC_NOCOW))
+       if (!(btrfs_stripe_allow_rmw(fs_info) & which))
                return false;
        bg = btrfs_lookup_block_group(fs_info, start);
        if (!bg)
@@ -1259,6 +1311,13 @@ void btrfs_log_settle_stripes(struct btrfs_inode *inode, u64 file_start,
        u64 ps_start = 0;
        u64 ps_len = 0;
 
+       /*
+        * The fsync overwrite policy waives the log-window guarantee:
+        * logged stripes may be extended and RMWed like before the
+        * close-at-log-commit machinery existed.
+        */
+       if (btrfs_stripe_allow_rmw(fs_info) & BTRFS_STRIPE_RMW_FSYNC)
+               return;
        if (list_empty_careful(&fs_info->open_stripe_bgs))
                return;
        bg = btrfs_lookup_block_group(fs_info, bytenr);
@@ -1429,6 +1488,17 @@ void btrfs_disable_stripe_alloc(struct btrfs_fs_info *fs_info)
  * is either filled before the run closes or never.
  */
 bool btrfs_stripe_in_open_run(struct btrfs_fs_info *fs_info, u64 logical)
+{
+       return btrfs_stripe_open_run_class(fs_info, logical, NULL);
+}
+
+/*
+ * Like btrfs_stripe_in_open_run(), also reporting the covering run's
+ * class: the raid56 parking gate treats NOCOW-class runs specially
+ * (sync writes into them never park; see rbio_try_park()).
+ */
+bool btrfs_stripe_open_run_class(struct btrfs_fs_info *fs_info, u64 logical,
+                                enum btrfs_stripe_run_class *class)
 {
        struct btrfs_block_group *bg;
        struct btrfs_open_stripe_run *run;
@@ -1441,6 +1511,8 @@ bool btrfs_stripe_in_open_run(struct btrfs_fs_info *fs_info, u64 logical)
        spin_lock_irqsave(&bg->stripe_run_lock, flags);
        list_for_each_entry(run, &bg->open_stripe_runs, list) {
                if (run->open && logical >= run->start && logical < run->end) {
+                       if (class)
+                               *class = run->class;
                        ret = true;
                        break;
                }
index 401e7fa197145b9bce8596a1aa89c95671f5a9d9..bec46f1597d0497b0e6e6087fd1930eab4ffdfbd 100644 (file)
@@ -443,13 +443,17 @@ struct btrfs_open_stripe_run *btrfs_get_open_stripe_run(
 void btrfs_close_bg_open_stripes(struct btrfs_block_group *bg);
 void btrfs_retire_block_group_stripes(struct btrfs_block_group *bg);
 bool btrfs_stripe_in_open_run(struct btrfs_fs_info *fs_info, u64 logical);
+bool btrfs_stripe_open_run_class(struct btrfs_fs_info *fs_info, u64 logical,
+                                enum btrfs_stripe_run_class *class);
 bool btrfs_stripe_run_range_usable(struct btrfs_block_group *bg,
                                   u64 run_start, u64 *run_len);
 bool btrfs_stripe_run_pad_start(struct btrfs_fs_info *fs_info,
                                u64 stripe_start, u64 stripe_len,
                                u64 *pad_from);
 bool btrfs_stripe_nocow_writable(struct btrfs_inode *inode, u64 start,
-                                u64 num_bytes);
+                                u64 num_bytes, u32 which);
+int btrfs_parse_stripe_rmw(const char *value, size_t len, u32 *mask);
+void btrfs_show_stripe_rmw(struct seq_file *seq, u32 mask);
 void btrfs_log_settle_stripes(struct btrfs_inode *inode, u64 file_start,
                              u64 file_len, u64 bytenr, u64 num_bytes);
 int btrfs_stripe_alloc_check_support(struct btrfs_fs_info *fs_info);
index 045071e32d734ab4ec4dcd4c40902e627188b603..43630a5af905184ec0cf7faa581ce1813977f8b3 100644 (file)
@@ -1361,6 +1361,9 @@ void btrfs_carry_log_tail(struct btrfs_inode *inode)
 
        if (!btrfs_test_opt(inode->root->fs_info, STRIPE_ALLOC))
                return;
+       if (btrfs_stripe_allow_rmw(inode->root->fs_info) &
+           BTRFS_STRIPE_RMW_FSYNC)
+               return;
        if (!READ_ONCE(inode->log_carry))
                return;
        copy = kmalloc(sizeof(*copy), GFP_NOFS);
index 703002d1981ef5bdc2fb7a281224f9bac390457e..557000c84175417936d0a00f382becd8add015fe 100644 (file)
@@ -1576,6 +1576,8 @@ int btrfs_sync_file(struct file *file, loff_t start, loff_t end, int datasync)
         * one fsync is taken as a signal that more will follow.
         */
        if (btrfs_test_opt(fs_info, STRIPE_ALLOC) &&
+           !(btrfs_stripe_allow_rmw(fs_info) &
+             BTRFS_STRIPE_RMW_FSYNC) &&
            !test_bit(BTRFS_INODE_LOG_ALLOC, &inode->runtime_flags))
                set_bit(BTRFS_INODE_LOG_ALLOC, &inode->runtime_flags);
 
index 2836056a0d41328e1710b3e68d50c91b4dccd56b..699bae2b2896bb7b2d6fad38189b2246cf982c22 100644 (file)
@@ -282,7 +282,19 @@ enum {
        BTRFS_MOUNT_IGNORESUPERFLAGS            = (1ULL << 32),
        BTRFS_MOUNT_REF_TRACKER                 = (1ULL << 33),
        BTRFS_MOUNT_STRIPE_ALLOC                = (1ULL << 34),
-       BTRFS_MOUNT_STRIPE_ALLOC_NOCOW          = (1ULL << 35),
+};
+
+/*
+ * Cases in which stripe_alloc may permit the legacy unsafe RMW-in-place
+ * (the write hole returns for the stripes such a write touches, confined
+ * as each case's machinery allows).  Set from the
+ * btrfs.stripe_alloc_allow_rmw property and/or the mount option of
+ * the same name; the effective policy is their union.
+ */
+enum {
+       BTRFS_STRIPE_RMW_NODATACOW      = (1U << 0),
+       BTRFS_STRIPE_RMW_PREALLOC               = (1U << 1),
+       BTRFS_STRIPE_RMW_FSYNC          = (1U << 2),
 };
 
 /* These mount options require a full read-only fs, no new transaction is allowed. */
@@ -792,6 +804,9 @@ struct btrfs_fs_info {
 
        u32 data_chunk_allocations;
        u32 metadata_ratio;
+       /* BTRFS_STRIPE_RMW_* masks; see btrfs_stripe_allow_rmw(). */
+       u32 stripe_rmw_opt;
+       u32 stripe_rmw_prop;
 
        /* Private scrub information */
        struct mutex scrub_lock;
@@ -1206,6 +1221,14 @@ static inline int btrfs_need_cleaner_sleep(const struct btrfs_fs_info *fs_info)
                btrfs_fs_closing(fs_info);
 }
 
+static inline u32 btrfs_stripe_allow_rmw(const struct btrfs_fs_info *fs_info)
+{
+       if (!btrfs_test_opt(fs_info, STRIPE_ALLOC))
+               return 0;
+       return READ_ONCE(fs_info->stripe_rmw_opt) |
+              READ_ONCE(fs_info->stripe_rmw_prop);
+}
+
 static inline void btrfs_wake_unfinished_drop(struct btrfs_fs_info *fs_info)
 {
        clear_and_wake_up_bit(BTRFS_FS_UNFINISHED_DROPS, &fs_info->flags);
index 5d5ba0cc5df9ff5afef7919795a0019fa420da84..a87dc3df8cab45ba56c84d92deefc6a543458b96 100644 (file)
@@ -1887,7 +1887,10 @@ static int can_nocow_file_extent(struct btrfs_path *path,
        if (!btrfs_is_data_reloc_root(root) &&
            btrfs_stripe_alloc_forces_cow(root->fs_info, io_start) &&
            !btrfs_stripe_nocow_writable(inode, io_start,
-                                        args->file_extent.num_bytes))
+                                        args->file_extent.num_bytes,
+                                        extent_type == BTRFS_FILE_EXTENT_PREALLOC ?
+                                               BTRFS_STRIPE_RMW_PREALLOC :
+                                               BTRFS_STRIPE_RMW_NODATACOW))
                goto out;
 
        /*
index b71cde6f502af1c6a773ecf21032632603880eb2..3752261a0bfc361befe10122eb7c5dedad300944 100644 (file)
@@ -422,6 +422,40 @@ static const char *prop_stripe_alloc_extract(const struct btrfs_inode *inode)
        return NULL;
 }
 
+static int prop_stripe_rmw_validate(const struct btrfs_inode *inode,
+                                         const char *value, size_t len)
+{
+       u32 mask;
+
+       if (!prop_stripe_alloc_scope_ok(inode))
+               return -EINVAL;
+       if (!value || len == 0)
+               return 0;
+       return btrfs_parse_stripe_rmw(value, len, &mask);
+}
+
+static int prop_stripe_rmw_apply(struct btrfs_inode *inode,
+                                      const char *value, size_t len)
+{
+       struct btrfs_fs_info *fs_info = inode->root->fs_info;
+       u32 mask = 0;
+
+       /* Ignore stray copies; only the top-level root carries policy. */
+       if (!prop_stripe_alloc_scope_ok(inode))
+               return 0;
+       if (value && len &&
+           btrfs_parse_stripe_rmw(value, len, &mask))
+               return -EINVAL;
+       WRITE_ONCE(fs_info->stripe_rmw_prop, mask);
+       return 0;
+}
+
+static const char *prop_stripe_rmw_extract(const struct btrfs_inode *inode)
+{
+       /* Not inheritable; nothing regenerates the value from inode state. */
+       return NULL;
+}
+
 static bool prop_stripe_alloc_ignore(const struct btrfs_inode *inode)
 {
        return false;
@@ -458,6 +492,14 @@ static struct prop_handler prop_handlers[] = {
                .ignore = prop_stripe_alloc_ignore,
                .inheritable = 0
        },
+       {
+               .xattr_name = XATTR_BTRFS_PREFIX "stripe_alloc_allow_rmw",
+               .validate = prop_stripe_rmw_validate,
+               .apply = prop_stripe_rmw_apply,
+               .extract = prop_stripe_rmw_extract,
+               .ignore = prop_stripe_alloc_ignore,
+               .inheritable = 0
+       },
 };
 
 int btrfs_inode_inherit_props(struct btrfs_trans_handle *trans,
index 2c76c29da4413321333e0655fe9dafdc63d23c6c..506b610bf73417798e88edde05853954b95d9f07 100644 (file)
@@ -84,6 +84,7 @@ struct btrfs_fs_context {
        u32 commit_interval;
        u32 metadata_ratio;
        u32 thread_pool_size;
+       u32 stripe_rmw_opt;
        unsigned long long mount_opt;
        unsigned long compress_type:4;
        int compress_level;
@@ -117,7 +118,7 @@ enum {
        Opt_skip_balance,
        Opt_space_cache,
        Opt_stripe_alloc,
-       Opt_stripe_alloc_nocow,
+       Opt_stripe_alloc_allow_rmw,
        Opt_space_cache_version,
        Opt_ssd,
        Opt_ssd_spread,
@@ -241,7 +242,8 @@ static const struct fs_parameter_spec btrfs_fs_parameters[] = {
        fsparam_flag_no("space_cache", Opt_space_cache),
        fsparam_enum("space_cache", Opt_space_cache_version, btrfs_parameter_space_cache),
        fsparam_flag_no("stripe_alloc", Opt_stripe_alloc),
-       fsparam_flag_no("stripe_alloc_nocow", Opt_stripe_alloc_nocow),
+       fsparam_string("stripe_alloc_allow_rmw",
+                      Opt_stripe_alloc_allow_rmw),
        fsparam_flag_no("ssd", Opt_ssd),
        fsparam_flag_no("ssd_spread", Opt_ssd_spread),
        fsparam_string("subvol", Opt_subvol),
@@ -493,11 +495,15 @@ static int btrfs_parse_param(struct fs_context *fc, struct fs_parameter *param)
                else
                        btrfs_set_opt(ctx->mount_opt, STRIPE_ALLOC);
                break;
-       case Opt_stripe_alloc_nocow:
-               if (result.negated)
-                       btrfs_clear_opt(ctx->mount_opt, STRIPE_ALLOC_NOCOW);
-               else
-                       btrfs_set_opt(ctx->mount_opt, STRIPE_ALLOC_NOCOW);
+       case Opt_stripe_alloc_allow_rmw:
+               if (btrfs_parse_stripe_rmw(param->string,
+                                                strlen(param->string),
+                                                &ctx->stripe_rmw_opt)) {
+                       btrfs_err(NULL,
+               "invalid stripe_alloc_allow_rmw value %s",
+                                 param->string);
+                       return -EINVAL;
+               }
                break;
        case Opt_ratio:
                ctx->metadata_ratio = result.uint_32;
@@ -727,11 +733,6 @@ bool btrfs_check_options(const struct btrfs_fs_info *info,
                ret = false;
        }
 
-       if (btrfs_raw_test_opt(*mount_opt, STRIPE_ALLOC_NOCOW) &&
-           !btrfs_raw_test_opt(*mount_opt, STRIPE_ALLOC)) {
-               btrfs_err(info, "stripe_alloc_nocow requires stripe_alloc");
-               ret = false;
-       }
        if (btrfs_raw_test_opt(*mount_opt, STRIPE_ALLOC)) {
                /*
                 * Only v1 is a problem: its cache inode is nodatacow and
@@ -1165,8 +1166,10 @@ static int btrfs_show_options(struct seq_file *seq, struct dentry *dentry)
                seq_puts(seq, ",flushoncommit");
        if (btrfs_test_opt(info, STRIPE_ALLOC))
                seq_puts(seq, ",stripe_alloc");
-       if (btrfs_test_opt(info, STRIPE_ALLOC_NOCOW))
-               seq_puts(seq, ",stripe_alloc_nocow");
+       if (info->stripe_rmw_opt) {
+               seq_puts(seq, ",stripe_alloc_allow_rmw=");
+               btrfs_show_stripe_rmw(seq, info->stripe_rmw_opt);
+       }
        if (btrfs_test_opt(info, DISCARD_SYNC))
                seq_puts(seq, ",discard");
        if (btrfs_test_opt(info, DISCARD_ASYNC))
@@ -1468,6 +1471,7 @@ static void btrfs_ctx_to_info(struct btrfs_fs_info *fs_info, struct btrfs_fs_con
        fs_info->max_inline = ctx->max_inline;
        fs_info->commit_interval = ctx->commit_interval;
        fs_info->metadata_ratio = ctx->metadata_ratio;
+       fs_info->stripe_rmw_opt = ctx->stripe_rmw_opt;
        fs_info->thread_pool_size = ctx->thread_pool_size;
        fs_info->mount_opt = ctx->mount_opt;
        fs_info->compress_type = ctx->compress_type;