* [PATCH 1/4] btrfs: zoned: flush active metadata block group at btree_writepages() start
2026-07-22 11:30 [PATCH 0/4] btrfs: zoned: btree inode writeback fixes Johannes Thumshirn
@ 2026-07-22 11:30 ` Johannes Thumshirn
2026-07-22 17:08 ` Boris Burkov
2026-07-22 11:30 ` [PATCH 2/4] btrfs: zoned: drop stranded dirty metadata on transaction abort Johannes Thumshirn
` (2 subsequent siblings)
3 siblings, 1 reply; 9+ messages in thread
From: Johannes Thumshirn @ 2026-07-22 11:30 UTC (permalink / raw)
To: linux-btrfs; +Cc: Naohiro Aota, Qu Wenruo, Filipe Manana, Johannes Thumshirn
On a zoned filesystem btree_writepages() writes metadata in ascending
logical address order and keeps a single active metadata/system block group,
pivoting it via btrfs_check_meta_write_pointer() -> check_bg_is_active() as
writeback moves between block groups.
If the active block group is at a higher address than another block group
that also has dirty metadata, the walk reaches the lower one first and the
pivot must finish the active block group. It cannot finish one that still has
unsent IO, and refuses to wait for it during WB_SYNC_ALL commit writeback, so
btrfs_check_meta_write_pointer() returns -EAGAIN and the transaction is
aborted, forcing the filesystem read-only. This happens intermittently under
metadata-heavy relocation (e.g. fstests btrfs/187).
Flush the active metadata and system block groups at the start of
btree_writepages(), under the fs_info->zoned_meta_io_lock it already holds, so
they have no unsent IO and the later pivot can finish them.
Fixes: 13bb483d32ab ("btrfs: zoned: activate metadata block group on write time")
Assisted-by: LLM (debugging, commit message)
Signed-off-by: Johannes Thumshirn <johannes.thumshirn@wdc.com>
---
fs/btrfs/extent_io.c | 62 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 62 insertions(+)
diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
index 97bd18d515af..bac3edabe7c9 100644
--- a/fs/btrfs/extent_io.c
+++ b/fs/btrfs/extent_io.c
@@ -2383,6 +2383,52 @@ void btrfs_btree_wait_writeback_range(struct btrfs_fs_info *fs_info, u64 start,
}
}
+/* Write out the dirty metadata extent buffers of a single block group. */
+static void flush_active_meta_bg(struct address_space *mapping,
+ struct writeback_control *wbc,
+ struct btrfs_eb_write_context *ctx,
+ struct btrfs_block_group *bg)
+{
+ struct btrfs_fs_info *fs_info = inode_to_fs_info(mapping->host);
+ unsigned long index = bg->start >> fs_info->nodesize_bits;
+ unsigned long end = (btrfs_block_group_end(bg) - 1) >> fs_info->nodesize_bits;
+ struct eb_batch batch;
+ unsigned int nr_ebs;
+
+ ASSERT(btrfs_is_zoned(fs_info));
+ lockdep_assert_held(&fs_info->zoned_meta_io_lock);
+
+ eb_batch_init(&batch);
+ while (index <= end &&
+ (nr_ebs = buffer_tree_get_ebs_tag(fs_info, &index, end,
+ PAGECACHE_TAG_DIRTY, &batch))) {
+ struct extent_buffer *eb;
+
+ while ((eb = eb_batch_next(&batch)) != NULL) {
+ ctx->eb = eb;
+
+ /*
+ * Best effort: if the eb is not writable at the write
+ * pointer (e.g. a hole), stop flushing this bg and let
+ * the main walk deal with it.
+ */
+ if (btrfs_check_meta_write_pointer(eb->fs_info, ctx)) {
+ eb_batch_release(&batch);
+ return;
+ }
+
+ if (!lock_extent_buffer_for_io(eb, wbc))
+ continue;
+
+ if (ctx->zoned_bg)
+ ctx->zoned_bg->meta_write_pointer += eb->len;
+ write_one_eb(eb, wbc);
+ }
+ eb_batch_release(&batch);
+ cond_resched();
+ }
+}
+
int btree_writepages(struct address_space *mapping, struct writeback_control *wbc)
{
struct btrfs_eb_write_context ctx = { .wbc = wbc };
@@ -2418,6 +2464,22 @@ int btree_writepages(struct address_space *mapping, struct writeback_control *wb
else
tag = PAGECACHE_TAG_DIRTY;
btrfs_zoned_meta_io_lock(fs_info);
+
+ /*
+ * On a zoned filesystem, flush the currently active metadata/system
+ * block group(s) first, under this same lock, so the ascending-address
+ * walk below can pivot the active block group instead of aborting the
+ * commit with -EAGAIN.
+ */
+ if (btrfs_is_zoned(fs_info) && wbc->sync_mode == WB_SYNC_ALL &&
+ !wbc->for_sync) {
+ if (fs_info->active_meta_bg)
+ flush_active_meta_bg(mapping, wbc, &ctx,
+ fs_info->active_meta_bg);
+ if (fs_info->active_system_bg)
+ flush_active_meta_bg(mapping, wbc, &ctx,
+ fs_info->active_system_bg);
+ }
retry:
if (wbc->sync_mode == WB_SYNC_ALL)
buffer_tree_tag_for_writeback(fs_info, index, end);
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* Re: [PATCH 1/4] btrfs: zoned: flush active metadata block group at btree_writepages() start
2026-07-22 11:30 ` [PATCH 1/4] btrfs: zoned: flush active metadata block group at btree_writepages() start Johannes Thumshirn
@ 2026-07-22 17:08 ` Boris Burkov
2026-07-22 19:01 ` Boris Burkov
0 siblings, 1 reply; 9+ messages in thread
From: Boris Burkov @ 2026-07-22 17:08 UTC (permalink / raw)
To: Johannes Thumshirn; +Cc: linux-btrfs, Naohiro Aota, Qu Wenruo, Filipe Manana
On Wed, Jul 22, 2026 at 01:30:13PM +0200, Johannes Thumshirn wrote:
> On a zoned filesystem btree_writepages() writes metadata in ascending
wording nit: isn't this true on non-zoned too? Maybe something like:
btree_writepages() writes metadata in ascending logical order. On a
zoned filsystem, which keeps a single .... <something terrible happens>
> logical address order and keeps a single active metadata/system block group,
> pivoting it via btrfs_check_meta_write_pointer() -> check_bg_is_active() as
> writeback moves between block groups.
>
> If the active block group is at a higher address than another block group
> that also has dirty metadata, the walk reaches the lower one first and the
> pivot must finish the active block group. It cannot finish one that still has
> unsent IO, and refuses to wait for it during WB_SYNC_ALL commit writeback, so
Can you add some more detail on why it refuses? Or why the pivot can't
be the one to do these submissions? I think that would help fully
motivate this change.
> btrfs_check_meta_write_pointer() returns -EAGAIN and the transaction is
> aborted, forcing the filesystem read-only. This happens intermittently under
> metadata-heavy relocation (e.g. fstests btrfs/187).
>
> Flush the active metadata and system block groups at the start of
> btree_writepages(), under the fs_info->zoned_meta_io_lock it already holds, so
> they have no unsent IO and the later pivot can finish them.
>
> Fixes: 13bb483d32ab ("btrfs: zoned: activate metadata block group on write time")
> Assisted-by: LLM (debugging, commit message)
> Signed-off-by: Johannes Thumshirn <johannes.thumshirn@wdc.com>
> ---
> fs/btrfs/extent_io.c | 62 ++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 62 insertions(+)
>
> diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
> index 97bd18d515af..bac3edabe7c9 100644
> --- a/fs/btrfs/extent_io.c
> +++ b/fs/btrfs/extent_io.c
> @@ -2383,6 +2383,52 @@ void btrfs_btree_wait_writeback_range(struct btrfs_fs_info *fs_info, u64 start,
> }
> }
>
> +/* Write out the dirty metadata extent buffers of a single block group. */
This comment is quite generic, and not zoned specific, but the function
name is much more zoned focused (and we assert zoned)
I am also not a huge fan of this code mostly duplicating the loop in
btree_writepages while just leaving that spot open coded. Is that an
intentional decision because of important distinctions between the two?
Is there a variant we could lift to "write a range" and make
flush_active_meta_bg() and btree_writepages share it? (like the while
loop basically? or maybe the per-eb bit?)
note the missing call to btrfs_schedule_zone_finish_bg() I noted inline
as a concrete reason it's helpful to not duplicate.
> +static void flush_active_meta_bg(struct address_space *mapping,
> + struct writeback_control *wbc,
> + struct btrfs_eb_write_context *ctx,
> + struct btrfs_block_group *bg)
> +{
> + struct btrfs_fs_info *fs_info = inode_to_fs_info(mapping->host);
> + unsigned long index = bg->start >> fs_info->nodesize_bits;
> + unsigned long end = (btrfs_block_group_end(bg) - 1) >> fs_info->nodesize_bits;
> + struct eb_batch batch;
> + unsigned int nr_ebs;
> +
> + ASSERT(btrfs_is_zoned(fs_info));
> + lockdep_assert_held(&fs_info->zoned_meta_io_lock);
> +
> + eb_batch_init(&batch);
> + while (index <= end &&
> + (nr_ebs = buffer_tree_get_ebs_tag(fs_info, &index, end,
> + PAGECACHE_TAG_DIRTY, &batch))) {
> + struct extent_buffer *eb;
> +
> + while ((eb = eb_batch_next(&batch)) != NULL) {
> + ctx->eb = eb;
> +
> + /*
> + * Best effort: if the eb is not writable at the write
> + * pointer (e.g. a hole), stop flushing this bg and let
> + * the main walk deal with it.
> + */
> + if (btrfs_check_meta_write_pointer(eb->fs_info, ctx)) {
> + eb_batch_release(&batch);
> + return;
> + }
> +
> + if (!lock_extent_buffer_for_io(eb, wbc))
> + continue;
> +
> + if (ctx->zoned_bg)
the btree_writepages code also calls btrfs_schedule_zone_finish_bg()
here, is that intentionally omitted?
> + ctx->zoned_bg->meta_write_pointer += eb->len;
> + write_one_eb(eb, wbc);
> + }
> + eb_batch_release(&batch);
> + cond_resched();
> + }
> +}
> +
> int btree_writepages(struct address_space *mapping, struct writeback_control *wbc)
> {
> struct btrfs_eb_write_context ctx = { .wbc = wbc };
> @@ -2418,6 +2464,22 @@ int btree_writepages(struct address_space *mapping, struct writeback_control *wb
> else
> tag = PAGECACHE_TAG_DIRTY;
> btrfs_zoned_meta_io_lock(fs_info);
> +
> + /*
> + * On a zoned filesystem, flush the currently active metadata/system
> + * block group(s) first, under this same lock, so the ascending-address
> + * walk below can pivot the active block group instead of aborting the
> + * commit with -EAGAIN.
> + */
> + if (btrfs_is_zoned(fs_info) && wbc->sync_mode == WB_SYNC_ALL &&
> + !wbc->for_sync) {
> + if (fs_info->active_meta_bg)
> + flush_active_meta_bg(mapping, wbc, &ctx,
> + fs_info->active_meta_bg);
> + if (fs_info->active_system_bg)
> + flush_active_meta_bg(mapping, wbc, &ctx,
> + fs_info->active_system_bg);
> + }
I think this placement makes sense, but I also wouldn't mind just having
a more generic "eb ordering" concept that zoned could use to plug the
active ebs to the front of the list then fallback to the normal order,
or something. Not a deal breaker for me, just food for thought.
I think fewer "tricks" for zoned writeback and more "it falls out
correctly from a generic form" would make it easier to maintain in the
long run. Obviously there are real, important differences so this kind
of thing is never free...
Thanks,
Boris
> retry:
> if (wbc->sync_mode == WB_SYNC_ALL)
> buffer_tree_tag_for_writeback(fs_info, index, end);
> --
> 2.54.0
>
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH 1/4] btrfs: zoned: flush active metadata block group at btree_writepages() start
2026-07-22 17:08 ` Boris Burkov
@ 2026-07-22 19:01 ` Boris Burkov
2026-07-23 14:02 ` Johannes Thumshirn
0 siblings, 1 reply; 9+ messages in thread
From: Boris Burkov @ 2026-07-22 19:01 UTC (permalink / raw)
To: Johannes Thumshirn; +Cc: linux-btrfs, Naohiro Aota, Qu Wenruo, Filipe Manana
On Wed, Jul 22, 2026 at 10:08:35AM -0700, Boris Burkov wrote:
> On Wed, Jul 22, 2026 at 01:30:13PM +0200, Johannes Thumshirn wrote:
> > On a zoned filesystem btree_writepages() writes metadata in ascending
>
> wording nit: isn't this true on non-zoned too? Maybe something like:
> btree_writepages() writes metadata in ascending logical order. On a
> zoned filsystem, which keeps a single .... <something terrible happens>
>
> > logical address order and keeps a single active metadata/system block group,
> > pivoting it via btrfs_check_meta_write_pointer() -> check_bg_is_active() as
> > writeback moves between block groups.
> >
> > If the active block group is at a higher address than another block group
> > that also has dirty metadata, the walk reaches the lower one first and the
> > pivot must finish the active block group. It cannot finish one that still has
> > unsent IO, and refuses to wait for it during WB_SYNC_ALL commit writeback, so
>
> Can you add some more detail on why it refuses? Or why the pivot can't
> be the one to do these submissions? I think that would help fully
> motivate this change.
>
> > btrfs_check_meta_write_pointer() returns -EAGAIN and the transaction is
> > aborted, forcing the filesystem read-only. This happens intermittently under
> > metadata-heavy relocation (e.g. fstests btrfs/187).
> >
> > Flush the active metadata and system block groups at the start of
> > btree_writepages(), under the fs_info->zoned_meta_io_lock it already holds, so
> > they have no unsent IO and the later pivot can finish them.
> >
> > Fixes: 13bb483d32ab ("btrfs: zoned: activate metadata block group on write time")
> > Assisted-by: LLM (debugging, commit message)
> > Signed-off-by: Johannes Thumshirn <johannes.thumshirn@wdc.com>
> > ---
> > fs/btrfs/extent_io.c | 62 ++++++++++++++++++++++++++++++++++++++++++++
> > 1 file changed, 62 insertions(+)
> >
> > diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
> > index 97bd18d515af..bac3edabe7c9 100644
> > --- a/fs/btrfs/extent_io.c
> > +++ b/fs/btrfs/extent_io.c
> > @@ -2383,6 +2383,52 @@ void btrfs_btree_wait_writeback_range(struct btrfs_fs_info *fs_info, u64 start,
> > }
> > }
> >
> > +/* Write out the dirty metadata extent buffers of a single block group. */
>
> This comment is quite generic, and not zoned specific, but the function
> name is much more zoned focused (and we assert zoned)
>
> I am also not a huge fan of this code mostly duplicating the loop in
> btree_writepages while just leaving that spot open coded. Is that an
> intentional decision because of important distinctions between the two?
> Is there a variant we could lift to "write a range" and make
> flush_active_meta_bg() and btree_writepages share it? (like the while
> loop basically? or maybe the per-eb bit?)
>
> note the missing call to btrfs_schedule_zone_finish_bg() I noted inline
> as a concrete reason it's helpful to not duplicate.
>
> > +static void flush_active_meta_bg(struct address_space *mapping,
> > + struct writeback_control *wbc,
> > + struct btrfs_eb_write_context *ctx,
> > + struct btrfs_block_group *bg)
> > +{
> > + struct btrfs_fs_info *fs_info = inode_to_fs_info(mapping->host);
> > + unsigned long index = bg->start >> fs_info->nodesize_bits;
> > + unsigned long end = (btrfs_block_group_end(bg) - 1) >> fs_info->nodesize_bits;
> > + struct eb_batch batch;
> > + unsigned int nr_ebs;
> > +
> > + ASSERT(btrfs_is_zoned(fs_info));
> > + lockdep_assert_held(&fs_info->zoned_meta_io_lock);
> > +
> > + eb_batch_init(&batch);
> > + while (index <= end &&
> > + (nr_ebs = buffer_tree_get_ebs_tag(fs_info, &index, end,
> > + PAGECACHE_TAG_DIRTY, &batch))) {
> > + struct extent_buffer *eb;
> > +
> > + while ((eb = eb_batch_next(&batch)) != NULL) {
> > + ctx->eb = eb;
> > +
> > + /*
> > + * Best effort: if the eb is not writable at the write
> > + * pointer (e.g. a hole), stop flushing this bg and let
> > + * the main walk deal with it.
> > + */
> > + if (btrfs_check_meta_write_pointer(eb->fs_info, ctx)) {
> > + eb_batch_release(&batch);
> > + return;
> > + }
> > +
> > + if (!lock_extent_buffer_for_io(eb, wbc))
> > + continue;
> > +
> > + if (ctx->zoned_bg)
>
> the btree_writepages code also calls btrfs_schedule_zone_finish_bg()
> here, is that intentionally omitted?
>
> > + ctx->zoned_bg->meta_write_pointer += eb->len;
> > + write_one_eb(eb, wbc);
> > + }
> > + eb_batch_release(&batch);
> > + cond_resched();
> > + }
> > +}
> > +
> > int btree_writepages(struct address_space *mapping, struct writeback_control *wbc)
> > {
> > struct btrfs_eb_write_context ctx = { .wbc = wbc };
> > @@ -2418,6 +2464,22 @@ int btree_writepages(struct address_space *mapping, struct writeback_control *wb
> > else
> > tag = PAGECACHE_TAG_DIRTY;
> > btrfs_zoned_meta_io_lock(fs_info);
> > +
> > + /*
> > + * On a zoned filesystem, flush the currently active metadata/system
> > + * block group(s) first, under this same lock, so the ascending-address
> > + * walk below can pivot the active block group instead of aborting the
> > + * commit with -EAGAIN.
> > + */
> > + if (btrfs_is_zoned(fs_info) && wbc->sync_mode == WB_SYNC_ALL &&
> > + !wbc->for_sync) {
> > + if (fs_info->active_meta_bg)
> > + flush_active_meta_bg(mapping, wbc, &ctx,
> > + fs_info->active_meta_bg);
> > + if (fs_info->active_system_bg)
> > + flush_active_meta_bg(mapping, wbc, &ctx,
> > + fs_info->active_system_bg);
> > + }
>
> I think this placement makes sense, but I also wouldn't mind just having
> a more generic "eb ordering" concept that zoned could use to plug the
> active ebs to the front of the list then fallback to the normal order,
> or something. Not a deal breaker for me, just food for thought.
>
Furthermore, I feel like this is actually quite redundant with the whole
EBUSY pivot logic.
So for random reclaim writeback, we walk the ebs in index order and skip
any not in the active bg if the active bg has dirty ebs. Then we reach
the active bg and flush it, submitting everything. Subsequent tries to
writeback the non-active will now wait on that submitted IO and
ultimately win and get to become active, and do more submission.
But we could save the bother and make things more direct and simple if
we just had btree_writepages() always write out the active bg first then
the rest. I don't know if we could fully delete the EBUSY, though, maybe
there is some racy case where it would still happen a bit, even if
mostly it didn't?
> I think fewer "tricks" for zoned writeback and more "it falls out
> correctly from a generic form" would make it easier to maintain in the
> long run. Obviously there are real, important differences so this kind
> of thing is never free...
>
> Thanks,
> Boris
>
> > retry:
> > if (wbc->sync_mode == WB_SYNC_ALL)
> > buffer_tree_tag_for_writeback(fs_info, index, end);
> > --
> > 2.54.0
> >
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH 1/4] btrfs: zoned: flush active metadata block group at btree_writepages() start
2026-07-22 19:01 ` Boris Burkov
@ 2026-07-23 14:02 ` Johannes Thumshirn
2026-07-23 15:40 ` Boris Burkov
0 siblings, 1 reply; 9+ messages in thread
From: Johannes Thumshirn @ 2026-07-23 14:02 UTC (permalink / raw)
To: Boris Burkov; +Cc: linux-btrfs, Naohiro Aota, Qu Wenruo, Filipe Manana
On 7/22/26 9:01 PM, Boris Burkov wrote:
> Furthermore, I feel like this is actually quite redundant with the whole
> EBUSY pivot logic.
>
> So for random reclaim writeback, we walk the ebs in index order and skip
> any not in the active bg if the active bg has dirty ebs. Then we reach
> the active bg and flush it, submitting everything. Subsequent tries to
> writeback the non-active will now wait on that submitted IO and
> ultimately win and get to become active, and do more submission.
>
> But we could save the bother and make things more direct and simple if
> we just had btree_writepages() always write out the active bg first then
> the rest. I don't know if we could fully delete the EBUSY, though, maybe
> there is some racy case where it would still happen a bit, even if
> mostly it didn't?
I got a v2 with your comments (and Sashiko's) addressed. I can look into
that as well, but that feels a bit more involved and we're seeing random
hangs in our CI system due to these stalled folios and we have one
report on the list because of the hole -EAGAIN, so I'd like to get it
(hot-)fixed possibly in this cycle and take care about that once the
hotfix is there.
How does that sound?
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH 1/4] btrfs: zoned: flush active metadata block group at btree_writepages() start
2026-07-23 14:02 ` Johannes Thumshirn
@ 2026-07-23 15:40 ` Boris Burkov
0 siblings, 0 replies; 9+ messages in thread
From: Boris Burkov @ 2026-07-23 15:40 UTC (permalink / raw)
To: Johannes Thumshirn; +Cc: linux-btrfs, Naohiro Aota, Qu Wenruo, Filipe Manana
On Thu, Jul 23, 2026 at 04:02:27PM +0200, Johannes Thumshirn wrote:
> On 7/22/26 9:01 PM, Boris Burkov wrote:
> > Furthermore, I feel like this is actually quite redundant with the whole
> > EBUSY pivot logic.
> >
> > So for random reclaim writeback, we walk the ebs in index order and skip
> > any not in the active bg if the active bg has dirty ebs. Then we reach
> > the active bg and flush it, submitting everything. Subsequent tries to
> > writeback the non-active will now wait on that submitted IO and
> > ultimately win and get to become active, and do more submission.
> >
> > But we could save the bother and make things more direct and simple if
> > we just had btree_writepages() always write out the active bg first then
> > the rest. I don't know if we could fully delete the EBUSY, though, maybe
> > there is some racy case where it would still happen a bit, even if
> > mostly it didn't?
>
> I got a v2 with your comments (and Sashiko's) addressed. I can look into
> that as well, but that feels a bit more involved and we're seeing random
> hangs in our CI system due to these stalled folios and we have one report on
> the list because of the hole -EAGAIN, so I'd like to get it (hot-)fixed
> possibly in this cycle and take care about that once the hotfix is there.
>
> How does that sound?
>
100% fine to fix without going crazy removing EBUSY or whatever. Sorry
for holding you up on that, I just found the problem interesting.
All else being equal, I would *prefer* to not have the eb submit loop
totally duplicated even in the short term fix. But again, OK with
whatever you need to land to unbreak things.
Thanks,
Boris
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 2/4] btrfs: zoned: drop stranded dirty metadata on transaction abort
2026-07-22 11:30 [PATCH 0/4] btrfs: zoned: btree inode writeback fixes Johannes Thumshirn
2026-07-22 11:30 ` [PATCH 1/4] btrfs: zoned: flush active metadata block group at btree_writepages() start Johannes Thumshirn
@ 2026-07-22 11:30 ` Johannes Thumshirn
2026-07-22 11:30 ` [PATCH 3/4] btrfs: zoned: drop stranded dirty metadata buffers at unmount Johannes Thumshirn
2026-07-22 11:30 ` [PATCH 4/4] btrfs: zoned: don't clobber the extent buffer when zeroing it out Johannes Thumshirn
3 siblings, 0 replies; 9+ messages in thread
From: Johannes Thumshirn @ 2026-07-22 11:30 UTC (permalink / raw)
To: linux-btrfs; +Cc: Naohiro Aota, Qu Wenruo, Filipe Manana, Johannes Thumshirn
On a zoned filesystem a freed tree block is not cleared but kept dirty and
flagged EXTENT_BUFFER_ZONED_ZEROOUT, so a later writeback zeroes it out and
advances the zone write pointer. A transaction abort turns the filesystem
read-only before that writeback runs, so these buffers stay dirty and stranded
ahead of the write pointer where btree_writepages() can no longer write them.
They survive to the final iput() of the btree inode at unmount, which submits
the write after the endio workqueues are gone, hanging unmount in
folio_wait_writeback().
Clear the dirty state of such buffers when cleaning up the aborted transaction,
where the buffer tree still references all of them.
Fixes: aa6313e6ff2b ("btrfs: zoned: don't clear dirty flag of extent buffer")
Assisted-by: LLM (debugging, commit message)
Signed-off-by: Johannes Thumshirn <johannes.thumshirn@wdc.com>
---
fs/btrfs/disk-io.c | 1 +
fs/btrfs/extent_io.c | 73 ++++++++++++++++++++++++++++++++++----------
fs/btrfs/extent_io.h | 1 +
3 files changed, 59 insertions(+), 16 deletions(-)
diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index 37fc0d6b960d..e84ee395ac7f 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -4987,6 +4987,7 @@ static int btrfs_cleanup_transaction(struct btrfs_fs_info *fs_info)
btrfs_assert_delayed_root_empty(fs_info);
btrfs_destroy_all_delalloc_inodes(fs_info);
btrfs_drop_all_logs(fs_info);
+ btrfs_zoned_release_dirty_metadata(fs_info);
btrfs_free_all_qgroup_pertrans(fs_info);
mutex_unlock(&fs_info->transaction_kthread_mutex);
diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
index bac3edabe7c9..042093470312 100644
--- a/fs/btrfs/extent_io.c
+++ b/fs/btrfs/extent_io.c
@@ -3879,6 +3879,32 @@ void free_extent_buffer_stale(struct extent_buffer *eb)
release_extent_buffer(eb);
}
+static void clear_extent_buffer_dirty(struct extent_buffer *eb)
+{
+ struct btrfs_fs_info *fs_info = eb->fs_info;
+
+ if (!test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags))
+ return;
+
+ buffer_tree_clear_mark(eb, PAGECACHE_TAG_DIRTY);
+ percpu_counter_add_batch(&fs_info->dirty_metadata_bytes, -(s64)eb->len,
+ fs_info->dirty_metadata_batch);
+
+ for (int i = 0; i < num_extent_folios(eb); i++) {
+ struct folio *folio = eb->folios[i];
+ bool last;
+
+ if (!folio_test_dirty(folio))
+ continue;
+ folio_lock(folio);
+ last = btrfs_meta_folio_clear_and_test_dirty(folio, eb);
+ if (last)
+ btrfs_clear_folio_dirty_tag(folio);
+ folio_unlock(folio);
+ }
+ WARN_ON(refcount_read(&eb->refs) == 0);
+}
+
void btrfs_clear_buffer_dirty(struct btrfs_trans_handle *trans,
struct extent_buffer *eb)
{
@@ -3903,26 +3929,41 @@ void btrfs_clear_buffer_dirty(struct btrfs_trans_handle *trans,
return;
}
- if (!test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags))
- return;
+ clear_extent_buffer_dirty(eb);
+}
- buffer_tree_clear_mark(eb, PAGECACHE_TAG_DIRTY);
- percpu_counter_add_batch(&fs_info->dirty_metadata_bytes, -(s64)eb->len,
- fs_info->dirty_metadata_batch);
+/*
+ * On a zoned filesystem a freed tree block is kept dirty and flagged as
+ * EXTENT_BUFFER_ZONED_ZEROOUT so a later writeback zeroes it out and advances
+ * the zone write pointer. Buffers still dirty when the filesystem is torn down
+ * can no longer be written back and are stale. if left dirty they hang the
+ * final iput() of the btree inode. Drop their dirty state, and the deferred
+ * zero-out along with it.
+ */
+void btrfs_zoned_release_dirty_metadata(struct btrfs_fs_info *fs_info)
+{
+ struct eb_batch batch;
+ unsigned long index = 0;
- for (int i = 0; i < num_extent_folios(eb); i++) {
- struct folio *folio = eb->folios[i];
- bool last;
+ if (!btrfs_is_zoned(fs_info))
+ return;
- if (!folio_test_dirty(folio))
- continue;
- folio_lock(folio);
- last = btrfs_meta_folio_clear_and_test_dirty(folio, eb);
- if (last)
- btrfs_clear_folio_dirty_tag(folio);
- folio_unlock(folio);
+ btrfs_zoned_meta_io_lock(fs_info);
+ eb_batch_init(&batch);
+ while (buffer_tree_get_ebs_tag(fs_info, &index, ULONG_MAX,
+ PAGECACHE_TAG_DIRTY, &batch)) {
+ struct extent_buffer *eb;
+
+ while ((eb = eb_batch_next(&batch)) != NULL) {
+ btrfs_tree_lock(eb);
+ clear_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags);
+ clear_extent_buffer_dirty(eb);
+ btrfs_tree_unlock(eb);
+ }
+ eb_batch_release(&batch);
+ cond_resched();
}
- WARN_ON(refcount_read(&eb->refs) == 0);
+ btrfs_zoned_meta_io_unlock(fs_info);
}
void set_extent_buffer_dirty(struct extent_buffer *eb)
diff --git a/fs/btrfs/extent_io.h b/fs/btrfs/extent_io.h
index 869925337699..ad4ffce32702 100644
--- a/fs/btrfs/extent_io.h
+++ b/fs/btrfs/extent_io.h
@@ -393,6 +393,7 @@ void extent_clear_unlock_delalloc(struct btrfs_inode *inode, u64 start, u64 end,
u32 bits_to_clear, unsigned long page_ops);
void btrfs_clear_buffer_dirty(struct btrfs_trans_handle *trans,
struct extent_buffer *buf);
+void btrfs_zoned_release_dirty_metadata(struct btrfs_fs_info *fs_info);
static inline void btrfs_clear_folio_dirty_tag(struct folio *folio)
{
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH 3/4] btrfs: zoned: drop stranded dirty metadata buffers at unmount
2026-07-22 11:30 [PATCH 0/4] btrfs: zoned: btree inode writeback fixes Johannes Thumshirn
2026-07-22 11:30 ` [PATCH 1/4] btrfs: zoned: flush active metadata block group at btree_writepages() start Johannes Thumshirn
2026-07-22 11:30 ` [PATCH 2/4] btrfs: zoned: drop stranded dirty metadata on transaction abort Johannes Thumshirn
@ 2026-07-22 11:30 ` Johannes Thumshirn
2026-07-22 11:30 ` [PATCH 4/4] btrfs: zoned: don't clobber the extent buffer when zeroing it out Johannes Thumshirn
3 siblings, 0 replies; 9+ messages in thread
From: Johannes Thumshirn @ 2026-07-22 11:30 UTC (permalink / raw)
To: linux-btrfs; +Cc: Naohiro Aota, Qu Wenruo, Filipe Manana, Johannes Thumshirn
On a zoned filesystem a freed tree block is kept dirty and flagged
EXTENT_BUFFER_ZONED_ZEROOUT so a later writeback zeroes it out and advances the
zone write pointer. Unsynced tree-log updates (e.g. from rename or link) leave
such buffers behind when the log is freed at commit, and across log generations
they can end up ahead of the write pointer behind a hole, so btree_writepages()
can never write them. During normal operation the space is later reclaimed by a
zone reset; at unmount it is not, and the buffers survive to the final iput() of
the btree inode, which hangs in folio_wait_writeback() once the endio
workqueues are stopped.
They cannot be written back from where they are freed (free_log_tree(), inside
the committing transaction) without deadlocking against that commit, and they
are stale anyway, not referenced by the committed superblock. Drop their dirty
state in close_ctree(), before btrfs_stop_all_workers().
Fixes: aa6313e6ff2b ("btrfs: zoned: don't clear dirty flag of extent buffer")
Signed-off-by: Johannes Thumshirn <johannes.thumshirn@wdc.com>
---
fs/btrfs/disk-io.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index e84ee395ac7f..48374cec77d8 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -4539,6 +4539,13 @@ void __cold close_ctree(struct btrfs_fs_info *fs_info)
free_root_pointers(fs_info, true);
btrfs_free_fs_roots(fs_info);
+ /*
+ * Drop metadata left stranded ahead of a zone write pointer while the
+ * endio workqueues are still up, so the final iput() of the btree inode
+ * below does not hang submitting a write that can no longer complete.
+ */
+ btrfs_zoned_release_dirty_metadata(fs_info);
+
/*
* We must make sure there is not any read request to
* submit after we stop all workers.
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH 4/4] btrfs: zoned: don't clobber the extent buffer when zeroing it out
2026-07-22 11:30 [PATCH 0/4] btrfs: zoned: btree inode writeback fixes Johannes Thumshirn
` (2 preceding siblings ...)
2026-07-22 11:30 ` [PATCH 3/4] btrfs: zoned: drop stranded dirty metadata buffers at unmount Johannes Thumshirn
@ 2026-07-22 11:30 ` Johannes Thumshirn
3 siblings, 0 replies; 9+ messages in thread
From: Johannes Thumshirn @ 2026-07-22 11:30 UTC (permalink / raw)
To: linux-btrfs; +Cc: Naohiro Aota, Qu Wenruo, Filipe Manana, Johannes Thumshirn
On a zoned filesystem a freed-but-still-dirty tree block is written out as
zeros (EXTENT_BUFFER_ZONED_ZEROOUT) only to keep the zone write pointer
advancing. btree_csum_one_bio() implemented this by memzeroing the extent
buffer's own folios before submission.
That destroys the in-memory buffer while it may still be referenced. In
particular btrfs_free_tree_block() can run on it afterwards and reads the
header to add a delayed reference; once the header has been zeroed it frees
bytenr 0 and corrupts the extent tree (the btrfs_header_bytenr(buf) != 0
ASSERT in btrfs_free_tree_block(), or an "unable to find ref" abort). It is
flaky and reproduces under fsstress, e.g. generic/461 and generic/013.
Write the zeros to disk from the shared zero page instead and leave the
extent buffer content untouched, so any later reference - including the
delayed reference from btrfs_free_tree_block() - still sees a valid header.
end_bbio_meta_write() now clears writeback on the buffer's own folios, as the
bio no longer carries them.
Fixes: aa6313e6ff2b ("btrfs: zoned: don't clear dirty flag of extent buffer")
Assisted-by: LLM (debugging, commit message)
Signed-off-by: Johannes Thumshirn <johannes.thumshirn@wdc.com>
---
fs/btrfs/disk-io.c | 13 +++++++------
fs/btrfs/extent_io.c | 31 ++++++++++++++++++++++++-------
2 files changed, 31 insertions(+), 13 deletions(-)
diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index 48374cec77d8..4583adb50367 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -271,14 +271,15 @@ int btree_csum_one_bio(struct btrfs_bio *bbio)
return -EIO;
/*
- * If an extent_buffer is marked as EXTENT_BUFFER_ZONED_ZEROOUT, don't
- * checksum it but zero-out its content. This is done to preserve
- * ordering of I/O without unnecessarily writing out data.
+ * An extent_buffer marked EXTENT_BUFFER_ZONED_ZEROOUT is written out as
+ * zeros to preserve ordering of I/O without persisting the now
+ * unnecessary block. The bio is fed from the shared zero page (see
+ * write_one_eb()), so there is nothing to checksum here. Crucially, the
+ * buffer's own content is left intact: it may still be referenced, e.g.
+ * btrfs_free_tree_block() reads its header to add a delayed reference.
*/
- if (test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags)) {
- memzero_extent_buffer(eb, 0, eb->len);
+ if (test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags))
return 0;
- }
if (WARN_ON_ONCE(found_start != eb->start))
return -EIO;
diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
index 042093470312..f1579633a996 100644
--- a/fs/btrfs/extent_io.c
+++ b/fs/btrfs/extent_io.c
@@ -2266,14 +2266,17 @@ static struct extent_buffer *find_extent_buffer_nolock(
static void end_bbio_meta_write(struct btrfs_bio *bbio)
{
struct extent_buffer *eb = bbio->private;
- struct folio_iter fi;
if (bbio->bio.bi_status != BLK_STS_OK)
set_btree_ioerr(eb);
- bio_for_each_folio_all(fi, &bbio->bio) {
- btrfs_meta_folio_clear_writeback(fi.folio, eb);
- }
+ /*
+ * Clear writeback on the buffer's own folios. The bio may carry the
+ * shared zero page instead (EXTENT_BUFFER_ZONED_ZEROOUT), so iterate
+ * the extent buffer folios rather than the bio folios.
+ */
+ for (int i = 0; i < num_extent_folios(eb); i++)
+ btrfs_meta_folio_clear_writeback(eb->folios[i], eb);
buffer_tree_clear_mark(eb, PAGECACHE_TAG_WRITEBACK);
clear_and_wake_up_bit(EXTENT_BUFFER_WRITEBACK, &eb->bflags);
@@ -2314,7 +2317,8 @@ static noinline_for_stack void write_one_eb(struct extent_buffer *eb,
struct btrfs_fs_info *fs_info = eb->fs_info;
struct btrfs_bio *bbio;
- prepare_eb_write(eb);
+ if (!test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags))
+ prepare_eb_write(eb);
bbio = btrfs_bio_alloc(INLINE_EXTENT_BUFFER_PAGES,
REQ_OP_WRITE | REQ_META | wbc_to_write_flags(wbc),
@@ -2334,8 +2338,21 @@ static noinline_for_stack void write_one_eb(struct extent_buffer *eb,
btrfs_meta_folio_set_writeback(folio, eb);
if (!folio_test_dirty(folio))
wbc->nr_to_write -= folio_nr_pages(folio);
- bio_add_folio_nofail(&bbio->bio, folio, range_len,
- offset_in_folio(folio, range_start));
+ if (test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags)) {
+ u32 off = 0;
+
+ while (off < range_len) {
+ u32 add = min_t(u32, PAGE_SIZE, range_len - off);
+
+ bio_add_folio_nofail(&bbio->bio,
+ page_folio(ZERO_PAGE(0)),
+ add, 0);
+ off += add;
+ }
+ } else {
+ bio_add_folio_nofail(&bbio->bio, folio, range_len,
+ offset_in_folio(folio, range_start));
+ }
wbc_account_cgroup_owner(wbc, folio, range_len);
folio_unlock(folio);
}
--
2.54.0
^ permalink raw reply related [flat|nested] 9+ messages in thread