]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: write-in-place for stripe-isolated nocow extents
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Thu, 30 Jul 2026 21:40:29 +0000 (17:40 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Mon, 3 Aug 2026 07:21:55 +0000 (03:21 -0400)
Re-enable write-in-place for nodatacow files and preallocated extents
under stripe-exclusive allocation, with the understood caveat that the
write hole cannot be prevented for data that opts out of COW: an
in-place write RMWs its stripe's parity, so a degraded crash can tear
the stripe.  What CAN be guaranteed is the blast radius: in-place is
permitted only for extents whose full stripes are isolated to the
writing inode, so such a crash can tear only the writing file's own
data -- the nodatacow contract, no worse.

The gate is per extent, decided where nocow eligibility is already
checked: fast path, the stripe belongs to one of the inode's own
NOCOW-class runs (which only ever held its extents); slow path, the
committed extent tree proves sole ownership (single plain data ref,
count 1, matching root and objectid; anything shared, foreign or
metadata fails).  The fully-free claim rule keeps uncommitted foreign
extents out of partially used stripes, so the committed tree is
authoritative.  Extents that fail -- anything allocated before stripe
isolation existed, extents shared through reflink or snapshots,
relocated extents -- simply stay force-COWed, and because their rewrite
is steered into the inode's private NOCOW run, the next overwrite of
the same data passes: legacy nocow files migrate themselves to
isolation in one COW generation, with no tool and no flag day.

Log-commit settling skips NOCOW-class runs: nodatacow data gets no
fsync survival guarantee (its own later in-place writes can always
tear it), so closing the run would trap its tail for nothing.  The
write-hole debug checker skips groups that hosted NOCOW-class runs,
like relocation-used groups, since isolated in-place writes land in
stripes whose runs have drained.

The policy is enabled by a stripe_alloc_nocow mount option (requires
stripe_alloc), provisional until the persistent-property interface for
it is settled.  Off by default: stripe_alloc alone keeps forcing COW
exactly as before.

Assisted-by: Claude:claude-fable-5
fs/btrfs/block-group.c
fs/btrfs/block-group.h
fs/btrfs/extent-tree.c
fs/btrfs/fs.h
fs/btrfs/inode.c
fs/btrfs/super.c

index ba321dbf821159bcda456f0a3e74111f5475fc01..d3535d0ec67568669cb1f0bea95c2ece523651a1 100644 (file)
@@ -919,6 +919,195 @@ bool btrfs_stripe_run_range_usable(struct btrfs_block_group *bg,
        return true;
 }
 
+/*
+ * Does every committed extent in [start, start + len) belong solely to
+ * @ino in @root_id -- exactly one reference, a plain data ref, count 1?
+ * Any metadata block, shared reference, or foreign owner fails.  Caller
+ * limits the range to full stripes; uncommitted allocations need not be
+ * visible because nothing can allocate into a partially used stripe (the
+ * fully-free claim rule), so a foreign extent can only appear here after
+ * the whole stripe frees and commits -- which the extent tree shows.
+ */
+static bool stripe_extents_owned_by(struct btrfs_fs_info *fs_info,
+                                   u64 root_id, u64 ino, u64 start, u64 len)
+{
+       struct btrfs_root *extent_root = btrfs_extent_root(fs_info, start);
+       struct btrfs_path *path;
+       struct btrfs_key key;
+       bool ret = true;
+       int err;
+
+       if (!extent_root)
+               return false;
+       path = btrfs_alloc_path();
+       if (!path)
+               return false;
+
+       key.objectid = start;
+       key.type = BTRFS_EXTENT_ITEM_KEY;
+       key.offset = 0;
+       err = btrfs_search_slot(NULL, extent_root, &key, path, 0, 0);
+       if (err < 0) {
+               ret = false;
+               goto out;
+       }
+       /* An extent starting before @start may still overlap it. */
+       err = btrfs_previous_extent_item(extent_root, path, 0);
+       if (err < 0) {
+               ret = false;
+               goto out;
+       }
+       if (err > 0) {
+               btrfs_release_path(path);
+               key.objectid = start;
+               key.type = BTRFS_EXTENT_ITEM_KEY;
+               key.offset = 0;
+               err = btrfs_search_slot(NULL, extent_root, &key, path, 0, 0);
+               if (err < 0) {
+                       ret = false;
+                       goto out;
+               }
+       }
+
+       while (true) {
+               struct extent_buffer *leaf = path->nodes[0];
+               int slot = path->slots[0];
+               struct btrfs_extent_item *ei;
+               struct btrfs_extent_inline_ref *iref;
+               struct btrfs_extent_data_ref *dref;
+               int type;
+
+               if (slot >= btrfs_header_nritems(leaf)) {
+                       err = btrfs_next_leaf(extent_root, path);
+                       if (err < 0) {
+                               ret = false;
+                               goto out;
+                       }
+                       if (err > 0)
+                               break;
+                       continue;
+               }
+               btrfs_item_key_to_cpu(leaf, &key, slot);
+               if (key.objectid >= start + len)
+                       break;
+               if (key.type == BTRFS_METADATA_ITEM_KEY) {
+                       /*
+                        * key.offset is the level, not a length; a tree
+                        * block spans nodesize bytes.  One from before the
+                        * range (e.g. where the pre-range positioning landed
+                        * on a metadata block group's last item) is harmless.
+                        */
+                       if (key.objectid + fs_info->nodesize > start) {
+                               ret = false;
+                               goto out;
+                       }
+                       path->slots[0]++;
+                       continue;
+               }
+               if (key.type != BTRFS_EXTENT_ITEM_KEY ||
+                   key.objectid + key.offset <= start) {
+                       path->slots[0]++;
+                       continue;
+               }
+
+               ei = btrfs_item_ptr(leaf, slot, struct btrfs_extent_item);
+               if (btrfs_extent_refs(leaf, ei) != 1 ||
+                   !(btrfs_extent_flags(leaf, ei) & BTRFS_EXTENT_FLAG_DATA)) {
+                       ret = false;
+                       goto out;
+               }
+               iref = (struct btrfs_extent_inline_ref *)(ei + 1);
+               type = btrfs_get_extent_inline_ref_type(leaf, iref,
+                                                       BTRFS_REF_TYPE_DATA);
+               if (type != BTRFS_EXTENT_DATA_REF_KEY) {
+                       ret = false;
+                       goto out;
+               }
+               dref = (struct btrfs_extent_data_ref *)(&iref->offset);
+               if (btrfs_extent_data_ref_root(leaf, dref) != root_id ||
+                   btrfs_extent_data_ref_objectid(leaf, dref) != ino ||
+                   btrfs_extent_data_ref_count(leaf, dref) != 1) {
+                       ret = false;
+                       goto out;
+               }
+               path->slots[0]++;
+       }
+out:
+       btrfs_free_path(path);
+       return ret;
+}
+
+/*
+ * May [start, start + num_bytes) be written in place under
+ * stripe-exclusive allocation?  Only when the stripe_alloc_nocow policy
+ * is enabled and every full stripe the range touches is isolated to
+ * @inode: an in-place write RMWs the stripe's parity, so a degraded
+ * crash can tear every extent sharing the stripe -- isolation confines
+ * that blast radius to the writing file, which opted out of COW
+ * protection.  Fast path: the stripe belongs to one of the inode's own
+ * NOCOW-class runs (such stripes have only ever held its extents).
+ * Slow path: the committed extent tree proves sole ownership.  Extents
+ * that fail simply stay force-COWed -- and their rewrite is steered
+ * into the inode's private NOCOW run, so the next overwrite of the same
+ * data passes.
+ */
+bool btrfs_stripe_nocow_writable(struct btrfs_inode *inode, u64 start,
+                                u64 num_bytes)
+{
+       struct btrfs_fs_info *fs_info = inode->root->fs_info;
+       struct btrfs_block_group *bg;
+       const u64 root_id = btrfs_root_id(inode->root);
+       const u64 ino = btrfs_ino(inode);
+       u64 fsl;
+       u64 cur;
+       u64 end;
+       bool ret = true;
+
+       if (!btrfs_test_opt(fs_info, STRIPE_ALLOC_NOCOW))
+               return false;
+       bg = btrfs_lookup_block_group(fs_info, start);
+       if (!bg)
+               return false;
+       if (!btrfs_is_stripe_alloc_bg(bg)) {
+               /* Not a raid56 data group: nothing forces COW here anyway. */
+               btrfs_put_block_group(bg);
+               return true;
+       }
+
+       fsl = bg->full_stripe_len;
+       cur = bg->start + round_down(start - bg->start, fsl);
+       end = start + num_bytes;
+       while (cur < end) {
+               struct btrfs_open_stripe_run *run;
+               unsigned long flags;
+               int verdict = 0;        /* 0 = unknown, 1 = ok, -1 = foreign */
+
+               spin_lock_irqsave(&bg->stripe_run_lock, flags);
+               list_for_each_entry(run, &bg->open_stripe_runs, list) {
+                       if (cur < run->start || cur >= run->end)
+                               continue;
+                       if (run->class == BTRFS_STRIPE_RUN_NOCOW &&
+                           run->owner == ino)
+                               verdict = 1;
+                       else
+                               verdict = -1;
+                       break;
+               }
+               spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
+
+               if (verdict == 0 &&
+                   !stripe_extents_owned_by(fs_info, root_id, ino, cur, fsl))
+                       verdict = -1;
+               if (verdict < 0) {
+                       ret = false;
+                       break;
+               }
+               cur += fsl;
+       }
+       btrfs_put_block_group(bg);
+       return ret;
+}
+
 /*
  * For the raid56 pad-to-full optimization: a sub-stripe write into a
  * stripe covered by a live stripe run may zero-fill (instead of read) any
@@ -1082,6 +1271,14 @@ void btrfs_log_settle_stripes(struct btrfs_inode *inode, u64 file_start,
 
                if (bytenr < run->start || bytenr >= run->offset)
                        continue;
+               /*
+                * A nodatacow file's logged extents get no fsync survival
+                * guarantee -- the file's own later in-place writes can
+                * always tear them -- so closing its run would trap the
+                * tail for nothing.
+                */
+               if (run->class == BTRFS_STRIPE_RUN_NOCOW)
+                       break;
                flush_start = run->start;
                flush_len = run->end - run->start;
                /*
@@ -1606,7 +1803,8 @@ void btrfs_stripe_check_write(struct btrfs_fs_info *fs_info,
        if (!bg)
                return;
        if (!btrfs_is_stripe_alloc_bg(bg) ||
-           test_bit(BLOCK_GROUP_FLAG_STRIPE_RELOC_USED, &bg->runtime_flags))
+           test_bit(BLOCK_GROUP_FLAG_STRIPE_RELOC_USED, &bg->runtime_flags) ||
+           test_bit(BLOCK_GROUP_FLAG_STRIPE_NOCOW_USED, &bg->runtime_flags))
                goto out;
        fsl = bg->full_stripe_len;
        spin_lock_irqsave(&bg->stripe_run_lock, flags);
index c16eee5409352fad1575d941223181b6104d8cdf..401e7fa197145b9bce8596a1aa89c95671f5a9d9 100644 (file)
@@ -146,6 +146,14 @@ enum btrfs_block_group_flags {
         * counter must be rescanned at commit (stripe_alloc only).
         */
        BLOCK_GROUP_FLAG_STRIPE_UNUSABLE_DIRTY,
+       /*
+        * The group has hosted NOCOW-class stripe runs.  With the
+        * stripe_alloc_nocow policy, isolated nodatacow/prealloc extents
+        * are legitimately overwritten in place after their runs drain, so
+        * the write-hole debug check skips such groups (sticky, debug
+        * only), like relocation above.
+        */
+       BLOCK_GROUP_FLAG_STRIPE_NOCOW_USED,
 };
 
 enum btrfs_caching_type {
@@ -440,6 +448,8 @@ bool btrfs_stripe_run_range_usable(struct btrfs_block_group *bg,
 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);
 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 ec2760bc66619cb4af6db22b57c2f256d1988905..00cfac52a0438d93988027011c6eaeb198b5360c 100644 (file)
@@ -4265,6 +4265,9 @@ static int do_allocation_stripe(struct btrfs_block_group *block_group,
                if (ret == -ENOMEM)
                        return ret;
                if (!ret) {
+                       if (ffe_ctl->steer_class == BTRFS_STRIPE_RUN_NOCOW)
+                               set_bit(BLOCK_GROUP_FLAG_STRIPE_NOCOW_USED,
+                                       &block_group->runtime_flags);
                        WRITE_ONCE(steer_inode->log_run_hint, offset);
                        ffe_ctl->found_offset = offset;
                        ffe_ctl->search_start = offset;
index 7acf990094536fa015379979a04bf6081c58c506..2836056a0d41328e1710b3e68d50c91b4dccd56b 100644 (file)
@@ -282,6 +282,7 @@ 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),
 };
 
 /* These mount options require a full read-only fs, no new transaction is allowed. */
index 327ec35a4a4a3fc12ce8a9b6d1c1dc30121bb01d..5d5ba0cc5df9ff5afef7919795a0019fa420da84 100644 (file)
@@ -1879,9 +1879,15 @@ static int can_nocow_file_extent(struct btrfs_path *path,
         * data relocation inode is exempt: its preallocated extents live in
         * relocation-class stripe runs that never share stripes with other
         * data, and relocation depends on its writes landing in place.
+        * With the stripe_alloc_nocow policy, an extent whose stripes are
+        * isolated to this inode may be written in place after all: the
+        * write hole it reintroduces can tear only the writing file's own
+        * data, which is the nodatacow/prealloc contract.
         */
        if (!btrfs_is_data_reloc_root(root) &&
-           btrfs_stripe_alloc_forces_cow(root->fs_info, io_start))
+           btrfs_stripe_alloc_forces_cow(root->fs_info, io_start) &&
+           !btrfs_stripe_nocow_writable(inode, io_start,
+                                        args->file_extent.num_bytes))
                goto out;
 
        /*
index 671c2c287ae283c681018d27d1813997d3be5011..2c76c29da4413321333e0655fe9dafdc63d23c6c 100644 (file)
@@ -117,6 +117,7 @@ enum {
        Opt_skip_balance,
        Opt_space_cache,
        Opt_stripe_alloc,
+       Opt_stripe_alloc_nocow,
        Opt_space_cache_version,
        Opt_ssd,
        Opt_ssd_spread,
@@ -240,6 +241,7 @@ 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_flag_no("ssd", Opt_ssd),
        fsparam_flag_no("ssd_spread", Opt_ssd_spread),
        fsparam_string("subvol", Opt_subvol),
@@ -491,6 +493,12 @@ 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);
+               break;
        case Opt_ratio:
                ctx->metadata_ratio = result.uint_32;
                break;
@@ -719,6 +727,11 @@ 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
@@ -1152,6 +1165,8 @@ 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 (btrfs_test_opt(info, DISCARD_SYNC))
                seq_puts(seq, ",discard");
        if (btrfs_test_opt(info, DISCARD_ASYNC))