From c6ce65cb17aa9321687d1b8a842487f839e1a548 Mon Sep 17 00:00:00 2001 From: Wilfred Mallawa Date: Sun, 1 Mar 2026 10:34:35 +1000 Subject: [PATCH 01/41] xfs: add write pointer to xfs_rtgroup_geometry There is currently no XFS ioctl that allows userspace to retrieve the write pointer for a specific realtime group block for zoned XFS. On zoned block devices, userspace can obtain this information via zone reports from the underlying device. However, for zoned XFS operating on regular block devices, no equivalent mechanism exists. Access to the realtime group write pointer is useful to userspace development and analysis tools such as Zonar [1]. So extend the existing struct xfs_rtgroup_geometry to add a new rg_writepointer field. This field is valid if XFS_RTGROUP_GEOM_WRITEPOINTER flag is set. The rg_writepointer field specifies the location of the current writepointer as a block offset into the respective rtgroup. [1] https://lwn.net/Articles/1059364/ Signed-off-by: Wilfred Mallawa Reviewed-by: Darrick J. Wong Reviewed-by: Christoph Hellwig Signed-off-by: Carlos Maiolino --- fs/xfs/libxfs/xfs_fs.h | 5 ++++- fs/xfs/xfs_ioctl.c | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/fs/xfs/libxfs/xfs_fs.h b/fs/xfs/libxfs/xfs_fs.h index d165de607d17..185f09f327c0 100644 --- a/fs/xfs/libxfs/xfs_fs.h +++ b/fs/xfs/libxfs/xfs_fs.h @@ -995,7 +995,8 @@ struct xfs_rtgroup_geometry { __u32 rg_sick; /* o: sick things in ag */ __u32 rg_checked; /* o: checked metadata in ag */ __u32 rg_flags; /* i/o: flags for this ag */ - __u32 rg_reserved[27]; /* o: zero */ + __u32 rg_writepointer; /* o: write pointer block offset for zoned */ + __u32 rg_reserved[26]; /* o: zero */ }; #define XFS_RTGROUP_GEOM_SICK_SUPER (1U << 0) /* superblock */ #define XFS_RTGROUP_GEOM_SICK_BITMAP (1U << 1) /* rtbitmap */ @@ -1003,6 +1004,8 @@ struct xfs_rtgroup_geometry { #define XFS_RTGROUP_GEOM_SICK_RMAPBT (1U << 3) /* reverse mappings */ #define XFS_RTGROUP_GEOM_SICK_REFCNTBT (1U << 4) /* reference counts */ +#define XFS_RTGROUP_GEOM_WRITEPOINTER (1U << 0) /* write pointer */ + /* Health monitor event domains */ /* affects the whole fs */ diff --git a/fs/xfs/xfs_ioctl.c b/fs/xfs/xfs_ioctl.c index facffdc8dca8..46e234863644 100644 --- a/fs/xfs/xfs_ioctl.c +++ b/fs/xfs/xfs_ioctl.c @@ -37,12 +37,15 @@ #include "xfs_ioctl.h" #include "xfs_xattr.h" #include "xfs_rtbitmap.h" +#include "xfs_rtrmap_btree.h" #include "xfs_file.h" #include "xfs_exchrange.h" #include "xfs_handle.h" #include "xfs_rtgroup.h" #include "xfs_healthmon.h" #include "xfs_verify_media.h" +#include "xfs_zone_priv.h" +#include "xfs_zone_alloc.h" #include #include @@ -413,6 +416,7 @@ xfs_ioc_rtgroup_geometry( { struct xfs_rtgroup *rtg; struct xfs_rtgroup_geometry rgeo; + xfs_rgblock_t highest_rgbno; int error; if (copy_from_user(&rgeo, arg, sizeof(rgeo))) @@ -433,6 +437,21 @@ xfs_ioc_rtgroup_geometry( if (error) return error; + if (xfs_has_zoned(mp)) { + xfs_rtgroup_lock(rtg, XFS_RTGLOCK_RMAP); + if (rtg->rtg_open_zone) { + rgeo.rg_writepointer = rtg->rtg_open_zone->oz_allocated; + } else { + highest_rgbno = xfs_rtrmap_highest_rgbno(rtg); + if (highest_rgbno == NULLRGBLOCK) + rgeo.rg_writepointer = 0; + else + rgeo.rg_writepointer = highest_rgbno + 1; + } + xfs_rtgroup_unlock(rtg, XFS_RTGLOCK_RMAP); + rgeo.rg_flags |= XFS_RTGROUP_GEOM_WRITEPOINTER; + } + if (copy_to_user(arg, &rgeo, sizeof(rgeo))) return -EFAULT; return 0; From db8367f63b301bbdff6eb00c2e09fad4f2ae75e9 Mon Sep 17 00:00:00 2001 From: Carlos Maiolino Date: Tue, 10 Mar 2026 18:36:46 +0100 Subject: [PATCH 02/41] xfs: factor out isize updates from xfs_dio_write_end_io This is the only code needed for zoned inodes, so factor it out so we can move zoned inodes ioend to its own callback. Signed-off-by: Carlos Maiolino Reviewed-by: Christoph Hellwig Reviewed-by: Damien Le Moal Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_file.c | 60 +++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/fs/xfs/xfs_file.c b/fs/xfs/xfs_file.c index 6246f34df9fd..fce6be55d90c 100644 --- a/fs/xfs/xfs_file.c +++ b/fs/xfs/xfs_file.c @@ -560,6 +560,42 @@ xfs_zoned_write_space_reserve( flags, ac); } +/* + * We need to lock the test/set EOF update as we can be racing with + * other IO completions here to update the EOF. Failing to serialise + * here can result in EOF moving backwards and Bad Things Happen when + * that occurs. + * + * As IO completion only ever extends EOF, we can do an unlocked check + * here to avoid taking the spinlock. If we land within the current EOF, + * then we do not need to do an extending update at all, and we don't + * need to take the lock to check this. If we race with an update moving + * EOF, then we'll either still be beyond EOF and need to take the lock, + * or we'll be within EOF and we don't need to take it at all. + */ +static int +xfs_dio_endio_set_isize( + struct inode *inode, + loff_t offset, + ssize_t size) +{ + struct xfs_inode *ip = XFS_I(inode); + + if (offset + size <= i_size_read(inode)) + return 0; + + spin_lock(&ip->i_flags_lock); + if (offset + size <= i_size_read(inode)) { + spin_unlock(&ip->i_flags_lock); + return 0; + } + + i_size_write(inode, offset + size); + spin_unlock(&ip->i_flags_lock); + + return xfs_setfilesize(ip, offset, size); +} + static int xfs_dio_write_end_io( struct kiocb *iocb, @@ -623,30 +659,8 @@ xfs_dio_write_end_io( * with the on-disk inode size being outside the in-core inode size. We * have no other method of updating EOF for AIO, so always do it here * if necessary. - * - * We need to lock the test/set EOF update as we can be racing with - * other IO completions here to update the EOF. Failing to serialise - * here can result in EOF moving backwards and Bad Things Happen when - * that occurs. - * - * As IO completion only ever extends EOF, we can do an unlocked check - * here to avoid taking the spinlock. If we land within the current EOF, - * then we do not need to do an extending update at all, and we don't - * need to take the lock to check this. If we race with an update moving - * EOF, then we'll either still be beyond EOF and need to take the lock, - * or we'll be within EOF and we don't need to take it at all. */ - if (offset + size <= i_size_read(inode)) - goto out; - - spin_lock(&ip->i_flags_lock); - if (offset + size > i_size_read(inode)) { - i_size_write(inode, offset + size); - spin_unlock(&ip->i_flags_lock); - error = xfs_setfilesize(ip, offset, size); - } else { - spin_unlock(&ip->i_flags_lock); - } + error = xfs_dio_endio_set_isize(inode, offset, size); out: memalloc_nofs_restore(nofs_flag); From 02a5d8993b09fe9a6754e57d0e25399baffe9a06 Mon Sep 17 00:00:00 2001 From: Carlos Maiolino Date: Tue, 10 Mar 2026 18:36:47 +0100 Subject: [PATCH 03/41] xfs: factor out xfs_dio_write_zoned_end_io Stop sharing direct IO end_io between regular and zoned devices by factoring out zoned dio end_io to its own function. Signed-off-by: Carlos Maiolino Reviewed-by: Christoph Hellwig Reviewed-by: Damien Le Moal Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_file.c | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/fs/xfs/xfs_file.c b/fs/xfs/xfs_file.c index fce6be55d90c..7918968e1d62 100644 --- a/fs/xfs/xfs_file.c +++ b/fs/xfs/xfs_file.c @@ -596,6 +596,36 @@ xfs_dio_endio_set_isize( return xfs_setfilesize(ip, offset, size); } +static int +xfs_zoned_dio_write_end_io( + struct kiocb *iocb, + ssize_t size, + int error, + unsigned flags) +{ + struct inode *inode = file_inode(iocb->ki_filp); + struct xfs_inode *ip = XFS_I(inode); + unsigned int nofs_flag; + + ASSERT(!(flags & (IOMAP_DIO_UNWRITTEN | IOMAP_DIO_COW))); + + trace_xfs_end_io_direct_write(ip, iocb->ki_pos, size); + + if (xfs_is_shutdown(ip->i_mount)) + return -EIO; + + if (error || !size) + return error; + + XFS_STATS_ADD(ip->i_mount, xs_write_bytes, size); + + nofs_flag = memalloc_nofs_save(); + error = xfs_dio_endio_set_isize(inode, iocb->ki_pos, size); + memalloc_nofs_restore(nofs_flag); + + return error; +} + static int xfs_dio_write_end_io( struct kiocb *iocb, @@ -608,8 +638,7 @@ xfs_dio_write_end_io( loff_t offset = iocb->ki_pos; unsigned int nofs_flag; - ASSERT(!xfs_is_zoned_inode(ip) || - !(flags & (IOMAP_DIO_UNWRITTEN | IOMAP_DIO_COW))); + ASSERT(!xfs_is_zoned_inode(ip)); trace_xfs_end_io_direct_write(ip, offset, size); @@ -702,7 +731,7 @@ xfs_dio_zoned_submit_io( static const struct iomap_dio_ops xfs_dio_zoned_write_ops = { .bio_set = &iomap_ioend_bioset, .submit_io = xfs_dio_zoned_submit_io, - .end_io = xfs_dio_write_end_io, + .end_io = xfs_zoned_dio_write_end_io, }; /* From 3bdc20b005c20ce1bf9b098d1ee2caa1d994141e Mon Sep 17 00:00:00 2001 From: Carlos Maiolino Date: Tue, 10 Mar 2026 18:36:48 +0100 Subject: [PATCH 04/41] xfs: factor out xfs_zone_inc_written Move the written blocks increment and full zone check into a new helper. Also add an assert to ensure rmap lock is held here. Signed-off-by: Carlos Maiolino Reviewed-by: Christoph Hellwig Reviewed-by: Damien Le Moal Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_alloc.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/fs/xfs/xfs_zone_alloc.c b/fs/xfs/xfs_zone_alloc.c index e3d19b6dc64a..97149bfc2512 100644 --- a/fs/xfs/xfs_zone_alloc.c +++ b/fs/xfs/xfs_zone_alloc.c @@ -189,6 +189,18 @@ xfs_open_zone_mark_full( xfs_zone_account_reclaimable(rtg, rtg_blocks(rtg) - used); } +static inline void +xfs_zone_inc_written( + struct xfs_open_zone *oz, + xfs_filblks_t len) +{ + xfs_assert_ilocked(rtg_rmap(oz->oz_rtg), XFS_ILOCK_EXCL); + + oz->oz_written += len; + if (oz->oz_written == rtg_blocks(oz->oz_rtg)) + xfs_open_zone_mark_full(oz); +} + static void xfs_zone_record_blocks( struct xfs_trans *tp, @@ -206,9 +218,7 @@ xfs_zone_record_blocks( xfs_rtgroup_trans_join(tp, rtg, XFS_RTGLOCK_RMAP); rmapip->i_used_blocks += len; ASSERT(rmapip->i_used_blocks <= rtg_blocks(rtg)); - oz->oz_written += len; - if (oz->oz_written == rtg_blocks(rtg)) - xfs_open_zone_mark_full(oz); + xfs_zone_inc_written(oz, len); xfs_trans_log_inode(tp, rmapip, XFS_ILOG_CORE); } @@ -227,9 +237,7 @@ xfs_zone_skip_blocks( trace_xfs_zone_skip_blocks(oz, 0, len); xfs_rtgroup_lock(rtg, XFS_RTGLOCK_RMAP); - oz->oz_written += len; - if (oz->oz_written == rtg_blocks(rtg)) - xfs_open_zone_mark_full(oz); + xfs_zone_inc_written(oz, len); xfs_rtgroup_unlock(rtg, XFS_RTGLOCK_RMAP); xfs_add_frextents(rtg_mount(rtg), len); From 01478f356ff794c7676803c7af04eaeaebfbb455 Mon Sep 17 00:00:00 2001 From: Carlos Maiolino Date: Tue, 10 Mar 2026 18:36:49 +0100 Subject: [PATCH 05/41] xfs: opencode xfs_zone_record_blocks We only have a single caller, no need to keep it in its own function. Signed-off-by: Carlos Maiolino [hch: add zone_record_blocks trace back] Reviewed-by: Christoph Hellwig Reviewed-by: Damien Le Moal Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_alloc.c | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/fs/xfs/xfs_zone_alloc.c b/fs/xfs/xfs_zone_alloc.c index 97149bfc2512..9d02160c5334 100644 --- a/fs/xfs/xfs_zone_alloc.c +++ b/fs/xfs/xfs_zone_alloc.c @@ -201,27 +201,6 @@ xfs_zone_inc_written( xfs_open_zone_mark_full(oz); } -static void -xfs_zone_record_blocks( - struct xfs_trans *tp, - struct xfs_open_zone *oz, - xfs_fsblock_t fsbno, - xfs_filblks_t len) -{ - struct xfs_mount *mp = tp->t_mountp; - struct xfs_rtgroup *rtg = oz->oz_rtg; - struct xfs_inode *rmapip = rtg_rmap(rtg); - - trace_xfs_zone_record_blocks(oz, xfs_rtb_to_rgbno(mp, fsbno), len); - - xfs_rtgroup_lock(rtg, XFS_RTGLOCK_RMAP); - xfs_rtgroup_trans_join(tp, rtg, XFS_RTGLOCK_RMAP); - rmapip->i_used_blocks += len; - ASSERT(rmapip->i_used_blocks <= rtg_blocks(rtg)); - xfs_zone_inc_written(oz, len); - xfs_trans_log_inode(tp, rmapip, XFS_ILOG_CORE); -} - /* * Called for blocks that have been written to disk, but not actually linked to * an inode, which can happen when garbage collection races with user data @@ -252,6 +231,8 @@ xfs_zoned_map_extent( xfs_fsblock_t old_startblock) { struct xfs_bmbt_irec data; + struct xfs_rtgroup *rtg = oz->oz_rtg; + struct xfs_inode *rmapip = rtg_rmap(rtg); int nmaps = 1; int error; @@ -310,7 +291,15 @@ xfs_zoned_map_extent( } } - xfs_zone_record_blocks(tp, oz, new->br_startblock, new->br_blockcount); + trace_xfs_zone_record_blocks(oz, + xfs_rtb_to_rgbno(tp->t_mountp, new->br_startblock), + new->br_blockcount); + xfs_rtgroup_lock(rtg, XFS_RTGLOCK_RMAP); + xfs_rtgroup_trans_join(tp, rtg, XFS_RTGLOCK_RMAP); + rmapip->i_used_blocks += new->br_blockcount; + ASSERT(rmapip->i_used_blocks <= rtg_blocks(rtg)); + xfs_zone_inc_written(oz, new->br_blockcount); + xfs_trans_log_inode(tp, rmapip, XFS_ILOG_CORE); /* Map the new blocks into the data fork. */ xfs_bmap_map_extent(tp, ip, XFS_DATA_FORK, new); From 770323d418ed5848cc21af172f77377b2cc0542d Mon Sep 17 00:00:00 2001 From: Damien Le Moal Date: Mon, 16 Mar 2026 20:40:17 +0900 Subject: [PATCH 06/41] xfs: avoid unnecessary open zone check in xfs_select_zone_nowait() When xfs_select_zone_nowait() is called with pack_tight equal to true, the function xfs_select_open_zone_mru() is called if no open zone is returned by xfs_select_open_zone_lru(), that is, when oz is NULL. The open zone pointer return of xfs_select_zone_nowait() is then checked, but this check is outside of the "if (pack_tight)" that trigered the call to xfs_select_open_zone_mru(). In other word, this check is unnecessarily done even when pack_tight is false. Move the check for the return value of the call to xfs_select_open_zone_mru() inside the if that controls the call to this function, so that we do not uselessly test again the value of oz when pack_tight is false. No functional changes. Signed-off-by: Damien Le Moal Reviewed-by: Hans Holmberg Reviewed-by: Christoph Hellwig Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_alloc.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fs/xfs/xfs_zone_alloc.c b/fs/xfs/xfs_zone_alloc.c index 9d02160c5334..612fcafd3a0c 100644 --- a/fs/xfs/xfs_zone_alloc.c +++ b/fs/xfs/xfs_zone_alloc.c @@ -678,10 +678,11 @@ xfs_select_zone_nowait( if (oz) goto out_unlock; - if (pack_tight) + if (pack_tight) { oz = xfs_select_open_zone_mru(zi, write_hint); - if (oz) - goto out_unlock; + if (oz) + goto out_unlock; + } /* * See if we can open a new zone and use that so that data for different From 6a82a691b08070ad03b237d7db89aa0bfef389e2 Mon Sep 17 00:00:00 2001 From: Damien Le Moal Date: Mon, 16 Mar 2026 20:40:18 +0900 Subject: [PATCH 07/41] xfs: fix a comment typo in xfs_select_zone_nowait() Fix a typo in the comment describing the second call to xfs_select_open_zone_lru() in xfs_select_zone_nowait(). Signed-off-by: Damien Le Moal Reviewed-by: Hans Holmberg Reviewed-by: Christoph Hellwig Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_alloc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/xfs/xfs_zone_alloc.c b/fs/xfs/xfs_zone_alloc.c index 612fcafd3a0c..06e2cb79030e 100644 --- a/fs/xfs/xfs_zone_alloc.c +++ b/fs/xfs/xfs_zone_alloc.c @@ -693,7 +693,7 @@ xfs_select_zone_nowait( goto out_unlock; /* - * Try to find an zone that is an ok match to colocate data with. + * Try to find a zone that is an ok match to colocate data with. */ oz = xfs_select_open_zone_lru(zi, write_hint, XFS_ZONE_ALLOC_OK); if (oz) From 68aa101bf2046aa8365333a3768cece07975ca5f Mon Sep 17 00:00:00 2001 From: Damien Le Moal Date: Mon, 16 Mar 2026 20:40:19 +0900 Subject: [PATCH 08/41] xfs: display more zone related information in mountstats Modify xfs_zoned_show_stats() to add to the information displayed with /proc/self/mountstats the total number of zones (RT groups) and the number of open zones together with the maximum number of open zones. Signed-off-by: Damien Le Moal Reviewed-by: Hans Holmberg Reviewed-by: Christoph Hellwig Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_info.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fs/xfs/xfs_zone_info.c b/fs/xfs/xfs_zone_info.c index 53eabbc3334c..a2af44011654 100644 --- a/fs/xfs/xfs_zone_info.c +++ b/fs/xfs/xfs_zone_info.c @@ -90,9 +90,14 @@ xfs_zoned_show_stats( seq_printf(m, "\tRT GC required: %d\n", xfs_zoned_need_gc(mp)); + seq_printf(m, "\ttotal number of zones: %u\n", + mp->m_sb.sb_rgcount); seq_printf(m, "\tfree zones: %d\n", atomic_read(&zi->zi_nr_free_zones)); - seq_puts(m, "\topen zones:\n"); + spin_lock(&zi->zi_open_zones_lock); + seq_printf(m, "\tnumber of open zones: %u / %u\n", + zi->zi_nr_open_zones, mp->m_max_open_zones); + seq_puts(m, "\topen zones:\n"); list_for_each_entry(oz, &zi->zi_open_zones, oz_entry) xfs_show_open_zone(m, oz); if (zi->zi_open_gc_zone) { From c1f955437440f92632e2efca4b591371bb3caefc Mon Sep 17 00:00:00 2001 From: Damien Le Moal Date: Mon, 16 Mar 2026 20:40:20 +0900 Subject: [PATCH 09/41] xfs: avoid unnecessary calculations in xfs_zoned_need_gc() If zonegc_low_space is set to zero (which is the default), the second condition in xfs_zoned_need_gc() that triggers GC never evaluates to true because the calculated threshold will always be 0. So there is no need to calculate the threshold and to evaluate that condition. Return early when zonegc_low_space is zero. While at it, add comments to document the intent of each of the 3 tests used to determine the return value to control the execution of garbage collection. Signed-off-by: Damien Le Moal Reviewed-by: Hans Holmberg Reviewed-by: Christoph Hellwig Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_gc.c | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/fs/xfs/xfs_zone_gc.c b/fs/xfs/xfs_zone_gc.c index 7efeecd2d85f..aaa0a3119d91 100644 --- a/fs/xfs/xfs_zone_gc.c +++ b/fs/xfs/xfs_zone_gc.c @@ -171,25 +171,37 @@ xfs_zoned_need_gc( s64 available, free, threshold; s32 remainder; + /* If we have no reclaimable blocks, running GC is useless. */ if (!xfs_zoned_have_reclaimable(mp->m_zone_info)) return false; + /* + * In order to avoid file fragmentation as much as possible, we should + * make sure that we can open enough zones. So trigger GC if the number + * of blocks immediately available for writes is lower than the total + * number of blocks from all possible open zones. + */ available = xfs_estimate_freecounter(mp, XC_FREE_RTAVAILABLE); - if (available < xfs_rtgs_to_rfsbs(mp, mp->m_max_open_zones - XFS_OPEN_GC_ZONES)) return true; - free = xfs_estimate_freecounter(mp, XC_FREE_RTEXTENTS); + /* + * For cases where the user wants to be more aggressive with GC, + * the sysfs attribute zonegc_low_space may be set to a non zero value, + * to indicate that GC should try to maintain at least zonegc_low_space + * percent of the free space to be directly available for writing. Check + * this here. + */ + if (!mp->m_zonegc_low_space) + return false; + free = xfs_estimate_freecounter(mp, XC_FREE_RTEXTENTS); threshold = div_s64_rem(free, 100, &remainder); threshold = threshold * mp->m_zonegc_low_space + remainder * div_s64(mp->m_zonegc_low_space, 100); - if (available < threshold) - return true; - - return false; + return available < threshold; } static struct xfs_zone_gc_data * From 8da6fd088472f4db6199fb68af6ec87fa26247ca Mon Sep 17 00:00:00 2001 From: Lukas Herbolt Date: Tue, 10 Mar 2026 13:30:22 +0100 Subject: [PATCH 10/41] xfs: Use xarray to track SB UUIDs instead of plain array. Removing the plain array to track the UUIDs and switch to xarray makes it more readable. Signed-off-by: Lukas Herbolt [cem: remove unneeded return from xfs_uuid_unmount] Reviewed-by: Darrick J. Wong Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_mount.c | 75 ++++++++++++++++++++++------------------------ fs/xfs/xfs_mount.h | 3 ++ 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/fs/xfs/xfs_mount.c b/fs/xfs/xfs_mount.c index 9c295abd0a0a..265011333904 100644 --- a/fs/xfs/xfs_mount.c +++ b/fs/xfs/xfs_mount.c @@ -44,17 +44,36 @@ #include "xfs_healthmon.h" static DEFINE_MUTEX(xfs_uuid_table_mutex); -static int xfs_uuid_table_size; -static uuid_t *xfs_uuid_table; +static DEFINE_XARRAY_ALLOC(xfs_uuid_table); + +static uuid_t * +xfs_uuid_search( + uuid_t *new_uuid) +{ + unsigned long index = 0; + uuid_t *uuid; + + xa_for_each(&xfs_uuid_table, index, uuid) { + if (uuid_equal(uuid, new_uuid)) + return uuid; + } + return NULL; +} + +static void +xfs_uuid_delete( + uuid_t *uuid, + unsigned int index) +{ + ASSERT(uuid_equal(xa_load(&xfs_uuid_table, index), uuid)); + xa_erase(&xfs_uuid_table, index); +} void xfs_uuid_table_free(void) { - if (xfs_uuid_table_size == 0) - return; - kfree(xfs_uuid_table); - xfs_uuid_table = NULL; - xfs_uuid_table_size = 0; + ASSERT(xa_empty(&xfs_uuid_table)); + xa_destroy(&xfs_uuid_table); } /* @@ -66,7 +85,7 @@ xfs_uuid_mount( struct xfs_mount *mp) { uuid_t *uuid = &mp->m_sb.sb_uuid; - int hole, i; + int ret; /* Publish UUID in struct super_block */ super_set_uuid(mp->m_super, uuid->b, sizeof(*uuid)); @@ -80,30 +99,17 @@ xfs_uuid_mount( } mutex_lock(&xfs_uuid_table_mutex); - for (i = 0, hole = -1; i < xfs_uuid_table_size; i++) { - if (uuid_is_null(&xfs_uuid_table[i])) { - hole = i; - continue; - } - if (uuid_equal(uuid, &xfs_uuid_table[i])) - goto out_duplicate; + if (unlikely(xfs_uuid_search(uuid))) { + xfs_warn(mp, "Filesystem has duplicate UUID %pU - can't mount", + uuid); + mutex_unlock(&xfs_uuid_table_mutex); + return -EINVAL; } - if (hole < 0) { - xfs_uuid_table = krealloc(xfs_uuid_table, - (xfs_uuid_table_size + 1) * sizeof(*xfs_uuid_table), - GFP_KERNEL | __GFP_NOFAIL); - hole = xfs_uuid_table_size++; - } - xfs_uuid_table[hole] = *uuid; + ret = xa_alloc(&xfs_uuid_table, &mp->m_uuid_table_index, uuid, + xa_limit_32b, GFP_KERNEL); mutex_unlock(&xfs_uuid_table_mutex); - - return 0; - - out_duplicate: - mutex_unlock(&xfs_uuid_table_mutex); - xfs_warn(mp, "Filesystem has duplicate UUID %pU - can't mount", uuid); - return -EINVAL; + return ret; } STATIC void @@ -111,21 +117,12 @@ xfs_uuid_unmount( struct xfs_mount *mp) { uuid_t *uuid = &mp->m_sb.sb_uuid; - int i; if (xfs_has_nouuid(mp)) return; mutex_lock(&xfs_uuid_table_mutex); - for (i = 0; i < xfs_uuid_table_size; i++) { - if (uuid_is_null(&xfs_uuid_table[i])) - continue; - if (!uuid_equal(uuid, &xfs_uuid_table[i])) - continue; - memset(&xfs_uuid_table[i], 0, sizeof(uuid_t)); - break; - } - ASSERT(i < xfs_uuid_table_size); + xfs_uuid_delete(uuid, mp->m_uuid_table_index); mutex_unlock(&xfs_uuid_table_mutex); } diff --git a/fs/xfs/xfs_mount.h b/fs/xfs/xfs_mount.h index ddd4028be8d6..d964bae096ef 100644 --- a/fs/xfs/xfs_mount.h +++ b/fs/xfs/xfs_mount.h @@ -346,6 +346,9 @@ typedef struct xfs_mount { /* Private data referring to a health monitor object. */ struct xfs_healthmon __rcu *m_healthmon; + + /* Index of uuid record in the uuid xarray. */ + unsigned int m_uuid_table_index; } xfs_mount_t; #define M_IGEO(mp) (&(mp)->m_ino_geo) From 92e9dff9ca5026805798b13b967760f8058794e8 Mon Sep 17 00:00:00 2001 From: Brian Foster Date: Wed, 11 Mar 2026 12:24:55 -0400 Subject: [PATCH 11/41] xfs: fix iomap hole map reporting for zoned zero range The hole mapping logic for zero range in zoned mode is not quite correct. It currently reports a hole whenever one exists in the data fork. If the first write to a sparse range has completed and not yet written back, the blocks exist in the COW fork as delalloc until writeback completes, at which point they are allocated and mapped into the data fork. If a zero range occurs on a range that has not yet populated the data fork, we will incorrectly report it as a hole. Note that this currently functions correctly because we are bailed out by the pagecache flush in iomap_zero_range(). If a hole or unwritten mapping is reported with dirty pagecache, it assumes there is pending data, flushes to induce any pending block allocations/remaps, and retries the lookup. We want to remove this hack from iomap, however, so update iomap_begin() to only report a hole for zeroing when one exists in both forks. Signed-off-by: Brian Foster Reviewed-by: Christoph Hellwig Reviewed-by: Darrick J. Wong Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_iomap.c | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/fs/xfs/xfs_iomap.c b/fs/xfs/xfs_iomap.c index be86d43044df..8c3469d2c73e 100644 --- a/fs/xfs/xfs_iomap.c +++ b/fs/xfs/xfs_iomap.c @@ -1651,14 +1651,6 @@ xfs_zoned_buffered_write_iomap_begin( &smap)) smap.br_startoff = end_fsb; /* fake hole until EOF */ if (smap.br_startoff > offset_fsb) { - /* - * We never need to allocate blocks for zeroing a hole. - */ - if (flags & IOMAP_ZERO) { - xfs_hole_to_iomap(ip, iomap, offset_fsb, - smap.br_startoff); - goto out_unlock; - } end_fsb = min(end_fsb, smap.br_startoff); } else { end_fsb = min(end_fsb, @@ -1690,6 +1682,16 @@ xfs_zoned_buffered_write_iomap_begin( count_fsb = min3(end_fsb - offset_fsb, XFS_MAX_BMBT_EXTLEN, XFS_B_TO_FSB(mp, 1024 * PAGE_SIZE)); + /* + * When zeroing, don't allocate blocks for holes as they are already + * zeroes, but we need to ensure that no extents exist in both the data + * and COW fork to ensure this really is a hole. + */ + if ((flags & IOMAP_ZERO) && srcmap->type == IOMAP_HOLE) { + xfs_hole_to_iomap(ip, iomap, offset_fsb, end_fsb); + goto out_unlock; + } + /* * The block reservation is supposed to cover all blocks that the * operation could possible write, but there is a nasty corner case From 2f46c239fce617ac26cc40d9520b1c0cf05cd34f Mon Sep 17 00:00:00 2001 From: Brian Foster Date: Wed, 11 Mar 2026 12:24:56 -0400 Subject: [PATCH 12/41] xfs: flush dirty pagecache over hole in zoned mode zero range For zoned filesystems a window exists between the first write to a sparse range (i.e. data fork hole) and writeback completion where we might spuriously observe holes in both the COW and data forks. This occurs because a buffered write populates the COW fork with delalloc, writeback submission removes the COW fork delalloc blocks and unlocks the inode, and then writeback completion remaps the physically allocated blocks into the data fork. If a zero range operation does a lookup during this window where both forks show a hole, it incorrectly reports a hole mapping for a range that contains data. This currently works because iomap checks for dirty pagecache over holes and unwritten mappings. If found, it flushes and retries the lookup. We plan to remove the hole flush logic from iomap, however, so lift the flush into xfs_zoned_buffered_write_iomap_begin() to preserve behavior and document the purpose for it. Zoned XFS filesystems don't support unwritten extents, so if zoned mode can come up with a way to close this transient hole window in the future, this flush can likely be removed. Signed-off-by: Brian Foster Reviewed-by: Christoph Hellwig Reviewed-by: Darrick J. Wong Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_iomap.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/fs/xfs/xfs_iomap.c b/fs/xfs/xfs_iomap.c index 8c3469d2c73e..d3b8c018c883 100644 --- a/fs/xfs/xfs_iomap.c +++ b/fs/xfs/xfs_iomap.c @@ -1590,6 +1590,7 @@ xfs_zoned_buffered_write_iomap_begin( { struct iomap_iter *iter = container_of(iomap, struct iomap_iter, iomap); + struct address_space *mapping = inode->i_mapping; struct xfs_zone_alloc_ctx *ac = iter->private; struct xfs_inode *ip = XFS_I(inode); struct xfs_mount *mp = ip->i_mount; @@ -1614,6 +1615,7 @@ xfs_zoned_buffered_write_iomap_begin( if (error) return error; +restart: error = xfs_ilock_for_iomap(ip, flags, &lockmode); if (error) return error; @@ -1686,8 +1688,25 @@ xfs_zoned_buffered_write_iomap_begin( * When zeroing, don't allocate blocks for holes as they are already * zeroes, but we need to ensure that no extents exist in both the data * and COW fork to ensure this really is a hole. + * + * A window exists where we might observe a hole in both forks with + * valid data in cache. Writeback removes the COW fork blocks on + * submission but doesn't remap into the data fork until completion. If + * the data fork was previously a hole, we'll fail to zero. Until we + * find a way to avoid this transient state, check for dirty pagecache + * and flush to wait on blocks to land in the data fork. */ if ((flags & IOMAP_ZERO) && srcmap->type == IOMAP_HOLE) { + if (filemap_range_needs_writeback(mapping, offset, + offset + count - 1)) { + xfs_iunlock(ip, lockmode); + error = filemap_write_and_wait_range(mapping, offset, + offset + count - 1); + if (error) + return error; + goto restart; + } + xfs_hole_to_iomap(ip, iomap, offset_fsb, end_fsb); goto out_unlock; } From a35bb0dec9552aa2bc69a24e3126c68c257bf55e Mon Sep 17 00:00:00 2001 From: Brian Foster Date: Wed, 11 Mar 2026 12:24:57 -0400 Subject: [PATCH 13/41] iomap, xfs: lift zero range hole mapping flush into xfs iomap zero range has a wart in that it also flushes dirty pagecache over hole mappings (rather than only unwritten mappings). This was included to accommodate a quirk in XFS where COW fork preallocation can exist over a hole in the data fork, and the associated range is reported as a hole. This is because the range actually is a hole, but XFS also has an optimization where if COW fork blocks exist for a range being written to, those blocks are used regardless of whether the data fork blocks are shared or not. For zeroing, COW fork blocks over a data fork hole are only relevant if the range is dirty in pagecache, otherwise the range is already considered zeroed. The easiest way to deal with this corner case is to flush the pagecache to trigger COW remapping into the data fork, and then operate on the updated on-disk state. The problem is that ext4 cannot accommodate a flush from this context due to being a transaction deadlock vector. Outside of the hole quirk, ext4 can avoid the flush for zero range by using the recently introduced folio batch lookup mechanism for unwritten mappings. Therefore, take the next logical step and lift the hole handling logic into the XFS iomap_begin handler. iomap will still flush on unwritten mappings without a folio batch, and XFS will flush and retry mapping lookups in the case where it would otherwise report a hole with dirty pagecache during a zero range. Note that this is intended to be a fairly straightforward lift and otherwise not change behavior. Now that the flush exists within XFS, follow on patches can further optimize it. Signed-off-by: Brian Foster Reviewed-by: Darrick J. Wong Reviewed-by: Christoph Hellwig Signed-off-by: Carlos Maiolino --- fs/iomap/buffered-io.c | 2 +- fs/xfs/xfs_iomap.c | 25 ++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c index a0c46aadb97d..e5e7127a4e92 100644 --- a/fs/iomap/buffered-io.c +++ b/fs/iomap/buffered-io.c @@ -1641,7 +1641,7 @@ iomap_zero_range(struct inode *inode, loff_t pos, loff_t len, bool *did_zero, srcmap->type == IOMAP_UNWRITTEN)) { s64 status; - if (range_dirty) { + if (range_dirty && srcmap->type == IOMAP_UNWRITTEN) { range_dirty = false; status = iomap_zero_iter_flush_and_stale(&iter); } else { diff --git a/fs/xfs/xfs_iomap.c b/fs/xfs/xfs_iomap.c index d3b8c018c883..2ace8b8ffc86 100644 --- a/fs/xfs/xfs_iomap.c +++ b/fs/xfs/xfs_iomap.c @@ -1811,6 +1811,7 @@ xfs_buffered_write_iomap_begin( if (error) return error; +restart: error = xfs_ilock_for_iomap(ip, flags, &lockmode); if (error) return error; @@ -1838,9 +1839,27 @@ xfs_buffered_write_iomap_begin( if (eof) imap.br_startoff = end_fsb; /* fake hole until the end */ - /* We never need to allocate blocks for zeroing or unsharing a hole. */ - if ((flags & (IOMAP_UNSHARE | IOMAP_ZERO)) && - imap.br_startoff > offset_fsb) { + /* We never need to allocate blocks for unsharing a hole. */ + if ((flags & IOMAP_UNSHARE) && imap.br_startoff > offset_fsb) { + xfs_hole_to_iomap(ip, iomap, offset_fsb, imap.br_startoff); + goto out_unlock; + } + + /* + * We may need to zero over a hole in the data fork if it's fronted by + * COW blocks and dirty pagecache. To make sure zeroing occurs, force + * writeback to remap pending blocks and restart the lookup. + */ + if ((flags & IOMAP_ZERO) && imap.br_startoff > offset_fsb) { + if (filemap_range_needs_writeback(inode->i_mapping, offset, + offset + count - 1)) { + xfs_iunlock(ip, lockmode); + error = filemap_write_and_wait_range(inode->i_mapping, + offset, offset + count - 1); + if (error) + return error; + goto restart; + } xfs_hole_to_iomap(ip, iomap, offset_fsb, imap.br_startoff); goto out_unlock; } From c35a3e273e86e89f73abc4e75e33648fac20eec9 Mon Sep 17 00:00:00 2001 From: Brian Foster Date: Wed, 11 Mar 2026 12:24:58 -0400 Subject: [PATCH 14/41] xfs: flush eof folio before insert range size update The flush in xfs_buffered_write_iomap_begin() for zero range over a data fork hole fronted by COW fork prealloc is primarily designed to provide correct zeroing behavior in particular pagecache conditions. As it turns out, this also partially masks some odd behavior in insert range (via zero range via setattr). Insert range bumps i_size the length of the new range, flushes, unmaps pagecache and cancels COW prealloc, and then right shifts extents from the end of the file back to the target offset of the insert. Since the i_size update occurs before the pagecache flush, this creates a transient situation where writeback around EOF can behave differently. This appears to be corner case situation, but if happens to be fronted by COW fork speculative preallocation and a large, dirty folio that contains at least one full COW block beyond EOF, the writeback after i_size is bumped may remap that COW fork block into the data fork within EOF. The block is zeroed and then shifted back out to post-eof, but this is unexpected in that it leads to a written post-eof data fork block. This can cause a zero range warning on a subsequent size extension, because we should never find blocks that require physical zeroing beyond i_size. To avoid this quirk, flush the EOF folio before the i_size update during insert range. The entire range will be flushed, unmapped and invalidated anyways, so this should be relatively unnoticeable. Signed-off-by: Brian Foster Reviewed-by: Christoph Hellwig Reviewed-by: Darrick J. Wong Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_file.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/fs/xfs/xfs_file.c b/fs/xfs/xfs_file.c index 7918968e1d62..845a97c9b063 100644 --- a/fs/xfs/xfs_file.c +++ b/fs/xfs/xfs_file.c @@ -1306,6 +1306,23 @@ xfs_falloc_insert_range( if (offset >= isize) return -EINVAL; + /* + * Let writeback clean up EOF folio state before we bump i_size. The + * insert flushes before it starts shifting and under certain + * circumstances we can write back blocks that should technically be + * considered post-eof (and thus should not be submitted for writeback). + * + * For example, a large, dirty folio that spans EOF and is backed by + * post-eof COW fork preallocation can cause block remap into the data + * fork. This shifts back out beyond EOF, but creates an expectedly + * written post-eof block. The insert is going to flush, unmap and + * cancel prealloc across this whole range, so flush EOF now before we + * bump i_size to provide consistent behavior. + */ + error = filemap_write_and_wait_range(inode->i_mapping, isize, isize); + if (error) + return error; + error = xfs_falloc_setsize(file, isize + len); if (error) return error; From a8eb41376df987887b33dbf7078d5b13c85f3e0c Mon Sep 17 00:00:00 2001 From: Brian Foster Date: Wed, 11 Mar 2026 12:24:59 -0400 Subject: [PATCH 15/41] xfs: look up cow fork extent earlier for buffered iomap_begin To further isolate the need for flushing for zero range, we need to know whether a hole in the data fork is fronted by blocks in the COW fork or not. COW fork lookup currently occurs further down in the function, after the zero range case is handled. As a preparation step, lift the COW fork extent lookup to earlier in the function, at the same time as the data fork lookup. Only the lookup logic is lifted. The COW fork branch/reporting logic remains as is to avoid any observable behavior change from an iomap reporting perspective. Signed-off-by: Brian Foster Reviewed-by: Darrick J. Wong Reviewed-by: Christoph Hellwig Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_iomap.c | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/fs/xfs/xfs_iomap.c b/fs/xfs/xfs_iomap.c index 2ace8b8ffc86..df1eab646cae 100644 --- a/fs/xfs/xfs_iomap.c +++ b/fs/xfs/xfs_iomap.c @@ -1830,14 +1830,29 @@ restart: goto out_unlock; /* - * Search the data fork first to look up our source mapping. We - * always need the data fork map, as we have to return it to the - * iomap code so that the higher level write code can read data in to - * perform read-modify-write cycles for unaligned writes. + * Search the data fork first to look up our source mapping. We always + * need the data fork map, as we have to return it to the iomap code so + * that the higher level write code can read data in to perform + * read-modify-write cycles for unaligned writes. + * + * Then search the COW fork extent list even if we did not find a data + * fork extent. This serves two purposes: first this implements the + * speculative preallocation using cowextsize, so that we also unshare + * block adjacent to shared blocks instead of just the shared blocks + * themselves. Second the lookup in the extent list is generally faster + * than going out to the shared extent tree. */ eof = !xfs_iext_lookup_extent(ip, &ip->i_df, offset_fsb, &icur, &imap); if (eof) imap.br_startoff = end_fsb; /* fake hole until the end */ + if (xfs_is_cow_inode(ip)) { + if (!ip->i_cowfp) { + ASSERT(!xfs_is_reflink_inode(ip)); + xfs_ifork_init_cow(ip); + } + cow_eof = !xfs_iext_lookup_extent(ip, ip->i_cowfp, offset_fsb, + &ccur, &cmap); + } /* We never need to allocate blocks for unsharing a hole. */ if ((flags & IOMAP_UNSHARE) && imap.br_startoff > offset_fsb) { @@ -1904,24 +1919,13 @@ restart: } /* - * Search the COW fork extent list even if we did not find a data fork - * extent. This serves two purposes: first this implements the - * speculative preallocation using cowextsize, so that we also unshare - * block adjacent to shared blocks instead of just the shared blocks - * themselves. Second the lookup in the extent list is generally faster - * than going out to the shared extent tree. + * Now that we've handled any operation specific special cases, at this + * point we can report a COW mapping if found. */ - if (xfs_is_cow_inode(ip)) { - if (!ip->i_cowfp) { - ASSERT(!xfs_is_reflink_inode(ip)); - xfs_ifork_init_cow(ip); - } - cow_eof = !xfs_iext_lookup_extent(ip, ip->i_cowfp, offset_fsb, - &ccur, &cmap); - if (!cow_eof && cmap.br_startoff <= offset_fsb) { - trace_xfs_reflink_cow_found(ip, &cmap); - goto found_cow; - } + if (xfs_is_cow_inode(ip) && + !cow_eof && cmap.br_startoff <= offset_fsb) { + trace_xfs_reflink_cow_found(ip, &cmap); + goto found_cow; } if (imap.br_startoff <= offset_fsb) { From c770f997a4227b6fc5f62275b2337622213e35af Mon Sep 17 00:00:00 2001 From: Brian Foster Date: Wed, 11 Mar 2026 12:25:00 -0400 Subject: [PATCH 16/41] xfs: only flush when COW fork blocks overlap data fork holes The zero range hole mapping flush case has been lifted from iomap into XFS. Now that we have more mapping context available from the ->iomap_begin() handler, we can isolate the flush further to when we know a hole is fronted by COW blocks. Rather than purely rely on pagecache dirty state, explicitly check for the case where a range is a hole in both forks. Otherwise trim to the range where there does happen to be overlap and use that for the pagecache writeback check. This might prevent some spurious zeroing, but more importantly makes it easier to remove the flush entirely. Signed-off-by: Brian Foster Reviewed-by: Christoph Hellwig Reviewed-by: Darrick J. Wong Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_iomap.c | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/fs/xfs/xfs_iomap.c b/fs/xfs/xfs_iomap.c index df1eab646cae..6229a0bf793b 100644 --- a/fs/xfs/xfs_iomap.c +++ b/fs/xfs/xfs_iomap.c @@ -1781,10 +1781,12 @@ xfs_buffered_write_iomap_begin( { struct iomap_iter *iter = container_of(iomap, struct iomap_iter, iomap); + struct address_space *mapping = inode->i_mapping; struct xfs_inode *ip = XFS_I(inode); struct xfs_mount *mp = ip->i_mount; xfs_fileoff_t offset_fsb = XFS_B_TO_FSBT(mp, offset); xfs_fileoff_t end_fsb = xfs_iomap_end_fsb(mp, offset, count); + xfs_fileoff_t cow_fsb = NULLFILEOFF; struct xfs_bmbt_irec imap, cmap; struct xfs_iext_cursor icur, ccur; xfs_fsblock_t prealloc_blocks = 0; @@ -1852,6 +1854,8 @@ restart: } cow_eof = !xfs_iext_lookup_extent(ip, ip->i_cowfp, offset_fsb, &ccur, &cmap); + if (!cow_eof) + cow_fsb = cmap.br_startoff; } /* We never need to allocate blocks for unsharing a hole. */ @@ -1866,17 +1870,37 @@ restart: * writeback to remap pending blocks and restart the lookup. */ if ((flags & IOMAP_ZERO) && imap.br_startoff > offset_fsb) { - if (filemap_range_needs_writeback(inode->i_mapping, offset, - offset + count - 1)) { + loff_t start, end; + + imap.br_blockcount = imap.br_startoff - offset_fsb; + imap.br_startoff = offset_fsb; + imap.br_startblock = HOLESTARTBLOCK; + imap.br_state = XFS_EXT_NORM; + + if (cow_fsb == NULLFILEOFF) + goto found_imap; + if (cow_fsb > offset_fsb) { + xfs_trim_extent(&imap, offset_fsb, + cow_fsb - offset_fsb); + goto found_imap; + } + + /* COW fork blocks overlap the hole */ + xfs_trim_extent(&imap, offset_fsb, + cmap.br_startoff + cmap.br_blockcount - offset_fsb); + start = XFS_FSB_TO_B(mp, imap.br_startoff); + end = XFS_FSB_TO_B(mp, + imap.br_startoff + imap.br_blockcount) - 1; + if (filemap_range_needs_writeback(mapping, start, end)) { xfs_iunlock(ip, lockmode); - error = filemap_write_and_wait_range(inode->i_mapping, - offset, offset + count - 1); + error = filemap_write_and_wait_range(mapping, start, + end); if (error) return error; goto restart; } - xfs_hole_to_iomap(ip, iomap, offset_fsb, imap.br_startoff); - goto out_unlock; + + goto found_imap; } /* From ce9d27ca8b2eafdd2457a15aafdab74218843138 Mon Sep 17 00:00:00 2001 From: Brian Foster Date: Wed, 11 Mar 2026 12:25:01 -0400 Subject: [PATCH 17/41] xfs: replace zero range flush with folio batch Now that the zero range pagecache flush is purely isolated to providing zeroing correctness in this case, we can remove it and replace it with the folio batch mechanism that is used for handling unwritten extents. This is still slightly odd in that XFS reports a hole vs. a mapping that reflects the COW fork extents, but that has always been the case in this situation and so a separate issue. We drop the iomap warning that assumes the folio batch is always associated with unwritten mappings, but this is mainly a development assertion as otherwise the core iomap fbatch code doesn't care much about the mapping type if it's handed the set of folios to process. Signed-off-by: Brian Foster Reviewed-by: Christoph Hellwig Reviewed-by: Darrick J. Wong Signed-off-by: Carlos Maiolino --- fs/iomap/buffered-io.c | 4 ---- fs/xfs/xfs_iomap.c | 20 ++++++-------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c index e5e7127a4e92..3be3627d4b50 100644 --- a/fs/iomap/buffered-io.c +++ b/fs/iomap/buffered-io.c @@ -1632,10 +1632,6 @@ iomap_zero_range(struct inode *inode, loff_t pos, loff_t len, bool *did_zero, while ((ret = iomap_iter(&iter, ops)) > 0) { const struct iomap *srcmap = iomap_iter_srcmap(&iter); - if (WARN_ON_ONCE((iter.iomap.flags & IOMAP_F_FOLIO_BATCH) && - srcmap->type != IOMAP_UNWRITTEN)) - return -EIO; - if (!(iter.iomap.flags & IOMAP_F_FOLIO_BATCH) && (srcmap->type == IOMAP_HOLE || srcmap->type == IOMAP_UNWRITTEN)) { diff --git a/fs/xfs/xfs_iomap.c b/fs/xfs/xfs_iomap.c index 6229a0bf793b..51a55510d4a5 100644 --- a/fs/xfs/xfs_iomap.c +++ b/fs/xfs/xfs_iomap.c @@ -1781,7 +1781,6 @@ xfs_buffered_write_iomap_begin( { struct iomap_iter *iter = container_of(iomap, struct iomap_iter, iomap); - struct address_space *mapping = inode->i_mapping; struct xfs_inode *ip = XFS_I(inode); struct xfs_mount *mp = ip->i_mount; xfs_fileoff_t offset_fsb = XFS_B_TO_FSBT(mp, offset); @@ -1813,7 +1812,6 @@ xfs_buffered_write_iomap_begin( if (error) return error; -restart: error = xfs_ilock_for_iomap(ip, flags, &lockmode); if (error) return error; @@ -1866,8 +1864,8 @@ restart: /* * We may need to zero over a hole in the data fork if it's fronted by - * COW blocks and dirty pagecache. To make sure zeroing occurs, force - * writeback to remap pending blocks and restart the lookup. + * COW blocks and dirty pagecache. Scan such file ranges for dirty + * cache and fill the iomap batch with folios that need zeroing. */ if ((flags & IOMAP_ZERO) && imap.br_startoff > offset_fsb) { loff_t start, end; @@ -1889,16 +1887,10 @@ restart: xfs_trim_extent(&imap, offset_fsb, cmap.br_startoff + cmap.br_blockcount - offset_fsb); start = XFS_FSB_TO_B(mp, imap.br_startoff); - end = XFS_FSB_TO_B(mp, - imap.br_startoff + imap.br_blockcount) - 1; - if (filemap_range_needs_writeback(mapping, start, end)) { - xfs_iunlock(ip, lockmode); - error = filemap_write_and_wait_range(mapping, start, - end); - if (error) - return error; - goto restart; - } + end = XFS_FSB_TO_B(mp, imap.br_startoff + imap.br_blockcount); + iomap_fill_dirty_folios(iter, &start, end, &iomap_flags); + xfs_trim_extent(&imap, offset_fsb, + XFS_B_TO_FSB(mp, start) - offset_fsb); goto found_imap; } From 388bb26b3d33de3c53a492824a4c5804151a0014 Mon Sep 17 00:00:00 2001 From: Brian Foster Date: Wed, 11 Mar 2026 12:25:02 -0400 Subject: [PATCH 18/41] xfs: report cow mappings with dirty pagecache for iomap zero range XFS has long supported the case where it is possible to have dirty data in pagecache backed by COW fork blocks and a hole in the data fork. This occurs for two reasons. On reflink enabled files, COW fork blocks are allocated with preallocation to help avoid fragmention. Second, if a mapping lookup for a write finds blocks in the COW fork, it consumes those blocks unconditionally. This might mean that COW fork blocks are backed by non-shared blocks or even a hole in the data fork, both of which are perfectly fine. This leaves an odd corner case for zero range, however, because it needs to distinguish between ranges that are sparse and thus do not require zeroing and those that are not. A range backed by COW fork blocks and a data fork hole might either be a legitimate hole in the file or a range with pending buffered writes that will be written back (which will remap COW fork blocks into the data fork). This "COW fork blocks over data fork hole" situation has historically been reported as a hole to iomap, which then has grown a flush hack as a workaround to ensure zeroing occurs correctly. Now that this has been lifted into the filesystem and replaced by the dirty folio lookup mechanism, we can do better and use the pagecache state to decide how to report the mapping. If a COW fork range exists with dirty folios in cache, then report a typical shared mapping. If the range is clean in cache, then we can consider the COW blocks preallocation and call it a hole. This doesn't fundamentally change behavior, but makes mapping reporting more accurate. Note that this does require splitting across the EOF boundary (similar to normal zero range) to ensure we don't spuriously perform post-eof zeroing. iomap will warn about zeroing beyond EOF because folios beyond i_size may not be written back. Signed-off-by: Brian Foster Reviewed-by: Darrick J. Wong Reviewed-by: Christoph Hellwig Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_iomap.c | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/fs/xfs/xfs_iomap.c b/fs/xfs/xfs_iomap.c index 51a55510d4a5..dbd49e838889 100644 --- a/fs/xfs/xfs_iomap.c +++ b/fs/xfs/xfs_iomap.c @@ -1786,6 +1786,7 @@ xfs_buffered_write_iomap_begin( xfs_fileoff_t offset_fsb = XFS_B_TO_FSBT(mp, offset); xfs_fileoff_t end_fsb = xfs_iomap_end_fsb(mp, offset, count); xfs_fileoff_t cow_fsb = NULLFILEOFF; + xfs_fileoff_t eof_fsb = XFS_B_TO_FSB(mp, XFS_ISIZE(ip)); struct xfs_bmbt_irec imap, cmap; struct xfs_iext_cursor icur, ccur; xfs_fsblock_t prealloc_blocks = 0; @@ -1868,7 +1869,8 @@ xfs_buffered_write_iomap_begin( * cache and fill the iomap batch with folios that need zeroing. */ if ((flags & IOMAP_ZERO) && imap.br_startoff > offset_fsb) { - loff_t start, end; + loff_t start, end; + unsigned int fbatch_count; imap.br_blockcount = imap.br_startoff - offset_fsb; imap.br_startoff = offset_fsb; @@ -1883,15 +1885,33 @@ xfs_buffered_write_iomap_begin( goto found_imap; } + /* no zeroing beyond eof, so split at the boundary */ + if (offset_fsb >= eof_fsb) + goto found_imap; + if (offset_fsb < eof_fsb && end_fsb > eof_fsb) + xfs_trim_extent(&imap, offset_fsb, + eof_fsb - offset_fsb); + /* COW fork blocks overlap the hole */ xfs_trim_extent(&imap, offset_fsb, cmap.br_startoff + cmap.br_blockcount - offset_fsb); start = XFS_FSB_TO_B(mp, imap.br_startoff); end = XFS_FSB_TO_B(mp, imap.br_startoff + imap.br_blockcount); - iomap_fill_dirty_folios(iter, &start, end, &iomap_flags); + fbatch_count = iomap_fill_dirty_folios(iter, &start, end, + &iomap_flags); xfs_trim_extent(&imap, offset_fsb, XFS_B_TO_FSB(mp, start) - offset_fsb); + /* + * Report the COW mapping if we have folios to zero. Otherwise + * ignore the COW blocks as preallocation and report a hole. + */ + if (fbatch_count) { + xfs_trim_extent(&cmap, imap.br_startoff, + imap.br_blockcount); + imap.br_startoff = end_fsb; /* fake hole */ + goto found_cow; + } goto found_imap; } @@ -1901,8 +1921,6 @@ xfs_buffered_write_iomap_begin( * unwritten extent. */ if (flags & IOMAP_ZERO) { - xfs_fileoff_t eof_fsb = XFS_B_TO_FSB(mp, XFS_ISIZE(ip)); - if (isnullstartblock(imap.br_startblock) && offset_fsb >= eof_fsb) goto convert_delay; From 67fe4303972eb6f911f62e2fe6ac7628b17d95c0 Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Mon, 23 Mar 2026 08:50:51 +0100 Subject: [PATCH 19/41] xfs: don't keep a reference for buffers on the LRU Currently the buffer cache adds a reference to b_hold for buffers that are on the LRU. This seems to go all the way back and allows releasing buffers from the LRU using xfs_buf_rele. But it makes xfs_buf_rele really complicated in differs from how other LRUs are implemented in Linux. Switch to not having a reference for buffers in the LRU, and use a separate negative hold value to mark buffers as dead. This simplifies xfs_buf_rele, which now just deal with the last "real" reference, and prepares for using the lockref primitive. This also removes the b_lock protection for removing buffers from the buffer hash. This is the desired outcome because the rhashtable is fully internally synchronized, and previously the lock was mostly held out of ordering constrains in xfs_buf_rele_cached. Signed-off-by: Christoph Hellwig Reviewed-by: Brian Foster Reviewed-by: Darrick J. Wong Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_buf.c | 134 +++++++++++++++++------------------------------ fs/xfs/xfs_buf.h | 8 +-- 2 files changed, 50 insertions(+), 92 deletions(-) diff --git a/fs/xfs/xfs_buf.c b/fs/xfs/xfs_buf.c index d2f3c50d80e7..61e393ac4952 100644 --- a/fs/xfs/xfs_buf.c +++ b/fs/xfs/xfs_buf.c @@ -80,11 +80,8 @@ xfs_buf_stale( spin_lock(&bp->b_lock); atomic_set(&bp->b_lru_ref, 0); - if (!(bp->b_state & XFS_BSTATE_DISPOSE) && - (list_lru_del_obj(&bp->b_target->bt_lru, &bp->b_lru))) - bp->b_hold--; - - ASSERT(bp->b_hold >= 1); + if (bp->b_hold >= 0) + list_lru_del_obj(&bp->b_target->bt_lru, &bp->b_lru); spin_unlock(&bp->b_lock); } @@ -442,7 +439,7 @@ xfs_buf_try_hold( struct xfs_buf *bp) { spin_lock(&bp->b_lock); - if (bp->b_hold == 0) { + if (bp->b_hold == -1) { spin_unlock(&bp->b_lock); return false; } @@ -862,76 +859,24 @@ xfs_buf_hold( } static void -xfs_buf_rele_uncached( +xfs_buf_destroy( struct xfs_buf *bp) { - ASSERT(list_empty(&bp->b_lru)); + ASSERT(bp->b_hold < 0); + ASSERT(!(bp->b_flags & _XBF_DELWRI_Q)); - spin_lock(&bp->b_lock); - if (--bp->b_hold) { - spin_unlock(&bp->b_lock); - return; - } - spin_unlock(&bp->b_lock); - xfs_buf_free(bp); -} + if (!xfs_buf_is_uncached(bp)) { + struct xfs_buf_cache *bch = + xfs_buftarg_buf_cache(bp->b_target, bp->b_pag); -static void -xfs_buf_rele_cached( - struct xfs_buf *bp) -{ - struct xfs_buftarg *btp = bp->b_target; - struct xfs_perag *pag = bp->b_pag; - struct xfs_buf_cache *bch = xfs_buftarg_buf_cache(btp, pag); - bool freebuf = false; - - trace_xfs_buf_rele(bp, _RET_IP_); - - spin_lock(&bp->b_lock); - ASSERT(bp->b_hold >= 1); - if (bp->b_hold > 1) { - bp->b_hold--; - goto out_unlock; - } - - /* we are asked to drop the last reference */ - if (atomic_read(&bp->b_lru_ref)) { - /* - * If the buffer is added to the LRU, keep the reference to the - * buffer for the LRU and clear the (now stale) dispose list - * state flag, else drop the reference. - */ - if (list_lru_add_obj(&btp->bt_lru, &bp->b_lru)) - bp->b_state &= ~XFS_BSTATE_DISPOSE; - else - bp->b_hold--; - } else { - bp->b_hold--; - /* - * most of the time buffers will already be removed from the - * LRU, so optimise that case by checking for the - * XFS_BSTATE_DISPOSE flag indicating the last list the buffer - * was on was the disposal list - */ - if (!(bp->b_state & XFS_BSTATE_DISPOSE)) { - list_lru_del_obj(&btp->bt_lru, &bp->b_lru); - } else { - ASSERT(list_empty(&bp->b_lru)); - } - - ASSERT(!(bp->b_flags & _XBF_DELWRI_Q)); rhashtable_remove_fast(&bch->bc_hash, &bp->b_rhash_head, xfs_buf_hash_params); - if (pag) - xfs_perag_put(pag); - freebuf = true; + + if (bp->b_pag) + xfs_perag_put(bp->b_pag); } -out_unlock: - spin_unlock(&bp->b_lock); - - if (freebuf) - xfs_buf_free(bp); + xfs_buf_free(bp); } /* @@ -942,10 +887,22 @@ xfs_buf_rele( struct xfs_buf *bp) { trace_xfs_buf_rele(bp, _RET_IP_); - if (xfs_buf_is_uncached(bp)) - xfs_buf_rele_uncached(bp); - else - xfs_buf_rele_cached(bp); + + spin_lock(&bp->b_lock); + if (!--bp->b_hold) { + if (xfs_buf_is_uncached(bp) || !atomic_read(&bp->b_lru_ref)) + goto kill; + list_lru_add_obj(&bp->b_target->bt_lru, &bp->b_lru); + } + spin_unlock(&bp->b_lock); + return; + +kill: + bp->b_hold = -1; + list_lru_del_obj(&bp->b_target->bt_lru, &bp->b_lru); + spin_unlock(&bp->b_lock); + + xfs_buf_destroy(bp); } /* @@ -1254,9 +1211,11 @@ xfs_buf_ioerror_alert( /* * To simulate an I/O failure, the buffer must be locked and held with at least - * three references. The LRU reference is dropped by the stale call. The buf - * item reference is dropped via ioend processing. The third reference is owned - * by the caller and is dropped on I/O completion if the buffer is XBF_ASYNC. + * two references. + * + * The buf item reference is dropped via ioend processing. The second reference + * is owned by the caller and is dropped on I/O completion if the buffer is + * XBF_ASYNC. */ void xfs_buf_ioend_fail( @@ -1514,19 +1473,14 @@ xfs_buftarg_drain_rele( if (!spin_trylock(&bp->b_lock)) return LRU_SKIP; - if (bp->b_hold > 1) { + if (bp->b_hold > 0) { /* need to wait, so skip it this pass */ spin_unlock(&bp->b_lock); trace_xfs_buf_drain_buftarg(bp, _RET_IP_); return LRU_SKIP; } - /* - * clear the LRU reference count so the buffer doesn't get - * ignored in xfs_buf_rele(). - */ - atomic_set(&bp->b_lru_ref, 0); - bp->b_state |= XFS_BSTATE_DISPOSE; + bp->b_hold = -1; list_lru_isolate_move(lru, item, dispose); spin_unlock(&bp->b_lock); return LRU_REMOVED; @@ -1581,7 +1535,7 @@ xfs_buftarg_drain( "Corruption Alert: Buffer at daddr 0x%llx had permanent write failures!", (long long)xfs_buf_daddr(bp)); } - xfs_buf_rele(bp); + xfs_buf_destroy(bp); } if (loop++ != 0) delay(100); @@ -1625,7 +1579,17 @@ xfs_buftarg_isolate( return LRU_ROTATE; } - bp->b_state |= XFS_BSTATE_DISPOSE; + /* + * If the buffer is in use, remove it from the LRU for now as we can't + * free it. It will be freed when the last reference drops. + */ + if (bp->b_hold > 0) { + list_lru_isolate(lru, &bp->b_lru); + spin_unlock(&bp->b_lock); + return LRU_REMOVED; + } + + bp->b_hold = -1; list_lru_isolate_move(lru, item, dispose); spin_unlock(&bp->b_lock); return LRU_REMOVED; @@ -1647,7 +1611,7 @@ xfs_buftarg_shrink_scan( struct xfs_buf *bp; bp = list_first_entry(&dispose, struct xfs_buf, b_lru); list_del_init(&bp->b_lru); - xfs_buf_rele(bp); + xfs_buf_destroy(bp); } return freed; diff --git a/fs/xfs/xfs_buf.h b/fs/xfs/xfs_buf.h index e25cd2a160f3..e7324d58bd96 100644 --- a/fs/xfs/xfs_buf.h +++ b/fs/xfs/xfs_buf.h @@ -68,11 +68,6 @@ typedef unsigned int xfs_buf_flags_t; { XBF_INCORE, "INCORE" }, \ { XBF_TRYLOCK, "TRYLOCK" } -/* - * Internal state flags. - */ -#define XFS_BSTATE_DISPOSE (1 << 0) /* buffer being discarded */ - struct xfs_buf_cache { struct rhashtable bc_hash; }; @@ -159,7 +154,7 @@ struct xfs_buf { xfs_daddr_t b_rhash_key; /* buffer cache index */ int b_length; /* size of buffer in BBs */ - unsigned int b_hold; /* reference count */ + int b_hold; /* reference count */ atomic_t b_lru_ref; /* lru reclaim ref count */ xfs_buf_flags_t b_flags; /* status flags */ struct semaphore b_sema; /* semaphore for lockables */ @@ -170,7 +165,6 @@ struct xfs_buf { */ struct list_head b_lru; /* lru list */ spinlock_t b_lock; /* internal state lock */ - unsigned int b_state; /* internal state flags */ wait_queue_head_t b_waiters; /* unpin waiters */ struct list_head b_list; struct xfs_perag *b_pag; From d02ee47bbeedd10d36cc408f92e645447cf5495d Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Mon, 23 Mar 2026 08:50:52 +0100 Subject: [PATCH 20/41] xfs: use a lockref for the buffer reference count The lockref structure allows incrementing/decrementing counters like an atomic_t for the fast path, while still allowing complex slow path operations as if the counter was protected by a lock. The only slow path operations that actually need to take the lock are the final put, LRU evictions and marking a buffer stale. Signed-off-by: Christoph Hellwig Reviewed-by: Brian Foster Reviewed-by: Darrick J. Wong Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_buf.c | 80 +++++++++++++++++++--------------------------- fs/xfs/xfs_buf.h | 4 +-- fs/xfs/xfs_trace.h | 10 +++--- 3 files changed, 39 insertions(+), 55 deletions(-) diff --git a/fs/xfs/xfs_buf.c b/fs/xfs/xfs_buf.c index 61e393ac4952..d53a1bdbc789 100644 --- a/fs/xfs/xfs_buf.c +++ b/fs/xfs/xfs_buf.c @@ -31,20 +31,20 @@ struct kmem_cache *xfs_buf_cache; * * xfs_buf_stale: * b_sema (caller holds) - * b_lock + * b_lockref.lock * lru_lock * * xfs_buf_rele: - * b_lock + * b_lockref.lock * lru_lock * * xfs_buftarg_drain_rele * lru_lock - * b_lock (trylock due to inversion) + * b_lockref.lock (trylock due to inversion) * * xfs_buftarg_isolate * lru_lock - * b_lock (trylock due to inversion) + * b_lockref.lock (trylock due to inversion) */ static void xfs_buf_submit(struct xfs_buf *bp); @@ -78,11 +78,11 @@ xfs_buf_stale( */ bp->b_flags &= ~_XBF_DELWRI_Q; - spin_lock(&bp->b_lock); + spin_lock(&bp->b_lockref.lock); atomic_set(&bp->b_lru_ref, 0); - if (bp->b_hold >= 0) + if (!__lockref_is_dead(&bp->b_lockref)) list_lru_del_obj(&bp->b_target->bt_lru, &bp->b_lru); - spin_unlock(&bp->b_lock); + spin_unlock(&bp->b_lockref.lock); } static void @@ -274,10 +274,8 @@ xfs_buf_alloc( * inserting into the hash table are safe (and will have to wait for * the unlock to do anything non-trivial). */ - bp->b_hold = 1; + lockref_init(&bp->b_lockref); sema_init(&bp->b_sema, 0); /* held, no waiters */ - - spin_lock_init(&bp->b_lock); atomic_set(&bp->b_lru_ref, 1); init_completion(&bp->b_iowait); INIT_LIST_HEAD(&bp->b_lru); @@ -434,20 +432,6 @@ xfs_buf_find_lock( return 0; } -static bool -xfs_buf_try_hold( - struct xfs_buf *bp) -{ - spin_lock(&bp->b_lock); - if (bp->b_hold == -1) { - spin_unlock(&bp->b_lock); - return false; - } - bp->b_hold++; - spin_unlock(&bp->b_lock); - return true; -} - static inline int xfs_buf_lookup( struct xfs_buf_cache *bch, @@ -460,7 +444,7 @@ xfs_buf_lookup( rcu_read_lock(); bp = rhashtable_lookup(&bch->bc_hash, map, xfs_buf_hash_params); - if (!bp || !xfs_buf_try_hold(bp)) { + if (!bp || !lockref_get_not_dead(&bp->b_lockref)) { rcu_read_unlock(); return -ENOENT; } @@ -511,7 +495,7 @@ xfs_buf_find_insert( error = PTR_ERR(bp); goto out_free_buf; } - if (bp && xfs_buf_try_hold(bp)) { + if (bp && lockref_get_not_dead(&bp->b_lockref)) { /* found an existing buffer */ rcu_read_unlock(); error = xfs_buf_find_lock(bp, flags); @@ -853,16 +837,14 @@ xfs_buf_hold( { trace_xfs_buf_hold(bp, _RET_IP_); - spin_lock(&bp->b_lock); - bp->b_hold++; - spin_unlock(&bp->b_lock); + lockref_get(&bp->b_lockref); } static void xfs_buf_destroy( struct xfs_buf *bp) { - ASSERT(bp->b_hold < 0); + ASSERT(__lockref_is_dead(&bp->b_lockref)); ASSERT(!(bp->b_flags & _XBF_DELWRI_Q)); if (!xfs_buf_is_uncached(bp)) { @@ -888,19 +870,20 @@ xfs_buf_rele( { trace_xfs_buf_rele(bp, _RET_IP_); - spin_lock(&bp->b_lock); - if (!--bp->b_hold) { + if (lockref_put_or_lock(&bp->b_lockref)) + return; + if (!--bp->b_lockref.count) { if (xfs_buf_is_uncached(bp) || !atomic_read(&bp->b_lru_ref)) goto kill; list_lru_add_obj(&bp->b_target->bt_lru, &bp->b_lru); } - spin_unlock(&bp->b_lock); + spin_unlock(&bp->b_lockref.lock); return; kill: - bp->b_hold = -1; + lockref_mark_dead(&bp->b_lockref); list_lru_del_obj(&bp->b_target->bt_lru, &bp->b_lru); - spin_unlock(&bp->b_lock); + spin_unlock(&bp->b_lockref.lock); xfs_buf_destroy(bp); } @@ -1471,18 +1454,18 @@ xfs_buftarg_drain_rele( struct xfs_buf *bp = container_of(item, struct xfs_buf, b_lru); struct list_head *dispose = arg; - if (!spin_trylock(&bp->b_lock)) + if (!spin_trylock(&bp->b_lockref.lock)) return LRU_SKIP; - if (bp->b_hold > 0) { + if (bp->b_lockref.count > 0) { /* need to wait, so skip it this pass */ - spin_unlock(&bp->b_lock); + spin_unlock(&bp->b_lockref.lock); trace_xfs_buf_drain_buftarg(bp, _RET_IP_); return LRU_SKIP; } - bp->b_hold = -1; + lockref_mark_dead(&bp->b_lockref); list_lru_isolate_move(lru, item, dispose); - spin_unlock(&bp->b_lock); + spin_unlock(&bp->b_lockref.lock); return LRU_REMOVED; } @@ -1564,18 +1547,19 @@ xfs_buftarg_isolate( struct list_head *dispose = arg; /* - * we are inverting the lru lock/bp->b_lock here, so use a trylock. - * If we fail to get the lock, just skip it. + * We are inverting the lru lock vs bp->b_lockref.lock order here, so + * use a trylock. If we fail to get the lock, just skip the buffer. */ - if (!spin_trylock(&bp->b_lock)) + if (!spin_trylock(&bp->b_lockref.lock)) return LRU_SKIP; + /* * Decrement the b_lru_ref count unless the value is already * zero. If the value is already zero, we need to reclaim the * buffer, otherwise it gets another trip through the LRU. */ if (atomic_add_unless(&bp->b_lru_ref, -1, 0)) { - spin_unlock(&bp->b_lock); + spin_unlock(&bp->b_lockref.lock); return LRU_ROTATE; } @@ -1583,15 +1567,15 @@ xfs_buftarg_isolate( * If the buffer is in use, remove it from the LRU for now as we can't * free it. It will be freed when the last reference drops. */ - if (bp->b_hold > 0) { + if (bp->b_lockref.count > 0) { list_lru_isolate(lru, &bp->b_lru); - spin_unlock(&bp->b_lock); + spin_unlock(&bp->b_lockref.lock); return LRU_REMOVED; } - bp->b_hold = -1; + lockref_mark_dead(&bp->b_lockref); list_lru_isolate_move(lru, item, dispose); - spin_unlock(&bp->b_lock); + spin_unlock(&bp->b_lockref.lock); return LRU_REMOVED; } diff --git a/fs/xfs/xfs_buf.h b/fs/xfs/xfs_buf.h index e7324d58bd96..3a1d066e1c13 100644 --- a/fs/xfs/xfs_buf.h +++ b/fs/xfs/xfs_buf.h @@ -14,6 +14,7 @@ #include #include #include +#include extern struct kmem_cache *xfs_buf_cache; @@ -154,7 +155,7 @@ struct xfs_buf { xfs_daddr_t b_rhash_key; /* buffer cache index */ int b_length; /* size of buffer in BBs */ - int b_hold; /* reference count */ + struct lockref b_lockref; /* refcount + lock */ atomic_t b_lru_ref; /* lru reclaim ref count */ xfs_buf_flags_t b_flags; /* status flags */ struct semaphore b_sema; /* semaphore for lockables */ @@ -164,7 +165,6 @@ struct xfs_buf { * bt_lru_lock and not by b_sema */ struct list_head b_lru; /* lru list */ - spinlock_t b_lock; /* internal state lock */ wait_queue_head_t b_waiters; /* unpin waiters */ struct list_head b_list; struct xfs_perag *b_pag; diff --git a/fs/xfs/xfs_trace.h b/fs/xfs/xfs_trace.h index 5e8190fe2be9..60d1e605dfa5 100644 --- a/fs/xfs/xfs_trace.h +++ b/fs/xfs/xfs_trace.h @@ -740,7 +740,7 @@ DECLARE_EVENT_CLASS(xfs_buf_class, __entry->dev = bp->b_target->bt_dev; __entry->bno = xfs_buf_daddr(bp); __entry->nblks = bp->b_length; - __entry->hold = bp->b_hold; + __entry->hold = bp->b_lockref.count; __entry->pincount = atomic_read(&bp->b_pin_count); __entry->lockval = bp->b_sema.count; __entry->flags = bp->b_flags; @@ -814,7 +814,7 @@ DECLARE_EVENT_CLASS(xfs_buf_flags_class, __entry->bno = xfs_buf_daddr(bp); __entry->length = bp->b_length; __entry->flags = flags; - __entry->hold = bp->b_hold; + __entry->hold = bp->b_lockref.count; __entry->pincount = atomic_read(&bp->b_pin_count); __entry->lockval = bp->b_sema.count; __entry->caller_ip = caller_ip; @@ -858,7 +858,7 @@ TRACE_EVENT(xfs_buf_ioerror, __entry->dev = bp->b_target->bt_dev; __entry->bno = xfs_buf_daddr(bp); __entry->length = bp->b_length; - __entry->hold = bp->b_hold; + __entry->hold = bp->b_lockref.count; __entry->pincount = atomic_read(&bp->b_pin_count); __entry->lockval = bp->b_sema.count; __entry->error = error; @@ -902,7 +902,7 @@ DECLARE_EVENT_CLASS(xfs_buf_item_class, __entry->buf_bno = xfs_buf_daddr(bip->bli_buf); __entry->buf_len = bip->bli_buf->b_length; __entry->buf_flags = bip->bli_buf->b_flags; - __entry->buf_hold = bip->bli_buf->b_hold; + __entry->buf_hold = bip->bli_buf->b_lockref.count; __entry->buf_pincount = atomic_read(&bip->bli_buf->b_pin_count); __entry->buf_lockval = bip->bli_buf->b_sema.count; __entry->li_flags = bip->bli_item.li_flags; @@ -5206,7 +5206,7 @@ DECLARE_EVENT_CLASS(xfbtree_buf_class, __entry->xfino = file_inode(xfbt->target->bt_file)->i_ino; __entry->bno = xfs_buf_daddr(bp); __entry->nblks = bp->b_length; - __entry->hold = bp->b_hold; + __entry->hold = bp->b_lockref.count; __entry->pincount = atomic_read(&bp->b_pin_count); __entry->lockval = bp->b_sema.count; __entry->flags = bp->b_flags; From 497560b9ef42a4ab22ada7f1ea975a89cd3c5dfa Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Mon, 23 Mar 2026 08:50:53 +0100 Subject: [PATCH 21/41] xfs: switch (back) to a per-buftarg buffer hash The per-AG buffer hashes were added when all buffer lookups took a per-hash look. Since then we've made lookups entirely lockless and removed the need for a hash-wide lock for inserts and removals as well. With this there is no need to sharding the hash, so reduce the used resources by using a per-buftarg hash for all buftargs. Long after writing this initially, syzbot found a problem in the buffer cache teardown order, which this happens to fix as well by doing the entire buffer cache teardown in one places instead of splitting it between destroying the buftarg and the perag structures. Link: https://lore.kernel.org/linux-xfs/aLeUdemAZ5wmtZel@dread.disaster.area/ Reported-by: syzbot+0391d34e801643e2809b@syzkaller.appspotmail.com Reviewed-by: Darrick J. Wong Tested-by: syzbot+0391d34e801643e2809b@syzkaller.appspotmail.com Signed-off-by: Christoph Hellwig Reviewed-by: Brian Foster Signed-off-by: Carlos Maiolino --- fs/xfs/libxfs/xfs_ag.c | 13 ++--------- fs/xfs/libxfs/xfs_ag.h | 2 -- fs/xfs/xfs_buf.c | 51 +++++++++++------------------------------- fs/xfs/xfs_buf.h | 10 +-------- fs/xfs/xfs_buf_mem.c | 11 ++------- 5 files changed, 18 insertions(+), 69 deletions(-) diff --git a/fs/xfs/libxfs/xfs_ag.c b/fs/xfs/libxfs/xfs_ag.c index bd8fbb40b49e..dcd2f93b6a6c 100644 --- a/fs/xfs/libxfs/xfs_ag.c +++ b/fs/xfs/libxfs/xfs_ag.c @@ -110,10 +110,7 @@ xfs_perag_uninit( struct xfs_group *xg) { #ifdef __KERNEL__ - struct xfs_perag *pag = to_perag(xg); - - cancel_delayed_work_sync(&pag->pag_blockgc_work); - xfs_buf_cache_destroy(&pag->pag_bcache); + cancel_delayed_work_sync(&to_perag(xg)->pag_blockgc_work); #endif } @@ -235,10 +232,6 @@ xfs_perag_alloc( INIT_RADIX_TREE(&pag->pag_ici_root, GFP_ATOMIC); #endif /* __KERNEL__ */ - error = xfs_buf_cache_init(&pag->pag_bcache); - if (error) - goto out_free_perag; - /* * Pre-calculated geometry */ @@ -250,12 +243,10 @@ xfs_perag_alloc( error = xfs_group_insert(mp, pag_group(pag), index, XG_TYPE_AG); if (error) - goto out_buf_cache_destroy; + goto out_free_perag; return 0; -out_buf_cache_destroy: - xfs_buf_cache_destroy(&pag->pag_bcache); out_free_perag: kfree(pag); return error; diff --git a/fs/xfs/libxfs/xfs_ag.h b/fs/xfs/libxfs/xfs_ag.h index 3cd4790768ff..16a9b43a3c27 100644 --- a/fs/xfs/libxfs/xfs_ag.h +++ b/fs/xfs/libxfs/xfs_ag.h @@ -85,8 +85,6 @@ struct xfs_perag { int pag_ici_reclaimable; /* reclaimable inodes */ unsigned long pag_ici_reclaim_cursor; /* reclaim restart point */ - struct xfs_buf_cache pag_bcache; - /* background prealloc block trimming */ struct delayed_work pag_blockgc_work; #endif /* __KERNEL__ */ diff --git a/fs/xfs/xfs_buf.c b/fs/xfs/xfs_buf.c index d53a1bdbc789..e4b65d0c9ef0 100644 --- a/fs/xfs/xfs_buf.c +++ b/fs/xfs/xfs_buf.c @@ -363,20 +363,6 @@ static const struct rhashtable_params xfs_buf_hash_params = { .obj_cmpfn = _xfs_buf_obj_cmp, }; -int -xfs_buf_cache_init( - struct xfs_buf_cache *bch) -{ - return rhashtable_init(&bch->bc_hash, &xfs_buf_hash_params); -} - -void -xfs_buf_cache_destroy( - struct xfs_buf_cache *bch) -{ - rhashtable_destroy(&bch->bc_hash); -} - static int xfs_buf_map_verify( struct xfs_buftarg *btp, @@ -434,7 +420,7 @@ xfs_buf_find_lock( static inline int xfs_buf_lookup( - struct xfs_buf_cache *bch, + struct xfs_buftarg *btp, struct xfs_buf_map *map, xfs_buf_flags_t flags, struct xfs_buf **bpp) @@ -443,7 +429,7 @@ xfs_buf_lookup( int error; rcu_read_lock(); - bp = rhashtable_lookup(&bch->bc_hash, map, xfs_buf_hash_params); + bp = rhashtable_lookup(&btp->bt_hash, map, xfs_buf_hash_params); if (!bp || !lockref_get_not_dead(&bp->b_lockref)) { rcu_read_unlock(); return -ENOENT; @@ -468,7 +454,6 @@ xfs_buf_lookup( static int xfs_buf_find_insert( struct xfs_buftarg *btp, - struct xfs_buf_cache *bch, struct xfs_perag *pag, struct xfs_buf_map *cmap, struct xfs_buf_map *map, @@ -488,7 +473,7 @@ xfs_buf_find_insert( new_bp->b_pag = pag; rcu_read_lock(); - bp = rhashtable_lookup_get_insert_fast(&bch->bc_hash, + bp = rhashtable_lookup_get_insert_fast(&btp->bt_hash, &new_bp->b_rhash_head, xfs_buf_hash_params); if (IS_ERR(bp)) { rcu_read_unlock(); @@ -530,16 +515,6 @@ xfs_buftarg_get_pag( return xfs_perag_get(mp, xfs_daddr_to_agno(mp, map->bm_bn)); } -static inline struct xfs_buf_cache * -xfs_buftarg_buf_cache( - struct xfs_buftarg *btp, - struct xfs_perag *pag) -{ - if (pag) - return &pag->pag_bcache; - return btp->bt_cache; -} - /* * Assembles a buffer covering the specified range. The code is optimised for * cache hits, as metadata intensive workloads will see 3 orders of magnitude @@ -553,7 +528,6 @@ xfs_buf_get_map( xfs_buf_flags_t flags, struct xfs_buf **bpp) { - struct xfs_buf_cache *bch; struct xfs_perag *pag; struct xfs_buf *bp = NULL; struct xfs_buf_map cmap = { .bm_bn = map[0].bm_bn }; @@ -570,9 +544,8 @@ xfs_buf_get_map( return error; pag = xfs_buftarg_get_pag(btp, &cmap); - bch = xfs_buftarg_buf_cache(btp, pag); - error = xfs_buf_lookup(bch, &cmap, flags, &bp); + error = xfs_buf_lookup(btp, &cmap, flags, &bp); if (error && error != -ENOENT) goto out_put_perag; @@ -584,7 +557,7 @@ xfs_buf_get_map( goto out_put_perag; /* xfs_buf_find_insert() consumes the perag reference. */ - error = xfs_buf_find_insert(btp, bch, pag, &cmap, map, nmaps, + error = xfs_buf_find_insert(btp, pag, &cmap, map, nmaps, flags, &bp); if (error) return error; @@ -848,11 +821,8 @@ xfs_buf_destroy( ASSERT(!(bp->b_flags & _XBF_DELWRI_Q)); if (!xfs_buf_is_uncached(bp)) { - struct xfs_buf_cache *bch = - xfs_buftarg_buf_cache(bp->b_target, bp->b_pag); - - rhashtable_remove_fast(&bch->bc_hash, &bp->b_rhash_head, - xfs_buf_hash_params); + rhashtable_remove_fast(&bp->b_target->bt_hash, + &bp->b_rhash_head, xfs_buf_hash_params); if (bp->b_pag) xfs_perag_put(bp->b_pag); @@ -1618,6 +1588,7 @@ xfs_destroy_buftarg( ASSERT(percpu_counter_sum(&btp->bt_readahead_count) == 0); percpu_counter_destroy(&btp->bt_readahead_count); list_lru_destroy(&btp->bt_lru); + rhashtable_destroy(&btp->bt_hash); } void @@ -1712,8 +1683,10 @@ xfs_init_buftarg( ratelimit_state_init(&btp->bt_ioerror_rl, 30 * HZ, DEFAULT_RATELIMIT_BURST); - if (list_lru_init(&btp->bt_lru)) + if (rhashtable_init(&btp->bt_hash, &xfs_buf_hash_params)) return -ENOMEM; + if (list_lru_init(&btp->bt_lru)) + goto out_destroy_hash; if (percpu_counter_init(&btp->bt_readahead_count, 0, GFP_KERNEL)) goto out_destroy_lru; @@ -1731,6 +1704,8 @@ out_destroy_io_count: percpu_counter_destroy(&btp->bt_readahead_count); out_destroy_lru: list_lru_destroy(&btp->bt_lru); +out_destroy_hash: + rhashtable_destroy(&btp->bt_hash); return -ENOMEM; } diff --git a/fs/xfs/xfs_buf.h b/fs/xfs/xfs_buf.h index 3a1d066e1c13..bf39d89f0f6d 100644 --- a/fs/xfs/xfs_buf.h +++ b/fs/xfs/xfs_buf.h @@ -69,13 +69,6 @@ typedef unsigned int xfs_buf_flags_t; { XBF_INCORE, "INCORE" }, \ { XBF_TRYLOCK, "TRYLOCK" } -struct xfs_buf_cache { - struct rhashtable bc_hash; -}; - -int xfs_buf_cache_init(struct xfs_buf_cache *bch); -void xfs_buf_cache_destroy(struct xfs_buf_cache *bch); - /* * The xfs_buftarg contains 2 notions of "sector size" - * @@ -113,8 +106,7 @@ struct xfs_buftarg { unsigned int bt_awu_min; unsigned int bt_awu_max; - /* built-in cache, if we're not using the perag one */ - struct xfs_buf_cache bt_cache[]; + struct rhashtable bt_hash; }; struct xfs_buf_map { diff --git a/fs/xfs/xfs_buf_mem.c b/fs/xfs/xfs_buf_mem.c index b0b3696bf599..b2fd7276b131 100644 --- a/fs/xfs/xfs_buf_mem.c +++ b/fs/xfs/xfs_buf_mem.c @@ -58,7 +58,7 @@ xmbuf_alloc( struct xfs_buftarg *btp; int error; - btp = kzalloc_flex(*btp, bt_cache, 1); + btp = kzalloc_obj(*btp); if (!btp) return -ENOMEM; @@ -81,10 +81,6 @@ xmbuf_alloc( /* ensure all writes are below EOF to avoid pagecache zeroing */ i_size_write(inode, inode->i_sb->s_maxbytes); - error = xfs_buf_cache_init(btp->bt_cache); - if (error) - goto out_file; - /* Initialize buffer target */ btp->bt_mount = mp; btp->bt_dev = (dev_t)-1U; @@ -95,15 +91,13 @@ xmbuf_alloc( error = xfs_init_buftarg(btp, XMBUF_BLOCKSIZE, descr); if (error) - goto out_bcache; + goto out_file; trace_xmbuf_create(btp); *btpp = btp; return 0; -out_bcache: - xfs_buf_cache_destroy(btp->bt_cache); out_file: fput(file); out_free_btp: @@ -122,7 +116,6 @@ xmbuf_free( trace_xmbuf_free(btp); xfs_destroy_buftarg(btp); - xfs_buf_cache_destroy(btp->bt_cache); fput(btp->bt_file); kfree(btp); } From 8166876aadef90744bb26addc9c5a16b1c8341b5 Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Mon, 23 Mar 2026 08:50:54 +0100 Subject: [PATCH 22/41] xfs: don't decrement the buffer LRU count for in-use buffers XFS buffers are added to the LRU when they are unused, but are only removed from the LRU lazily when the LRU list scan finds a used buffer. So far this only happen when the LRU counter hits 0, which is suboptimal as buffers that were added to the LRU, but are in use again still consume LRU scanning resources and are aged while actually in use. Fix this by checking for in-use buffers and removing the from the LRU before decrementing the LRU counter. Signed-off-by: Christoph Hellwig Reviewed-by: Darrick J. Wong Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_buf.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/fs/xfs/xfs_buf.c b/fs/xfs/xfs_buf.c index e4b65d0c9ef0..ee8c3944015a 100644 --- a/fs/xfs/xfs_buf.c +++ b/fs/xfs/xfs_buf.c @@ -1523,6 +1523,18 @@ xfs_buftarg_isolate( if (!spin_trylock(&bp->b_lockref.lock)) return LRU_SKIP; + /* + * If the buffer is in use, remove it from the LRU for now. We can't + * free it while someone is using it, and we should also not count + * eviction passed for it, just as if it hadn't been added to the LRU + * yet. + */ + if (bp->b_lockref.count > 0) { + list_lru_isolate(lru, &bp->b_lru); + spin_unlock(&bp->b_lockref.lock); + return LRU_REMOVED; + } + /* * Decrement the b_lru_ref count unless the value is already * zero. If the value is already zero, we need to reclaim the @@ -1533,16 +1545,6 @@ xfs_buftarg_isolate( return LRU_ROTATE; } - /* - * If the buffer is in use, remove it from the LRU for now as we can't - * free it. It will be freed when the last reference drops. - */ - if (bp->b_lockref.count > 0) { - list_lru_isolate(lru, &bp->b_lru); - spin_unlock(&bp->b_lockref.lock); - return LRU_REMOVED; - } - lockref_mark_dead(&bp->b_lockref); list_lru_isolate_move(lru, item, dispose); spin_unlock(&bp->b_lockref.lock); From 181ea4e2de422aa0a66f355bd59bccccdd169826 Mon Sep 17 00:00:00 2001 From: Hans Holmberg Date: Wed, 25 Mar 2026 13:43:12 +0100 Subject: [PATCH 23/41] xfs: start gc on zonegc_low_space attribute updates Start gc if the agressiveness of zone garbage collection is changed by the user (if the file system is not read only). Without this change, the new setting will not be taken into account until the gc thread is woken up by e.g. a write. Cc: stable@vger.kernel.org # v6.15 Fixes: 845abeb1f06a8a ("xfs: add tunable threshold parameter for triggering zone GC") Signed-off-by: Hans Holmberg Reviewed-by: Christoph Hellwig Reviewed-by: Damien Le Moal Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_sysfs.c | 7 ++++++- fs/xfs/xfs_zone_alloc.h | 4 ++++ fs/xfs/xfs_zone_gc.c | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/fs/xfs/xfs_sysfs.c b/fs/xfs/xfs_sysfs.c index 6c7909838234..4527119b2961 100644 --- a/fs/xfs/xfs_sysfs.c +++ b/fs/xfs/xfs_sysfs.c @@ -14,6 +14,7 @@ #include "xfs_log_priv.h" #include "xfs_mount.h" #include "xfs_zones.h" +#include "xfs_zone_alloc.h" struct xfs_sysfs_attr { struct attribute attr; @@ -724,6 +725,7 @@ zonegc_low_space_store( const char *buf, size_t count) { + struct xfs_mount *mp = zoned_to_mp(kobj); int ret; unsigned int val; @@ -734,7 +736,10 @@ zonegc_low_space_store( if (val > 100) return -EINVAL; - zoned_to_mp(kobj)->m_zonegc_low_space = val; + if (mp->m_zonegc_low_space != val) { + mp->m_zonegc_low_space = val; + xfs_zone_gc_wakeup(mp); + } return count; } diff --git a/fs/xfs/xfs_zone_alloc.h b/fs/xfs/xfs_zone_alloc.h index 4db02816d0fd..8b2ef98c81ef 100644 --- a/fs/xfs/xfs_zone_alloc.h +++ b/fs/xfs/xfs_zone_alloc.h @@ -51,6 +51,7 @@ int xfs_mount_zones(struct xfs_mount *mp); void xfs_unmount_zones(struct xfs_mount *mp); void xfs_zone_gc_start(struct xfs_mount *mp); void xfs_zone_gc_stop(struct xfs_mount *mp); +void xfs_zone_gc_wakeup(struct xfs_mount *mp); #else static inline int xfs_mount_zones(struct xfs_mount *mp) { @@ -65,6 +66,9 @@ static inline void xfs_zone_gc_start(struct xfs_mount *mp) static inline void xfs_zone_gc_stop(struct xfs_mount *mp) { } +static inline void xfs_zone_gc_wakeup(struct xfs_mount *mp) +{ +} #endif /* CONFIG_XFS_RT */ #endif /* _XFS_ZONE_ALLOC_H */ diff --git a/fs/xfs/xfs_zone_gc.c b/fs/xfs/xfs_zone_gc.c index 0ff710fa0ee7..a8f71231f351 100644 --- a/fs/xfs/xfs_zone_gc.c +++ b/fs/xfs/xfs_zone_gc.c @@ -1171,6 +1171,23 @@ xfs_zone_gc_stop( kthread_park(mp->m_zone_info->zi_gc_thread); } +void +xfs_zone_gc_wakeup( + struct xfs_mount *mp) +{ + struct super_block *sb = mp->m_super; + + /* + * If we are unmounting the file system we must not try to + * wake gc as m_zone_info might have been freed already. + */ + if (down_read_trylock(&sb->s_umount)) { + if (!xfs_is_readonly(mp)) + wake_up_process(mp->m_zone_info->zi_gc_thread); + up_read(&sb->s_umount); + } +} + int xfs_zone_gc_mount( struct xfs_mount *mp) From 7338419a5e4454cc7a2c9df6af712d6f5ab471e9 Mon Sep 17 00:00:00 2001 From: Ravi Singh Date: Mon, 30 Mar 2026 14:14:14 +0800 Subject: [PATCH 24/41] xfs: return default quota limits for IDs without a dquot When an ID has no dquot on disk, Q_XGETQUOTA returns -ENOENT even though default quota limits are configured and enforced against that ID. This means unprivileged users who have never used any resources cannot see the limits that apply to them. When xfs_qm_dqget() returns -ENOENT for a non-zero ID, return a zero-usage response with the default limits filled in from m_quotainfo rather than propagating the error. This is consistent with the enforcement behavior in xfs_qm_adjust_dqlimits(), which pushes the same default limits into a dquot when it is first allocated. Reviewed-by: Jan Kara Signed-off-by: Ravi Singh Reviewed-by: Carlos Maiolino Reviewed-by: Christoph Hellwig Reviewed-by: Darrick J. Wong Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_qm_syscalls.c | 43 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/fs/xfs/xfs_qm_syscalls.c b/fs/xfs/xfs_qm_syscalls.c index d50b7318cb5c..21a784986828 100644 --- a/fs/xfs/xfs_qm_syscalls.c +++ b/fs/xfs/xfs_qm_syscalls.c @@ -391,6 +391,38 @@ out_rele: return error; } +/* + * Fill out the default quota limits for an ID that has no dquot on disk. + * Returns 0 if default limits are configured + * and were filled in, -ENOENT otherwise. + */ +static int +xfs_qm_scall_getquota_fill_defaults( + struct xfs_mount *mp, + xfs_dqtype_t type, + struct qc_dqblk *dst) +{ + struct xfs_def_quota *defq; + + defq = xfs_get_defquota(mp->m_quotainfo, type); + + if (!defq->blk.soft && !defq->blk.hard && + !defq->ino.soft && !defq->ino.hard && + !defq->rtb.soft && !defq->rtb.hard) { + return -ENOENT; + } + + memset(dst, 0, sizeof(*dst)); + dst->d_spc_softlimit = XFS_FSB_TO_B(mp, defq->blk.soft); + dst->d_spc_hardlimit = XFS_FSB_TO_B(mp, defq->blk.hard); + dst->d_ino_softlimit = defq->ino.soft; + dst->d_ino_hardlimit = defq->ino.hard; + dst->d_rt_spc_softlimit = XFS_FSB_TO_B(mp, defq->rtb.soft); + dst->d_rt_spc_hardlimit = XFS_FSB_TO_B(mp, defq->rtb.hard); + + return 0; +} + /* Fill out the quota context. */ static void xfs_qm_scall_getquota_fill_qc( @@ -451,8 +483,17 @@ xfs_qm_scall_getquota( * set doalloc. If it doesn't exist, we'll get ENOENT back. */ error = xfs_qm_dqget(mp, id, type, false, &dqp); - if (error) + if (error) { + /* + * If there is no dquot on disk and default limits are + * configured, return them with zero usage so that + * unprivileged users can see what limits apply to them. + */ + if (error == -ENOENT && id != 0 && + !xfs_qm_scall_getquota_fill_defaults(mp, type, dst)) + return 0; return error; + } /* * If everything's NULL, this dquot doesn't quite exist as far as From 0f7d2a9e020812a787d7c6dfc98715f9c8f72f53 Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Mon, 30 Mar 2026 15:15:59 +0200 Subject: [PATCH 25/41] xfs: remove a duplicate assert in xfs_setattr_size There already is an assert that checks for uid and gid changes besides a lot of others at the beginning of the function. Signed-off-by: Christoph Hellwig Reviewed-by: Carlos Maiolino Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_iops.c | 1 - 1 file changed, 1 deletion(-) diff --git a/fs/xfs/xfs_iops.c b/fs/xfs/xfs_iops.c index 208543e57eda..b0256a4a8aa8 100644 --- a/fs/xfs/xfs_iops.c +++ b/fs/xfs/xfs_iops.c @@ -1109,7 +1109,6 @@ xfs_setattr_size( xfs_inode_clear_eofblocks_tag(ip); } - ASSERT(!(iattr->ia_valid & (ATTR_UID | ATTR_GID))); setattr_copy(idmap, inode, iattr); xfs_trans_log_inode(tp, ip, XFS_ILOG_CORE); From e92b3fc5b17c75d3ca31983a7d35e8b88786ee4e Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Mon, 30 Mar 2026 15:16:00 +0200 Subject: [PATCH 26/41] xfs: fold xfs_setattr_size into xfs_vn_setattr_size xfs_vn_setattr_size is the only caller of xfs_setattr_size, so merge the two functions. Signed-off-by: Christoph Hellwig Reviewed-by: Carlos Maiolino Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_iops.c | 38 +++++++++++--------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/fs/xfs/xfs_iops.c b/fs/xfs/xfs_iops.c index b0256a4a8aa8..325c2200c501 100644 --- a/fs/xfs/xfs_iops.c +++ b/fs/xfs/xfs_iops.c @@ -901,20 +901,18 @@ out_dqrele: /* * Truncate file. Must have write permission and not be a directory. - * - * Caution: The caller of this function is responsible for calling - * setattr_prepare() or otherwise verifying the change is fine. */ -STATIC int -xfs_setattr_size( +int +xfs_vn_setattr_size( struct mnt_idmap *idmap, struct dentry *dentry, - struct xfs_inode *ip, struct iattr *iattr) { + struct inode *inode = d_inode(dentry); + struct xfs_inode *ip = XFS_I(inode); struct xfs_mount *mp = ip->i_mount; - struct inode *inode = VFS_I(ip); - xfs_off_t oldsize, newsize; + xfs_off_t oldsize = inode->i_size; + xfs_off_t newsize = iattr->ia_size; struct xfs_trans *tp; int error; uint lock_flags = 0; @@ -927,8 +925,11 @@ xfs_setattr_size( ASSERT((iattr->ia_valid & (ATTR_UID|ATTR_GID|ATTR_ATIME|ATTR_ATIME_SET| ATTR_MTIME_SET|ATTR_TIMES_SET)) == 0); - oldsize = inode->i_size; - newsize = iattr->ia_size; + trace_xfs_setattr(ip); + + error = xfs_vn_change_ok(idmap, dentry, iattr); + if (error) + return error; /* * Short circuit the truncate case for zero length files. @@ -1128,23 +1129,6 @@ out_trans_cancel: goto out_unlock; } -int -xfs_vn_setattr_size( - struct mnt_idmap *idmap, - struct dentry *dentry, - struct iattr *iattr) -{ - struct xfs_inode *ip = XFS_I(d_inode(dentry)); - int error; - - trace_xfs_setattr(ip); - - error = xfs_vn_change_ok(idmap, dentry, iattr); - if (error) - return error; - return xfs_setattr_size(idmap, dentry, ip, iattr); -} - STATIC int xfs_vn_setattr( struct mnt_idmap *idmap, From 59e586d7dc7813910abe20a8281bbc3f1360e08e Mon Sep 17 00:00:00 2001 From: Yuto Ohnuki Date: Sat, 28 Mar 2026 17:34:09 +0000 Subject: [PATCH 27/41] xfs: fix integer overflow in deferred intent sort comparators xfs_extent_free_diff_items(), xfs_refcount_update_diff_items(), and xfs_rmap_update_diff_items() subtract two uint32_t group numbers and return the result as int, which can overflow when the difference exceeds INT_MAX. Use cmp_int() instead, as was done in commit 362c49098086 ("xfs: fix integer overflow in bmap intent sort comparator"). Fixes: c13418e8eb37 ("xfs: give xfs_rmap_intent its own perag reference") Fixes: f6b384631e1e ("xfs: give xfs_extfree_intent its own perag reference") Fixes: 00e7b3bac1dc ("xfs: give xfs_refcount_intent its own perag reference") Signed-off-by: Yuto Ohnuki Reviewed-by: Christoph Hellwig Reviewed-by: Carlos Maiolino Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_extfree_item.c | 2 +- fs/xfs/xfs_refcount_item.c | 2 +- fs/xfs/xfs_rmap_item.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/xfs/xfs_extfree_item.c b/fs/xfs/xfs_extfree_item.c index 749a4eb9793c..2266d56e37dc 100644 --- a/fs/xfs/xfs_extfree_item.c +++ b/fs/xfs/xfs_extfree_item.c @@ -387,7 +387,7 @@ xfs_extent_free_diff_items( struct xfs_extent_free_item *ra = xefi_entry(a); struct xfs_extent_free_item *rb = xefi_entry(b); - return ra->xefi_group->xg_gno - rb->xefi_group->xg_gno; + return cmp_int(ra->xefi_group->xg_gno, rb->xefi_group->xg_gno); } /* Log a free extent to the intent item. */ diff --git a/fs/xfs/xfs_refcount_item.c b/fs/xfs/xfs_refcount_item.c index 881c3f3a6a24..8bccf89a7766 100644 --- a/fs/xfs/xfs_refcount_item.c +++ b/fs/xfs/xfs_refcount_item.c @@ -266,7 +266,7 @@ xfs_refcount_update_diff_items( struct xfs_refcount_intent *ra = ci_entry(a); struct xfs_refcount_intent *rb = ci_entry(b); - return ra->ri_group->xg_gno - rb->ri_group->xg_gno; + return cmp_int(ra->ri_group->xg_gno, rb->ri_group->xg_gno); } /* Log refcount updates in the intent item. */ diff --git a/fs/xfs/xfs_rmap_item.c b/fs/xfs/xfs_rmap_item.c index a39fe08dcd8f..2a3a73a8566d 100644 --- a/fs/xfs/xfs_rmap_item.c +++ b/fs/xfs/xfs_rmap_item.c @@ -267,7 +267,7 @@ xfs_rmap_update_diff_items( struct xfs_rmap_intent *ra = ri_entry(a); struct xfs_rmap_intent *rb = ri_entry(b); - return ra->ri_group->xg_gno - rb->ri_group->xg_gno; + return cmp_int(ra->ri_group->xg_gno, rb->ri_group->xg_gno); } /* Log rmap updates in the intent item. */ From 553a13e2076d64774910d597977a17022625763d Mon Sep 17 00:00:00 2001 From: Yuto Ohnuki Date: Sat, 28 Mar 2026 17:34:10 +0000 Subject: [PATCH 28/41] xfs: fix integer overflow in busy extent sort comparator xfs_extent_busy_ag_cmp() subtracts two uint32_t values (group numbers and block numbers) and returns the result as s32. When the difference exceeds INT_MAX, the result overflows and the sort order is corrupted. Use cmp_int() instead, as was done in commit 362c49098086 ("xfs: fix integer overflow in bmap intent sort comparator"). Fixes: 4a137e09151e ("xfs: keep a reference to the pag for busy extents") Signed-off-by: Yuto Ohnuki Reviewed-by: Christoph Hellwig Reviewed-by: Carlos Maiolino Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_extent_busy.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/xfs/xfs_extent_busy.c b/fs/xfs/xfs_extent_busy.c index 3efdca3d675b..41cf0605ec22 100644 --- a/fs/xfs/xfs_extent_busy.c +++ b/fs/xfs/xfs_extent_busy.c @@ -690,9 +690,9 @@ xfs_extent_busy_ag_cmp( container_of(l2, struct xfs_extent_busy, list); s32 diff; - diff = b1->group->xg_gno - b2->group->xg_gno; + diff = cmp_int(b1->group->xg_gno, b2->group->xg_gno); if (!diff) - diff = b1->bno - b2->bno; + diff = cmp_int(b1->bno, b2->bno); return diff; } From 02367990bdcbeabb0ffd3e8e227e5f79a04186fc Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 31 Mar 2026 17:26:05 +0200 Subject: [PATCH 29/41] xfs: refactor xfs_mount_zones xfs_mount_zones has grown a bit too big and unorganized. Split the zone reporting loop into a separate helper, hiding the rtg variable there. Print the mount message last, and also keep the VFS writeback chunk size last instead of in the middle of the logic to calculate the free/available blocks. Signed-off-by: Christoph Hellwig Reviewed-by: Hans Holmberg Reviewed-by: Damien Le Moal Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_alloc.c | 54 ++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/fs/xfs/xfs_zone_alloc.c b/fs/xfs/xfs_zone_alloc.c index 06e2cb79030e..e9f1d9d08620 100644 --- a/fs/xfs/xfs_zone_alloc.c +++ b/fs/xfs/xfs_zone_alloc.c @@ -1230,6 +1230,29 @@ xfs_free_zone_info( kfree(zi); } +static int +xfs_report_zones( + struct xfs_mount *mp, + struct xfs_init_zones *iz) +{ + struct xfs_rtgroup *rtg = NULL; + + while ((rtg = xfs_rtgroup_next(mp, rtg))) { + xfs_rgblock_t write_pointer; + int error; + + error = xfs_query_write_pointer(iz, rtg, &write_pointer); + if (!error) + error = xfs_init_zone(iz, rtg, write_pointer); + if (error) { + xfs_rtgroup_rele(rtg); + return error; + } + } + + return 0; +} + int xfs_mount_zones( struct xfs_mount *mp) @@ -1238,7 +1261,6 @@ xfs_mount_zones( .zone_capacity = mp->m_groups[XG_TYPE_RTG].blocks, .zone_size = xfs_rtgroup_raw_size(mp), }; - struct xfs_rtgroup *rtg = NULL; int error; if (!mp->m_rtdev_targp) { @@ -1268,9 +1290,13 @@ xfs_mount_zones( if (!mp->m_zone_info) return -ENOMEM; - xfs_info(mp, "%u zones of %u blocks (%u max open zones)", - mp->m_sb.sb_rgcount, iz.zone_capacity, mp->m_max_open_zones); - trace_xfs_zones_mount(mp); + error = xfs_report_zones(mp, &iz); + if (error) + goto out_free_zone_info; + + xfs_set_freecounter(mp, XC_FREE_RTAVAILABLE, iz.available); + xfs_set_freecounter(mp, XC_FREE_RTEXTENTS, + iz.available + iz.reclaimable); /* * The writeback code switches between inodes regularly to provide @@ -1296,22 +1322,6 @@ xfs_mount_zones( XFS_FSB_TO_B(mp, min(iz.zone_capacity, XFS_MAX_BMBT_EXTLEN)) >> PAGE_SHIFT; - while ((rtg = xfs_rtgroup_next(mp, rtg))) { - xfs_rgblock_t write_pointer; - - error = xfs_query_write_pointer(&iz, rtg, &write_pointer); - if (!error) - error = xfs_init_zone(&iz, rtg, write_pointer); - if (error) { - xfs_rtgroup_rele(rtg); - goto out_free_zone_info; - } - } - - xfs_set_freecounter(mp, XC_FREE_RTAVAILABLE, iz.available); - xfs_set_freecounter(mp, XC_FREE_RTEXTENTS, - iz.available + iz.reclaimable); - /* * The user may configure GC to free up a percentage of unused blocks. * By default this is 0. GC will always trigger at the minimum level @@ -1322,6 +1332,10 @@ xfs_mount_zones( error = xfs_zone_gc_mount(mp); if (error) goto out_free_zone_info; + + xfs_info(mp, "%u zones of %u blocks (%u max open zones)", + mp->m_sb.sb_rgcount, iz.zone_capacity, mp->m_max_open_zones); + trace_xfs_zones_mount(mp); return 0; out_free_zone_info: From c6584888864e36d6225a6c16d8c39fd2aa9a45d8 Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 31 Mar 2026 17:26:06 +0200 Subject: [PATCH 30/41] xfs: handle too many open zones when mounting When running on conventional zones or devices, the zoned allocator does not have a real write pointer, but instead fakes it up at mount time based on the last block recorded in the rmap. This can create spurious "open" zones when the last written blocks in a conventional zone are invalidated. Add a loop to the mount code to find the conventional zone with the highest used block in the rmap tree and "finish" it until we are below the open zones limit. While we're at it, also error out if there are too many open sequential zones, which can only happen when the user overrode the max open zones limit (or with really buggy hardware reducing the limit, but not much we can do about that). Fixes: 4e4d52075577 ("xfs: add the zoned space allocator") Signed-off-by: Christoph Hellwig Reviewed-by: Hans Holmberg Reviewed-by: Damien Le Moal Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_trace.h | 1 + fs/xfs/xfs_zone_alloc.c | 75 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/fs/xfs/xfs_trace.h b/fs/xfs/xfs_trace.h index 60d1e605dfa5..c5ad26a1d7bb 100644 --- a/fs/xfs/xfs_trace.h +++ b/fs/xfs/xfs_trace.h @@ -461,6 +461,7 @@ DEFINE_EVENT(xfs_zone_alloc_class, name, \ DEFINE_ZONE_ALLOC_EVENT(xfs_zone_record_blocks); DEFINE_ZONE_ALLOC_EVENT(xfs_zone_skip_blocks); DEFINE_ZONE_ALLOC_EVENT(xfs_zone_alloc_blocks); +DEFINE_ZONE_ALLOC_EVENT(xfs_zone_spurious_open); TRACE_EVENT(xfs_zone_gc_select_victim, TP_PROTO(struct xfs_rtgroup *rtg, unsigned int bucket), diff --git a/fs/xfs/xfs_zone_alloc.c b/fs/xfs/xfs_zone_alloc.c index e9f1d9d08620..5f8b6cbeebfd 100644 --- a/fs/xfs/xfs_zone_alloc.c +++ b/fs/xfs/xfs_zone_alloc.c @@ -1253,6 +1253,77 @@ xfs_report_zones( return 0; } +static inline bool +xfs_zone_is_conv( + struct xfs_rtgroup *rtg) +{ + return !bdev_zone_is_seq(rtg_mount(rtg)->m_rtdev_targp->bt_bdev, + xfs_gbno_to_daddr(rtg_group(rtg), 0)); +} + +static struct xfs_open_zone * +xfs_find_fullest_conventional_open_zone( + struct xfs_mount *mp) +{ + struct xfs_zone_info *zi = mp->m_zone_info; + struct xfs_open_zone *found = NULL, *oz; + + spin_lock(&zi->zi_open_zones_lock); + list_for_each_entry(oz, &zi->zi_open_zones, oz_entry) { + if (!xfs_zone_is_conv(oz->oz_rtg)) + continue; + if (!found || oz->oz_allocated > found->oz_allocated) + found = oz; + } + spin_unlock(&zi->zi_open_zones_lock); + + return found; +} + +/* + * Find the fullest conventional zones and remove them from the open zone pool + * until we are at the open zone limit. + * + * We can end up with spurious "open" zones when the last blocks in a fully + * written zone were invalidate as there is no write pointer for conventional + * zones. + * + * If we are still over the limit when there is no conventional open zone left, + * the user overrode the max open zones limit using the max_open_zones mount + * option we should fail. + */ +static int +xfs_finish_spurious_open_zones( + struct xfs_mount *mp, + struct xfs_init_zones *iz) +{ + struct xfs_zone_info *zi = mp->m_zone_info; + + while (zi->zi_nr_open_zones > mp->m_max_open_zones) { + struct xfs_open_zone *oz; + xfs_filblks_t adjust; + + oz = xfs_find_fullest_conventional_open_zone(mp); + if (!oz) { + xfs_err(mp, +"too many open zones for max_open_zones limit (%u/%u)", + zi->zi_nr_open_zones, mp->m_max_open_zones); + return -EINVAL; + } + + xfs_rtgroup_lock(oz->oz_rtg, XFS_RTGLOCK_RMAP); + adjust = rtg_blocks(oz->oz_rtg) - oz->oz_written; + trace_xfs_zone_spurious_open(oz, oz->oz_written, adjust); + oz->oz_written = rtg_blocks(oz->oz_rtg); + xfs_open_zone_mark_full(oz); + xfs_rtgroup_unlock(oz->oz_rtg, XFS_RTGLOCK_RMAP); + iz->available -= adjust; + iz->reclaimable += adjust; + } + + return 0; +} + int xfs_mount_zones( struct xfs_mount *mp) @@ -1294,6 +1365,10 @@ xfs_mount_zones( if (error) goto out_free_zone_info; + error = xfs_finish_spurious_open_zones(mp, &iz); + if (error) + goto out_free_zone_info; + xfs_set_freecounter(mp, XC_FREE_RTAVAILABLE, iz.available); xfs_set_freecounter(mp, XC_FREE_RTEXTENTS, iz.available + iz.reclaimable); From 29a7b2614357393b176ef06ba5bc3ff5afc8df69 Mon Sep 17 00:00:00 2001 From: Haoxiang Li Date: Wed, 1 Apr 2026 12:02:41 +0800 Subject: [PATCH 31/41] xfs: fix a resource leak in xfs_alloc_buftarg() In the error path, call fs_put_dax() to drop the DAX device reference. Fixes: 6f643c57d57c ("xfs: implement ->notify_failure() for XFS") Cc: stable@vger.kernel.org Signed-off-by: Haoxiang Li Reviewed-by: Darrick J. Wong Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_buf.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/xfs/xfs_buf.c b/fs/xfs/xfs_buf.c index ee8c3944015a..580d40a5ee57 100644 --- a/fs/xfs/xfs_buf.c +++ b/fs/xfs/xfs_buf.c @@ -1756,6 +1756,7 @@ xfs_alloc_buftarg( return btp; error_free: + fs_put_dax(btp->bt_daxdev, mp); kfree(btp); return ERR_PTR(error); } From e771da0727c13ab2cb89ee47f2edcda8b87b2c73 Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 31 Mar 2026 17:27:23 +0200 Subject: [PATCH 32/41] xfs: delay initial open of the GC zone The code currently used to select the new GC target zone when the previous one is full also handles the case where there is no current GC target zone at all. Make use of that to simplify the logic in xfs_zone_gc_mount. Signed-off-by: Christoph Hellwig Reviewed-by: Hans Holmberg Reviewed-by: Damien Le Moal Reviewed-by: Carlos Maiolino Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_gc.c | 45 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/fs/xfs/xfs_zone_gc.c b/fs/xfs/xfs_zone_gc.c index a8f71231f351..00a996c13d8a 100644 --- a/fs/xfs/xfs_zone_gc.c +++ b/fs/xfs/xfs_zone_gc.c @@ -527,7 +527,7 @@ xfs_zone_gc_select_victim( return true; } -static struct xfs_open_zone * +static int xfs_zone_gc_steal_open( struct xfs_zone_info *zi) { @@ -538,15 +538,18 @@ xfs_zone_gc_steal_open( if (!found || oz->oz_allocated < found->oz_allocated) found = oz; } - - if (found) { - found->oz_is_gc = true; - list_del_init(&found->oz_entry); - zi->zi_nr_open_zones--; + if (!found) { + spin_unlock(&zi->zi_open_zones_lock); + return -EIO; } + trace_xfs_zone_gc_target_opened(found->oz_rtg); + found->oz_is_gc = true; + list_del_init(&found->oz_entry); + zi->zi_nr_open_zones--; + zi->zi_open_gc_zone = found; spin_unlock(&zi->zi_open_zones_lock); - return found; + return 0; } static struct xfs_open_zone * @@ -1194,31 +1197,24 @@ xfs_zone_gc_mount( { struct xfs_zone_info *zi = mp->m_zone_info; struct xfs_zone_gc_data *data; - struct xfs_open_zone *oz; int error; /* - * If there are no free zones available for GC, pick the open zone with + * If there are no free zones available for GC, or the number of open + * zones has reached the open zone limit, pick the open zone with * the least used space to GC into. This should only happen after an - * unclean shutdown near ENOSPC while GC was ongoing. - * - * We also need to do this for the first gc zone allocation if we - * unmounted while at the open limit. + * unclean shutdown while GC was ongoing. Otherwise a GC zone will + * be selected from the free zone pool on demand. */ if (!xfs_group_marked(mp, XG_TYPE_RTG, XFS_RTG_FREE) || - zi->zi_nr_open_zones == mp->m_max_open_zones) - oz = xfs_zone_gc_steal_open(zi); - else - oz = xfs_open_zone(mp, WRITE_LIFE_NOT_SET, true); - if (!oz) { - xfs_warn(mp, "unable to allocate a zone for gc"); - error = -EIO; - goto out; + zi->zi_nr_open_zones >= mp->m_max_open_zones) { + error = xfs_zone_gc_steal_open(zi); + if (error) { + xfs_warn(mp, "unable to steal an open zone for gc"); + return error; + } } - trace_xfs_zone_gc_target_opened(oz->oz_rtg); - zi->zi_open_gc_zone = oz; - data = xfs_zone_gc_data_alloc(mp); if (!data) { error = -ENOMEM; @@ -1241,7 +1237,6 @@ out_free_gc_data: kfree(data); out_put_gc_zone: xfs_open_zone_put(zi->zi_open_gc_zone); -out: return error; } From c2257d9f63bbf7e1f39fb2b5585b21ea7445e18f Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 31 Mar 2026 17:27:24 +0200 Subject: [PATCH 33/41] xfs: add a separate tracepoint for stealing an open zone for GC The case where we have to reuse an already open zone warrants a different trace point vs the normal opening of a GC zone. Signed-off-by: Christoph Hellwig Reviewed-by: Hans Holmberg Reviewed-by: Damien Le Moal Reviewed-by: Carlos Maiolino Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_trace.h | 1 + fs/xfs/xfs_zone_gc.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/xfs/xfs_trace.h b/fs/xfs/xfs_trace.h index c5ad26a1d7bb..1c098cfc5c00 100644 --- a/fs/xfs/xfs_trace.h +++ b/fs/xfs/xfs_trace.h @@ -394,6 +394,7 @@ DEFINE_ZONE_EVENT(xfs_zone_full); DEFINE_ZONE_EVENT(xfs_zone_opened); DEFINE_ZONE_EVENT(xfs_zone_reset); DEFINE_ZONE_EVENT(xfs_zone_gc_target_opened); +DEFINE_ZONE_EVENT(xfs_zone_gc_target_stolen); TRACE_EVENT(xfs_zone_free_blocks, TP_PROTO(struct xfs_rtgroup *rtg, xfs_rgblock_t rgbno, diff --git a/fs/xfs/xfs_zone_gc.c b/fs/xfs/xfs_zone_gc.c index 00a996c13d8a..0c1e22409ef1 100644 --- a/fs/xfs/xfs_zone_gc.c +++ b/fs/xfs/xfs_zone_gc.c @@ -543,7 +543,7 @@ xfs_zone_gc_steal_open( return -EIO; } - trace_xfs_zone_gc_target_opened(found->oz_rtg); + trace_xfs_zone_gc_target_stolen(found->oz_rtg); found->oz_is_gc = true; list_del_init(&found->oz_entry); zi->zi_nr_open_zones--; From a99ed5dbae018627faf4ac275cb322e94606a6f9 Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 31 Mar 2026 17:27:25 +0200 Subject: [PATCH 34/41] xfs: put the open zone later xfs_open_zone_put The open zone is what holds the rtg reference for us. This doesn't matter until we support shrinking, and even then is rather theoretical because we can't shrink away a just filled zone in a tiny race window, but let's play safe here. Signed-off-by: Christoph Hellwig Reviewed-by: Hans Holmberg Reviewed-by: Damien Le Moal Reviewed-by: Carlos Maiolino Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_alloc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/xfs/xfs_zone_alloc.c b/fs/xfs/xfs_zone_alloc.c index 5f8b6cbeebfd..17a3762aa951 100644 --- a/fs/xfs/xfs_zone_alloc.c +++ b/fs/xfs/xfs_zone_alloc.c @@ -182,11 +182,11 @@ xfs_open_zone_mark_full( list_del_init(&oz->oz_entry); } spin_unlock(&zi->zi_open_zones_lock); - xfs_open_zone_put(oz); wake_up_all(&zi->zi_zone_wait); if (used < rtg_blocks(rtg)) xfs_zone_account_reclaimable(rtg, rtg_blocks(rtg) - used); + xfs_open_zone_put(oz); } static inline void From ca0170a7fa41fa4ef5e31b0baf2225d84a385bfc Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 31 Mar 2026 17:27:26 +0200 Subject: [PATCH 35/41] xfs: rename xfs_zone_gc_iter_next to xfs_zone_gc_iter_irec This function returns the current iterator position, which makes the _next postfix a bit misleading. Signed-off-by: Christoph Hellwig Reviewed-by: Hans Holmberg Reviewed-by: Damien Le Moal Reviewed-by: Carlos Maiolino Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_gc.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/xfs/xfs_zone_gc.c b/fs/xfs/xfs_zone_gc.c index 0c1e22409ef1..cd4f52af295e 100644 --- a/fs/xfs/xfs_zone_gc.c +++ b/fs/xfs/xfs_zone_gc.c @@ -374,7 +374,7 @@ done: } static bool -xfs_zone_gc_iter_next( +xfs_zone_gc_iter_irec( struct xfs_mount *mp, struct xfs_zone_gc_iter *iter, struct xfs_rmap_irec *chunk_rec, @@ -691,7 +691,7 @@ xfs_zone_gc_start_chunk( if (xfs_is_shutdown(mp)) return false; - if (!xfs_zone_gc_iter_next(mp, iter, &irec, &ip)) + if (!xfs_zone_gc_iter_irec(mp, iter, &irec, &ip)) return false; oz = xfs_zone_gc_alloc_blocks(data, &irec.rm_blockcount, &daddr, &is_seq); From 53c1c822908d9804504596486b96d8b887b0bacd Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 31 Mar 2026 17:27:27 +0200 Subject: [PATCH 36/41] xfs: refactor GC zone selection helpers Merge xfs_zone_gc_ensure_target into xfs_zone_gc_select_target to keep all zone selection code together. Signed-off-by: Christoph Hellwig Reviewed-by: Hans Holmberg Reviewed-by: Damien Le Moal Reviewed-by: Carlos Maiolino Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_gc.c | 47 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/fs/xfs/xfs_zone_gc.c b/fs/xfs/xfs_zone_gc.c index cd4f52af295e..0f62051b0acc 100644 --- a/fs/xfs/xfs_zone_gc.c +++ b/fs/xfs/xfs_zone_gc.c @@ -552,6 +552,9 @@ xfs_zone_gc_steal_open( return 0; } +/* + * Ensure we have a valid open zone to write to. + */ static struct xfs_open_zone * xfs_zone_gc_select_target( struct xfs_mount *mp) @@ -559,12 +562,25 @@ xfs_zone_gc_select_target( struct xfs_zone_info *zi = mp->m_zone_info; struct xfs_open_zone *oz = zi->zi_open_gc_zone; - /* - * We need to wait for pending writes to finish. - */ - if (oz && oz->oz_written < rtg_blocks(oz->oz_rtg)) - return NULL; + if (oz) { + /* + * If we have space available, just keep using the existing + * zone. + */ + if (oz->oz_allocated < rtg_blocks(oz->oz_rtg)) + return oz; + /* + * Wait for all writes to the current zone to finish before + * picking a new one. + */ + if (oz->oz_written < rtg_blocks(oz->oz_rtg)) + return NULL; + } + + /* + * Open a new zone when there is none currently in use. + */ ASSERT(zi->zi_nr_open_zones <= mp->m_max_open_zones - XFS_OPEN_GC_ZONES); oz = xfs_open_zone(mp, WRITE_LIFE_NOT_SET, true); @@ -576,23 +592,6 @@ xfs_zone_gc_select_target( return oz; } -/* - * Ensure we have a valid open zone to write the GC data to. - * - * If the current target zone has space keep writing to it, else first wait for - * all pending writes and then pick a new one. - */ -static struct xfs_open_zone * -xfs_zone_gc_ensure_target( - struct xfs_mount *mp) -{ - struct xfs_open_zone *oz = mp->m_zone_info->zi_open_gc_zone; - - if (!oz || oz->oz_allocated == rtg_blocks(oz->oz_rtg)) - return xfs_zone_gc_select_target(mp); - return oz; -} - static void xfs_zone_gc_end_io( struct bio *bio) @@ -615,7 +614,7 @@ xfs_zone_gc_alloc_blocks( struct xfs_mount *mp = data->mp; struct xfs_open_zone *oz; - oz = xfs_zone_gc_ensure_target(mp); + oz = xfs_zone_gc_select_target(mp); if (!oz) return NULL; @@ -1019,7 +1018,7 @@ xfs_zone_gc_should_start_new_work( if (!data->scratch_available) return false; - oz = xfs_zone_gc_ensure_target(data->mp); + oz = xfs_zone_gc_select_target(data->mp); if (!oz || oz->oz_allocated == rtg_blocks(oz->oz_rtg)) return false; From 4c1b6e03e31c5933355f25fe7fa564be3a0f931d Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 31 Mar 2026 17:27:28 +0200 Subject: [PATCH 37/41] xfs: streamline GC zone selection Currently picking of the GC target zone is a bit odd as it is done both in the main "can we start new GC cycles" routine and in the low-level block allocator for GC. This was mostly done to work around the rules for when code in a waitqueue wait loop can sleep. But with a trick to check if the process state has been set to running to discover if the wait loop has to be retried, all this becomes much simpler. We can select a GC zone just before writing, and bail out of starting new work if we can't find a usable zone. Signed-off-by: Christoph Hellwig Reviewed-by: Hans Holmberg Reviewed-by: Damien Le Moal Reviewed-by: Carlos Maiolino Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_gc.c | 95 +++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 55 deletions(-) diff --git a/fs/xfs/xfs_zone_gc.c b/fs/xfs/xfs_zone_gc.c index 0f62051b0acc..2c2fa924fecd 100644 --- a/fs/xfs/xfs_zone_gc.c +++ b/fs/xfs/xfs_zone_gc.c @@ -383,9 +383,6 @@ xfs_zone_gc_iter_irec( struct xfs_rmap_irec *irec; int error; - if (!iter->victim_rtg) - return false; - retry: if (iter->rec_idx == iter->rec_count) { error = xfs_zone_gc_query(mp, iter); @@ -555,7 +552,7 @@ xfs_zone_gc_steal_open( /* * Ensure we have a valid open zone to write to. */ -static struct xfs_open_zone * +static bool xfs_zone_gc_select_target( struct xfs_mount *mp) { @@ -568,14 +565,14 @@ xfs_zone_gc_select_target( * zone. */ if (oz->oz_allocated < rtg_blocks(oz->oz_rtg)) - return oz; + return true; /* * Wait for all writes to the current zone to finish before * picking a new one. */ if (oz->oz_written < rtg_blocks(oz->oz_rtg)) - return NULL; + return false; } /* @@ -589,7 +586,7 @@ xfs_zone_gc_select_target( spin_lock(&zi->zi_open_zones_lock); zi->zi_open_gc_zone = oz; spin_unlock(&zi->zi_open_zones_lock); - return oz; + return !!oz; } static void @@ -604,7 +601,7 @@ xfs_zone_gc_end_io( wake_up_process(data->mp->m_zone_info->zi_gc_thread); } -static struct xfs_open_zone * +static bool xfs_zone_gc_alloc_blocks( struct xfs_zone_gc_data *data, xfs_extlen_t *count_fsb, @@ -612,11 +609,7 @@ xfs_zone_gc_alloc_blocks( bool *is_seq) { struct xfs_mount *mp = data->mp; - struct xfs_open_zone *oz; - - oz = xfs_zone_gc_select_target(mp); - if (!oz) - return NULL; + struct xfs_open_zone *oz = mp->m_zone_info->zi_open_gc_zone; *count_fsb = min(*count_fsb, XFS_B_TO_FSB(mp, data->scratch_available)); @@ -638,7 +631,7 @@ xfs_zone_gc_alloc_blocks( spin_unlock(&mp->m_sb_lock); if (!*count_fsb) - return NULL; + return false; *daddr = xfs_gbno_to_daddr(rtg_group(oz->oz_rtg), 0); *is_seq = bdev_zone_is_seq(mp->m_rtdev_targp->bt_bdev, *daddr); @@ -646,7 +639,7 @@ xfs_zone_gc_alloc_blocks( *daddr += XFS_FSB_TO_BB(mp, oz->oz_allocated); oz->oz_allocated += *count_fsb; atomic_inc(&oz->oz_ref); - return oz; + return true; } static void @@ -671,6 +664,28 @@ xfs_zone_gc_add_data( } while (len); } +static bool +xfs_zone_gc_can_start_chunk( + struct xfs_zone_gc_data *data) +{ + + if (xfs_is_shutdown(data->mp)) + return false; + if (!data->scratch_available) + return false; + + if (!data->iter.victim_rtg) { + if (kthread_should_stop() || kthread_should_park()) + return false; + if (!xfs_zoned_need_gc(data->mp)) + return false; + if (!xfs_zone_gc_select_victim(data)) + return false; + } + + return xfs_zone_gc_select_target(data->mp); +} + static bool xfs_zone_gc_start_chunk( struct xfs_zone_gc_data *data) @@ -678,7 +693,6 @@ xfs_zone_gc_start_chunk( struct xfs_zone_gc_iter *iter = &data->iter; struct xfs_mount *mp = data->mp; struct block_device *bdev = mp->m_rtdev_targp->bt_bdev; - struct xfs_open_zone *oz; struct xfs_rmap_irec irec; struct xfs_gc_bio *chunk; struct xfs_inode *ip; @@ -687,14 +701,15 @@ xfs_zone_gc_start_chunk( unsigned int len; bool is_seq; - if (xfs_is_shutdown(mp)) + if (!xfs_zone_gc_can_start_chunk(data)) return false; + set_current_state(TASK_RUNNING); if (!xfs_zone_gc_iter_irec(mp, iter, &irec, &ip)) return false; - oz = xfs_zone_gc_alloc_blocks(data, &irec.rm_blockcount, &daddr, - &is_seq); - if (!oz) { + + if (!xfs_zone_gc_alloc_blocks(data, &irec.rm_blockcount, &daddr, + &is_seq)) { xfs_irele(ip); return false; } @@ -713,7 +728,7 @@ xfs_zone_gc_start_chunk( chunk->new_daddr = daddr; chunk->is_seq = is_seq; chunk->data = data; - chunk->oz = oz; + chunk->oz = mp->m_zone_info->zi_open_gc_zone; chunk->victim_rtg = iter->victim_rtg; atomic_inc(&rtg_group(chunk->victim_rtg)->xg_active_ref); atomic_inc(&chunk->victim_rtg->rtg_gccount); @@ -1007,33 +1022,6 @@ xfs_zone_gc_reset_zones( } while (next); } -static bool -xfs_zone_gc_should_start_new_work( - struct xfs_zone_gc_data *data) -{ - struct xfs_open_zone *oz; - - if (xfs_is_shutdown(data->mp)) - return false; - if (!data->scratch_available) - return false; - - oz = xfs_zone_gc_select_target(data->mp); - if (!oz || oz->oz_allocated == rtg_blocks(oz->oz_rtg)) - return false; - - if (!data->iter.victim_rtg) { - if (kthread_should_stop() || kthread_should_park()) - return false; - if (!xfs_zoned_need_gc(data->mp)) - return false; - if (!xfs_zone_gc_select_victim(data)) - return false; - } - - return true; -} - /* * Handle the work to read and write data for GC and to reset the zones, * including handling all completions. @@ -1083,13 +1071,10 @@ xfs_zone_gc_handle_work( } blk_finish_plug(&plug); - if (xfs_zone_gc_should_start_new_work(data)) { - set_current_state(TASK_RUNNING); - blk_start_plug(&plug); - while (xfs_zone_gc_start_chunk(data)) - ; - blk_finish_plug(&plug); - } + blk_start_plug(&plug); + while (xfs_zone_gc_start_chunk(data)) + ; + blk_finish_plug(&plug); } /* From 1d0d9e9459c053fc1700739e267271adccdcaadc Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 31 Mar 2026 17:27:29 +0200 Subject: [PATCH 38/41] xfs: reduce special casing for the open GC zone Currently the open zone used for garbage collection is a special snow flake, and it has been a bit annoying for some further zoned XFS work I've been doing. Remove the zi_open_gc_field and instead track the open GC zone in the zi_open_zones list together with the normal open zones, and keep an extra pointer and a reference of in the GC thread's data structure. This means anything iterating over open zones just has to look at zi_open_zones, and the life time rules are consistent. It also helps to add support for multiple open GC zones if we ever need them, and removes a bit of code. Signed-off-by: Christoph Hellwig Reviewed-by: Carlos Maiolino Reviewed-by: Hans Holmberg Reviewed-by: Damien Le Moal Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_alloc.c | 19 +++++++---- fs/xfs/xfs_zone_gc.c | 71 ++++++++++++++++++++++------------------- fs/xfs/xfs_zone_info.c | 12 +++---- fs/xfs/xfs_zone_priv.h | 15 ++------- 4 files changed, 58 insertions(+), 59 deletions(-) diff --git a/fs/xfs/xfs_zone_alloc.c b/fs/xfs/xfs_zone_alloc.c index 17a3762aa951..a851b98143c0 100644 --- a/fs/xfs/xfs_zone_alloc.c +++ b/fs/xfs/xfs_zone_alloc.c @@ -174,16 +174,18 @@ xfs_open_zone_mark_full( WRITE_ONCE(rtg->rtg_open_zone, NULL); spin_lock(&zi->zi_open_zones_lock); - if (oz->oz_is_gc) { - ASSERT(current == zi->zi_gc_thread); - zi->zi_open_gc_zone = NULL; - } else { + if (oz->oz_is_gc) + zi->zi_nr_open_gc_zones--; + else zi->zi_nr_open_zones--; - list_del_init(&oz->oz_entry); - } + list_del_init(&oz->oz_entry); spin_unlock(&zi->zi_open_zones_lock); - wake_up_all(&zi->zi_zone_wait); + if (oz->oz_is_gc) + wake_up_process(zi->zi_gc_thread); + else + wake_up_all(&zi->zi_zone_wait); + if (used < rtg_blocks(rtg)) xfs_zone_account_reclaimable(rtg, rtg_blocks(rtg) - used); xfs_open_zone_put(oz); @@ -557,6 +559,9 @@ xfs_try_use_zone( struct xfs_open_zone *oz, unsigned int goodness) { + if (oz->oz_is_gc) + return false; + if (oz->oz_allocated == rtg_blocks(oz->oz_rtg)) return false; diff --git a/fs/xfs/xfs_zone_gc.c b/fs/xfs/xfs_zone_gc.c index 2c2fa924fecd..30bcc415eaeb 100644 --- a/fs/xfs/xfs_zone_gc.c +++ b/fs/xfs/xfs_zone_gc.c @@ -125,6 +125,7 @@ struct xfs_zone_gc_iter { */ struct xfs_zone_gc_data { struct xfs_mount *mp; + struct xfs_open_zone *oz; /* bioset used to allocate the gc_bios */ struct bio_set bio_set; @@ -525,9 +526,10 @@ xfs_zone_gc_select_victim( } static int -xfs_zone_gc_steal_open( - struct xfs_zone_info *zi) +xfs_zone_gc_steal_open_zone( + struct xfs_zone_gc_data *data) { + struct xfs_zone_info *zi = data->mp->m_zone_info; struct xfs_open_zone *oz, *found = NULL; spin_lock(&zi->zi_open_zones_lock); @@ -542,10 +544,12 @@ xfs_zone_gc_steal_open( trace_xfs_zone_gc_target_stolen(found->oz_rtg); found->oz_is_gc = true; - list_del_init(&found->oz_entry); zi->zi_nr_open_zones--; - zi->zi_open_gc_zone = found; + zi->zi_nr_open_gc_zones++; spin_unlock(&zi->zi_open_zones_lock); + + atomic_inc(&found->oz_ref); + data->oz = found; return 0; } @@ -554,39 +558,43 @@ xfs_zone_gc_steal_open( */ static bool xfs_zone_gc_select_target( - struct xfs_mount *mp) + struct xfs_zone_gc_data *data) { - struct xfs_zone_info *zi = mp->m_zone_info; - struct xfs_open_zone *oz = zi->zi_open_gc_zone; + struct xfs_zone_info *zi = data->mp->m_zone_info; - if (oz) { + if (data->oz) { /* * If we have space available, just keep using the existing * zone. */ - if (oz->oz_allocated < rtg_blocks(oz->oz_rtg)) + if (data->oz->oz_allocated < rtg_blocks(data->oz->oz_rtg)) return true; /* * Wait for all writes to the current zone to finish before * picking a new one. */ - if (oz->oz_written < rtg_blocks(oz->oz_rtg)) + if (data->oz->oz_written < rtg_blocks(data->oz->oz_rtg)) return false; + + xfs_open_zone_put(data->oz); } /* * Open a new zone when there is none currently in use. */ ASSERT(zi->zi_nr_open_zones <= - mp->m_max_open_zones - XFS_OPEN_GC_ZONES); - oz = xfs_open_zone(mp, WRITE_LIFE_NOT_SET, true); - if (oz) - trace_xfs_zone_gc_target_opened(oz->oz_rtg); + data->mp->m_max_open_zones - XFS_OPEN_GC_ZONES); + data->oz = xfs_open_zone(data->mp, WRITE_LIFE_NOT_SET, true); + if (!data->oz) + return false; + trace_xfs_zone_gc_target_opened(data->oz->oz_rtg); + atomic_inc(&data->oz->oz_ref); spin_lock(&zi->zi_open_zones_lock); - zi->zi_open_gc_zone = oz; + zi->zi_nr_open_gc_zones++; + list_add_tail(&data->oz->oz_entry, &zi->zi_open_zones); spin_unlock(&zi->zi_open_zones_lock); - return !!oz; + return true; } static void @@ -609,7 +617,7 @@ xfs_zone_gc_alloc_blocks( bool *is_seq) { struct xfs_mount *mp = data->mp; - struct xfs_open_zone *oz = mp->m_zone_info->zi_open_gc_zone; + struct xfs_open_zone *oz = data->oz; *count_fsb = min(*count_fsb, XFS_B_TO_FSB(mp, data->scratch_available)); @@ -683,7 +691,7 @@ xfs_zone_gc_can_start_chunk( return false; } - return xfs_zone_gc_select_target(data->mp); + return xfs_zone_gc_select_target(data); } static bool @@ -728,7 +736,7 @@ xfs_zone_gc_start_chunk( chunk->new_daddr = daddr; chunk->is_seq = is_seq; chunk->data = data; - chunk->oz = mp->m_zone_info->zi_open_gc_zone; + chunk->oz = data->oz; chunk->victim_rtg = iter->victim_rtg; atomic_inc(&rtg_group(chunk->victim_rtg)->xg_active_ref); atomic_inc(&chunk->victim_rtg->rtg_gccount); @@ -1134,6 +1142,8 @@ xfs_zoned_gcd( } xfs_clear_zonegc_running(mp); + if (data->oz) + xfs_open_zone_put(data->oz); if (data->iter.victim_rtg) xfs_rtgroup_rele(data->iter.victim_rtg); @@ -1183,6 +1193,10 @@ xfs_zone_gc_mount( struct xfs_zone_gc_data *data; int error; + data = xfs_zone_gc_data_alloc(mp); + if (!data) + return -ENOMEM; + /* * If there are no free zones available for GC, or the number of open * zones has reached the open zone limit, pick the open zone with @@ -1192,35 +1206,30 @@ xfs_zone_gc_mount( */ if (!xfs_group_marked(mp, XG_TYPE_RTG, XFS_RTG_FREE) || zi->zi_nr_open_zones >= mp->m_max_open_zones) { - error = xfs_zone_gc_steal_open(zi); + error = xfs_zone_gc_steal_open_zone(data); if (error) { xfs_warn(mp, "unable to steal an open zone for gc"); - return error; + goto out_free_gc_data; } } - data = xfs_zone_gc_data_alloc(mp); - if (!data) { - error = -ENOMEM; - goto out_put_gc_zone; - } - zi->zi_gc_thread = kthread_create(xfs_zoned_gcd, data, "xfs-zone-gc/%s", mp->m_super->s_id); if (IS_ERR(zi->zi_gc_thread)) { xfs_warn(mp, "unable to create zone gc thread"); error = PTR_ERR(zi->zi_gc_thread); - goto out_free_gc_data; + goto out_put_oz; } /* xfs_zone_gc_start will unpark for rw mounts */ kthread_park(zi->zi_gc_thread); return 0; +out_put_oz: + if (data->oz) + xfs_open_zone_put(data->oz); out_free_gc_data: kfree(data); -out_put_gc_zone: - xfs_open_zone_put(zi->zi_open_gc_zone); return error; } @@ -1231,6 +1240,4 @@ xfs_zone_gc_unmount( struct xfs_zone_info *zi = mp->m_zone_info; kthread_stop(zi->zi_gc_thread); - if (zi->zi_open_gc_zone) - xfs_open_zone_put(zi->zi_open_gc_zone); } diff --git a/fs/xfs/xfs_zone_info.c b/fs/xfs/xfs_zone_info.c index a2af44011654..dcdc1dd206b2 100644 --- a/fs/xfs/xfs_zone_info.c +++ b/fs/xfs/xfs_zone_info.c @@ -30,11 +30,12 @@ xfs_show_open_zone( struct seq_file *m, struct xfs_open_zone *oz) { - seq_printf(m, "\t zone %d, wp %u, written %u, used %u, hint %s\n", + seq_printf(m, "\t zone %d, wp %u, written %u, used %u, hint %s %s\n", rtg_rgno(oz->oz_rtg), oz->oz_allocated, oz->oz_written, rtg_rmap(oz->oz_rtg)->i_used_blocks, - xfs_write_hint_to_str(oz->oz_write_hint)); + xfs_write_hint_to_str(oz->oz_write_hint), + oz->oz_is_gc ? "(GC)" : ""); } static void @@ -58,9 +59,8 @@ xfs_show_full_zone_used_distribution( spin_unlock(&zi->zi_used_buckets_lock); full = mp->m_sb.sb_rgcount; - if (zi->zi_open_gc_zone) - full--; full -= zi->zi_nr_open_zones; + full -= zi->zi_nr_open_gc_zones; full -= atomic_read(&zi->zi_nr_free_zones); full -= reclaimable; @@ -100,10 +100,6 @@ xfs_zoned_show_stats( seq_puts(m, "\topen zones:\n"); list_for_each_entry(oz, &zi->zi_open_zones, oz_entry) xfs_show_open_zone(m, oz); - if (zi->zi_open_gc_zone) { - seq_puts(m, "\topen gc zone:\n"); - xfs_show_open_zone(m, zi->zi_open_gc_zone); - } spin_unlock(&zi->zi_open_zones_lock); seq_puts(m, "\tused blocks distribution (fully written zones):\n"); xfs_show_full_zone_used_distribution(m, mp); diff --git a/fs/xfs/xfs_zone_priv.h b/fs/xfs/xfs_zone_priv.h index 8fbf9a52964e..fcb57506d8e6 100644 --- a/fs/xfs/xfs_zone_priv.h +++ b/fs/xfs/xfs_zone_priv.h @@ -32,11 +32,7 @@ struct xfs_open_zone { */ enum rw_hint oz_write_hint; - /* - * Is this open zone used for garbage collection? There can only be a - * single open GC zone, which is pointed to by zi_open_gc_zone in - * struct xfs_zone_info. Constant over the life time of an open zone. - */ + /* Is this open zone used for garbage collection? */ bool oz_is_gc; /* @@ -68,6 +64,7 @@ struct xfs_zone_info { spinlock_t zi_open_zones_lock; struct list_head zi_open_zones; unsigned int zi_nr_open_zones; + unsigned int zi_nr_open_gc_zones; /* * Free zone search cursor and number of free zones: @@ -81,15 +78,9 @@ struct xfs_zone_info { wait_queue_head_t zi_zone_wait; /* - * Pointer to the GC thread, and the current open zone used by GC - * (if any). - * - * zi_open_gc_zone is mostly private to the GC thread, but can be read - * for debugging from other threads, in which case zi_open_zones_lock - * must be taken to access it. + * Pointer to the GC thread. */ struct task_struct *zi_gc_thread; - struct xfs_open_zone *zi_open_gc_zone; /* * List of zones that need a reset: From 62c89988dc198efc17be0119a43ad21cf32334d6 Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 31 Mar 2026 17:27:30 +0200 Subject: [PATCH 39/41] xfs: expose the number of open zones in sysfs Add a sysfs attribute for the current number of open zones so that it can be trivially read from userspace in monitoring or testing software. Signed-off-by: Christoph Hellwig Reviewed-by: Hans Holmberg Reviewed-by: Damien Le Moal Signed-off-by: Carlos Maiolino --- Documentation/admin-guide/xfs.rst | 4 ++++ fs/xfs/xfs_sysfs.c | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/Documentation/admin-guide/xfs.rst b/Documentation/admin-guide/xfs.rst index 746ea60eed3f..acdd4b65964c 100644 --- a/Documentation/admin-guide/xfs.rst +++ b/Documentation/admin-guide/xfs.rst @@ -550,6 +550,10 @@ For zoned file systems, the following attributes are exposed in: is limited by the capabilities of the backing zoned device, file system size and the max_open_zones mount option. + nr_open_zones (Min: 0 Default: Varies Max: UINTMAX) + This read-only attribute exposes the current number of open zones + used by the file system. + zonegc_low_space (Min: 0 Default: 0 Max: 100) Define a percentage for how much of the unused space that GC should keep available for writing. A high value will reclaim more of the space diff --git a/fs/xfs/xfs_sysfs.c b/fs/xfs/xfs_sysfs.c index 4527119b2961..676777064c2d 100644 --- a/fs/xfs/xfs_sysfs.c +++ b/fs/xfs/xfs_sysfs.c @@ -13,6 +13,7 @@ #include "xfs_log.h" #include "xfs_log_priv.h" #include "xfs_mount.h" +#include "xfs_zone_priv.h" #include "xfs_zones.h" #include "xfs_zone_alloc.h" @@ -719,6 +720,17 @@ max_open_zones_show( } XFS_SYSFS_ATTR_RO(max_open_zones); +static ssize_t +nr_open_zones_show( + struct kobject *kobj, + char *buf) +{ + struct xfs_zone_info *zi = zoned_to_mp(kobj)->m_zone_info; + + return sysfs_emit(buf, "%u\n", READ_ONCE(zi->zi_nr_open_zones)); +} +XFS_SYSFS_ATTR_RO(nr_open_zones); + static ssize_t zonegc_low_space_store( struct kobject *kobj, @@ -756,6 +768,7 @@ XFS_SYSFS_ATTR_RW(zonegc_low_space); static struct attribute *xfs_zoned_attrs[] = { ATTR_LIST(max_open_zones), + ATTR_LIST(nr_open_zones), ATTR_LIST(zonegc_low_space), NULL, }; From 9de45faed34d11f1821c386ea306d9788e9a6448 Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 31 Mar 2026 17:27:31 +0200 Subject: [PATCH 40/41] xfs: untangle the open zones reporting in mountinfo Keeping a value per line makes parsing much easier, so move the maximum number of open zones into a separate line, and also add a new line for the number of open open GC zones. While that has to be either 0 or 1 currently having a value future-proofs the interface for adding more open GC zones if needed. Signed-off-by: Christoph Hellwig Reviewed-by: Hans Holmberg Reviewed-by: Damien Le Moal Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_info.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/fs/xfs/xfs_zone_info.c b/fs/xfs/xfs_zone_info.c index dcdc1dd206b2..47b475e21af8 100644 --- a/fs/xfs/xfs_zone_info.c +++ b/fs/xfs/xfs_zone_info.c @@ -95,8 +95,12 @@ xfs_zoned_show_stats( seq_printf(m, "\tfree zones: %d\n", atomic_read(&zi->zi_nr_free_zones)); spin_lock(&zi->zi_open_zones_lock); - seq_printf(m, "\tnumber of open zones: %u / %u\n", - zi->zi_nr_open_zones, mp->m_max_open_zones); + seq_printf(m, "\tmax open zones: %u\n", + mp->m_max_open_zones); + seq_printf(m, "\tnr open zones: %u\n", + zi->zi_nr_open_zones); + seq_printf(m, "\tnr open GC zones: %u\n", + zi->zi_nr_open_gc_zones); seq_puts(m, "\topen zones:\n"); list_for_each_entry(oz, &zi->zi_open_zones, oz_entry) xfs_show_open_zone(m, oz); From 2ffc6900d5c3a7cd59becda2aa67581d9bd3858e Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Mon, 6 Apr 2026 07:54:17 +0200 Subject: [PATCH 41/41] xfs: fix number of GC bvecs GC scratch allocations can wrap around and use the same buffer twice, and the current code fails to account for that. So far this worked due to rounding in the block layer, but changes to the bio allocator drop the over-provisioning and generic/256 or generic/361 will now usually fail when running against the current block tree. Simplify the allocation to always pass the maximum value that is easier to verify, as a saving of up to one bvec per allocation isn't worth the effort to verify a complicated calculated value. Fixes: 102f444b57b3 ("xfs: rework zone GC buffer management") Signed-off-by: Christoph Hellwig Reviewed-by: Damien Le Moal Reviewed-by: Hans Holmberg Signed-off-by: Carlos Maiolino --- fs/xfs/xfs_zone_gc.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/fs/xfs/xfs_zone_gc.c b/fs/xfs/xfs_zone_gc.c index 30bcc415eaeb..e7b33d5a8b3d 100644 --- a/fs/xfs/xfs_zone_gc.c +++ b/fs/xfs/xfs_zone_gc.c @@ -706,7 +706,6 @@ xfs_zone_gc_start_chunk( struct xfs_inode *ip; struct bio *bio; xfs_daddr_t daddr; - unsigned int len; bool is_seq; if (!xfs_zone_gc_can_start_chunk(data)) @@ -722,15 +721,16 @@ xfs_zone_gc_start_chunk( return false; } - len = XFS_FSB_TO_B(mp, irec.rm_blockcount); - bio = bio_alloc_bioset(bdev, - min(howmany(len, XFS_GC_BUF_SIZE) + 1, XFS_GC_NR_BUFS), - REQ_OP_READ, GFP_NOFS, &data->bio_set); - + /* + * Scratch allocation can wrap around to the same buffer again, + * provision an extra bvec for that case. + */ + bio = bio_alloc_bioset(bdev, XFS_GC_NR_BUFS + 1, REQ_OP_READ, GFP_NOFS, + &data->bio_set); chunk = container_of(bio, struct xfs_gc_bio, bio); chunk->ip = ip; chunk->offset = XFS_FSB_TO_B(mp, irec.rm_offset); - chunk->len = len; + chunk->len = XFS_FSB_TO_B(mp, irec.rm_blockcount); chunk->old_startblock = xfs_rgbno_to_rtb(iter->victim_rtg, irec.rm_startblock); chunk->new_daddr = daddr; @@ -744,8 +744,9 @@ xfs_zone_gc_start_chunk( bio->bi_iter.bi_sector = xfs_rtb_to_daddr(mp, chunk->old_startblock); bio->bi_end_io = xfs_zone_gc_end_io; xfs_zone_gc_add_data(chunk); - data->scratch_head = (data->scratch_head + len) % data->scratch_size; - data->scratch_available -= len; + data->scratch_head = + (data->scratch_head + chunk->len) % data->scratch_size; + data->scratch_available -= chunk->len; XFS_STATS_INC(mp, xs_gc_read_calls);