Merge branches (11 topics incl. balance-resume) into 6.18/testing
Also merges '6.18/topics/delayed-ref-fixes' (resume the delayed-ref merge walk instead of restarting it).
Also merges '6.18/topics/logical-ino' (added 2026-09-19: BTRFS_LOGICAL_INO_ARGS_COMMIT_ROOT, an opt-in commit-root walk so LOGICAL_INO callers stop pinning the tree mod log).
btrfs: add BTRFS_LOGICAL_INO_ARGS_COMMIT_ROOT to resolve against the commit roots
The LOGICAL_INO ioctl is the only caller of iterate_extent_inodes() that
walks the live roots. Doing so attaches to the running transaction and
holds a tree mod log sequence for the whole walk, and while any sequence
is live every node-level tree change in the filesystem is recorded in the
tree mod log. The log is trimmed only below the oldest live sequence and
has no shrinker, so its size is the modification rate times the lifetime
of the slowest walker.
That product has no bound. A walk over an extent shared by 32047
snapshots takes most of a second on its own, and when the walker then has
to wait for the commit thread -- on the extent root, or on the mutex of a
delayed ref head with tens of thousands of pending refs -- it holds its
sequence for as long as the wait lasts. Meanwhile relocation of a block
group shared by all those snapshots COWs a path in every reloc tree for
every extent, and each root COW logs one element per key of the old root.
On a 16 GiB machine running a dedupe daemon that issues LOGICAL_INO
continuously, unreclaimable slab reached 13.6 GiB about an hour into such
a relocation, with four ioctl callers still holding their sequences behind
the commit thread; every allocation then went to direct reclaim with tree
locks held and the machine stopped responding.
Add a v2 flag, alongside BTRFS_LOGICAL_INO_ARGS_IGNORE_OFFSET, that makes
the walk use the commit roots, as scrub and send already do through the
same function. The answer lags the live tree by at most one transaction,
which a caller of this ioctl cannot distinguish from the live tree changing
after the call returns, and the walk no longer takes a sequence, logs
nothing, and does not contend with the commit thread for tree locks or
delayed ref heads. Callers that want the live view keep it by default.
btrfs: stripe_alloc: drop the mount options, the property is the interface
stripe_alloc and stripe_alloc_allow_rmw were mount options and filesystem
properties both. Upstream will not take new mount options for this, and the
two interfaces have to be kept in step by hand -- a bug class this series has
already paid for twice, once when the property skipped the mixed-block-group
refusal the option made, and once when the two disagreed about a cache-less
filesystem.
Remove the options. Nothing is lost:
- btrfs_stripe_alloc_check_support(), which the property calls, already
refuses everything btrfs_check_mountopts() refused -- v1 space cache,
zoned, mixed block groups -- and one thing more, a v1 cache still present
on disk from an earlier mount. So the mount-time block was the weaker of
the two checks, not the stronger.
- The runtime state is untouched. BTRFS_MOUNT_STRIPE_ALLOC and
fs_info->stripe_rmw_opt stay; btrfs_enable_stripe_alloc() and the
stripe_alloc_allow_rmw property already own them, and every
btrfs_test_opt(fs_info, STRIPE_ALLOC) in the allocator is unchanged.
- btrfs_parse_stripe_rmw() and btrfs_show_stripe_rmw() keep their other
callers in props.c and block-group.c.
The mount-time report collapses to one source, so it no longer has to say
which of two things turned the policy on, and btrfs_fill_super() no longer has
to sample the flag before open_ctree() to tell them apart. The remount path
loses its report with the option: a remount can no longer change the policy,
and the property announces its own changes.
Enabling the policy is now: mount, then
setfattr -n btrfs.stripe_alloc -v 1 /mnt
on the top-level root directory, which persists across mounts.
The "does not cover raid56 system chunks" warning moves from
btrfs_read_block_groups() to btrfs_enable_stripe_alloc(): block groups are
read before the root directory's properties apply, so with the option gone
the flag was always clear at the old call site and the warning could never
fire (bg-gate-test.sh caught it).
btrfs: resume the delayed-ref merge walk instead of restarting it
btrfs_merge_delayed_refs() restarts its walk of a head's ref tree from
rb_first_cached() every time merge_ref() reports a merge:
again:
for (node = rb_first_cached(&head->ref_tree); node;
node = rb_next(node)) {
ref = rb_entry(node, ...);
if (seq && ref->seq >= seq)
continue;
if (merge_ref(fs_info, delayed_refs, head, ref, seq))
goto again;
}
The restart is there because merge_ref() returns true only when it has
freed the caller's cursor node, so the caller cannot call rb_next() on it.
That makes the walk quadratic in the number of refs on the head, and it
runs with head->lock held -- a spinlock -- so it cannot be broken up.
On a filesystem doing continuous backref resolution this is not
theoretical. Each backref walk advances fs_info->tree_mod_seq, and
init_delayed_ref_common() stamps that seq into every new fs-tree ref, so
refs to the same block and root stop comparing equal and stop being merged
at insert time by insert_delayed_ref(). They accumulate as separate nodes
in one contiguous group instead, and the merge walk restarts across the
whole group on every cancelling pair.
Measured with the ftrace function profiler on a filesystem running a
dedupe agent and a verifier that both walk backrefs continuously, during a
stall, over a 60 second window:
comp_refs() is called once per node the merge walk compares, so that is
10,170 comparisons per walk. Solving the restart structure's N^2/2 puts
roughly 143 refs on a head. comp_refs() is a leaf, so its own total is
not distorted by nesting: 38.9 CPU-seconds of that 60 second window went
to comparing delayed refs and nothing else, with the transaction thread
pinned at 100% of a core and zero tasks anywhere in the machine waiting on
IO.
The cost is not subtle. Every other task queued behind the commit;
relocation of a single block group made no progress for three and a half
hours; snapshot deletion, which had been clearing about 49 subvolumes a
minute, stopped entirely. An earlier instance on the same machine
produced a 26 second soft lockup with the stack inside the merge walk.
merge_ref() does not actually need the caller to restart. It advances its
own cursor past a node before freeing that node:
so it always holds a position that survives the frees, and there are only
three ways it can end having freed the caller's node:
- it swapped, and the survivor keeps a non-zero ref_mod. The caller's node is
freed; the survivor is still in the tree and may still merge with what
follows, so the walk resumes at the survivor.
- the caller's ref_mod reached zero, with or without a swap. Both nodes are
gone, and the local cursor -- already advanced past the freed one -- is the
resume point, or NULL to end the walk.
Return whether @ref was freed, and hand the position back through an out
parameter. The two cannot be folded into one pointer: the resume position
is legitimately NULL when the freed node was last in the tree, and a NULL
return would then tell the caller its own node is still live. The walk becomes strictly forward and linear, and nothing is left
unmerged.
Skipping the re-examination that the restart performed does not lose
merges. comp_refs() keys on type and root-or-parent, neither of which any
merge changes, so merging cannot make two previously incomparable refs
comparable. Mergeable refs are contiguous, because the tree is sorted by
comp_refs(..., check_seq = true) and the merge key is the same comparison
without the seq -- a prefix of the sort key. And a ref skipped by the seq
test stays skipped, since seq is read once per btrfs_merge_delayed_refs()
call.
The restart has had this shape since commit 0e0adbcfdc90 ("btrfs: track
refs in a rb_tree instead of a list") in v4.15.
btrfs: stripe_alloc: hold a reference on the rbio being parked
rbio_try_park() publishes the rbio on stripe_hash_table->parked and then
keeps reading it: it drops parked_lock, queues the scan timer, and
rechecks rbio_is_full() to reclaim a park that a concurrent merge has
just filled. Nothing keeps the rbio alive across that.
A parked rbio has exactly two references, the initial one and the one
the stripe hash list took in lock_stripe_add(), and its completion
consumes both. So the moment parked_lock is dropped, a flusher
(btrfs_flush_parked_rbios(), unpark_ready_rbio(), or the park timer) can
take the rbio, start its RMW, and have it complete through
rbio_orig_end_io() -> unlock_stripe() -> free_raid_bio() -- while the
parking thread is still inside rbio_try_park(), about to dereference it.
lockdep sees it first, because rbio_is_full() takes rbio->bio_list_lock
and a freed rbio's lockdep key is gone:
INFO: trying to register non-static key.
turning off the locking correctness validator.
Workqueue: btrfs-rmw rmw_rbio_work
register_lock_class+0x565/0x570
__lock_acquire+0x3a8/0x23d0
lock_acquire+0xe5/0x330
_raw_spin_lock+0x3b/0x90
rmw_rbio_work+0x256/0x400
and 62 ms later the same worker falls over the list_head next to it:
list_del corruption, ffff8881810a56e0->next is NULL
WARNING: CPU: 3 PID: 715967 at lib/list_debug.c:52
__list_del_entry_valid_or_report+0x7b/0x150
Workqueue: btrfs-rmw rmw_rbio_work
That list_head is parked_node, at offset 0xe0 of struct btrfs_raid_bio.
The task then took a NULL dereference holding parked_lock, and every
other CPU piled into __pv_queued_spin_lock_slowpath() behind it: flushes
from transaction commit and the ordered-extent waiters, the park timeout
work, and other rmw_rbio_work()s. The box wedged with all CPUs spinning.
Most interleavings survive by luck, which is why this is rare: the
recheck tests RBIO_PARKED_BIT, and a legitimate unpark clears that bit
under parked_lock, so the stale thread usually just sees it clear and
returns. It stops being luck once the freed object has been reused.
Take a reference before publishing and drop it after the recheck. It
has to belong to the parking thread rather than to the parked list: a
list-owned reference is inherited and dropped by whoever unparks the
rbio, and that is precisely the thread racing us, so it protects
everything except the window it was meant to protect. This one cannot
be consumed by anyone else.
Dropping it is never the last put -- a parked rbio still owns the stripe
lock, and only the RMW a flusher starts after taking it off the list
completes it -- so the free still happens where it always did.
btrfs: stripe_alloc: say at mount whether the policy is on
Two paths turn stripe-exclusive allocation on and neither announced it.
The mount option sets the flag while btrfs_emit_options() has no entry to
print for it, and the btrfs.stripe_alloc property applies later still,
when btrfs_fill_super() reads the root directory's inode and its
properties. btrfs_enable_stripe_alloc() did print a line, but only on
the property path, and even there it is skipped when the option already
set the flag, because the function returns early in that case.
So the log said nothing. An absent line meant "off" and "on via the
mount option" equally well, and the only way to answer the question was
to read /proc/mounts on a live filesystem -- no use when the filesystem
is someone else's, or when all that is left is a log from before a crash.
Analysing a lockup from a filesystem whose btrfs.stripe_alloc property
was set, I read that silence as the policy being off and said so, which
was wrong twice over: the property was set, and the option would have
been just as invisible.
State it once in btrfs_fill_super(), after the root inode has been read
so the property has had its say, naming which of the two turned it on.
The option is sampled before open_ctree(), not merely before that read:
the mount context has already reached fs_info by the time
btrfs_fill_super() runs, and open_ctree() itself goes far enough into the
mount to read the root directory's inode and apply the property, so a
sample taken after it reports every property-enabled filesystem as an
option-enabled one. The directed test caught exactly that. A remount that changes the flag reports the
change as well. The property path keeps its own line for a set on a live
filesystem, where there is no mount to report; at mount it now stays
quiet so the line is not printed twice.
Nothing is printed when the policy is off, which keeps the common mount
quiet and makes the absence of the line mean exactly one thing.
Testing aid. The subject is marked so it stands out in git log --oneline
and gets dropped from the series before it is submitted; it carries the
zygo: prefix the lane uses for local-only commits as well.
The boot-time benchmark decides which gen_syndrome runs, so a bug in one
variant (the x1 preload of dptr[z0-1], fatal with one data disk) shows
up only on the boots where that variant happens to win. Let the test
rig pin it: raid6_pq.algo=avx2x1 selects that implementation without
benchmarking; an unknown or unusable name falls back to the benchmark.
It lives on this topic branch so that the pin the test rigs boot with
keeps working across rebases. Kept here after 2026-09-17, when it
turned out that carrying it only as a per-candidate tip commit had
silently stopped working: the rebase onto v6.18.52 rebuilt the lane from
its topics, so the knob went away while both rigs' grub entries and every
build script kept passing raid6_pq.algo=avx2x1. An unknown
<module>.<param>= is ignored without a word, so the variant was quietly
back to whatever the benchmark picked, and a raid6 pass would have
claimed a pin it was not applying. The validation driver's preflight now
refuses to run when a tree carrying this knob boots with a pin that the
running kernel does not honour.
btrfs: stripe_alloc: let the property accept a cache-less filesystem like the mount option does
The "stripe_alloc" filesystem property and the mount option are meant to
be two ways to turn on one policy, and they answer the same questions
about the filesystem -- except one. btrfs_check_mountopts() refuses the
option only for space_cache=v1: the v1 cache inode is nodatacow and
preallocated, so a commit overwrites it in place, a sub-stripe write
into committed stripes that no csum would ever show. The free space
tree and no cache at all are both fine, and nothing in the allocator
reads either. btrfs_stripe_alloc_check_support(), which the property
calls, demanded the free space tree instead, so a filesystem mounted
with nospace_cache took the option but refused the property, and the
error it gave named a requirement the option does not have.
Refuse what the mount path refuses: the v1 cache, both the live option
and cache inodes still on disk from an earlier mount, which
btrfs_read_block_groups() already refuses for the option (the property
can arrive on a live filesystem, after that check ran). The two paths
now agree, and bg-gate-test.sh's property cases hold them to it.
btrfs: stripe_alloc: release open stripe runs when a mount fails or an aborted filesystem unmounts
A block group with open stripe runs holds a reference for as long as it
is on fs_info->open_stripe_bgs, and only the commit-time retirement,
having released the group's last run, drops it. Two paths never commit
again: a mount whose log replay fails after it has already allocated
tree blocks (each allocation opened a metadata run), and the unmount of
a filesystem whose transaction aborted. btrfs_free_block_groups() then
finds the leftover references and asserts:
BTRFS: error (device loop0 state EAO) in btrfs_replay_log:2072:
errno=-5 IO failure (Failed to recover log tree)
assertion failed: refcount_read(&block_group->refs) == 1 :: 0, in
fs/btrfs/block-group.c:7479
kernel BUG at fs/btrfs/block-group.c:7479!
RIP: btrfs_free_block_groups.cold
open_ctree
btrfs_get_tree.cold
Hit by the raid56-metadata fsync-window characterization: a degraded
mount whose log replay got as far as allocating before it met the torn
log block, where an earlier run of the same case had failed while
walking the log and never allocated. The mount task died holding the
superblock lock and the next mount of the same devices hung on it.
Release every open run and the references before the block groups are
freed, on both paths. Nothing is in flight there: workers are stopped
and the roots dropped; a run that still counts bytes in flight is
reported and released anyway.
btrfs: raid56: do not call a covered stripe_meta group uncovered
report_uncovered_rmw() predates stripe_meta: it treats every metadata
read-modify-write as a write to a group the allocator does not cover
and says so, rate limited, on each one. With raid56 metadata under
stripe_alloc those writes are partial writes inside one transaction's
run -- the audit it calls first classifies them (meta_rmw_cur) and
warns on its own if one ever rewrites parity over a committed block --
so the message told users of the covered configuration that the write
hole applied to them on every commit:
read-modify-write of full stripe 42467328: this block group is not
covered by stripe_alloc, the raid56 write hole applies to it
Keep the counter and the audit, and keep the message for what it was
written for: system chunks and mixed groups.
btrfs: stripe_meta: sync the open-run remainder when the commit retires metadata runs
btrfs_retire_meta_stripes() closes the committing transaction's metadata
runs and returns their tails, which shrinks the group's open-run
remainder, but never folded that delta into the space_info's
bytes_stripe_open: the next allocation from the group carried it, so
the counter only lagged by a commit while the policy was on. After a
runtime disable nothing allocates from the group again and the
remainder of the runs the last commit retired stayed counted as used
metadata for the life of the mount (147456 bytes in
stripe-meta-toggle-test.sh, after the disable had already zeroed the
group's trapped and claimable bytes).
Sync after the closes, as the data-side retirement does.
btrfs: stripe_alloc: return a group's allocation cluster before scanning its stripes
The legacy allocator moves free space entries of a block group into an
allocation cluster -- for a metadata group, typically its whole initial
free extent -- where the group's own free space tree no longer lists
them although ctl->free_space still counts them. The stripe scan walks
that tree, so on a group armed at a runtime enable after the legacy
allocator had used it, the scan saw a fraction of the free space and
the debug check that every free byte lands in exactly one stripe
fired:
Without the assertion the group would have been armed with almost all
of its free space neither trapped nor claimable. Data groups were only
spared by the test rig: the legacy allocator clusters data only under
ssd_spread, and every stripe_alloc run so far has mounted without it.
Stripe-exclusive allocation never uses clusters, so hand the cluster's
entries back to the group before the scan, both when arming and at the
commit-time rescan (a legacy allocation already past the policy check
at the flip can still set one up). Found by stripe-meta-toggle-test.sh
on the first candidate that armed metadata groups at a runtime enable
(raid5 metadata, 4 devices, mkfs-time metadata chunk).
btrfs: stripe_alloc: charge the preallocation's stripe hold under the reservation lock
A preallocation is admitted for round_up(bytes, stripe) against the
whole-stripe supply and then holds one stripe in bytes_stripe_margin
until its allocations land, but the hold was added by the caller after
btrfs_alloc_data_chunk_ondemand() returned: __reserve_bytes() recorded
only the raw bytes in bytes_may_use and dropped space_info->lock in
between. Two sub-stripe preallocations admitted in that window each saw
the other's raw bytes only, both passed against one stripe of supply,
and a buffered write admitted alongside them could find no stripe at
writeback. The delalloc path does not have this gap (its margin is
probed into bytes_may_use with the bytes and kept there); make the
preallocation path the same.
Charge the hold inside __reserve_bytes(), in the critical section that
grants the bytes -- for a direct admission and for a ticket granted
later by btrfs_try_granting_tickets() -- and hand it back through a new
btrfs_alloc_data_chunk_ondemand_held() /
btrfs_reserve_data_bytes_held() to the four preallocating callers
(fallocate's range loop, zero-range, relocation's cluster preallocation,
the encoded write). The release is unchanged. Found by review
(2026-09-15); prealloc-race-test.sh (eight fallocate() storms against
buffered writers at the fill edge) did not reproduce the window in six
rounds before the change and stays clean after it.
btrfs: stripe_meta: carry raid56 metadata groups through a runtime enable or disable
The property path arms and disarms the trapped-space accounting of the
cached block groups, but stripe_alloc_sweep_groups() walked only DATA
space_infos: a runtime enable left already-cached raid56 metadata groups
unarmed until something else (a new chunk, a read-write transition)
initialised them, so their trapped and claimable bytes were missing from
metadata admission; a runtime disable left armed metadata groups' bytes
in the space_info after the option was gone. Walk METADATA space_infos
too, for both directions.
Two more things a runtime toggle needs under raid56 metadata:
The metadata retirement at commit returned at once without the option,
so after a disable the running transaction's open metadata runs were
never closed or drained and their block group references were held
until unmount, where btrfs_free_block_groups() asserts on them. Retire
unconditionally; the walk is over the list of groups with open runs and
is empty when there is nothing to do.
And the enable-time drain that keeps the write-hole check quiet until the
legacy allocator's writes have landed only waited for data (delalloc and
ordered extents). Tree blocks the legacy allocator placed in the
transaction that was running at the flip are written by that
transaction's commit, into the partly used stripes they were allocated
in -- under raid56 metadata that commit is the last legacy
read-modify-write, and the metadata RMW reporter says so:
read-modify-write of stripe 42860544 rewrites parity over committed
tree block 42860544 at generation 10 ... while writing generation 11:
write hole
btrfs: sub-stripe write to stripe 42860544 (block group 34603008)
outside any live stripe run: write hole window violated
WARNING: CPU: 1 PID: 408294 at fs/btrfs/block-group.c:2889
btrfs_stripe_check_write+0x109/0x150
Commit that transaction from the drain worker before arming the check.
The exposure of the enabling transaction itself is inherent -- its
blocks are already placed -- and bounded to that one commit.
Found by stripe-meta-toggle-test.sh (runtime enable with a fill to the
metadata edge, then a property-enabled filesystem disabled and
unmounted), on raid5 and raid6 metadata.
btrfs: stripe_alloc: saturate the read-only guard's claimable subtraction
The guard that keeps admitted bytes placeable when a group goes read-only
subtracts the group's claimable bytes from the space_info's total. The
two are not an atomic snapshot: stripe_claimable_mod() adds a delta to
bg->stripe_claimable under the free-space tree lock and takes
sinfo->lock only afterwards to add it to the aggregate, and
inc_block_group_ro() holds sinfo->lock and the group's lock but not the
tree lock, so it can read a group value that is ahead of the total. The
unsigned subtraction then wraps to a huge supply, the guard admits the
transition, and the group's stripes leave the supply under writers the
gate already admitted -- the writeback allocation failure and data loss
the guard exists to prevent.
Saturate the subtraction. Found by review (2026-09-15), not by a test;
the window is a few instructions wide, and the fill-edge and reclaim
tests would report it only as a rare unexplained drop.
btrfs: stripe_alloc: hold the probe margin until the metadata reservation charges it
btrfs_check_data_free_space() admits a write for its bytes plus a
whole-stripe margin per extent, then releases the margin at once, on the
understanding that btrfs_delalloc_reserve_metadata() re-charges it into
bytes_stripe_margin moments later. Between the two nothing holds it:
at the fill edge, eight fsstress writers were admitted into each other's
released margins, and the stripes their claims then needed had already
gone to their neighbours. The failure-time dump showed exactly that
shape -- claimable 0, bytes_may_use holding only the failing write, one
stripe of margin per outstanding extent, nothing stranded -- and it
survived charging preallocation by whole stripes and holding a stripe
across it, because it was never preallocation's stripe.
Let the callers that go on to reserve metadata keep the probe's margin
in bytes_may_use and release it only after that reservation has charged
bytes_stripe_margin: the buffered write, page_mkwrite, the block
truncation, the direct IO path (carried in btrfs_dio_data across the
two functions), the log-tail carry and btrfs_delalloc_reserve_space().
The one caller that reserves no metadata, the v1 space cache write-out,
keeps the immediate release.
btrfs: stripe_alloc: hold a stripe of margin across a preallocation
Admission now charges a preallocation the roundup of its bytes to whole
stripes, but that charge lives only for the check: between the
reservation and the allocation only the raw bytes sit in bytes_may_use.
fallocate() is quick, but eight fsstress processes preallocating at the
fill edge were quick together, and each in-flight preallocation short of
a stripe still took a stripe the gate had counted for someone else: the
failure-time dump kept showing admitted writes with the supply gone.
Do for preallocation what the delalloc margin does per outstanding
extent: hold one stripe in bytes_stripe_margin from the successful data
reservation until the caller's allocations are done, at the four users
of btrfs_alloc_data_chunk_ondemand() -- fallocate's range loop, the
zero-range preallocation, relocation's cluster preallocation and the
encoded write's extent reservation. Released as soon as the claims have
landed, so the hold costs nothing beyond that window.
btrfs: stripe_alloc: admit data reservations by whole stripes
An allocation of N bytes that has to open a stripe run claims
round_up(N, full stripe) whole stripes. A delalloc write is admitted
with a full-stripe margin per outstanding extent, which covers that
roundup for itself, and relocation is admitted for round_up(N) + 1
stripe. A preallocation was admitted for N alone: fallocate() has no
outstanding extent and holds no margin, so each one short of a stripe
took up to a stripe more from the whole-stripe supply than the gate had
charged. The supply behind writes admitted earlier shrank by that much
and at the fill edge their claims found nothing.
With the stranded-stripe accounting and the relocation margin in place,
the failure-time dump on a degraded fsstress fill showed exactly this:
every failed write had bytes_stripe_claimable 0, bytes_may_use equal to
its own size, one stripe of margin per outstanding extent, nothing
stranded, no group read-only and no relocation group dedicated -- the
gate's arithmetic was consistent and still the stripes were gone, with
a preallocation's reservation in flight in the dump.
Charge every non-relocation admission the roundup to whole stripes. The
excess is not held past admission (a preallocation allocates in the same
call), so a concurrent admission in that window can still overrun by a
stripe; holding it would need the reservation to carry the roundup until
the allocation lands, a later refinement.
btrfs: stripe_alloc: charge the held stripe margin against every admission
The data admission gate is meant to refuse a write at reservation time
when the whole-stripe supply cannot cover it, so that write() returns
ENOSPC instead of writeback finding no stripe later. It compared
bytes_may_use + bytes against bytes_stripe_claimable and left out
bytes_stripe_margin, the whole-stripe collateral every outstanding data
extent holds against the commit that closes its open run and traps the
tail. The margin is charged against total_bytes through
btrfs_space_info_used(), but not against the supply the admitted bytes
actually draw on.
Each commit shrinks that supply by the trapped tails, with nothing
holding it, so at the fill edge a write that passed admission reached
find_free_extent() and failed: "data writeback allocation of 4096 bytes
returned ENOSPC despite reservation margin; buffered data in this range
will be dropped", followed by the cow_file_range() failures 35 seconds
later in the same fill. Relocation was admitted the same way, against
supply the margin was reserving, and at the fill edge of a degraded
acceptance run it consumed the whole stripes that small writes' claims
then needed: about fifty 4-32 KiB writes per run failed at writeback in
bursts that tracked the block group relocations to the minute.
The margin is owed to writers already admitted, so every later
admission must respect it: add the held margin to the admitted bytes on
both the ordinary and the relocation path. This over-refuses by the
margin, which the commit rescan raising bytes_stripe_claimable and the
FLUSH_DATA ticket retry heal, and stays within phase 1's pessimistic
contract; relocation that cannot be covered waits on its ticket like
anyone else, and the margin returns as the writes complete.
btrfs: stripe_alloc: count nocow run lifecycles and stranded stripes in sysfs
Add a stripe_run_stats file beside stripe_park_stats with lifetime
counters for the paths the previous two patches touched, so a test can
show that it exercised them rather than merely passed: private NOCOW
runs opened, claim candidates refused for overlapping a live run, bytes
of whole stripes the scans found stranded behind live runs, and rescans
requested by a freed run to credit its stripes back.
btrfs: stripe_alloc: give nocow extents a private run only while they may be written in place
Preallocated extents and a nodatacow inode's extents are steered into a
per-inode NOCOW-class stripe run, so that their stripes never hold any
other file's data and an in-place write can be confined to the file that
waived COW protection. Such a run survives transaction commits, so a
slowly appended nocow file does not burn a stripe per commit.
Both the steering and the survival were unconditional, but whether a
nocow extent can be written in place at all is decided by the
stripe_alloc_allow_rmw policy, and with nothing waived (the default)
btrfs_stripe_nocow_writable() forces COW for every such write. The
private runs then hold only ordinary COWed extents and unwritten
preallocation, never in-place data, and their survival protects nothing:
it only keeps the run object alive until the inode is evicted, and every
whole stripe freed inside the run's range meanwhile -- a prealloc extent
overwritten by a COWed one, or unlinked -- is stranded, counted free but
refused to every claim. An fsstress run on a filesystem filled to
ENOSPC left 387 and 976 such runs in single block groups, holding 99% of
the group's free whole stripes.
Steer into a NOCOW run only when the policy lets that kind of extent
(prealloc or nodatacow) be written in place, and let a NOCOW run outlive
commits only when the policy allowed in-place writes when the run was
opened. The verdict is recorded in the run at open time so the commit's
close and settle predicates cannot disagree with each other when the
policy changes underneath them: a run opened under one policy keeps its
lifecycle until it is closed by filling up, by the inode's eviction or
by a forced quiesce.
btrfs: stripe_alloc: count whole stripes stranded behind live runs as trapped
The data admission gate admits a reservation while the whole-stripe
supply, bytes_stripe_claimable, covers it, and the per-commit scan
measures that supply as the block group's wholly free full stripes.
But a wholly free stripe inside a live stripe run's range cannot be
claimed: btrfs_stripe_run_range_usable() rejects any claim overlapping
a run object, because a second run over the same range would make the
range-to-run lookups ambiguous. The run was claimed over the stripe,
the extents allocated there were freed again, and the run has not been
freed yet. For a COW run that window closes at the next commit; a
nodatacow inode's private run deliberately survives commits and lives
until the inode is evicted.
On a raid56 filesystem filled to ENOSPC by fsstress (thousands of
fallocated files, so thousands of cached inodes each holding a private
NOCOW run) a live walk of the free space cache showed the accounting
exact -- the wholly free stripes summed to bytes_stripe_claimable to the
byte -- and 99% of them inside the ranges of open NOCOW runs with no
inflight IO: 22.8 MiB of 22.8 MiB behind 387 runs in one group, 73.4 MiB
of 73.4 MiB behind 976 runs in another. Every claim failed in O(1) on
the "no claimable run" verdict, every write() admitted against the 99 MiB
of phantom supply failed at writeback, and the data was dropped ("data
writeback allocation ... returned ENOSPC despite reservation margin"),
about 100,000 delalloc ranges per run of the degraded acceptance suite
where the stock allocator drops none.
Have the scan take such stripes out of the claimable supply: count them
in stripe_unusable, which btrfs_space_info_used() already charges so the
gate cannot admit against them, and record them separately as
stripe_stranded so that btrfs_stripe_bg_wants_reclaim() does not ask
relocation to recover space that comes back by itself. When a run is
freed, mark its group for rescan so the next commit credits the stripes
back. The space_info total is exposed as bytes_stripe_stranded in sysfs
and in the space info dump.
This makes the gate honest -- write() gets the ENOSPC instead of
writeback dropping the data -- but does not return the stranded space
while the runs live; that is the run lifecycle's business.
btrfs: stripe_alloc: do not read an inline backref off a keyed-ref extent item
stripe_extents_owned_by() decides whether every extent in a stripe range
belongs to one inode by reading the single inline backref of each data
extent item. A data extent's only backref is keyed rather than inline
when the leaf had no room to grow the item: insert_inline_extent_backref()
returns -EAGAIN and insert_extent_backref() adds a separate
EXTENT_DATA_REF item. Such an extent item ends right after the header,
so the walk read the next item's bytes as an inline ref and
btrfs_get_extent_inline_ref_type() warned:
The answer was already the conservative "not owned" (the garbage type is
not EXTENT_DATA_REF_KEY), so only the warning and the taint were wrong.
Check the item size before reading the inline ref and treat a keyed-ref
extent as not owned.
btrfs: stripe_alloc: tell a dead tree block from a live one in the metadata RMW audit
The audit reports any older-generation header under a read-modify-write
as a write hole, and counted over six hundred per metadata fill test.
Since runs close at every transaction and freed stripes are reclaimed
whole, most such headers belong to blocks that died with their stripe:
the stripe was claimed fully free, the dead blocks kept their headers,
and a torn parity write over them damages nothing a reader can reach.
Ask the committed extent tree, the witness a degraded read after a
crash would consult, whether the block is still referenced. Count and
report a live one as before (now with its bytenr and reference count),
and count a dead one as meta_rmw_ghost without a warning.
A sector the run has already re-allocated in the current transaction
carries an extent item of the current generation for the new block
while the old header is still on disk (the new write has not landed):
that is not a live block either, so the item generation must match the
header generation. Without that check every single-stripe run of the
current transaction reported its dead predecessors as holes (90 of 135
warnings in one fill test; the other 45 were the raid5 system chunk).
btrfs: stripe_meta: let a group go read-only for relocation against the reserve
The read-only guard keeps admitted bytes placeable by refusing to take a
group's usable free space out of the supply when the space_info could
not absorb it. With the whole-stripe reserve counted as used, that
refusal comes by the width of the reserve at the fill edge: two
eligible metadata groups sat marked for reclaim through a 300 second
wait and the worker dropped each on inc_block_group_ro() -- the
space_info was 7 MiB short of the check with 376 MiB of whole stripes
claimable, 352 MiB of them the reserve that exists for this move.
Going read-only is the first step of relocation, so let the guard draw
on the reserve the way relocation's own reservations do.
btrfs: stripe_meta: let the transaction machinery's own reservations draw on the reserve
With the whole-stripe reserve counted as used for everyone but the
relocation task, a committing transaction whose handle reserve runs dry
mid-COW gets ENOSPC from btrfs_use_block_rsv(): its NO_FLUSH fallback
reservation is refused as soon as the claimable supply is below the
reserve, and the global reserve behind it has been refilled by fiat
against stripes that do not exist. Seen on the delete phase of the
metadata fill test with relocation running: claimable 163 MiB, reserve
352 MiB, delayed refs freeing the deleted leaves, and the extent tree
COW that freeing needs aborted the transaction with ENOSPC while
163 MiB of whole stripes sat unused (no allocator canary: the allocator
was never asked).
The reserve exists to hold back user operations so relocation and the
commit that follows it have room; it must not hold back the commit.
Let NO_FLUSH, FLUSH_LIMIT (delayed refs, delayed inodes) and EMERGENCY
reservations draw on it like relocation does. They are bounded and
kernel-internal; user reservations (FLUSH_ALL, ALL_STEAL, EVICT, DATA)
still stop at the reserve.
btrfs: stripe_meta: do not steal from the global reserve below the relocation reserve
With the whole-stripe reserve in place the fill stopped cleanly at the
edge: 374 MiB claimable, the reserve plus margin plus the outstanding
reservations, no canary, no abort. Then the test deleted half of its
tiny files, and every unlink that the gate refused came back through
the global reserve steal, which does not look at stripes. Deleting an
inline file is pure COW churn under stripe_meta: it frees no stripe,
since the leaf it lived in stays, and each transaction of unlinks takes
fresh stripes. Metadata used did not move by a megabyte while the
claimable supply went from 374 MiB to zero, and the next commit aborted
with the canary ("claimable 0 open 1490944 trapped 1506082816").
Refuse the steal once the claimable supply is down to the relocation
reserve plus the claim margin. The unlink fails with ENOSPC, which is
recoverable, instead of the abort, which is not, and the reserve stays
with the background relocation that is the only thing that turns
trapped space back into whole stripes.
btrfs: stripe_meta: hold back whole stripes so tree blocks always land and trapped groups can be reclaimed
Two failures at the metadata fill edge under stripe_meta, both seen
with the tiny-file fill test on raid5 metadata.
The first is a transaction abort. A tree block is reserved in bytes
and admitted while claimable whole stripes cover the outstanding
reservations, but it is placed in whole stripes: the open run it would
have joined is closed at the transaction boundary, so every group
claims at least one fresh full stripe per transaction. The last few
megabytes of claimable supply go to those claims, made for reservations
admitted earlier, and the next admitted tree block finds no stripe at
all ("stripe_meta: tree block allocation of 16384 bytes returned
ENOSPC: claimable 0 open 1622016 trapped 1614266368"), which aborts the
transaction. Pre-pay two transactions' worth of claims -- twice the
number of writable groups times the widest full stripe -- in the
space_info's stripe margin, recomputed at each commit scan. Best-fit
run selection opens a fresh run only when a group's runs are full, so
this is a few megabytes, not a fraction of the space_info.
The second is a filesystem that quietly runs out at half capacity.
COW frees trap stripes rather than free them: a stripe is claimable
again only once every tree block in it is dead, which random deaths in
a sixteen-block stripe essentially never achieve, so the METADATA
space_info settles around half used, half trapped and no claimable
stripe left (3.04 GiB metadata, 49% used, fs not writable, deletes
included). Only relocation packs live tree blocks back into whole
stripes, and it needs the moved group's live bytes claimable elsewhere
before it starts -- by the time the trigger fires there is nothing
left. Hold back a reserve of whole stripes -- the largest writable
group's length, capped at a quarter of the space_info, nothing with a
single group -- from every metadata reservation except those made by
the relocation task, identified by a task pointer that reloc_ctl
already implies. Reservations admitted against the reserve reach the
same tickets, so the flag travels with the ticket. The reserve is an
accounting hold-back, not a partition: a committing transaction still
allocates from those stripes when it must, and the next scan restates
the reserve from what remains. Both counters are cleared when
stripe_alloc is turned off, since the scan that maintains them stops
with it.
The reserve is reported through sysfs as bytes_stripe_reserve and in
the space_info dump, whose stripe_claimable and stripe_margin values
were printed under each other's label.
btrfs: stripe_meta: back the global reserve with usable stripes, and say when a tree block finds none
With trapped and open-run bytes counted as used in the METADATA
space_info, the overcommit check already admits tree block reservations
only against whole free stripes plus unallocated space, so no separate
metadata gate is needed. Two loose ends remain.
btrfs_update_global_block_rsv() forces a chunk allocation when the
global reserve is at least the space_info's total, but under stripe_meta
the reserve can only be spent in whole stripes: compare it against the
capacity minus the trapped bytes so a metadata chunk is allocated while
unallocated space still exists, instead of the reserve running dry at
the fill edge.
And when a raid56 stripe_meta tree block allocation does fail with
ENOSPC -- which aborts the transaction -- print the stripe counters,
the metadata twin of the data-side "returned ENOSPC despite reservation
margin" warning, so an accounting hole is identifiable from the log.
btrfs: stripe_meta: maintain trapped free space for metadata groups like data
btrfs_block_group_init_stripe_unusable() arms stripe_meta groups and adds
their trapped bytes to the METADATA space_info, but the disarm, rescan
and commit-time scan only accept btrfs_is_stripe_alloc_bg() (DATA), so
a metadata group's contribution was frozen at its first value: never
updated as tree blocks were freed inside stripes, never dropped when the
group went read-only or was removed. Tree blocks can only be placed in
fully free stripes, so the trapped bytes are as unusable as for data and
need the same maintenance.
Accept stripe_meta groups on the three maintenance paths, let the commit
scan sum METADATA space_infos, and apply the read-only subtraction to
the metadata branch of inc_block_group_ro() as well.
btrfs: stripe_alloc: relocate a queued stripe group even while it has reserved or pinned bytes
The reclaim worker defers any group that has bytes reserved or pinned,
on the theory that it was queued for falling below the used threshold
and may be about to go empty on its own. A stripe_alloc or stripe_meta
group is queued for the opposite reason -- free space trapped in
partially used stripes, which no amount of waiting frees -- and under
COW churn a metadata group has blocks reserved or pinned at every
moment, so the worker skipped every one of seven queued metadata
groups for the whole of a fill-to-the-edge test and relocated none.
Relocation already waits for in-flight allocations and writers on its
own; take the group.
btrfs: stripe_alloc: hand groups full of trapped free space to the reclaim worker
Zoned filesystems mark a block group for reclaim as soon as its
unusable bytes reach bg_reclaim_threshold percent of its capacity, so
space that can no longer be written is recycled without an operator
running balance. stripe_alloc had no equivalent. The non-zoned
trigger consults the threshold only where a free drops a group's used
bytes across it, and the test hosts were already running every mount
with bg_reclaim_threshold=75 when this was measured: trapped tails
still accumulated until balance (1.86 GiB of 6.5 GiB after a few hours
of churn), while every byte-based check counted them as free, because
that predicate keys on used bytes only and fires only at the crossing
-- a group hollowed out after it was already below the threshold, or
one whose used bytes never moved while its stripes trapped, is never
queued, and a group at the same used fraction with nothing trapped is.
Mark a group for the existing reclaim worker when its trapped bytes
reach the threshold; the worker re-checks with
should_reclaim_block_group(). The predicate, factored out as
btrfs_stripe_bg_wants_reclaim(), compares the trapped bytes with the
group's free space (length minus used) rather than its length -- a
stripe_alloc group also holds claimable whole stripes, and in practice
fragmentation settles around two thirds of the group trapped with a
fifth used, nothing left worth writing into yet below any sensible
fraction of the length -- and requires room for the group's live data
in the rest of the claimable supply after the bytes already promised to
admitted writers, so a group that cannot be relocated is left alone
until space is freed instead of being retried every commit while the
read-only transitions starve writers.
The trigger runs from the cleaner, which the transaction thread wakes
every cycle whether or not a transaction exists, as well as at the
commit-time rescan: on an idle filesystem nothing rescans, and a group
already on another list (a chunk created in the running transaction is
still on trans->new_bgs) cannot be linked to the reclaim list, so the
five-minute hysteresis stamp is taken only when the mark actually
lands. The worker re-checks whatever it takes, so a stale read costs at
most one wasted attempt. bg_reclaim_threshold defaults to 0 on
non-zoned filesystems, so nothing changes until the sysfs knob is set;
the default is left for review. Relocation needs inc_block_group_ro()
to succeed, hence the earlier fix.
btrfs: stripe_alloc: let a data write fall back to the relocation group rather than drop
Dedicating a group to relocation takes its claimable stripes away from
every other writer at once. Admission stops counting them from then
on, but a write admitted a moment earlier against exactly those stripes
still has to be placed, and with the group hard-excluded it was not:
ten milliseconds after "relocating block group 2976120832" a 16K
writeback allocation returned ENOSPC and its data was dropped, the one
drop left in a fill-to-the-edge run under reclaim. The read-only
transition has a guard for this; the dedication had none.
Keep the exclusion for the allocator's first passes and lift it once
the search reaches the chunk-allocation loop, i.e. only when no other
group can serve the write. The class tag on the stripe runs already
guarantees the two sides never share a stripe, so the fallback costs
relocation a few stripes, not its correctness. Zoned's hard exclusion
follows from append-only zones, which do not apply here.
btrfs: stripe_alloc: keep the relocation group's stripes out of the admission supply
do_allocation_stripe() dedicates one block group to data relocation
(fs_info->data_reloc_bg, as the zoned allocator does): while a
relocation runs, its allocations go only there and everyone else's skip
it, until the relocation finishes or the group fills. The admission
gate did not know: it counted that group's claimable stripes in the
supply for ordinary writes. At the fill edge with background reclaim
running, the dedicated group was the only one with claimable stripes
left, ordinary writes were admitted against them, and their writebacks
were dropped:
stripe_alloc DROP DUMP: sinfo may_use 3837952 claimable 561053696 ...
bg 7048265728 ... claimable 561053696 ... runs open <- data_reloc_bg
every other group: claimable 0
Track the dedicated group's claimable bytes in the space_info
(bytes_stripe_claimable_reloc: set when a group is dedicated, cleared
when the dedication is released or the group is removed, followed by
the per-group claimable deltas and re-summed by the commit rescan) and
admit ordinary reservations against the supply minus that amount.
Relocation itself is admitted against the whole supply, dedicated group
included. Confining it to the dedicated group's bytes was tried first
and refused relocation as soon as that group -- the first one the
allocator visits, often holding only a couple of free stripes -- ran
short of a reservation's whole stripes plus one, while hundreds of
megabytes of whole stripes sat in other groups: the allocator would
have dropped the dedication and moved on had it been asked, but the
gate failed each queued group with ENOSPC within fifteen milliseconds
and the reclaim worker retried every thirty seconds, with no relocation
ever completing. bytes_may_use is one counter for both sides, so the
sum admitted still fits in the supply.
btrfs: stripe_alloc: charge relocation a margin per reservation, not per extent
The stripe margin is one full stripe per outstanding extent, collateral
for the tails a commit traps under delalloc that has not been placed
yet. The data relocation inode charged it too, for extents that are
already allocated: relocating a group of 16-48K files inflated
bytes_stripe_margin to 150 MB, more than the claimable supply, so the
next cluster reservation failed at once and the reclaim trigger retried
the same six groups 130 times in 15 minutes while the margin swung
between zero and 150 MB with nothing dirty.
Do not charge the per-extent margin for the data relocation root.
Instead, when a relocation reservation is checked against the
whole-stripe supply, round it up to whole stripes and add one stripe of
collateral, which is what relocation's own open runs can lose at the
next commit.
btrfs: stripe_alloc: give a queued stripe group a chunk to relocate into when the supply is short
A relocation data reservation never takes a ticket, so the flush state
machine never runs for it, and with it goes the chunk allocation every
other data reservation gets when its space_info is full. That did not
show while other groups still had whole stripes. Once the reclaim
worker had emptied and removed five of seven data groups, the two left
were the trapped ones it was queued to move next, their claimable
supply was under a megabyte, and every relocation failed with ENOSPC
while 2.6 GiB of the devices sat unallocated.
Have the reclaim worker allocate one chunk before it relocates a stripe
group whose live bytes exceed the claimable supply of the other groups.
An earlier version did this from the reservation path instead, for any
refused relocation reservation; that let a live device shrink -- which
relocates into space it does not have and is meant to fail cleanly at
that reservation -- grow the data space_info chunk by chunk until the
raid1 metadata had nowhere to go and the relocation aborted the
transaction in merge_reloc_roots() (6.18 acceptance suite T6). The
worker is the one caller that should be making room.
btrfs: stripe_alloc: never let relocation wait on the whole-stripe gate
The data admission gate refuses reservations that would not fit in the
whole-stripe supply, and a refused reservation waits as a flush ticket
for space to appear. Relocation reserves its prealloc clusters through
the same path, so at the fill edge the reclaim worker sat for half an
hour in
holding the exclusive operation and the group it was emptying, while the
gate waited for exactly the stripes that relocation was supposed to
free. User writers queued behind that head ticket and unmount hung on
their writeback.
Exempting relocation from the gate altogether is wrong too: it then
takes whole stripes that admitted writers were promised, and their
writebacks are dropped at the edge. Give relocation its own flush type,
BTRFS_RESERVE_FLUSH_DATA_RELOC, that is subject to the same bound but
never waits: if the reservation does not fit beyond the bytes already
promised it fails at once, the relocation is abandoned cleanly and is
retried when the trigger next finds room. Zoned relocation already
has a flush type of its own for the same reason.
btrfs: stripe_alloc: keep the admitted bytes placeable when a group goes read-only
inc_block_group_ro() checks that the rest of the space_info can absorb
the group's free space in bytes. Under stripe_alloc the data admission
gate promised writers whole stripes: bytes_may_use plus the held margin
against bytes_stripe_claimable. Making a group read-only removes its
claimable stripes from that supply after the promise was made, so with
the reclaim worker relocating a group at the fill edge, admitted
writebacks found no stripe and were dropped:
allocation failed flags 129, wanted 8192 ...
space_info DATA has 450560 free, is full ... block group ... [readonly]
Refuse the read-only transition while the admitted bytes would no
longer fit in the claimable supply without this group. Scrub, balance
and reclaim retry later; this is the fail-early direction and only bites
transiently at the edge, since bytes_may_use drains as writeback
completes.
btrfs: stripe_alloc: do not count trapped free space twice when making a block group read-only
inc_block_group_ro() refuses to make a data block group read-only
unless the other groups of the space_info have at least as much free
space as this group is withdrawing: a read-only group's free space
leaves the pool writers allocate from, data never overcommits, and the
check (sinfo_used + num_bytes <= total_bytes) keeps concurrent writers
from failing allocations they could otherwise have been placed in while
the group is read-only. Scrub takes the same path as balance, since
the space is demanded for those writers, not for moving data.
With stripe_alloc, the part of this group's free space that is trapped
in partially used stripes is already counted in btrfs_space_info_used()
through bytes_stripe_unusable, and it is also part of num_bytes, so the
check demands room elsewhere for those bytes twice -- although once the
group is read-only its trapped bytes leave the aggregate anyway, and
writers never had them to begin with. A fragmented raid56 data group
could therefore never be made read-only.
RAID56 groups must be read-only for scrub: scrub keeps its own copy of
the stripe, which a concurrent read-modify-write would corrupt, so
unlike other profiles it cannot proceed without the flag. On a
filesystem whose data groups had been churned (2.98 GiB group, 51%
used, 1.47 GiB of the remainder trapped) every device's scrub failed
with
BTRFS warning: scrub: failed setting block group ro: -28
while the same filesystem without stripe_alloc scrubbed at 100% used.
Device replace takes the same path. Freeing space made scrub work
again.
Subtract the group's own trapped bytes from the space it needs to find
elsewhere. The counter may lag the space_info total between commits;
the check is a buffer heuristic, and the bound by num_bytes keeps it
from going negative.
btrfs: stripe_alloc: drain the legacy allocator's writes before arming the write-hole check at a runtime enable
Enabling stripe_alloc on a live filesystem (the btrfs.stripe_alloc property)
flips the mount option while the legacy allocator may still have writes in
flight. Those extents sit in partly used stripes that no stripe run covers,
so the first of them to reach rmw_rbio() after the flip trips
btrfs_stripe_check_write()'s "write to stripe outside any live stripe run"
WARN_ONCE and taints the kernel, although nothing violated the policy: the
data was placed before the policy existed. punch-hole-warn-repro.sh hits it
on the switch when its killed writers still have dirty data (bhive, 7.3-rc2
lane, 2026-09-12); the same code on 6.18 escaped only by timing.
Keep the check off from the flip until every ordered extent that could have
been allocated before it has completed. The property is applied inside a
transaction, where flushing delalloc cannot wait, so the drain runs in a
worker: start delalloc on all roots, wait for all ordered extents, clear the
flag. Extents allocated after the flip have stripe runs and are unaffected;
a mount-time enable (the root directory's property) has nothing in flight and
does not queue the drain. close_ctree() flushes the worker so it cannot
outlive the filesystem.
Zygo Blaxell [Sun, 9 Aug 2026 22:05:48 +0000 (18:05 -0400)]
btrfs: stripe_meta: allocate tree blocks by whole stripes
Metadata gets the placement rule data already has: tree blocks are handed
out from runs of fully free stripes, and a stripe is never revisited once
the transaction that filled it has written it. No separate mount option: the
chunk profile already decides it. raid56 metadata has the same write hole
as raid56 data, so it gets the same protection at the same time, and
metadata on any other profile has no write hole to close -- the policy's
RAID56_MASK test simply does not match it, so nothing changes there.
It does cost a stranded stripe tail per commit, and a metadata allocation
that fails aborts the transaction rather than failing one write. Those
are the price of covering raid56 metadata at all rather than a reason to
make it opt-in separately: a filesystem unwilling to pay them does not
want raid56 metadata, and converting it to raid1c3/raid1c4 removes both
the cost and the write hole.
Only system chunks are left uncovered: btrfs_is_stripe_meta_bg() tests the
METADATA flag and a system chunk does not carry it, so a raid56 system
chunk keeps the legacy read-modify-write. The mount-time warning is
narrowed to say exactly that instead of claiming metadata is uncovered,
which it no longer is.
The machinery is the data machinery, including the per-sector liveness
map the previous patch gave it. Metadata needed three things of its own:
- A completion report. A run drains when every byte allocated from it
has been accounted for, and metadata had no equivalent of ordered
extent completion, so its runs could never drain -- the deadlock that
mixed block groups hit. end_bbio_meta_write() now reports, and
btrfs_open_stripe_write_abandoned() covers a tree block freed before
it was ever written (COWed twice in one transaction, say).
- A two-phase closure. btrfs_retire_open_stripes() runs before the
commit writes tree blocks, so it skips metadata runs; they are closed
and drained afterwards by btrfs_retire_meta_stripes(), once every block
is on disk. Closing earlier would strand blocks the commit has not
allocated yet, draining earlier would wait for writes it has not
issued. Metadata runs are also freed there rather than when their
counter reaches zero: a stripe's columns can be written by separate
rbios, and freeing on the first completion leaves the second with no
run to pad or merge against.
- Whole stripes per writeback pass. The opportunistic writeback passes
do not know about stripes and will happily write three tree blocks now
and five later; btrfs_defer_stripe_meta_write() leaves the blocks
dirty for the commit (or a WB_SYNC_ALL waiter) instead, the same shape
as the zoned write-pointer deferral.
Two free paths bypass the usual completion reporting and must report the
block abandoned themselves: btrfs_free_tree_block() can send a
same-transaction unwritten block through the delayed-ref machinery when
check_ref_cleanup() finds its ref head already processed, and
clean_log_buffer() discards unwritten log tree blocks via
btrfs_pin_reserved_extent() without ever visiting btrfs_free_tree_block().
Either miss leaves the block counted as inflight forever: the run never
drains, the block group reference never drops, and unmount asserts. Both
were found by the dm-log-writes replay matrix, whose crash-recovery mounts
exercise both paths routinely.
Zygo Blaxell [Sun, 16 Aug 2026 07:54:18 +0000 (03:54 -0400)]
btrfs: stripe_alloc: cache a failed stripe run scan per block group
On a large aged filesystem (85 TiB, mostly legacy pre-stripe_alloc data,
heavily fragmented free space), the delalloc workers spin at 100% CPU
while commits crawl at KiB/s. NMI backtraces put the time in
btrfs_claim_free_stripe_run() called from btrfs_alloc_from_open_stripe()
for every small compressed extent allocation.
The cost is in find_free_stripe_run_slow(): when the single-entry fast
path misses (always, on fragmented block groups), the slow path walks
candidate stripes from the START of the block group, doing an rbtree
lookup per free-space piece, all under ctl->tree_lock. When the block
group has no claimable run at all - the common case for block groups
filled with legacy data - the scan traverses the entire block group,
returns -ENOSPC, and remembers nothing: the next 4K allocation repeats
the whole scan. Per-allocation cost is O(block group size) in the
fragmented worst case, multiplied by every block group find_free_extent
visits.
The stock allocator avoids exactly this pathology by caching the largest
extent a failed search saw (max_extent_size); the stripe claim path is a
new search primitive built beside it and never inherited the idea. The
per-block-group stripe_claimable counter cannot serve this role: it is a
commit-scan-granular byte estimate that can err high (block groups where
the accounting promises claimable bytes but the scan finds no aligned,
usable run are precisely where the scan storm happens).
Add bg->max_claimable_run, an upper bound learned from the scans
themselves: (u64)-1 unknown, 0 after an exhaustive scan proved no
claimable run exists (find_free_stripe_run_slow() accepts any run of at
least one full stripe, so its failure is that proof). Claims fail in
O(1) while the bound is below one full stripe, or when total free space
in the block group is smaller than a full stripe. Adding free space
resets the bound to unknown in __btrfs_add_free_space(), which covers
all paths that can create a run: frees, unpins, and stripe run returns.
The bound is only trusted and only set once the block group is fully
cached, because cache loading links entries directly and would bypass
the reset.
Zygo Blaxell [Fri, 14 Aug 2026 01:12:02 +0000 (21:12 -0400)]
btrfs: stripe_alloc: gate data admission on claimable whole-stripe supply
Wire stripe_claimable into the DATA reservation path so write() returns -ENOSPC
when there is no fully-free whole stripe to place the write crash-safely --
matching statfs f_bavail, which already subtracts the trapped partial-stripe
space. Add stripe_claimable_admit() and AND it into the two data-admit sites:
__reserve_bytes() and btrfs_try_granting_tickets() (the latter is required, or a
flushed data ticket would be granted by the plain used<=total rule, bypassing the
gate). Scoped to raid56 stripe_alloc DATA (stripe_margin_unit != 0); metadata,
non-raid56 and zoned space_infos are unaffected.
The gate refuses at reservation time, before the range becomes delalloc and later
parks at writeback with nowhere to land -- so ENOSPC is clean and there is no
fill-edge parking collapse.
Phase 1 is pessimistic: it gates on bytes_stripe_claimable alone, which errs LOW,
so it can refuse a write that would have landed in a partially-open stripe
(over-refuses by the open remainder, healed by the commit rescan raising claimable
and the FLUSH_DATA ticket retry). A later change adds bytes_stripe_open to the
bound for statfs-exact behavior.
Zygo Blaxell [Tue, 11 Aug 2026 06:53:38 +0000 (02:53 -0400)]
btrfs: raid56: adaptive park deadline from the parked-rbio backlog
Full-stripe batching parks a sub-stripe write's rbio for a deadline so the
writes that fill the rest of its stripe can merge into it, turning a
read-modify-write into one full-stripe write. The deadline is a fixed
per-filesystem value. It is long enough to catch those merges, but at a
small-file fill to ENOSPC -- where a run's stripes never fill because each
file's neighbours land in the next file's stripe rather than this one --
every partial write waits the deadline out, and then its 10x stuck cap, on
an arrival that never comes before padding to a full stripe and going down.
Lowering the deadline globally fixes that fill but throws away the merges
the deadline exists to catch on ordinary moderate writes.
Feed the deadline back from the parking machinery's own backlog instead of
guessing at the workload. Track stripe_parked_now, the number of
currently-parked rbios, incremented as a partial write parks and
decremented as it unparks. When it runs ahead of the stripe_park_congestion
knob (0 disables it, the default), a new async partial write takes the short
sync deadline through the existing sync-park path rather than the full one.
This is negative feedback: the shorter deadline drains the backlog, so once
it falls back below the knob later writes regain the full deadline and full-
stripe batching. No space-state estimate is needed -- a small-file flood
builds the backlog directly, while a large sequential fill (already full
stripes, never parked) and a moderate steady stream do not.
Measured on an eight-device raid5. A small-file balance-reclaim soak that
times out at the default deadline completes at a congestion of 4; on a
throttled moderate stream, where the backlog stays low, a congestion of 32
never trips, keeps the full deadline, and merges three times as much with
38% fewer RMW reads as the same filesystem under a flat 3ms deadline. The
gauge and a congestion_short counter join the stripe_park_stats sysfs file
so the knob can be set from measurement. Off by default, and no data-path
change: a shortened park still pads or reads exactly as it would have, only
sooner.
Data reservations are admitted against arithmetic -- free space minus
trapped fragments minus open-run remainders minus a per-extent margin --
and under sustained near-full churn the arithmetic and the claim rule
disagree for long enough that admitted buffered writes reach writeback
with nothing claimable left: measured, ~5.2-5.5k reserved writebacks
dropped per fsstress churn run, silently for anything not waiting on
fsync. Stock refuses the same write()s up front. Nothing between
"durable by fsync" and "refused by write()" is acceptable when an
operator accident fills the disk.
First step, accounting only: measure the constraint instead of deriving
it. bg->stripe_claimable counts bytes of fully free whole stripes --
exactly what btrfs_claim_free_stripe_run() can take, the complement of
stripe_unusable within each stripe. The commit rescan derives it from
the same pass that computes trapped bytes (free minus trapped) and is
the sole upward correction; incremental maintenance under
ctl->tree_lock only ever DEBITS -- removals round their decrement OUT
to every touched stripe, claims subtract exactly what they took. The
counter can therefore only under-count between commits, never
over-count.
Crediting freed whole stripes incrementally on the add side is
deliberately not done, having been tried and dropped. Several add
paths re-add free space that a low-level remove never de-credited --
the async discard trim (unlink_free_space / bitmap_clear_bits, then
do_trimming's re-add through __btrfs_add_free_space) and
btrfs_remove_free_space's middle-split tail re-add -- so crediting on
add double-counts and drives the counter ABOVE the authoritative scan.
That is the dangerous direction: once admission gates on this counter,
an over-count admits reservations against phantom supply that writeback
then drops, while an under-count is an early, clean write()-time
ENOSPC. Debit-only makes over-counting structurally impossible,
whatever a future re-add path forgets to de-credit. (Reproduced with
compress+autodefrag+discard=async on a legacy-converted raid5
filesystem, incremental ~= 2x scanned; credit_return attribution
confirmed the phantom entered through do_trimming's re-add, not the
open-stripe allocator returns.)
A WARN_RATELIMIT at the rescan catches the dangerous direction anyway
(incremental above scanned), which under a debit-only rule means a
missed consumption site, and localizes it; the clamp to the scanned
value keeps every rescan authoritative regardless. Rate limited rather
than _ONCE because such a gap recurs on every commit, and reporting
only the first hit hides that it is ongoing.
The space_info aggregate bytes_stripe_claimable follows the
stripe_unusable pattern (incremental between commits, re-totaled from
armed groups at the rescan), shows in the ENOSPC dump and in sysfs.
No admission change yet; that comes once the counter proves accurate
under the fill, churn, balance and reclaim suites. The by-size
fast-fail in the claim keeps its early exit: the point of measuring is
to make such inputs trustworthy, not to search harder around them.
Data block groups only for now; raid56 metadata and mixed block groups
join when the data counter has settled.
Zygo Blaxell [Sun, 9 Aug 2026 03:14:23 +0000 (23:14 -0400)]
btrfs: stripe_alloc: track per-sector liveness and pad from the map
Padding used to ask where the run's allocation frontier was, which
cannot see an allocation abandoned below it -- a reservation released, an
allocation the finder discarded, an extent freed before its write was
ever issued. Runs now carry a bit per sector, set at allocation and
cleared by the new btrfs_open_stripe_write_abandoned() report, and
padding fills exactly the sectors that are dead: this replaces
btrfs_stripe_run_pad_start() with btrfs_stripe_run_pad_mask(). The map
is sized once, with headroom for frontier growth, because the growth
path holds the run lock where nothing may sleep; a run that would
outgrow its map stops growing instead. A closed run's final stripe
overhangs its shrunk end by construction, and those sectors -- the
returned tail, unclaimable while the stripe holds live data -- are dead
and paddable too.
The abandonment report is what closes a writeback ENOSPC race the
fsync-heavy near-full workload hits: an allocation undone without a
report left phantom inflight bytes and phantom-live sectors, and
reservations admitted against that state failed at writeback
(cow_file_range -28) where stock refuses the write() up front. With
the report in place the same workload shows zero writeback failures at
every reservation margin setting, including margin disabled.
Claiming also learns an O(1) fast fail: the by-size index walk is
extracted into find_free_stripe_run(), stopping at the first entry whose
largest contiguous free run cannot hold a full stripe -- near-full, where
free space degenerates into many sub-stripe holes, this replaces a scan
of the whole tree on every allocation.
Moved out of the stripe_meta patch, which needs all of this for tree
blocks but introduced it tangled with the metadata machinery; data wants
it on its own.
Zygo Blaxell [Sat, 8 Aug 2026 15:27:36 +0000 (11:27 -0400)]
btrfs: stripe_alloc: complete parked writes on allocation coverage, not clocks
A parked partial-stripe write goes down its RMW path when its deadline
expires or a retirement flush kicks it, even when every byte it is
missing below the run's frontier belongs to an allocation whose data IO
is already in flight -- allocation happens at writeback submission, and
the commit's retirement drain waits for exactly those arrivals. Timing
out such a park buys nothing and costs a stripe read plus a second write
of the same stripe: measured on an 8-device raid5 buffered fill, the
100ms deadline turns ~2200 parked stripes per 2GB into read-modify-
writes (pad_decline_live), and stretching the clock 20x recovers only
14% -- the clock is the wrong instrument.
Replace the clock with a coverage test. A parked rbio is ready when its
gathered bios cover everything the covering run has allocated inside its
stripe (btrfs_stripe_run_alloc_ceiling); the remainder lies at or past
the frontier, so the existing pad turns it into a single full-stripe
write with no read phase. Merges check readiness as they land, the park
timer holds unready parks instead of expiring them, and run-retirement
flushes leave unready parks parked: the retirement has already closed
the run (freezing the allocated prefix) and its drain waits on the very
arrivals that will complete them. Waiter-driven flushes (fsync's
pre-writeback kick, ordered-extent waits) and sync parks keep today's
forced behaviour: a blocked waiter's latency beats a saved stripe read.
A stuck cap (10x the park deadline) bounds the wait when an arrival can
never come: a writeback error abandoned the allocation, or the stripe
was already written once by a forced sync park. Such parks are forced
down the old path and counted.
New sysfs stripe_park_stats counters: unparked_ready (parks completed by
the coverage test) and stuck (parks forced at the cap).
Zygo Blaxell [Sat, 8 Aug 2026 07:57:22 +0000 (03:57 -0400)]
btrfs: stripe_alloc: trace why padding declined, at the moment it declined
The counters say how often padding refused and broadly why, but not what
the run looked like when it happened, and reconstructing that from the
extent tree afterwards cannot distinguish "the stripe was already
allocated when this write arrived" from "it was allocated shortly after".
Those want different fixes, so record the decision where it is made.
Also count the second refusal, which had no counter at all: the oracle can
allow padding and the sector walk still refuse, because a sector below the
frontier is not covered by this write. That is the case where another
allocation shares the stripe and its data is not in this rbio -- a
different thing from the oracle finding the whole stripe allocated.
The tracepoint carries the stripe, how much of it this write covers, the
refusal reason, and the run's start/end/frontier/inflight, so a trace says
directly whether the frontier had already run past the stripe when the
write showed up.
Zygo Blaxell [Sat, 8 Aug 2026 06:03:40 +0000 (02:03 -0400)]
btrfs: stripe_alloc: tunable park deadlines, and say why padding declined
The deadline constants are meant to be judged "from measurements rather
than taste", but they are compile-time, so every data point costs a kernel
build and a reboot. Measuring a buffered fill -- dd, no fsync, so nothing
kicks the park and the backstop is the only exit -- found 1861 of 3984
parks expiring at the 100ms deadline, which is precisely the no-waiter case
the constant exists for and the one with no numbers behind it.
Expose both deadlines as writable sysfs files, clamped to 60s because a
parked rbio holds its stripe lock, with 0 disabling parking for that class
(useful as an experiment in itself):
Add the counters the stats file cannot supply. rmw_reads counts only
parked writes that still had to read, so it cannot answer "does this
workload read-modify-write at all"; data_rmw counts every data RMW, which
with allow_rmw empty on a covered block group is the number the
stripe-exclusive claim is about, and it should be zero.
And record why padding refused, because the reasons are different defects:
the rest of the stripe is allocated and its data has not arrived yet
(wait longer, or kick when the frontier advances); no open run covers the
stripe, so the run closed before its own write went down; or the frontier
never reached the stripe at all. Attributing them turns "some RMW
remains" into a specific thing to fix.
Zygo Blaxell [Wed, 5 Aug 2026 22:15:06 +0000 (18:15 -0400)]
btrfs: raid56: say whether a metadata read-modify-write is a write hole
meta_rmw counts sub-stripe metadata writes, which is a proxy for exposure
rather than a measurement of it. A stripe modified in place is only a write
hole if it holds data some completed transaction is relying on; a stripe
that two writes of the *same* transaction happen to split costs an extra
read but risks nothing, because a tear loses that whole transaction anyway.
The counter cannot tell those apart, so it cannot answer the only question
that matters.
The rbio can. A read-modify-write has already read every column the write
does not cover, so at the point of the report it is holding the stripe's
on-disk contents. Walk the uncovered tree block positions and read their
headers: a block whose bytenr and fsid match belongs there, and its
generation says which transaction put it there. Compare that against the
generation of the blocks this write is carrying, taken from the same rbio,
so a transaction committing concurrently cannot skew the verdict:
meta_rmw_cur the same transaction's own blocks -- a cost
meta_rmw_old an earlier transaction's -- a write hole
meta_rmw_free no tree block there at all
System block groups are classified too, not just metadata ones. A system
chunk carries no METADATA bit, so a flags test excludes it, but it holds
the chunk tree: the same tree block header, the same generation field, and
a worse consequence if a degraded crash tears it, since without the chunk
tree no logical address can be mapped at all. meta_rmw already counted
system chunks; only the classification skipped them, so their read-modify-
writes were visible as a rate and never as a verdict. Whether a raid56
system chunk ever rewrites parity over a committed tree block is therefore
an open question that this answers by measurement rather than by reasoning
about how the chunk tree is laid out.
This trusts nothing the allocator says about itself. The stripe runs, the
liveness map and the drain accounting are all bookkeeping that could be
wrong in the same way twice; the header in the sector is what a degraded
read would actually have to reconstruct.
On a 3-device raid5 filesystem with raid5 metadata, 6000 small files with
periodic syncs and then a third rewritten, plain stripe_alloc reports 249
sub-stripe metadata writes of which 174 rewrite parity over committed tree
blocks. Adding stripe_meta leaves 37 sub-stripe writes and none of them.
Zygo Blaxell [Tue, 4 Aug 2026 19:51:54 +0000 (15:51 -0400)]
btrfs: stripe_alloc: report read-modify-write of uncovered stripes
The mount-time warning says which block groups stripe_alloc does not
cover. It cannot say whether anything is actually landing there, and on
a filesystem with raid56 metadata that is the interesting question: every
sub-stripe write to an uncovered stripe is a write hole window, where
parity and data reach the disk separately and a crash in between leaves
the stripe unreconstructible.
Report it from the one place that knows the write is going out as a
read-modify-write rather than as a full or padded stripe. Rate limited,
because a raid56-metadata filesystem does this continuously and the point
is to make the exposure visible rather than to fill the log, and paired
with a meta_rmw counter in the existing stripe_park_stats sysfs file so
the rate can be read off without grepping dmesg.
Silent when stripe_alloc is off: there the whole filesystem works this
way and the user has asked for nothing else.
Zygo Blaxell [Tue, 4 Aug 2026 06:43:20 +0000 (02:43 -0400)]
btrfs: stripe_alloc: warn at mount when raid56 metadata is not covered
stripe_alloc closes the write hole for raid56 data. It does nothing for
raid56 metadata, which keeps read-modify-write and keeps the hole, and
on a mixed-block-group filesystem it now does nothing at all. A user
who mounts -o stripe_alloc on -d raid5 -m raid5 has every reason to
believe the filesystem is covered, and finds out otherwise only after a
crash on a degraded array -- with metadata damage, which is worse than
the data damage they were protecting against.
Say it once at mount, after the block groups are read, so the message
reflects what is actually on disk rather than what was asked for. A
warning, not an error: the data guarantee is real and worth having on
its own, and a filesystem can be converted to raid1c3/raid1c4 metadata
without unmounting.
Zygo Blaxell [Fri, 7 Aug 2026 01:35:56 +0000 (21:35 -0400)]
btrfs: stripe_alloc: claim fully-free stripes that span free space entries
The claim fast path searches single entries, so a fully-free full
stripe whose free space spans an entry boundary -- an extent entry
adjoining a bitmap, or two neighbouring bitmap windows -- was
unclaimable. stripe_unusable accounting is entry-blind and counts
exactly those stripes as claimable, so admission reserves data against
them; at writeback the claim finds nothing, cow_file_range() gets
-ENOSPC, and the already-dirtied pages are dropped. full_stripe_len
is not a power of two, so stripe boundaries drift through the fixed
128M bitmap windows and a straddling stripe is a certainty near full,
not a corner case.
Observed live at the raid6 fill edge (rolling-failure, then isolated
by fill-edge-debug with enospc_debug): two 384K stripes, each
straddling a bitmap window boundary, held the space_info accounting
786432 bytes above what the claim could reach, and every writeback
allocation against that phantom margin failed -ENOSPC while ~991MB of
genuinely trapped free space sat in the cache. The reservation margin
cannot absorb this: the gap is per straddling stripe, not per
outstanding extent.
Add an entry-blind slow path: one offset-ordered walk accumulating
contiguous free coverage across entry boundaries, and a piecewise
removal that runs in the same tree_lock critical section as the find,
so a racing claimer cannot see a half-removed run. The fast path is
unchanged and still serves the common case; the slow path runs only
after it fails, which is the near-full case where a stranded stripe
matters most.
Zygo Blaxell [Mon, 3 Aug 2026 07:22:52 +0000 (03:22 -0400)]
btrfs: stripe_alloc: pessimistic data reservation margin
A byte-counted data reservation holds no collateral against the stripe
claim rule. Between admission at write() time and the allocation at
writeback, the trapped-space picture keeps moving: commits close open
stripe runs and trap their sub-stripe tails, and free space returning
mid-transaction (unpinned deletions, same-transaction frees, drained
run tails, freed reservations) lands in the free space cache as
fragments the claim rule can never hand out but that no counter yet
reflects. The counters were honest at every instant for NEW
admissions while OLD admissions were left holding air; delalloc
writeback gets no second chance, so the pages were dropped. Measured
at the fill edge: ~19 MiB of orphaned bytes_may_use with accounting
and allocator in perfect agreement, and in a later round the admitted
may_use+margin exactly equaled the phantom mid-transaction fragments.
Be pessimistic at reservation time and optimistic at allocation time,
the same shape metadata reservations already use:
- bytes_stripe_margin: one full stripe width (fs_info->
stripe_margin_unit, the widest raid56 data full stripe) per
outstanding delalloc extent, charged and released inside
btrfs_mod_outstanding_extents() so it stays exact across delalloc
merges and splits -- including async compression, where the io-tree
split hook grows the margin to one stripe per compressed piece,
matching the true worst case. Counted in btrfs_space_info_used()
so it holds back admissions. The margin exists only for in-flight
dirty data; steady-state capacity and statfs are untouched.
- Admission probes len + margin through the ticketed FLUSH_DATA path
(then immediately re-releases the probe, which the delalloc hooks
re-charge), so a writer that cannot be covered waits for the
flusher -- commits convert pinned deletions into claimable whole
stripes -- and receives an honest ENOSPC at write(2) if flushing
cannot produce cover.
- Every btrfs_add_free_space() into an armed group counts the added
range's sub-stripe head and tail fragments as trapped immediately;
whole stripes fully inside the range are trivially fully free and
stay claimable. A bounded overcount in the safe direction, never
an undercount; the commit rescan remains the authority and
reconciles (and now grants tickets when it lowers the counter,
since admission waiters may be blocked on exactly that headroom).
This closes the retire-to-rescan window the previous patch left
open, and closes it in the one place every returning range passes
through: counting at the individual close/drain call sites instead
misses paths (the drain returns have two) and gets the arithmetic
wrong (round_up() on the non-power-of-two 448K stripe width).
- When a margin-backed data allocation must split across free space
fragments, every non-final piece is kept a whole-stripe multiple
(rounddown; the width is not a power of two), so at most one piece
per delalloc extent can strand a tail -- the margin pre-paid
exactly one.
- A data writeback allocation returning ENOSPC anyway is loudly
warned: the margin should make it impossible, and the pages are
dropped.
With this, the fill-to-ENOSPC harness goes fully clean for the first
time: zero writeback drops, zero warnings, zero allocation failures,
margin drains to zero at idle, and fill capacity is unchanged. A
writeback ENOSPC retry loop that this replaces is dropped entirely.
Known bounds, accepted: compressed writes are probed one unit per
128M range but charged per 128K piece (probe optimistic, charge
honest); direct IO is probed but carries no margin (its reserve-to-
allocate window is one syscall); a remount toggling stripe_alloc
with dirty delalloc drains the counter (charges are gated on the
mount option, releases are not, and the counter clamps at zero).
Zygo Blaxell [Sun, 2 Aug 2026 07:35:36 +0000 (03:35 -0400)]
btrfs: stripe_alloc: count open stripe run remainders against data reservations
Close the remaining reservation-vs-allocation windows the commit-time
stripe_unusable rescan cannot see:
- bytes_stripe_open (new): the sum of open runs' unallocated
remainders, maintained per block group under stripe_run_lock at
every open/alloc/grow/close, synced into the space_info after each
mutation, and counted in btrfs_space_info_used(). Claimed bytes are
invisible to the free space cache and will either be allocated or
become trapped tails at close, so reservations must not be admitted
against them.
A closed run's returned tail is still only counted as trapped by the
commit-time rescan, so reservations can race the retire-to-rescan window
inside a commit and be admitted against freshly trapped tails. The next
patch closes that window from the free space cache side, where every
returning range is seen and no call site can be missed.
Measured on the fill-to-ENOSPC rolling test before this change (with
only the bytes_stripe_unusable accounting): tree 'b' still lost 3.3%
(8532 blocks) and tree 'c' 30% (62371 blocks) to silent writeback
allocation failure as trapping compounded.
btrfs: raid56: skip parking for sync rbios finished by their unplug batch
Parking exists to widen the merge window, and for plugged submissions
the unplug callback is the natural end of that window from the
submitter's side: once raid_unplug() has sorted and merged the batch,
nothing more is coming from it. A sync rbio parked after that point --
a sync(2) or WB_SYNC_ALL sweep, whose whole flush shares one plug --
has a waiter behind it and can only sit out the sync deadline. Mark
rbios leaving an unplug batch and let sync ones skip parking.
Unplugged sync submissions (the fsync flush) still park: their sibling
bios arrive one by one and merge into the parked rbio, and the fsync
path kicks it as soon as they have all been submitted, so parking there
is the merge mechanism and the deadline already never fires.
/sys/fs/btrfs/<uuid>/stripe_park_stats reports lifetime counters for
the parking machinery: rbios parked, bytes merged into parked rbios,
and how each park ended (filled to a full stripe, kicked by a waiter or
a settle/retire flush, or expired at its deadline), plus how many
flushed writes were padded to full stripes versus still needing the
RMW read phase. The filled/kicked/expired split shows directly whether
the deadlines are sized right for a given system -- expired parks that
later reappear as rmw_reads are the batching the window failed to
capture -- so any future retuning (or an adaptive deadline) can argue
from measurements instead of taste.
btrfs: raid56: do not park sync writes into nocow runs
An in-place overwrite of a nodatacow file under stripe_alloc parked
like any partial write, but with a waiter behind it and nothing worth
merging: in-place overwrites arrive one fsync at a time, and unlike
the datacow fsync path nothing kicks the parked rbio before the page
writeback wait, so every fsync ate the full sync park deadline.
Measured against stock nodatacow on the same rig, that deadline was
the bulk of a ~3.7ms per-fsync regression. Skip parking for sync
writes whose stripe belongs to a NOCOW-class run; async writeback
still parks and merges there.
btrfs: stripe_alloc: persist nocow runs across commits and remounts
A nodatacow inode's private stripe run used to close at every
transaction commit like all runs, so a slowly appended nocow file burned
a fresh stripe per commit -- and after a remount its partial stripe's
free tail was abandoned outright. Neither cost buys anything: the run
machinery's commit-time closing exists for invariant I2, and a nocow
stripe holds only the owner's write-hole-waived data, so there is
nothing for I2 to protect.
Keep NOCOW-class runs open across commits: the commit-time retirement
and its drain predicate skip them (their extents insert without the
window-sequence deferral, which is fine -- the data is on disk when the
ordered extent finishes, and later same-stripe writes can tear only the
owner's own data). They still close on forced quiescing (read-only,
removal, unmount) and now on the owning inode's eviction, so a cached
but idle inode cannot pin a claimed tail forever.
Across remounts, re-adopt instead: when a nocow allocation's hint
points into a partial stripe that the committed extent tree proves is
wholly owned by the allocating inode, claim exactly the stripe's free
tail (a new exact-range claim that verifies every byte is free before
removing; nothing else can consume free space inside a partially used
stripe, so verify-then-remove cannot race) and continue the run at the
old frontier. Appends to a nocow file then pack sequentially through
commits, evictions and remounts alike.
The re-adoption lookup must not block. It runs from
btrfs_alloc_from_inode_stripe_run(), i.e. inside find_free_extent(),
which holds space_info->groups_sem for read, and stripe_extents_owned_by()
does a full btrfs_search_slot() on the extent tree. The reverse order is
longstanding upstream -- delayed ref processing holds extent tree locks
and then calls find_free_extent(), which takes groups_sem -- so waiting
here closes a cycle. Both sides only take groups_sem for read, which is
not enough: it is an rw_semaphore and block group creation and removal
take it for write, so a writer queued between the two readers blocks the
second one. The check cannot be hoisted above the lock, since it is a
question about the specific stripe the allocator has just settled on, but
it does not have to block: it is read-only and advisory, and every caller
already treats "not provably ours" as a reason to decline the
optimisation rather than as a fact about the extents. So set path->nowait
for the groups_sem caller and let -EAGAIN fall into the existing error
paths; the cost on contention is a re-adoption that does not happen.
btrfs_stripe_nocow_writable() keeps the blocking search -- it runs from
the nocow check with no groups_sem held, and a spurious "not ours" there
would force COW on an extent that does not need it.
btrfs: stripe_alloc: allow-rmw policy, and write-in-place for isolated nocow extents
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 interface is 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 require
the per-extent stripe isolation above (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.
btrfs: stripe_alloc: isolate nodatacow and preallocated extents by stripe
Preallocated extents and nodatacow files' extents are candidates for
write-in-place, which reintroduces the raid56 write hole for every
stripe such a write touches: the RMW recomputes parity that also covers
whatever else shares the stripe. Before write-in-place can be
re-enabled for them (a later change; stripe_alloc still forces COW
today), their placement must guarantee the blast radius: such an extent
must never share a stripe with any other file's data.
Steer them into private per-inode stripe runs of a new NOCOW class,
reusing the log-active inode machinery: runs owned by one inode, never
in the shared band slots, found by owner-and-class lookup. Successive
allocations of the same file pack sequentially into the file's own
stripes; different files, and the datacow/relocation/log classes, never
share a stripe with them. A preallocation signals itself through a new
btrfs_reserve_extent() parameter; nodatacow files are recognized by the
inode flag. Placement remains best effort: when no fully-free stripes
are left for a private run the allocation falls back to the shared
runs, which is safe -- an extent that lands in a shared stripe simply
stays force-COWed when write-in-place arrives.
btrfs: raid56: pad sub-stripe writes to full stripes in open runs
A sub-stripe write does a full RMW: read every untouched data sector of
the stripe, recompute parity, write. Under stripe-exclusive allocation
the read phase is usually pointless: a stripe covered by a live stripe
run has never been written at or past the run's allocation frontier, so
the sectors being read contain nothing.
When every data sector the rbio does not cover lies at or past the
frontier, zero-fill those sectors instead of reading them and write
them out with the stripe -- the zeros must reach the disk, or the
parity (computed over them) would not match what scrub reads back. The
partial write becomes one full-stripe write: no read phase, one parity
pass. A frontier that grows during the attempt is safe: the newer
allocation's write serializes behind this rbio's stripe lock and lands
over the zeros. If any uncovered sector is below the frontier (already
allocated to someone else), fall back to the normal RMW.
Together with kicking parked rbios before the fast fsync's writeback
wait, this removes both stalls the raid56 layer added to fsync under
stripe_alloc: the park deadline and the RMW read round trip.
btrfs: stripe_alloc: kick parked rbios before the fast fsync's writeback wait
The fast fsync path waits for page writeback, which completes only when
the raid56 layer writes the data -- but a partial-stripe rbio parks to
collect merges until its sync deadline, and on this path nothing unparks
it before the wait: the stripes are settled only later, in the logging
itself. The full-sync path does not have this problem because the
ordered extent wait already flushes parked rbios before sleeping.
All of the fsync's writes are submitted before the wait, so nothing more
can merge into its stripes; flush the parked rbios covering the attached
ordered extents instead of sleeping out their deadline. Removes the
sync park timeout from the fast fsync critical path.
btrfs: stripe_alloc: carry an inode's log tail forward at fsync
Closing a log-active inode's private stripe run at each log commit
traps the run's final partial stripe tail every time the inode fsyncs:
a steady fsync stream burns one stripe per log commit until the stripes
free or balance runs.
Reclaim the tails by carrying the partial stripe's live data forward.
At each log commit, the settling walk now computes the closed run's
final partial stripe, and the logging paths record the file ranges of
the logged extents that live inside it on a small per-inode table
(bounded; overflow just means an extent is not carried). At the
inode's next fsync, before its delalloc flush, each recorded range that
still maps to the recorded disk bytenr is re-dirtied, defrag style
(reserve, reset delalloc state, mark the folios dirty; absent folios
are read back, which is a plain read of settled data). The flush then
COWs the carried ranges together with the new data into the inode's
current private run, and the old partial stripe empties and frees
whole.
Everything downstream is the ordinary COW pipeline: new extent maps,
ordered extents, checksums, file extent items and delayed refs, and
the log's modified-extents snapshot -- taken inside btrfs_log_inode
after the flush -- picks up the new addresses by itself. Reflinked
ranges need no special handling: foreign referents keep the old extent
alive in its closed (never again written) stripe, and only the space
reclaim degrades. A range that was rewritten, truncated, punched or
compressed simply fails the mapping check and stays put.
Crash safety does not regress. A carried extent's old copy is dropped
through the normal paths: if it was committed, the free pins until the
transaction commits; if it was logged but never committed, its ADD and
DROP delayed refs cancel in cleanup_ref_head(), which also pins
must_insert_reserved heads -- either way the old blocks cannot be
reallocated before the log that references them is superseded.
The settling walk also learned to report the partial stripe through a
widened btrfs_log_settle_stripes() signature (inode and file range
instead of fs_info); compressed extents pass a zero file length to opt
out of carrying while still settling.
The re-dirty reserves with NO_FLUSH. carry_one_range() takes the folio
locks and the extent range lock before reserving, and a flushing
(ticketed) reservation can sleep in wait_reserve_ticket() until the
flusher makes progress -- but every way forward needs the locks we
hold: FLUSH_DELALLOC has to write back the carried range, which blocks
in __folio_lock() on our locked folio, and a transaction commit (with
flushoncommit) waits for the same writeback. The whole filesystem then
wedges behind the stuck commit. Carrying is best effort by design and
the carried range's data is already durable in its old stripe, so on
ENOSPC just skip the carry and let the old stripe free the slow way.
btrfs: stripe_alloc: private per-inode stripe runs for log-active inodes
Closing the open stripe run covering every logged extent at each log
commit made completed fsyncs crash-safe, but at a placement cost: an
fsync-heavy inode shares the open band runs with every other writer, so
each of its log commits closes a shared run, fragmenting concurrent
write streams and trapping the shared run's tail.
Give log-active inodes their own runs instead. The first fsync of an
inode sets a sticky runtime flag (at btrfs_sync_file entry, before the
fsync flushes its own delalloc, so even that first fsync's allocations
are steered). Datacow allocations for a flagged inode come from a
private LOG-class run: claimed like any run but never placed in the
shared band slots, owned by the inode, and found by owner lookup under
the block group's stripe_run_lock. A per-inode hint seeds the
allocator's search with the run's location; the hint is advisory (a
stale hint costs a lookup miss, never a wrong run, since the owner
match is authoritative). When no fully-free stripes remain for a
private run the allocation falls back to the shared runs, restoring
the previous placement with unchanged safety.
One inode's log commit now settles only its own stripes: other
writers' runs stay open and their streams stay contiguous, and the
only trapped tails are the fsyncing inode's own. Private runs also
give each log-active inode exclusive stripes, which a later change
uses to copy live tails forward and reclaim them without touching
foreign extents.
The commit-time retirement closed runs by walking the band slots,
which private runs never occupy; walk the block group's run list
instead so every open run, slotted or private, is closed and drained
under invariant I2 (no open run survives a transaction commit).
btrfs: stripe_alloc: settle a logged extent's stripes at log commit
Stripe-exclusive allocation bounds degraded-crash damage to the current
transaction, but within that window two log commits can still share a
stripe: fsync 1 writes the head of a stripe, the open run keeps filling
it, and a later write's read-modify-write rewrites the parity that
protects fsync 1's data. A degraded crash during the second write tears
the first -- the write hole's shape, confined to the log window, and the
reason the fsync guarantee has so far been "loss is detectable" rather
than "completed fsyncs survive".
Close it with a case analysis. A logged extent in a full stripe is
already safe: nothing ever writes a full stripe again, since COW never
overwrites and there are no free sectors left to allocate. A logged
extent in a partial stripe is exposed only to future writes into that
stripe's remaining sectors -- so at log commit, close the stripe's open
run (nothing further allocates into it), kick any parked partial writes
for it, and wait for its in-flight data IO before the log super is
written. This is the log-window analogue of invariant I2, which the
commit-time retirement provides for full commits. Both log paths are
hooked: the fast path per extent map in log_one_extent(), and the
full-sync path where copy_items() walks new data extents (old-transaction
extents are skipped there, and are exactly the ones already settled by
their own commit).
The drain is bounded and join-free: run inflight is counted from
allocation, which happens during writeback with submission following in
the same pass, so the wait is bio flight time plus the parked-write
deadline that the flush short-circuits; write_done reporting needs no
transaction join, so waiting under the inode log mutex and a running
transaction handle is safe.
The cost is spatial: every fsync that logs an extent in a partial stripe
retires that stripe early, trapping its unwritten tail like any other
partially filled stripe until it frees or balance reclaims it.
Fsync-heavy workloads therefore burn a stripe tail per touched stripe
per fsync; the follow-up per-inode log runs and copy-forward relocation
exist to reclaim exactly that cost, and are optimizations on top of the
guarantee this patch completes.
btrfs: stripe_alloc: control the policy with a filesystem property
A mount option is an awkward fit for write-hole protection: it occupies
a mount-option bit, and protection silently lapses whenever the option
is forgotten -- an fstab edit, a rescue mount, a recovery boot -- which
is exactly when a degraded raid56 is most likely to be written.
Control the policy with a "stripe_alloc" filesystem property instead,
following the property system's compression precedent: a btrfs.
namespace xattr, user-visible and admin-controlled, on the top-level
subvolume's root directory. Set it once (setfattr -n btrfs.stripe_alloc
-v 1, or btrfs property once btrfs-progs learns the name) and it is
persistent: the kernel applies it when the root directory inode loads
its properties during mount, before any user IO. Deleting the xattr
disables the policy; open runs drain at the next commit's retirement,
which runs unconditionally. Stray copies of the xattr -- a received or
cloned subvolume -- are ignored: only the top-level root carries the
policy, so receiving a stream from a stripe_alloc filesystem cannot
flip the policy on the destination.
The support checks (free space tree, not zoned, no remap-tree, not
mixed block groups) are enforced both when the property is set and when
it is applied at mount. The mixed block group test matters most here.
btrfs_check_mountopts() refuses mixed for the mount option, and without
the same test this would be a second way in that skips it -- enabling a
policy the series does not support there, after the block group read
that would have warned about uncovered metadata has already run, so the
user is told the filesystem is protected and it is not.
This stays within the series' no-on-disk-format-change constraint, and
that is the compatibility story: an older kernel mounts the filesystem
read-write and simply uses the legacy allocator, which is fully
compatible because the on-disk layout is unchanged. New kernels honor
the property during the feature's long-tail testing period; if no use
case surfaces where stripe-exclusive allocation is worse than the write
hole it protects against, it can eventually become the only allocation
mode and the property a no-op.
Known old-kernel interactions with the btrfs. namespace: existing
kernels list and read the xattr (the btrfs. get path is a plain xattr
read) but refuse to set or remove unknown property names, so the flag
can only be managed from a kernel that knows it. btrfs-progs
interaction (check, property list) with an unrecognized property is a
userspace compatibility item to verify and, if needed, patch.
The mount option is kept for now as a non-persistent override. Later
protection stages with different risk profiles (the log-tree
full-stripe relocation) should be gated by their own property rather
than widening this one, so their testing exposure can be controlled
independently.
btrfs: stripe_alloc: never claim stripes covered by a live run
The range-to-run lookups -- attaching an ordered extent to its stripe run
and reporting completed data IO by bytenr -- assume that at most one run
on a block group's list covers any given address. Nothing enforced that.
A reservation that is discarded before anything references it (the
cow_file_range error path under ENOSPC, the find_free_extent backout
paths) returns its bytes to the free space cache immediately, with no
pinning: there is no committed state to protect. When an ENOSPC failure
storm discards every allocation in a stripe, the stripe is fully free
again and the claim rule -- correctly, by its own lights -- hands it out
as part of a new run while the old run object is still on the list
draining its other stripes' IO. Full-stripe write batching widened a
run's post-close drain from microseconds to the parked-write timeout, and
the soak test hit the overlap within minutes: new allocations' ordered
extents attached to the old run (first match by range), their completions
drained the old run's inflight accounting into an assertion failure, and
the new run's accounting never drained, wedging the commit's retire wait.
The overlap is harmless to data -- a stripe can only be re-claimed if
every byte of it is free, and discarded reservations never issued bios --
but the accounting corruption is fatal.
Freed committed extents cannot reproduce this: they return to the free
cache only in the unpin phase at the tail of a commit, and the same
commit's retirement already drained -- and freed -- every run opened
before it. Only the unpinned immediate-free paths race with a draining
run.
Rather than enumerate those paths, enforce the lookups' assumption at the
claim site: btrfs_claim_free_stripe_run() now trims a candidate to end
before the first live run overlapping it, or rejects it if its head
overlaps. Re-claiming such stripes just waits until the old run drains
off the list, which only comes up inside ENOSPC failure storms.
Reproduced with concurrent fill-to-ENOSPC/delete cycles, balance, and
fsstress on a 4-device raid5: the assertion fired within ~15 minutes
unpatched, and ~10 hours of the same load ran clean with this fix.
btrfs: raid56: batch stripe_alloc partial writes into full-stripe writes
A raid56 sub-stripe write pays a read-modify-write: read the rest of the
stripe, recompute parity, write. The plug callback merges rbios submitted
within one plug window and the stripe cache saves re-reads for
back-to-back writes, but measurement shows what survives them: on a
4-device raid5, small-file writeback produces 100% sub-stripe rbios (zero
full-stripe writes) and a mixed 4K-512K workload ~55%, so parity is
recomputed and rewritten once per file that lands in a stripe.
Stripe-exclusive allocation gives raid56 something the allocator never
guaranteed before: the rest of an open run's stripe is either written
before the run closes -- within the current transaction -- or not at all.
That makes holding a partial write a sound bet, so park it: a partial
write rbio whose stripe belongs to a stripe run keeps the stripe
lock but does not start its RMW. Later writes to the stripe merge into
it through the existing lock_stripe_add() path, and the moment its data
bitmap covers the stripe it is submitted as a single full-stripe write:
one parity computation, no reads.
A draining run counts, not just an open one. A run closes the moment it
is fully allocated -- typically milliseconds before its last writes reach
the raid56 layer -- so gating on "open" would leave exactly those tail
writes unable to park and unable to pad, and they are the bulk of the
read-modify-writes this patch exists to remove. A draining run is in
fact the safest thing to park against: its frontier is frozen, so
everything below it is inflight IO the commit drain already waits for
(arrival guaranteed) and everything above it is dead space the pad may
fill.
Completion accounting can never trigger the flush of runts -- parked bios
have not been submitted, and preallocated or discarded allocations
produce no bios at all -- so liveness comes from explicit flushes: the
run retirement paths flush before the commit's IO drain (which waits on
the very bios parked here), ordered-extent waiters flush their range on a
short retry period, and a timer bounds everything else. Writes with a
blocked waiter (REQ_SYNC: fsync- and sync-driven writeback) park with a
~3ms deadline instead of 100ms -- long enough for the rest of one
writeback pass over the stripe to merge in, short enough to stay
invisible in fsync latency -- and a sync bio merging into a parked rbio
pulls the deadline in. Only the bg->open_stripe slots are consulted for
eligibility, and a parked rbio that a racing retirement closed out from
under is bounded by the timer, never by the drain.
Measured (same 4-device raid5, deterministic workloads, classifying every
write rbio at rmw_rbio):
fsync latency is 12ms vs 8ms unpatched (each fsync pays one short park).
Batching also collapses trapped free space in a mixed-size forward fill
from ~87 MiB to ~3 MiB: stripes now fill completely before the commit
closes their run, so the frontier strands almost nothing.
Validated: btrfs selftests; mixed-size + concurrency + churn + balance
workload with trapped-space accounting returning to zero after deleting
everything; scrub and full sha256 read-back over 4005 mixed random files;
and the dm-log-writes write-hole crash matrix (24/24 controls clean, 0/36
armed cases with committed data damage) -- parking does not perturb the
stripe-exclusivity guarantee it rides on.
btrfs: raid56: do not unhash a cached rbio that has rbios plugged on it
unlock_stripe() caches the finished rbio with cache_rbio() before it
retakes the bucket and bio_list locks, and RBIO_RMW_LOCKED_BIT is still
set at that point. A lock_stripe_add() for the same full stripe that
runs in that window finds a cached rbio it can neither steal (still RMW
locked) nor merge with (cached) and plugs itself onto it, which is
correct: unlock_stripe() then sees the plug list and hands the stripe
lock on to the plugged rbio.
But if the rbio is removed from the cache in that same window --
btrfs_raid56_uncache_range() from the commit's unpin, or the cache's own
shrink in cache_rbio() -- __remove_rbio_from_cache() finds an empty
bio_list, takes the rbio off the hash list, drops the hash reference and
hits BUG_ON(!list_empty(&rbio->plug_list)). Seen on a degraded raid5
under fsstress: the transaction kthread died in that BUG_ON with the
bucket lock held and the rmw workers spun on it until the machine was
reset.
An rbio with rbios plugged on it is busy, like one with bios: leave it
on the hash list and let unlock_stripe() hand the lock on; drop only the
cache's reference here.
btrfs: stripe_alloc: grow the frontier run instead of stranding its tail
When an allocation is larger than every open run's remainder, the
allocator claims fresh fully-free stripes and opens a separate run,
leaving the old run's tail behind. The banded cursors keep that tail
open for a while, but a band collision or the commit eventually closes
it, and the tail -- perfectly usable space that a misfit merely jumped
over -- is stranded in a partially filled stripe. In a forward fill
this happens at every size upshift, and measurement shows it is where
nearly all of stripe_alloc's trapped space comes from: on a mixed-size
fill it strands ~14% of the data written, a figure that band-granularity
tuning moves by at most a tenth and commit frequency does not move at
all.
Grow the frontier run instead. If the freshly claimed stripes directly
follow the end of an open run of the same class, extend that run to
absorb them and place the allocation at the run's old tail, spilling
contiguously into the new stripes. Nothing is stranded, and the extent
is physically contiguous. In a forward fill the by-size claim naturally
returns the stripes adjacent to the frontier, so the single frontier run
just keeps growing -- the allocator packs the way the stock allocator
does, while keeping stripe exclusivity: the grown stripes were claimed
fully free, the run still closes at the next commit and is never
reopened, so every stripe of the run is filled within one open-run
lifetime. A grown run keeps its open_seq; it can only still be in a
band slot if the retire walk for its window has not yet run, which the
open_stripe_lock nesting at the claim site excludes from racing.
Measured on a 4-device raid5 with a deterministic interleaved 4K-512K
fill (trapped space via the stripe_unusable counter, byte-identical data
both sides): 111.1 MiB trapped without growing, 7.2 MiB with -- a 94%
reduction, and usable capacity at ENOSPC within 0.5% of the stock
allocator (1561 vs 1569 MiB, vs 1421 MiB without growing). The benefit
survives churn: with age-correlated deletion the grown runs' contiguous
packing lets whole stripes free together, and steady-state usable
capacity stays at 1486-1561 MiB versus 1284-1421 MiB without growing
across correlated and adversarial small-file deletion patterns. Data
integrity verified by scrub and full sha256 read-back over mixed
random-content files, and trapped-space accounting still returns to
zero when all files are deleted.
The open-stripe selftest is updated: the misfit allocation now grows the
run and lands at the run's old tail, spanning the stripe boundary, and
the relocation-class case doubles as a check that a claim adjacent to
another class's run never grows it.
btrfs: stripe_alloc: keep an open run per size band to trap less
A single open stripe run per class forces every allocation that does not
fit the current run's remainder to close that run -- trapping its
unallocated tail in a now partially filled stripe -- and claim a fresh
fully-free stripe. A workload that interleaves small and large extents
(the common case) therefore strands a tail on every size change, even
though a later small allocation could have filled it.
Keep one open run per power-of-two band of remaining free space instead
(band k holds a run with remaining in [2^k, 2^(k+1)) sectors), per class.
An allocation takes the run in the smallest band that still fits --
segregated best fit -- so a small write lands in an already-open, nearly
full stripe rather than opening a new one, while a large write that no
open run can hold opens a fresh stripe and leaves the smaller runs open
for the small writes that do fit them. After each allocation the run is
re-placed into the band its new remainder falls in; when two runs collide
in a band the fuller one is kept -- it can serve larger future
allocations, and closing it would trap more -- and the other is closed,
its tail returned to the free space cache.
16 bands cover a full stripe of up to 2^16 sectors, far more than any
raid56 geometry (nr_data_stripes * stripe_len / sectorsize is 158 sectors
at 10 data stripes and a 64K stripe). Commit-time retirement and the
read-only / removal paths now walk every band of every class; only the
bg->open_stripe[][] slots can ever hold an open run, so one pass still
suffices, and the obsolete single-slot assertions are dropped.
This does not touch the write-hole guarantee: every run is still a
contiguous fill of fully-free stripes, closed at commit and never
reopened. It only changes which open run an allocation joins, reducing
the free space trapped in partial stripes -- and thus stripe_unusable --
for mixed-size workloads.
The open-stripe selftest is rewritten to the multi-cursor contract: a
misfit allocation now keeps the old run open (its tail is not returned to
the cache) and a later small allocation backfills it, so fewer stripes are
claimed before the free space is exhausted.
btrfs: account stripe_alloc trapped free space for honest statfs
Free space in the partially filled stripes of a stripe_alloc (raid56
write-hole-safe) block group is real free space -- it is in the free
space tree and cache -- but the stripe-exclusive allocator cannot hand it
out until the whole stripe frees. statfs therefore over-reports available
space, promising free space that a later allocation refuses with ENOSPC.
Track this trapped space per block group as stripe_unusable, summed into
space_info->bytes_stripe_unusable. It is derived, so there is no on-disk
format change and the free space tree and cache are left untouched
(preserving the extent-tree/free-space-tree consistency btrfs check
verifies).
stripe_unusable is defined by a scan that accumulates every free byte of
the block group into a per-stripe array and then sums the stripes that are
partially filled. A per-stripe accumulator, rather than a streaming sweep
or a per-stripe cache search, is what makes the result correct regardless
of the order the free space cache yields its ranges -- an offset-sorted
bitmap entry can emit runs that lie past a following extent entry, so the
ranges are not globally monotonic -- and regardless of whether free space
is stored as extents or bitmaps. A debug-build assertion checks that the
scan distributes exactly the cache's free space. Because trapped space
only settles at commit -- when the retire path returns partially filled
stripes and deleted extents are unpinned -- the scan is recomputed at
commit for groups whose free space changed (flagged cheaply on the
allocation and free paths), which suits the timescale of the reclaim it
feeds far better than a running per-extent tally. The space_info total is
recomputed as the sum of the armed groups after those rescans, so it
cannot drift.
statfs subtracts bytes_stripe_unusable from the data f_bavail so df
reports what can actually be allocated, and the counter is exposed at
/sys/fs/btrfs/<uuid>/allocation/data/bytes_stripe_unusable. It is
deliberately not part of btrfs_space_info_used(), so the reservation layer
is unchanged and this carries no ENOSPC-behaviour risk.
The counter is armed once a group's free space cache is loaded, so a group
contributes zero until then and statfs starts optimistic and settles to
honest as groups cache and commit. It is disarmed when a group turns
read-only (its free space is then accounted as read-only and already
excluded from statfs) and re-armed by a rescan on the way back to
read-write, so a balance both recovers trapped space and updates the
counter. A CONFIG_BTRFS_DEBUG-only sysfs trigger, stripe_unusable_rescan,
forces a full recompute for auditing the accounting.
Two deliberate imprecisions, matching existing behaviour rather than
bettering it: statfs subtracts bytes_stripe_unusable but not
bytes_zone_unusable, so stripe_alloc df reports availability before reclaim
while zoned df reports it after; and superblock stripes, permanently
unusable in every profile, are left to the existing bytes_super accounting
rather than separately reported here. Both await a maintainer decision on
a common convention.
Zygo Blaxell [Wed, 5 Aug 2026 03:33:32 +0000 (23:33 -0400)]
btrfs: stripe_alloc: refuse space_cache=v1
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.
btrfs: add a write-hole invariant checker to the raid56 write path
With stripe-exclusive allocation, a raid56 data stripe may only be
written while an open or draining stripe run covers it: after its run
retires and drains at a transaction commit, nothing may ever write to
it again, and a write outside any run means an allocation bypassed the
policy. Both cases are the write hole about to happen.
Check the invariant (under CONFIG_BTRFS_DEBUG) for every raid56 write
operation, full-stripe and sub-stripe alike, at rmw_rbio() time. This
turns every upstream violation -- a missed allocation path, a
retirement ordering bug, an accounting leak -- into a deterministic
WARN at the moment of the offending write, instead of silent damage
that needs a crash plus a device failure plus a scrub to observe. The
bios gathered in an rbio have not reported their IO done yet, so their
runs cannot drain under the check: no false positives from completion
races.
Block groups that ever hosted relocation-class runs are skipped
(sticky, debug-only flag): relocation legitimately overwrites its
preallocated extents in place after their runs drain, and the write
path cannot tell those writes from violations.
btrfs: retire stripe runs at commit and gate stripe_alloc
Hook stripe run retirement into the transaction commit, right after
TRANS_STATE_COMMIT_DOING stops accepting joins with a single writer
left. At that point every data extent the transaction references was
inserted by an ordered extent completion that joined earlier, implying
its own data IO is done; the retirement drain waits out in-flight
neighbour writes in the same stripes (a pure data-IO wait, never an
ordered extent wait, which would deadlock on the blocked join). After
the drain, no stripe this transaction references can ever be written
again, so a crash after the superblock write cannot tear it -- closing
the raid56 write hole for stripe-allocated block groups without
requiring flushoncommit. A defensive retirement in
btrfs_free_block_groups() covers the transaction abort path.
An ordered extent whose stripe run was opened after the committing
transaction's retire point cannot insert its file extent into that
transaction, so btrfs_finish_one_ordered() ends its handle and waits
for the commit's critical section to end. That wait must not be taken
under the range's extent lock: buffered writers block on the locked
range in lock_and_cleanup_extent_if_need() while holding their
prepared, locked folios, which stops the writeback flusher and
kcompactd behind those folios, which stops reclaim -- and the
committing transaction is itself draining data bios whose submission
path can need memory (dm-crypt bounce pages on the host where this was
caught). Drop the extent lock and the FINISHING_ORDERED tag before the
wait and re-take them before re-joining, preserving the
extent-lock-before-join ordering. The window is safe: the range is
still covered by the pending ordered extent, so a writer that takes the
lock finds it, releases its folios and waits -- which is exactly what
unbinds writeback and reclaim.
Force COW for in-place writes that would land in raid56 data block
groups while stripe_alloc is enabled (nodatacow files and writes into
preallocated extents): a single in-place write could tear a stripe
containing other files' committed extents, making the guarantee
conditional on the whole filesystem's usage. The check sits in
can_nocow_file_extent(), covering buffered and direct IO with one
choke point, and applies per extent so nocow to non-raid56 profiles
keeps working. The data relocation inode is exempt: its extents live
in relocation-class stripe runs that never share stripes with other
data, and relocation depends on in-place writes.
Validate the option at mount: it requires the free space tree (the v1
space cache overwrites its data in place during commit), and is
refused with the remap-tree feature (whose relocation writes bypass
the ordered extent accounting) and on zoned filesystems (which have
their own allocator and no write hole).
Mixed data+metadata filesystems are refused too. The immediate symptom
is a hang: a run drains when it is closed and its inflight_bytes reach
zero, and the only things that subtract are ordered extent completion, a
discarded allocation and a freed reservation. Metadata has no ordered
extent -- btrfs_alloc_tree_block() allocates with is_data false and
end_bbio_meta_write() reports nothing back to the run -- so a metadata
allocation raises inflight_bytes and nothing ever lowers it, the run
never drains, and btrfs_retire_open_stripes() waits for a completion
that has no code path to arrive from. The hung task detector stays
quiet, because wait_var_event() sleeps in a state it exempts; the
filesystem just stops. Reproduced deterministically on
mkfs.btrfs -M -d raid5 -m raid5 mounted -o stripe_alloc: the first sync
after a few hundred small files never returns.
That symptom is not why the refusal is permanent: a later patch
(stripe_meta) gives metadata its own completion report, and the drain
then terminates. These are why:
- raid56 deliberately skips csum lookup for mixed block groups, to
avoid recursing into a metadata read while holding the full stripe
lock (see the comment above the map_type test in fill_data_csums()).
That test reads the block group's flags, so in a mixed group it
cannot tell a data stripe from a metadata stripe and disables
verification for every one of them. Stripe-exclusive allocation
could keep the two apart -- allocation classes already never share a
run, so never a stripe -- but nothing durable records which class a
stripe held, and asking the extent tree while holding the full stripe
lock is the recursion the test exists to avoid. The recovery paths
here assume a rebuilt data sector can be checked; in a mixed group it
cannot.
- Data and metadata draw on one space_info and one free space pool.
Data reservations carry a pessimistic whole-stripe margin, metadata
reservations carry none, and a metadata claim that finds no fully
free full stripe returns ENOSPC -- which for metadata aborts the
transaction rather than failing one write. A data fill can starve
metadata into an abort, which separate block groups cannot do.
Mixed block groups are a mkfs-time property of small filesystems, where
raid56 is least appropriate and stripe_alloc's trapped space costs
proportionally most, and the option cannot become applicable later. So
do not carry a half-supported mode.
btrfs_is_stripe_alloc_bg() therefore requires DATA without METADATA as
belt and braces for a block group that somehow reaches the allocator
anyway, and moves out of extent-tree.c's file scope so block-group.c
can share the one copy of the rule. (Metadata and system block groups
never carried the DATA flag, so they were already excluded.)
btrfs: report stripe run data IO through the ordered extent lifecycle
Pair every byte reserved from an open stripe run with exactly one
"write done" report, so commit-time retirement can wait for all data IO
into a window's stripes:
- Ordered extents get a stripe_run pointer, attached at creation by a
range lookup (cheap: gated on the fs having any stripe runs at all,
and a run's block group membership is established before its
allocation returns, so the gate cannot miss). NOCOW and PREALLOC
ordered extents write into previously allocated extents, which can
never lie inside a run claimed from fully-free stripes, and are
skipped. The report fires once at IO completion (the IO_DONE moment
in can_finish_ordered_extent(), before any transaction join, so the
commit-time drain can never deadlock on a blocked join), with a
catch-all when an ordered extent is freed without completing IO.
- Reservations freed without an ordered extent ever owning them (error
paths) report through btrfs_free_reserved_extent(); the one caller
that frees a range an ordered extent did own (the finish-error path)
uses btrfs_free_reserved_extent_ordered() to avoid double reporting.
- Preallocated extents never issue data IO and report at insertion;
this also covers relocation's data inode preallocations.
btrfs: add the stripe_alloc allocation policy for raid56 data
Wire the open stripe run allocator into find_free_extent() as a per-
block-group policy: with the new stripe_alloc mount option, allocations
from raid56 data block groups go through btrfs_alloc_from_open_stripe()
instead of the clustered allocator, and only ever land in fully-free,
stripe-aligned runs. Since the loop's terminal LOOP_NO_EMPTY_SIZE
degradation only applies to the clustered path and stripe block groups
ignore empty_size/empty_cluster, the natural terminal behaviour is:
no fully-free stripe in any block group -> allocate a chunk -> ENOSPC.
The availability hint feeds max_extent_size so callers retry with
smaller allocations instead of failing early.
Stripe runs get an allocation class: relocation overwrites its
preallocated extents in place, so its allocations must never share a
stripe with ordinary cow data. Classes never share a run, and one
block group at a time is softly dedicated to relocation by reusing
fs_info->data_reloc_bg (btrfs_clear_data_reloc_bg() moves from zoned
code to generic code for this; the class tag, not the dedication, is
what carries correctness). The dedication is dropped when relocation
finishes or the dedicated group runs out of stripes.
Setting a block group read-only now retires its stripe runs (in
btrfs_inc_block_group_ro(), covering scrub, relocation and unused
block group deletion), pairing with the allocator's ->ro check under
the stripe_run_lock so no run can survive into or be created in a
read-only group.
btrfs: add open stripe run tracking for stripe-exclusive allocation
Add the in-memory state and lifecycle for "open stripe runs", the
allocation windows of the raid56 stripe-exclusive allocation policy. A
run is a contiguous stripe-aligned region claimed whole from the free
space cache via btrfs_claim_free_stripe_run() and filled strictly
sequentially by btrfs_alloc_from_open_stripe(). A run closes when it is
exhausted, when an allocation does not fit its remainder, or when the
transaction commit retires it; closed runs are never reopened and their
unallocated tails return to the free space cache, where the fully-free
claim rule makes them unallocatable until the whole stripe frees. This
is what will guarantee that a full stripe only receives writes within
one commit window, closing the raid56 write hole for these block groups.
Each run counts reserved bytes whose data IO has not completed yet,
maintained under the block group lock and reported back through
btrfs_open_stripe_write_done(). btrfs_retire_open_stripes() implements
commit-time retirement: bump the retire sequence, close every run opened
before it, and wait for their inflight bytes to drain. It is a pure
data-IO wait, deliberately not an ordered extent wait: it is designed to
run after the committing transaction stops accepting joins
(TRANS_STATE_COMMIT_DOING with a single writer), where waiting for
ordered extent completion would deadlock on the blocked transaction
join, and where every extent the transaction references already has its
own data on disk. Allocations racing with the commit open runs stamped
with a newer sequence and are neither retired nor waited for; their
extents can only be referenced by the next transaction. Block groups
with runs are tracked on an fs_info list whose membership is
established before an allocation returns, which is what lets the retire
walk rely on the sequence stamp.
Exercised by a new sanity self-test; the raid56 stripe allocation
policy and the commit hook will be the first non-test users.
btrfs: add btrfs_claim_free_stripe_run() for stripe-exclusive allocation
Add a free space cache primitive that finds and removes a contiguous,
fully-free, stripe-aligned run of full stripes from a block group. This
is the building block for a raid56 allocation policy that never issues
sub-stripe writes into stripes containing committed data, closing the
raid56 write hole for datacow writes: because a partially-filled stripe
can never satisfy the fully-free requirement, stripes retired at commit
time become unallocatable without any persistent allocator state.
The search walks the by-size free space index (largest max contiguous
free run first) rather than the by-offset tree: an entry whose largest
contiguous free run is smaller than a full stripe cannot contain a
fully-free stripe, and every following entry is no larger, so the search
stops at the first such entry. Near-full, where the free space
degenerates into many sub-stripe holes, that is an O(1) fast fail
instead of a scan of the whole free space tree on every allocation --
the dominant cost of the known raid56 near-full allocation slowdown.
Full stripe geometry is relative to the block group start and supports
non-power-of-two stripe widths. Unaligned head and tail remainders are
returned to the free space cache with their trim state preserved. Runs
are found within a single free space entry; a fully-free stripe split
across an extent entry and a bitmap neighbour is deliberately not found,
which errs toward missing a claimable stripe, never toward claiming a
non-free byte.
The function is exercised by new sanity self-tests covering aligned and
capped claims, partial-stripe exclusion, head/tail carving, block-group-
relative geometry, bitmap entries, and non-power-of-two stripe widths.
The raid56 stripe allocation policy will be its first non-test user.
btrfs: raid56: keep a reference on a stolen cached rbio until it is dropped
lock_stripe_add() steals the pages of a cached rbio for the same full
stripe, drops the rbio's hash-list reference under the bucket lock, and
only after releasing that lock calls remove_rbio_from_cache() on it.
Between those two steps the rbio is held by nothing but the cache
reference, and anyone who removes it from the cache in that window --
the cache shrink in cache_rbio(), or btrfs_raid56_uncache_range() when
the stripe's extents are freed -- frees it. remove_rbio_from_cache()
then dereferences a freed rbio:
The oops leaves the cache and bucket locks held, and every other RMW
worker and the transaction thread soft-lock behind them. Seen after
8h of the device-corruption acceptance suite on 6.18, where freeing of
extents during read errors made the uncache path frequent; the window
exists with the plain cache shrink as well.
Keep the hash-list reference across the steal and drop it after the
cache removal, so a concurrent remover can clear the cache bit and drop
its own reference but never free the rbio under our feet.
Zygo Blaxell [Thu, 27 Aug 2026 20:38:12 +0000 (16:38 -0400)]
btrfs: do not write repaired read sectors back on a read-only mount
btrfs_end_repair_bio() submits btrfs_repair_io_failure() for every bad
mirror once a repair read has produced good data, with no check of the
mount state. On a read-only mount the write trips
ASSERT(!(fs_info->sb->s_flags & SB_RDONLY)) in btrfs_repair_io_failure()
(kernel BUG on assert-enabled builds); without assertions it would issue
a write to a filesystem the user was promised is read-only.
Easily reproduced with a degraded raid56 mounted ro,degraded: every
read of a stray sector on the missing device reconstructs through
parity, then tries to write the reconstruction back.
Serve the read from the repaired data but skip the write-back when the
filesystem is read-only.
Zygo Blaxell [Wed, 5 Aug 2026 06:59:50 +0000 (02:59 -0400)]
btrfs: raid56: verify reconstructed tree blocks
Data sectors recovered from parity are checked against the csum tree
before they are trusted: recover_vertical() calls verify_one_sector() for
each rebuilt sector, and a mismatch fails the read. Metadata gets none of
that. fill_data_csums() returns early for anything that is not a data
block group, so csum_bitmap is NULL, and verify_one_sector() then returns
success without looking at anything:
if (!rbio->csum_bitmap || !rbio->csum_buf)
return 0;
So a metadata rbio reconstructs a lost column, marks it uptodate having
verified nothing, and hands it back. A wrong reconstruction is caught
much later by validate_extent_buffer(), one layer up, after the block has
already been returned and cached -- and in the raid6 case, where several
candidate reconstructions exist, raid56 cannot tell which one is right
because it has no way to check any of them.
A tree block can be verified, just not a sector at a time: its csum lives
in its own header and covers the whole nodesize block, so the check has to
wait until every sector of the block has been recovered. Do it after the
vertical recovery loop rather than inside it. A tree block never straddles
a column -- nodesize and BTRFS_STRIPE_LEN are both powers of two with
nodesize <= BTRFS_STRIPE_LEN, and full stripes are stripe-length aligned,
so each column starts on a nodesize boundary and tree blocks tile it
exactly.
Only the blocks the caller asked for are checked. Those are known to be
live tree blocks; the rest of a stripe may be free space, which has no
header and would fail every test. For the same reason this runs only when
rebuilding a read: recover_sectors() is also reached from the
read-modify-write path, where the sectors actually reconstructed are the
ones read off the surviving columns and any of them may be free space. A
reconstruction feeding an RMW's parity therefore stays unverified; knowing
which ranges hold live metadata is not something this layer can do.
Tested with a 3-device raid5 filesystem with raid5 metadata, 3000 files:
each device in turn missing and corrupted recovers byte-exactly, and with
two devices corrupted -- where reconstruction necessarily produces a wrong
block -- raid56 now rejects it itself:
Zygo Blaxell [Mon, 3 Aug 2026 07:24:38 +0000 (03:24 -0400)]
btrfs: raid56: drop cached rbios when their stripes' extents are freed
A cached rbio keeps an in-memory copy of its full stripe's data
sectors so that a later sub-stripe write can steal the pages (see
steal_rbio()) and skip the RMW read. That is only safe while the
cache and the disk agree. Nothing invalidates cache entries when the
stripe's extents are freed, so an entry can outlive the data it
describes: the space is reallocated, a sub-stripe write to the reused
stripe steals the stale pages, and parity is generated from the
cache's memory of the stripe's previous life instead of from disk.
If the on-disk content diverged from that memory in the meantime, the
result is silent, latent damage. The divergence does not require a
second write: a device error that corrupts a column holding only
freed data is invisible (no live extent, no csum to fail, nothing for
scrub to check), but the stale cached copy of that column predates
the corruption. The poisoned write then produces parity consistent
with the cache's memory and inconsistent with the actual on-disk
column. Every read of the live data still passes csum and scrub
reports nothing, so the loss surfaces only when a device failure
forces reconstruction -- which XORs the real on-disk garbage against
parity that remembers different bytes, and hands back trash.
This was found and proven byte-exactly on a raid5 test array: a
stripe's parity solved uniquely as the XOR of fresh data with the
PRE-corruption content of a free-space column -- bytes that existed
nowhere on disk at write time and could only have come from the rbio
cache. The resulting single-column loss was undetectable by scrub
and unrecoverable by reconstruction.
Drop cache entries overlapping freed extents at unpin time, when the
space returns to circulation. The walk is gated on raid56 block
groups, so filesystems without raid56 never touch it, and the LRU
list is bounded (RBIO_CACHE_SIZE entries).
The cost of dropping entries was measured on an instrumented stock
kernel that marks affected entries instead of dropping them, and
counts a reintroduced RMW full-stripe read whenever a read-skip was
only possible because of a marked entry. In a worst-case free-heavy
small-file churn on a 5-device raid5 array with raid5 metadata --
conditions chosen to flatter the cache, and it delivers, with 87% of
sub-stripe RMWs skipping their read -- only 1% of those skips (64 of
6430) were enabled by entries this patch drops, costing 0.04-0.12
MB/s of extra reads against the workload's own 0.3-0.7 MB/s of
writes. 93% of the entries the patch drops were evicted without
ever being stolen again.
Zygo Blaxell [Sat, 1 Aug 2026 22:31:19 +0000 (18:31 -0400)]
btrfs: raid56: only verify recovered sectors that have a checksum
verify_one_sector() checks that the rbio has a csum_buf/csum_bitmap at
all, but never tests the bitmap bit for the sector it is about to
verify. A recovered sector that has no csum -- free space, or a sector
whose ordered extent has not yet committed its csums, including the
very sector a sub-stripe write is replacing -- is then compared against
an all-zero csum_buf slot and fails with EIO.
The visible symptom: sub-stripe writes to a degraded raid5/6 fail with
EIO whenever the missing device's column in the target stripe is only
partially covered by csums. The RMW read phase marks every sector of
the missing device as an error, recover_sectors() rebuilds the column
and verifies each rebuilt sector, and the first csum-less sector kills
the write:
BTRFS error (device loop3): dropping unwritten extent at root 5
ino 1061 offset [0,4095] disk bytenr 638771200 length 4096
with no underlying device error and no corruption counted anywhere.
Writing new data into partially filled stripes is exactly what a
degraded array must do to keep operating, so this makes a degraded
raid56 filesystem effectively read-only for small writes, and each
failure drops the unwritten extent (detectable data loss).
fill_data_csums() only covers sectors that have csum items, and its
sibling loop verify_bio_data_sectors() already skips sectors whose
bitmap bit is clear. Do the same here.
Fixes: 7a3150723061 ("btrfs: raid56: do data csum verification during RMW cycle") CC: stable@vger.kernel.org # 6.2+ Signed-off-by: Zygo Blaxell <ce3g8jdj@umail.furryterror.org> Assisted-by: Claude:claude-fable-5
Zygo Blaxell [Sat, 1 Aug 2026 05:31:24 +0000 (01:31 -0400)]
btrfs: propagate a split bio's error when the failing split completes last
btrfs_bio_end_io() saves the first error of any split bio in the original
btrfs_bio's ->status, then loads it into ->bio.bi_status when the last
split completes -- but only if that last completion itself succeeded:
if (atomic_dec_and_test(&bbio->pending_ios)) {
/* Load split bio's error which might be set above. */
if (status == BLK_STS_OK)
bbio->bio.bi_status = READ_ONCE(bbio->status);
The condition assumes that a failing final completion has already stored
its error in the original bio at the top of the function. That holds only
when the bio was never split. For a split, "bbio->bio.bi_status = status"
is applied to the clone, which is then freed and bbio switched to the
original; the original's bi_status keeps whatever an earlier, successful
split left in it, and the error never reaches the caller.
The ordering that loses the error is not rare. A read that fails checksum
verification completes only after read repair has tried every mirror,
which takes far longer than the healthy splits of the same bio, so the
failing split completes last essentially every time.
On a degraded raid56 array the result is silent corruption. Reads are
split per stripe, so for any read of BTRFS_STRIPE_LEN or more, a sector
whose reconstruction from a torn stripe fails its checksum is returned to
userspace as zeros with no error -- while btrfs logs the checksum failure
and increments the device corruption counter. A 4 KiB read of the same
sector is not split and correctly returns EIO. Profiles that do not split
at 64 KiB (single, dup, raid1) are unaffected, which is how this has gone
unnoticed.
Reproducer: on a raid5 filesystem, tear one stripe row's parity (leaving
the data intact), withhold one data member of that row and mount degraded.
Reconstructing the missing column then yields wrong bytes for that column
while every other column still verifies. Reads of 64 KiB or more over the
affected range return zeros and succeed; 4 KiB reads return EIO.
Load the saved status unconditionally. It is the first error seen by any
split, including this one, which is the value the caller wants in either
case.
btrfs: raid56: fix use-after-free of rbio in bio end_io wakeups
The read/write submission rounds in raid56 count in-flight bios in
rbio->stripes_pending and wait with a bare
wait_event(rbio->io_wait, atomic_read(&rbio->stripes_pending) == 0),
while each bio's end_io does
"if (atomic_dec_and_test(&stripes_pending)) wake_up(&rbio->io_wait)".
Once the final decrement makes the counter visible as zero, the waiter
can pass its condition check without consuming the wakeup -- via
wait_event()'s fast path when all bios complete before the waiter
arrives, or a condition recheck after an earlier wakeup -- and proceed
to free the rbio through rbio_orig_end_io(). The end_io context is
then still inside wake_up() operating on the freed rbio's embedded
waitqueue lock.
This shows up under sustained raid56 RMW load in KVM guests as
recurring "pvqspinlock: lock ... has corrupted value 0x0!" warnings
from __pv_queued_spin_unlock_slowpath with a
__wake_up <- raid_wait_write_end_io <- bio_endio call trace: paravirt
spinlocks detect the unlock of the recycled lock word. On bare metal
the use-after-free is silent and almost always harmless, which is how
it has survived; it is a plausible match for long-standing sporadic
crash reports on busy raid5 filesystems that never reproduce under
sanitizer kernels (the instrumentation widens the dec-to-wake window
so the race is always lost).
Convert the pair to a completion. wait_for_completion()'s fast path
takes the completion's own lock, so it cannot return before complete()
has released it: the completing context is provably finished with the
rbio before the waiter can free it. A submitter-held bias count keeps
one completion per submission round even when a round submits zero
bios or every bio finishes before the submitter starts waiting; the
scrub path only waits when finish_parity_scrub() actually began a
round.
btrfs: don't abort raid56 data scrub on first uncorrectable sector
In commit 1009254bf22a ("btrfs: scrub: use scrub_stripe to implement
RAID56 P/Q scrub") a new function scrub_raid56_parity_stripe() was
introduced to recalculate the parity stripe after correcting any
correctable errors. When it detected unrepaired sectors in a data
stripe, it returned -EIO, which propagated up the stack through
scrub_stripe, scrub_chunk, and scrub_enumerate_chunks, where an error
makes the scrub break out of its loop early and return the error to
userspace.
That was a regression: uncorrectable data blocks are one of the
expected possible events that occur during a scrub, so scrub should
continue until it has counted and reported all of the uncorrectable
stripes in the filesystem. Perhaps more importantly, scrub should fix
up any correctable errors that might exist in other stripes beyond the
first uncorrectable stripe. Errors from this function are only
appropriate when scrub cannot do its job at all: unable to read csums,
unable to map an extent's data blocks, or out of memory.
On current kernels the bug has changed shape: at the point where
unrepaired sectors are detected, ret no longer holds -EIO. It holds
the leftover return value of the last scrub_find_fill_first_stripe()
call from the loop that populates the data stripes, which is 0, or 1
when the last data stripe of the full stripe contains no extents.
When the stale 1 leaks out, the caller treats any nonzero value as an
error ("if (ret) goto out;" in scrub_stripe()) and aborts the whole
scrub with a meaningless positive return value; when it is 0, the
scrub continues only by accident.
scrub_raid56_parity_stripe() has successfully completed its task as
soon as it has performed its data correction and verification steps.
Return 0 explicitly at that point, regardless of the outcome of the
verification. The P/Q update step is still skipped for the affected
full stripe, so no garbage is written back to the devices. Detected
errors are reported to the user via device stats and dmesg messages,
not via the return code of the scrub ioctl.
Fixes: 1009254bf22a ("btrfs: scrub: use scrub_stripe to implement RAID56 P/Q scrub") Signed-off-by: Zygo Blaxell <ce3g8jdj@umail.furryterror.org>
btrfs: log a message when dropping an extent due to IO error
When an IO error occurs while writing a datacow file, btrfs will drop the
extent containing the unwritten blocks. This prevents potential leaks
of information stored in the unwritten data blocks back to userspace, but
it is also a data loss event that is not easily visible from userspace.
The existing code does notify userspace of the error via the inode,
and the notification can be received through fsync (for the inode) or
syncfs (for any dirty inode on the filesystem) return values; however,
for the common case of a process that writes to a file and exits with
its data still in dirty cache pages, there is no process left to notify
when the ordered writes eventually fail.
In most cases the extent drop is preceded by error messages from the
write operation, but the IOERR bit can be set for less visible or obvious
reasons, like running out of memory, bad metadata, or as a result of
bugs that appear in other parts of btrfs from time to time.
Some of the existing error messages for writes contain only logical
addresses, while other messages contain only disk bytenrs. For read
errors, only one address is necessary, as the user can look up the other
with `btrfs ins log -o` or `btrfs ins sub/ino`, but for write errors, the
mapping between these addresses is removed when the error is detected in
btrfs_finish_one_ordered, so we can't derive one address from the other.
We need both addresses for separate purposes: the logical address to
identify which files have missing data, and the disk bytenr to identify
which part of the filesystem address space is failing to accept writes
(resolvable to device sectors through the chunk tree).
For now, inform the sysadmin that the data has been dropped by logging a
message when it happens, including both the logical subvol/inode/offset
triple and the disk bytenr, and the corresponding sizes.
In the future we might want to add some stats counters for this event,
similar to the dev stats counters, but not tied to any specific device.
The first step is to detect and report the events at all.
Other notes:
Because the extent is dropped, the lost data cannot be detected by a csum
mismatch or read failure, i.e. everything will look OK if the file is read
or the filesystem scrubbed, but an application will notice a truncated
file or a file with data replaced by zeros. For datasum files, we could
simply keep the extent as-is, allow future reads or csum verification
to fail, and report EIO errors that way. That is a larger and riskier
change to btrfs behavior than simply adding some passive monitoring of
the status quo. This approach also has problems with csum collisions,
it doesn't work for nodatasum+datacow files, and it takes much longer
for the errors to be detected and reported to a sysadmin.
nodatacow files don't have their extents dropped; instead, they will
simply have garbage in the blocks where the writes failed. In most
use cases for nodatacow files (e.g. VM disk images or databases), the
user will be running all writes through fsync(), O_DIRECT, or similar
mechanisms that can observe the existing EIO return value notification
path, so there's less need for diagnostic coverage in the kernel log
for nodatacow files.
The btrfs.compression property validator matches only the algorithm
name prefix, so values like "zstdgarbage" or "zstd:banana" are accepted
and stored verbatim. Now that ":level" suffixes are meaningful,
validate new values strictly: accept exactly an algorithm name,
optionally followed by a ":level" suffix that btrfs_compress_str2level()
can parse, mirroring the mount option validation from commit b98b20830057 ("btrfs: reject invalid compression level"). Out of range
levels are clamped, also matching the mount options. "no" and "none"
are accepted as before. Embedded NUL bytes are rejected before parsing
the length-delimited xattr value: otherwise the temporary
NUL-terminated suffix buffer would let a value such as "zstd:3" followed
by a NUL and junk pass validation and be stored verbatim.
Values stored by old kernels are not affected: property loading goes
through the apply hook, which remains permissive, so existing inodes
with sloppy stored values keep working; only new setxattr calls see the
stricter checks.
This is a user-visible behavior change: applications that set malformed
property values, which were previously accepted and ignored, will now
receive EINVAL. It is split into its own commit so that it can be
accepted or rejected independently of per-inode compression level
support.
Zygo Blaxell [Sun, 16 Aug 2026 20:26:51 +0000 (16:26 -0400)]
btrfs: relocation: reserve all of a folio's slices before dirtying any
relocate_one_folio() locks the target folio and then, for each cluster
extent slice inside it, takes a flushing delalloc metadata reservation,
marks the slice delalloc and re-dirties it. From the second slice on,
the flushing reservation runs while we hold a locked, partially dirty
folio: under pressure it sleeps waiting for a reservation ticket, and
the flush states that ticket depends on -- FLUSH_DELALLOC writing back
the relocation inode (the data reloc root sits on
fs_info->delalloc_roots like any other, and BTRFS_INODE_NO_DELALLOC_FLUSH
is only honoured under in_reclaim_context), or a transaction commit,
which with flushoncommit waits for the same writeback -- block in
__folio_lock() on the folio we hold. Note the extent lock is released
at the end of every slice iteration, so the folio lock is the resource
still held across the next reservation. Relocation wedges the
filesystem in exactly the situation (space pressure) that balance is
usually run to relieve.
Reproduced on a plain single-device filesystem (mkfs.btrfs -d single -m
single, mounted -o noatime,max_inline=0,flushoncommit) by filling a
block group with 4K extents and running balance against concurrent
rewrites, snapshots and fsstress under space pressure. In the capture
below the task blocked on the folio is the async reclaim worker itself
-- the worker whose job is to serve the ticket the balance task is
waiting for -- so the cycle closes with no third party and without
needing flushoncommit at all; flush_space()'s FLUSH_DELALLOC state
alone suffices. Commits were frozen at 5519:
A second capture of the same run shows the longer route, via a
wb_workfn writeback thread in __folio_lock() under
extent_write_cache_pages() with the commit waiting on it.
One slice per folio -- the 4K page, 4K sectorsize, order-0 case --
cannot deadlock: the single reservation happens while the folio is
still clean, and writeback has no business with a clean folio.
Multiple slices per folio need folio_size > extent size. That was
originally only reachable with sub-page sector sizes, but 041c39da53c2 ("btrfs: enable large data folios for data reloc inode")
made it reachable on 4K pages under CONFIG_BTRFS_EXPERIMENTAL, and 9bce95edb1b4 ("btrfs: move large data folios out of experimental
features") makes it reachable in default builds: on 4K sectors
calc_block_max_order() yields order 6, so this loop can be handed a
256K folio spanning up to 64 slices.
Restructure the function to take all of the folio's slice reservations
up front, while the folio is still clean, keeping the per-slice
reservation granularity (each slice becomes its own delalloc extent
because of EXTENT_BOUNDARY, so per-slice accounting is the accurate
form). A clean folio carries no dirty tag, so writeback never returns
it and never asks for its lock, and its range has no EXTENT_DELALLOC
yet, so find_lock_delalloc_range() cannot reach it either; flushing
there is safe. The dirtying loop then consumes the reservations
without ever flushing under a dirty folio, and unwinding on error walks
the same slice geometry so reserve/release pair exactly. ENOSPC
semantics are unchanged: the up-front reservations flush exactly like
the old in-loop ones, just at a point where flushing can still make
progress.
This relies on prealloc_file_extent_cluster() calling
filemap_invalidate_inode() with flush=true for the cluster range: a
folio shared with the previous cluster is written back and dropped
there, so it is genuinely clean when we reserve for it here.
The deadlock is a race -- the reservation has to ticket while
relocation is between slices of one folio -- so single runs prove
little in either direction. Over repeated 15-minute runs of the above
workload, three viable runs on the unfixed kernel deadlocked twice,
while three on the fixed kernel completed with no occurrence, and with
the relocating task still observed blocked on a metadata reservation
ticket and no writeback task ever blocked on a folio: the reservation
still tickets and still flushes, it just no longer traps the flusher.
Fixes: c2832898126f ("btrfs: make relocate_one_page() handle subpage case") Assisted-by: Claude:claude-fable-5
btrfs: drain pending NOCOW writes before making a block group read-only
A buffered write to a nodatacow or preallocated range decides at write()
time, in btrfs_check_nocow_lock(), that it will be written in place, and
records that with EXTENT_NORESERVE: no data space is reserved for it.
The block group's nocow_writers count, which relocation and scrub wait
for, is only taken at writeback, in run_delalloc_nocow(). Between the
two nothing stops the group from going read-only -- scrub, balance and
zoned reclaim all call btrfs_inc_block_group_ro() without flushing
anything -- and when the writeback then finds ->ro set it falls back to
COW: fallback_to_cow() charges the data space without any admission
check, and on a full filesystem cow_file_range() fails and the pages are
dropped, the error surfacing only at fsync or close. Snapshots and
reflinks flush the range before changing its sharing, so this window is
the read-only transition's alone.
Drain the group first. btrfs_extent_readonly() now refuses NOCOW into a
group that is being made read-only and notes, per group, that a write()
has decided to NOCOW into it; btrfs_check_nocow_lock() holds an fs-wide
in-flight count until btrfs_check_nocow_unlock(), i.e. until the pages
are dirtied. btrfs_inc_block_group_ro() blocks new decisions, waits for
the in-flight ones, and -- only if a NOCOW decision has landed on the
group since it was last drained -- flushes delalloc and waits for the
group's ordered extents, so every pending range is written in place
while the group is still writable, before the transaction is joined and
the group flipped. The block is lifted if the transition fails and when
the group returns to read-write.
Zygo Blaxell [Fri, 7 Aug 2026 01:43:39 +0000 (21:43 -0400)]
btrfs: props: add per-inode compression level support
Setting a per-file compression level is an often requested feature, and
the btrfs.compression property has silently accepted level suffixes
("zstd:9") since compression types were added to it: the property
validator only matches the algorithm name prefix, the verbatim string is
stored in the xattr and returned by getxattr, and everything after the
algorithm name is ignored when the value is parsed into the in-memory
compression type.
All of the pieces needed to honor the level already exist: the level is
a per-call argument down the whole compression path, per-inode levels
are already implemented for the defrag ioctl (defrag_compress_level),
and btrfs_compress_str2level() already parses and clamps ":level"
suffixes for the mount options. Wire the property path up to them:
* Cache the parsed level in a new btrfs_inode::prop_compress_level,
with 0 meaning no level was specified, in which case the level from
the mount options is used as before. The field is signed to allow
negative (realtime) zstd levels.
* Parse an optional ":level" suffix in prop_compression_apply().
Values stored by old kernels were never validated, so an unparseable
suffix falls back to the default level rather than making the inode's
properties fail to load. Levels for lzo parse and clamp to nothing,
matching commit 6db1df415d73 ("btrfs: accept and ignore compression
level for lzo").
* Use the level in compress_file_range() when compression is selected
by the property. The defrag ioctl retains precedence.
* Regenerate the canonical "type:level" string with a new helper,
btrfs_prop_compression_extract(), so that directory inheritance
propagates the level to new inodes, and so that FS_IOC_SETFLAGS,
which rewrites the property when setting FS_COMPR_FL, preserves the
level instead of truncating the value to the bare algorithm name.
The prop_handler extract hook itself is unchanged: it still returns
a static string, and the inheritance loop regenerates the leveled
value only for the compression property.
There is no disk format change: the level lives in the already-existing
xattr value string. Note that levels stored by old kernels (which were
accepted but ignored) become effective after this change.
Zygo Blaxell [Sun, 16 Aug 2026 07:26:53 +0000 (03:26 -0400)]
btrfs: reflink: never block on space reservations while the locked range has delalloc
The previous commit made clone_copy_inline_extent() take its transaction
handle before dirtying a folio in the locked destination range, closing a
flushoncommit deadlock for the single-extent inline clone. Two gaps
remained, and one of them was captured live on a test box within hours:
1) A source file can have an inline extent at offset 0 followed by more
items (e.g. created small, then extended by an append). After the
inline extent's data is copied into a folio - leaving delalloc inside
the locked destination range - the clone loop continues to the next
item and btrfs_replace_file_extents() starts more transactions under
the lock. The captured deadlock (drgn against a live hung kernel):
dedupe: btrfs_replace_file_extents() at [4096,40959], blocked in
__reserve_bytes() on a metadata reservation ticket, while
holding the dst range lock with extent_state [0,4095] =
EXTENT_LOCKED | EXTENT_DELALLOC (dirtied by the inline
copy of the source's first item)
reclaim: flush_space() -> btrfs_commit_current_transaction() ->
btrfs_start_delalloc_flush() -> try_to_writeback_inodes_sb()
flusher: find_lock_delalloc_range() on that inode's [0,4095],
blocked on the dedupe's extent lock
Everything else on the filesystem then queues behind the starved
ticket. Note the blocking point here is a reservation ticket, not
TRANS_STATE_COMMIT_START: ticket servicing commits the transaction,
so with flushoncommit ANY blocking reservation made while the locked
range has delalloc can deadlock.
Fix: once the inline copy's inode update is committed, unlock the
dirtied block before the clone continues. Every byte still locked is
clean, so the commit-time flusher never needs our lock, and i_rwsem
(held for the whole remap) keeps writers away from the unlocked block
until the clone finishes.
2) Delalloc may already exist inside the range when it is first locked:
the pre-lock flush waits for ordered extents, but compressed writeback
is asynchronous and may not have created them yet. Check for
EXTENT_DELALLOC after locking; if present, unlock, flush and retry,
giving up with -EAGAIN after a few attempts.
Fixes: 05a5a7621ce6 ("Btrfs: implement full reflink support for inline extents") Assisted-by: Claude:claude-fable-5
btrfs: release the space of delalloc ranges abandoned by a failed writeback
When a folio holds more than one delalloc range (large folios, or
sector size smaller than page size) and btrfs_run_delalloc_range()
fails on one of them, writepage_delalloc() only unlocks the ranges
that follow it. The folio's dirty flag was already cleared by
folio_clear_dirty_for_io() for this writeback, so those ranges are never
written back again: EXTENT_DELALLOC stays set on a clean folio, and when
memory reclaim later releases the folio, try_release_extent_state()
clears the bit without EXTENT_CLEAR_META_RESV or EXTENT_CLEAR_DATA_RESV.
Everything reserved for the abandoned ranges leaks: the inode's
outstanding extents, csum_bytes and block_rsv, and the data space_info's
bytes_may_use. On eviction btrfs_destroy_inode() warns about the
inode's block_rsv.reserved, block_rsv.size and csum_bytes, and at unmount
check_removing_space_info() warns about the data space_info's
bytes_may_use.
Seen on a raid56 filesystem with the stripe-exclusive allocator filled
to ENOSPC, where cow_file_range() fails with -ENOSPC at writeback time;
the same path is reached whenever run_delalloc_range() fails on a
multi-range folio, e.g. -EIO or -ENOSPC on a NOCOW fallback. A per-inode
record of delalloc clears made without EXTENT_CLEAR_META_RESV showed
that on each leaked inode the last such clear came from
over a 4096 byte range, which was exactly the leftover csum_bytes; the
inodes had never been through the cow error cleanup themselves.
Fail the remaining ranges the same way btrfs_run_delalloc_range() failed
the range that hit the error: clear the delalloc bits releasing the
metadata and data reservations, free the qgroup reservation, and mark
the blocks written back so the error, which extent_writepage() already
set on the mapping, reaches the writer.
Fixes: d034cdb4cc8a ("btrfs: lock subpage ranges in one go for writepage_delalloc()") Cc: stable@vger.kernel.org Assisted-by: Claude:claude-opus-4-8
Zygo Blaxell [Sun, 17 Aug 2025 20:57:07 +0000 (16:57 -0400)]
btrfs: allow NODATACOW | NOCOMPRESS
Commit f37c563bab42 ("btrfs: add missing check for nocow and compression
inode flags") added conflict checks for certain inode flag combinations,
and commit 0e852ab8974c ("btrfs: do not allow compression on nodatacow
files") extended the same logic to xattrs.
Both commits also forbade the combination of FS_NOCOW_FL (NODATACOW)
and FS_NOCOMP_FL (NOCOMPRESS). This restriction is undocumented, has
no technical basis, and provides no benefit. NODATACOW files cannot be
compressed in any case, so the NOCOMPRESS bit is a no-op: behavior is
the same whether the flag is present or not.
Forcing an unnecessary conflict makes inode flags harder to use.
Portable applications may inherit COMPRESS, NOCOMPRESS, or NODATACOW bits
from parent directories and combine them with unrelated flags such as
IMMUTABLE or NOATIME. Rejecting otherwise valid flag sets with EINVAL
creates surprises for applications that do not know about btrfs-specific
interactions and only touch the flags they care about.
Fix by permitting the combination of NODATACOW and NOCOMPRESS, both via
FS_IOC_SETFLAGS and by setting btrfs.compression to "no"/"none" through
xattrs.
Fixes: f37c563bab42 ("btrfs: add missing check for nocow and compression inode flags") Fixes: 0e852ab8974c ("btrfs: do not allow compression on nodatacow files") Signed-off-by: Zygo Blaxell <ce3g8jdj@umail.furryterror.org>
Zygo Blaxell [Sun, 16 Aug 2026 02:20:56 +0000 (22:20 -0400)]
btrfs: fix flushoncommit deadlock when cloning an inline extent inside i_size
Commit b48c980b6a7e ("btrfs: fix deadlock between reflink and transaction
commit when using flushoncommit") fixed a deadlock between a transaction
commit and a reflink that copied an inline extent's data to a folio beyond
the destination's i_size: commit-time delalloc flushing tries to invalidate
the beyond-EOF folio and blocks on the extent range lock held by the
reflink task, which itself waits for the commit when starting a transaction
to update the inode item.
The same cycle still triggers when the destination offset is inside i_size
(e.g. deduplicating a small file into a larger one, where the destination
inode's i_size exceeds the inline extent's length):
clone_copy_inline_extent() copies the inline data to a folio - dirtying it
inside the range that stays locked in the io tree for the whole clone - and
only then starts a transaction. If a commit has reached
TRANS_STATE_COMMIT_START by that point, the reflink task blocks in
wait_current_trans() while holding the range lock. With flushoncommit the
committing task flushes dirty inodes through the generic writeback path,
which knows nothing about BTRFS_INODE_NO_DELALLOC_FLUSH, and the flusher
blocks forever in find_lock_delalloc_range() on the locked range - writing
the folio back rather than invalidating it, since it is inside i_size:
reflink: holds the dst range lock, blocked in wait_current_trans()
committer: btrfs_commit_transaction() -> btrfs_start_delalloc_flush()
-> try_to_writeback_inodes_sb(), waiting for the flusher
flusher: writepage_delalloc() -> find_lock_delalloc_range(), blocked
on the dst range lock
Reproduced on a plain single-device filesystem mounted with
-o flushoncommit,compress=zstd in under a minute by running concurrently:
truncating rewrites of small compressible files with fsync, FIDEDUPERANGE
over the same files (inline source extents are the essential ingredient)
and a "btrfs filesystem sync" loop.
Fix it by reserving space and starting the transaction before copying the
inline data into the folio. While the locked range is still clean the
flusher has no reason to touch it, so blocking on the transaction start is
safe, and once the handle is held the reflink task can no longer block
waiting for a commit. The space reservation moves out of
copy_inline_to_page() so it stays ordered before the transaction start,
since reserving space may itself flush and wait for a commit. The i_size
update from commit b48c980b6a7e is kept right after the copy so a folio
dirtied beyond EOF is still written back instead of being discarded by
folio invalidation.
Fixes: 05a5a7621ce6 ("Btrfs: implement full reflink support for inline extents") Assisted-by: Claude:claude-fable-5
Zygo Blaxell [Sun, 17 Aug 2025 02:20:01 +0000 (22:20 -0400)]
btrfs: fix nodatacow vs compression inode flag conflict check
Applications expect inode flags to be orthogonal: changes can be combined
or applied separately in any order, as long as each intermediate state is
valid and unchanged flags are left untouched.
Commit f37c563bab42 ("btrfs: add missing check for nocow and compression
inode flags") intended to forbid combining FS_NOCOW_FL with either
FS_NOCOMP_FL or FS_COMPR_FL. The implementation contained a bug and
introduced multiple regressions.
Bug: FS_NOCOW_FL (+C) and FS_NOCOMP_FL (+m) can still be set together in
a single FS_IOC_SETFLAGS call, even though the commit message states this
combination should be rejected.
Regression 1: Switching from +C+m back to -C-m only works if both flag
changes are combined into one ioctl; separate -C and -m calls are
rejected.
Regression 2: Switching between +C-c and -C+c only works if the changes
are split across multiple ioctls; a combined -C+c call is rejected.
Regression 3: Inodes created on kernels before commit f37c563bab42
("btrfs: add missing check for nocow and compression inode flags") with
both COMPR and NOCOW set cannot have any other fsattrs changed on newer
kernels. Even unrelated operations (such as adding +i, or clearing just
one of the compression bits) fail with EINVAL, because the conflict
check rejects the entire ioctl whenever conflicting bits are present,
even if those bits are not being modified. This makes it impossible to
manage older files without first undoing their existing flags.
Fix by:
* Rewriting the conflict checks so FS_NOCOW_FL cannot be combined with
FS_NOCOMP_FL or FS_COMPR_FL in any ioctl (fixes the original bug and
regressions 1-2).
* Allowing existing conflicting flags to remain if they are not modified
by the ioctl (fixes regression 3).
* Moving the check later in the flag-validation sequence so that it
occurs after handling the long-standing rule that FS_NOCOW_FL changes
are silently ignored on non-empty files. This preserves the pre-existing
behavior while still applying the corrected conflict logic.
Also commit the new inode flags to the inode before setting the
compression property, restoring them if the property cannot be set:
property validation rejects compression on nodatacow inodes based on
the inode's current flags, which would otherwise reject a single ioctl
that both clears NOCOW and sets COMPR (regression 2) even with the
conflict check corrected.
Fixes: f37c563bab42 ("btrfs: add missing check for nocow and compression inode flags") Signed-off-by: Zygo Blaxell <ce3g8jdj@umail.furryterror.org>
Zygo Blaxell [Sun, 17 Aug 2025 02:50:20 +0000 (22:50 -0400)]
btrfs: preserve btrfs.compression when setting inode flags
Any call to FS_IOC_SETFLAGS (e.g. via chattr), even when no flag bits
change, and even for flags unrelated to compression, overwrites the
btrfs.compression property with the mount default compression type.
Example:
# mount ... -o compress=zstd ...
$ touch zero
$ setfattr -n btrfs.compression -v zlib zero
$ getfattr -n btrfs.compression zero | grep =
btrfs.compression="zlib"
$ lsattr zero
--------c------------- zero
$ chattr +A zero
$ lsattr zero
-------Ac------------- zero
$ getfattr -n btrfs.compression zero | grep =
btrfs.compression="zstd"
Here, `+A` modifies only the atime flag, but the compression property was
silently replaced. The same happens even if the ioctl writes back the
same flags value that was already set.
The problem is that btrfs_fileattr_set unconditionally regenerates the
compression string from fs_info->compress_type (or falls back to "zlib")
and overwrites any existing property.
Fix this by first checking for an existing per-inode compression property
and using it if present. Only fall back to fs_info->compress_type or zlib
when no property has been set. This ensures that inode-flag updates no
longer clobber user-configured compression settings.
Fixes: 63541927c8d1 ("Btrfs: add support for inode properties") Signed-off-by: Zygo Blaxell <ce3g8jdj@umail.furryterror.org>