Re: [PATCH v2 2/4] btrfs: zoned: drop stranded dirty metadata on transaction abort

Boris Burkov <[email protected]> Thu, 23 Jul 2026 09:54:10 -0700
Newsgroups org.kernel.vger.linux-btrfs
Message-ID <[email protected]>
On Thu, Jul 23, 2026 at 05:54:26PM +0200, Johannes Thumshirn wrote:
> 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.
> 
> Assisted-by: LLM (debugging, commit message)

Reviewed-by: Boris Burkov <[email protected]>

> Signed-off-by: Johannes Thumshirn <[email protected]>
> ---
>  fs/btrfs/disk-io.c   |  1 +
>  fs/btrfs/extent_io.c | 74 ++++++++++++++++++++++++++++++++++----------
>  fs/btrfs/extent_io.h |  1 +
>  3 files changed, 60 insertions(+), 16 deletions(-)
> 
> diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
> index 67171e0d41b3..8d0afc2842cc 100644
> --- a/fs/btrfs/disk-io.c
> +++ b/fs/btrfs/disk-io.c
> @@ -4997,6 +4997,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 e3125ef701ce..fa473476b7db 100644
> --- a/fs/btrfs/extent_io.c
> +++ b/fs/btrfs/extent_io.c
> @@ -3911,6 +3911,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)
>  {
> @@ -3935,26 +3961,42 @@ 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. Such 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);
> +			if (test_and_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.55.0
>