FS/XFS testing framework
 help / color / mirror / Atom feed
From: "Darrick J. Wong" <djwong@kernel.org>
To: Brian Foster <bfoster@redhat.com>
Cc: Matt Fleming <matt@readmodwrite.com>,
	fstests@vger.kernel.org, Zorro Lang <zlang@kernel.org>,
	linux-xfs@vger.kernel.org, Christoph Hellwig <hch@lst.de>,
	kernel-team@cloudflare.com
Subject: Re: [PATCH] xfs: Test sparse inode allocation near AGFL limits
Date: Wed, 26 Aug 2026 08:22:52 -0700	[thread overview]
Message-ID: <20260826152252.GZ6072@frogsfrogsfrogs> (raw)
In-Reply-To: <ao75xtB2oPDSr13n@bfoster>

On Wed, Aug 26, 2026 at 10:35:50AM -0400, Brian Foster wrote:
> On Thu, Jul 30, 2026 at 02:45:20PM +0100, Matt Fleming wrote:
> > From: Matt Fleming <mfleming@cloudflare.com>
> > 
> > Add a regression test for a shutdown in the sparse inode allocation
> > path. The test builds an AG with no free inodes, a full inobt leaf, full
> > bnobt/cntbt leaves, and little allocatable space outside the AG
> > reservations.
> > 
> > The final create reaches the sparse inode fallback. Removing that extent
> > from free space can grow the free-space btrees and drain the AGFL. The
> > subsequent inobt insert currently trips a corruption check and shuts
> > down the filesystem:
> > 
> >   Internal error i != 1 at line 3768 of file fs/xfs/libxfs/xfs_btree.c.
> > 
> > Link: https://lore.kernel.org/linux-xfs/20260717130429.1838767-1-matt@readmodwrite.com/
> > Signed-off-by: Matt Fleming <mfleming@cloudflare.com>
> > ---
> 
> First off thanks for the test. Whether you end up getting this included
> or not, it is very useful to have a reproducer for the prospective
> fixes.
> 
> One thing of note.. it doesn't appear able to reproduce with
> CONFIG_XFS_DEBUG enabled. I haven't dug into why, but it doesn't end up
> passing the "one full inobt leaf" stage.
> 
> >  .gitignore            |   1 +
> >  src/Makefile          |   2 +-
> >  src/xfs_inode_alloc.c | 285 +++++++++++++++++++++++++++++++++++
> >  tests/xfs/842         | 341 ++++++++++++++++++++++++++++++++++++++++++
> >  tests/xfs/842.out     |   2 +
> >  5 files changed, 630 insertions(+), 1 deletion(-)
> >  create mode 100644 src/xfs_inode_alloc.c
> >  create mode 100755 tests/xfs/842
> >  create mode 100644 tests/xfs/842.out
> > 
> > diff --git a/.gitignore b/.gitignore
> > index dd77ee30..7c92e5eb 100644
> > --- a/.gitignore
> > +++ b/.gitignore
> > @@ -185,6 +185,7 @@ tags
> >  /src/uuid_ioctl
> >  /src/writemod
> >  /src/writev_on_pagefault
> > +/src/xfs_inode_alloc
> >  /src/xfsctl
> >  /src/xfsfind
> >  /src/aio-dio-regress/aio-dio-append-write-fallocate-race
> > diff --git a/src/Makefile b/src/Makefile
> > index 31ac43b2..9e26ebd1 100644
> > --- a/src/Makefile
> > +++ b/src/Makefile
> > @@ -36,7 +36,7 @@ LINUX_TARGETS = xfsctl bstat t_mtab getdevicesize preallo_rw_pattern_reader \
> >  	fscrypt-crypt-util bulkstat_null_ocount splice-test chprojid_fail \
> >  	detached_mounts_propagation ext4_resize t_readdir_3 splice2pipe \
> >  	uuid_ioctl t_snapshot_deleted_subvolume fiemap-fault min_dio_alignment \
> > -	rw_hint fs-monitor
> > +	rw_hint fs-monitor xfs_inode_alloc
> >  
> >  EXTRA_EXECS = dmerror fill2attr fill2fs fill2fs_check scaleread.sh \
> >  	      btrfs_crc32c_forged_name.py popdir.pl popattr.py \
> > diff --git a/src/xfs_inode_alloc.c b/src/xfs_inode_alloc.c
> > new file mode 100644
> > index 00000000..9995a047
> > --- /dev/null
> > +++ b/src/xfs_inode_alloc.c
> ...
> 
> The thing that stands out most with this supporting helper program is
> that it seems to duplicate or reimplement a bunch of functionality. For
> example, the inode creation should be easy enough to do in a bash loop,

Don't use bash loops if you can avoid it, they're very slow as compared
to...

> the punch and consume commands look like they could be reduced to single
> xfs_io falloc or fpunch commands, and trigger is also just another
> simple file creation.
> 
> The fill and fragment commands look potentially more unique, but I still
> wonder if we have mechanisms that can be reused. For fragment, we have
> src/punch-alternating, which looks to me it could achieve the same basic
> results. I.e., rather than alloc and free single blocks across inodes,
> alloc a bunch of blocks to a single file and punch out every other and
> fragment free space that way. We also have _fill_fs() under
> common/populate that looks like it can consume space in general.

...src/pop{dir,attr}.pl, where you only pay the exec overhead once.

--D

> I could be missing some details, but all in all I suspect this whole
> helper program could be simplified away.
> 
> > diff --git a/tests/xfs/842 b/tests/xfs/842
> > new file mode 100755
> > index 00000000..fdab240a
> > --- /dev/null
> > +++ b/tests/xfs/842
> > @@ -0,0 +1,341 @@
> > +#! /bin/bash
> > +# SPDX-License-Identifier: GPL-2.0
> > +# Copyright (c) 2026 Cloudflare, Inc.  All Rights Reserved.
> > +#
> > +# FS QA Test No. 842
> > +#
> > +# Regression test for an inode allocation failure after a sparse inode extent
> > +# grows full free-space btrees and raises the AGFL minimum.  The subsequent
> > +# inobt leaf split must not trip the i != 1 corruption check.
> > +#
> > +. ./common/preamble
> > +_begin_fstest auto enospc punch prealloc
> > +
> > +_cleanup()
> > +{
> > +	cd /
> > +	_scratch_unmount 2>/dev/null
> > +	rm -f "$tmp".*
> > +}
> > +
> > +. ./common/filter
> > +
> > +_require_scratch
> > +_require_scratch_size $((1024 * 1024))
> > +_require_check_dmesg
> > +_require_xfs_db_command agresv
> > +_require_xfs_io_command bmap
> > +_require_xfs_io_command fsync
> > +
> > +# The state builder depends on an internal log and no realtime section.
> > +unset SCRATCH_LOGDEV
> > +unset SCRATCH_RTDEV
> > +
> > +_require_xfs_sparse_inodes
> > +_require_xfs_mkfs_finobt
> > +
> > +REPRO=$here/src/xfs_inode_alloc
> > +TARGET_ALLOCATED=16128 # 252 inode records * 64 inodes
> > +INOBT_MAXLEVELS=2
> > +FRAGMENT_FILES=1002
> > +
> > +[[ -x $REPRO ]] || _notrun "xfs_inode_alloc helper is not built"
> > +
> > +mkfs_opts="-q -K -s size=4096 \
> > +	-m crc=1,finobt=1,rmapbt=1,reflink=1,inobtcount=1,bigtime=1 \
> > +	-i size=512,sparse=1,maxpct=25,nrext64=1 \
> > +	-d size=1g,agcount=2,su=512k,sw=2"
> > +_scratch_mkfs_xfs_supported $mkfs_opts >/dev/null 2>&1 || \
> > +	_notrun "mkfs.xfs does not support the required feature set"
> > +
> > +min_value()
> > +{
> > +	if [ "$1" -lt "$2" ]; then
> > +		printf '%s\n' "$1"
> > +	else
> > +		printf '%s\n' "$2"
> > +	fi
> > +}
> > +
> > +min_freelist()
> > +{
> > +	local bno_level=$1 cnt_level=$2 rmap_level=$3
> > +	local alloc_maxlevels=$4 rmap_maxlevels=$5 has_rmap=$6
> > +	local bno_next cnt_next rmap_next total
> > +
> > +	bno_next=$(min_value $((bno_level + 1)) "$alloc_maxlevels")
> > +	cnt_next=$(min_value $((cnt_level + 1)) "$alloc_maxlevels")
> > +	total=$((bno_next * 2 - 2 + cnt_next * 2 - 2))
> > +	if [ "$has_rmap" -ne 0 ]; then
> > +		rmap_next=$(min_value $((rmap_level + 1)) "$rmap_maxlevels")
> > +		total=$((total + rmap_next * 2 - 2))
> > +	fi
> > +	printf '%s\n' "$total"
> > +}
> > +
> > +space_available()
> > +{
> > +	local freeblks=$1 flcount=$2 reservation=$3 min_free=$4 minleft=$5
> > +	local agflcount
> > +
> > +	agflcount=$(min_value "$flcount" "$min_free")
> > +	printf '%s\n' $((freeblks + agflcount - reservation - min_free - minleft))
> > +}
> > +
> > +agi_field()
> > +{
> > +	local field=$1
> > +	$XFS_DB_PROG -r -c 'agi 0' -c "p $field" "$SCRATCH_DEV" 2>/dev/null |
> > +		awk -v key="$field" '$1 == key { print $3; exit }'
> > +}
> > +
> > +inobt_root_field()
> > +{
> > +	local field=$1
> > +	$XFS_DB_PROG -r -c 'agi 0' -c 'addr root' -c "p $field" \
> > +		"$SCRATCH_DEV" 2>/dev/null |
> > +		awk -v key="$field" '$1 == key { print $3; exit }'
> > +}
> > +
> > +agf_field()
> > +{
> > +	local field=$1
> > +	$XFS_DB_PROG -r -c 'agf 0' -c "p $field" "$SCRATCH_DEV" 2>/dev/null |
> > +		awk -v key="$field" '$1 == key { print $3; exit }'
> > +}
> > +
> > +alloc_root_field()
> > +{
> > +	local root=$1 field=$2
> > +	$XFS_DB_PROG -r -c 'agf 0' -c "addr $root" -c "p $field" \
> > +		"$SCRATCH_DEV" 2>/dev/null |
> > +		awk -v key="$field" '$1 == key { print $3; exit }'
> > +}
> > +
> > +ag0_reservation_state()
> > +{
> > +	$XFS_DB_PROG -r -c agresv "$SCRATCH_DEV" 2>/dev/null |
> > +		awk '$1 == "AG" && $2 == "0:" { print $6, $8, $10; exit }'
> > +}
> 
> Seems like a lot of these helpers could be replaced with calls to
> _scratch_xfs_get_metadata_field()..?
> 
> > +
> > +free_record_covering()
> > +{
> > +	local agbno=$1
> > +	$XFS_DB_PROG -r -c 'agf 0' -c 'addr bnoroot' -c 'type bnobt' \
> > +		-c 'btdump -i' "$SCRATCH_DEV" 2>/dev/null |
> > +		awk -v target="$agbno" '/^[0-9]+:\[/ {
> > +			record = $0
> > +			sub(/^[^[]*\[/, "", record)
> > +			sub(/\].*$/, "", record)
> > +			split(record, value, /,/)
> > +			start = value[1] + 0
> > +			blocks = value[2] + 0
> > +			if (start <= target && target < start + blocks) {
> > +				print 0, start, blocks
> > +				exit
> > +			}
> > +		}'
> > +}
> > +
> > +largest_ag0_free_extent()
> > +{
> > +	$XFS_DB_PROG -r -c 'agf 0' -c 'addr bnoroot' -c 'type bnobt' \
> > +		-c 'btdump -i' "$SCRATCH_DEV" 2>/dev/null |
> > +		awk '/^[0-9]+:\[/ {
> > +			record = $0
> > +			sub(/^[^[]*\[/, "", record)
> > +			sub(/\].*$/, "", record)
> > +			split(record, value, /,/)
> > +			if (value[2] + 0 > max) {
> > +				start = value[1] + 0
> > +				max = value[2] + 0
> > +			}
> > +		} END { if (max) print start, max }'
> > +}
> > +
> > +inspect_unmounted()
> > +{
> > +	sync -f "$SCRATCH_MNT"
> > +	_scratch_unmount
> > +}
> > +
> > +mount_fs()
> > +{
> > +	_scratch_mount -o noatime,inode64,logbufs=8,logbsize=32k,noquota
> > +}
> > +
> 
> Both of the above seem kind of spurious. Could they just be replaced
> with direct calls to the underlying helpers?
> 
> > +release_ag1()
> > +{
> > +	local cutoff bytes
> > +
> > +	cutoff=$($XFS_IO_PROG -r -c 'bmap -v' "$SCRATCH_MNT/filler" |
> > +		awk '$4 == 1 {
> > +			range = $2
> > +			gsub(/[\[\]:]/, "", range)
> > +			split(range, sector, /\.\./)
> > +			printf "%.0f\n", sector[1] / 8
> > +			exit
> > +		}')
> > +	[[ -n $cutoff ]] || return 1
> > +	bytes=$((cutoff * 4096))
> > +	truncate -s "$bytes" "$SCRATCH_MNT/filler"
> > +	$XFS_IO_PROG -c fsync "$SCRATCH_MNT/filler"
> > +}
> > +
> > +align_filler_eof()
> > +{
> > +	local target=$1 cutoff current bytes
> > +
> > +	cutoff=$($XFS_IO_PROG -r -c 'bmap -v' "$SCRATCH_MNT/filler" 2>/dev/null |
> > +		awk -v target="$target" '$4 == 0 {
> > +			file_range = $2
> > +			ag_range = $5
> > +			gsub(/[\[\]:]/, "", file_range)
> > +			gsub(/[()]/, "", ag_range)
> > +			split(file_range, file_sector, /\.\./)
> > +			split(ag_range, ag_sector, /\.\./)
> > +			if (int(ag_sector[2] / 8) + 1 == target) {
> > +				printf "%.0f\n", (file_sector[2] + 1) / 8
> > +				exit
> > +			}
> > +		}')
> > +	[[ -n $cutoff ]] || return 1
> > +	current=$(( $(_get_filesize "$SCRATCH_MNT/filler") / 4096 ))
> > +	if (( cutoff < current )); then
> > +		bytes=$((cutoff * 4096))
> > +		truncate -s "$bytes" "$SCRATCH_MNT/filler"
> > +		$XFS_IO_PROG -c fsync "$SCRATCH_MNT/filler"
> > +	fi
> > +}
> > +
> > +find_trigger_hole_offset()
> > +{
> > +	$XFS_IO_PROG -r -c 'bmap -v' "$SCRATCH_MNT/filler" |
> > +		awk '$4 == 0 {
> > +			file_range = $2
> > +			ag_range = $5
> > +			gsub(/[\[\]:]/, "", file_range)
> > +			gsub(/[()]/, "", ag_range)
> > +			split(file_range, f, /\.\./)
> > +			split(ag_range, a, /\.\./)
> > +			file_start = f[1] / 8
> > +			ag_start = a[1] / 8
> > +			map_blocks = (f[2] - f[1] + 1) / 8
> > +			delta = (2 - (ag_start % 8) + 8) % 8
> > +			if (map_blocks >= delta + 16) {
> > +				printf "%.0f %.0f\n", file_start + delta, ag_start + delta
> > +				exit
> > +			}
> > +		}'
> > +}
> > +
> > +consume_ag0_headroom()
> > +{
> > +	local values free reserved used outside
> > +
> > +	values=$(ag0_reservation_state)
> > +	read -r free reserved used <<<"$values"
> > +	outside=$((free - (reserved - used)))
> > +	(( outside <= 0 )) && return 0
> > +
> > +	mount_fs
> > +	"$REPRO" consume "$SCRATCH_MNT/filler" "$outside" >>"$seqres.full" 2>&1 || true
> > +	inspect_unmounted
> > +}
> > +
> > +_scratch_mkfs_xfs $mkfs_opts >>"$seqres.full" 2>&1
> > +mount_fs
> > +: > "$SCRATCH_MNT/filler"
> > +inspect_unmounted
> > +
> > +count=$(agi_field count)
> > +freecount=$(agi_field freecount)
> > +allocated=$((count - freecount))
> > +need=$((TARGET_ALLOCATED - allocated))
> > +(( need > 0 )) || _fail "invalid initial inode state"
> > +
> 
> This check doesn't seem all that useful.. verify we need to allocate
> inodes on a newly formatted fs..?
> 
> > +mount_fs
> > +"$REPRO" create "$SCRATCH_MNT" "$need" >>"$seqres.full" 2>&1 || \
> > +	_fail "inode population failed"
> > +inspect_unmounted
> > +
> > +count=$(agi_field count)
> > +freecount=$(agi_field freecount)
> > +inobt_level=$(inobt_root_field level)
> > +inobt_recs=$(inobt_root_field numrecs)
> > +if (( count != TARGET_ALLOCATED || freecount != 0 ||
> > +      inobt_level != 0 || inobt_recs != 252 )); then
> > +	_fail "failed to build one full inobt leaf"
> > +fi
> > +
> 
> Something I'm curious about in general is whether this can be made more
> generic beyond creating/expecting a very specific geometry and
> allocation state, but I understand this is quite hard to reproduce and
> may not be possible.
> 
> That aside, I think some brief comments for some of the helpers used
> below would be helpful to review/maintain this sort of test long term.
> I.e., things like release_ag1, consume_ag0_headroom, etc. aren't very
> self explanatory.
> 
> > +mount_fs
> > +"$REPRO" fragment "$SCRATCH_MNT" "$FRAGMENT_FILES" >>"$seqres.full" 2>&1 || \
> > +	_fail "free-space fragmentation failed"
> > +"$REPRO" fill "$SCRATCH_MNT/filler" >>"$seqres.full" 2>&1 || \
> > +	_fail "free-space fill failed"
> > +release_ag1 || _fail "failed to release AG1 filler mappings"
> > +inspect_unmounted
> > +
> > +consume_ag0_headroom
> > +read -r largest_start _ <<<"$(largest_ag0_free_extent)"
> > +mount_fs
> > +align_filler_eof "$largest_start" || _fail "cannot align filler EOF"
> > +inspect_unmounted
> > +consume_ag0_headroom
> > +
> > +mount_fs
> > +read -r file_offset agblock <<<"$(find_trigger_hole_offset)"
> > +[[ -n $file_offset && -n $agblock ]] || _fail "no suitable sparse inode extent"
> > +"$REPRO" punch "$SCRATCH_MNT/filler" "$file_offset" 8 >>"$seqres.full" 2>&1 || \
> > +	_fail "failed to create sparse inode extent"
> > +inspect_unmounted
> > +
> > +read -r free reserved used <<<"$(ag0_reservation_state)"
> > +reservation=$((reserved - used))
> > +outside=$((free - reservation))
> > +bno_level=$(agf_field bnolevel)
> > +cnt_level=$(agf_field cntlevel)
> > +rmap_level=$(agf_field rmaplevel)
> > +flcount=$(agf_field flcount)
> > +bno_recs=$(alloc_root_field bnoroot numrecs)
> > +cnt_recs=$(alloc_root_field cntroot numrecs)
> > +inobt_level=$(inobt_root_field level)
> > +inobt_recs=$(inobt_root_field numrecs)
> > +minfree=$(min_freelist "$bno_level" "$cnt_level" "$rmap_level" 5 5 1)
> > +pagf_free=$((free - flcount))
> > +available=$(space_available "$pagf_free" "$flcount" "$reservation" \
> > +	"$minfree" "$INOBT_MAXLEVELS")
> > +cover=$(free_record_covering "$agblock")
> > +
> > +printf 'gate free=%d reservation=%d outside=%d fl=%d minfree=%d available=%d levels=%d/%d/%d alloc_recs=%d/%d inobt=%d/%d hole=%d record="%s"\n' \
> > +	"$free" "$reservation" "$outside" "$flcount" "$minfree" \
> > +	"$available" "$bno_level" "$cnt_level" "$rmap_level" \
> > +	"$bno_recs" "$cnt_recs" "$inobt_level" "$inobt_recs" \
> > +	"$agblock" "$cover" >>"$seqres.full"
> > +
> > +if (( available < 7 || available >= 15 || bno_level != 1 ||
> > +      cnt_level != 1 || bno_recs != 505 || cnt_recs != 505 ||
> > +      inobt_level != 0 || inobt_recs != 252 || flcount != minfree )) ||
> > +   [[ $cover != "0 $agblock 8" ]]; then
> > +	_fail "combined inode allocation precondition not met"
> > +fi
> 
> This is kind of where I wonder if we've made this so specific and
> targeted that while it works right now, some future change to allocation
> behavior, geometry, etc. could render it ineffective long term. This
> somewhat relates to the question of why this doesn't seem to work for
> XFS_DEBUG.
> 
> Also FWIW it might not be worth having these various setup condition
> failures trigger an actual test failure like this. Perhaps we could
> replace some or most of these with _notrun?
> 
> Getting back to sparse inodes in general, it looks like we only really
> have one other test in xfs/076, and that looks like a basic
> functionality test. It fills up the fs, punches out fragments of free
> space, then confirms we can allocate inodes into the fragmented space.
> Clearly that hasn't been sufficient to reproduce this insert failure
> problem, even though it repeatedly runs into ENOSPC. It would be great
> IMO if we could come up with something generic to better test these
> boundary conditions, even if it wasn't an immediate reproducer. Hmm..
> maybe there's something we could do in kernel (i.e. debug mode or with
> injection knobs) that would help produce more AGFL and/or allocbt
> split/merge activity at < max levels or something. More thought
> required...
> 
> Anyways, I'm not opposed to having a super specific test like this on
> its own. I do think this will probably require a few rounds of
> significant simplification and whatnot though to make it a bit more
> palatable for upstream merge. Thanks.
> 
> Brian
> 
> > +
> > +mount_fs
> > +set +e
> > +"$REPRO" trigger "$SCRATCH_MNT" >>"$seqres.full" 2>&1
> > +trigger_status=$?
> > +set -e
> > +sleep 1
> > +
> > +if _check_dmesg_for 'Internal error i != 1|invalid sparse inode record|Shutting down filesystem'; then
> > +	_fail "sparse inode allocation shut down the filesystem"
> > +fi
> > +(( trigger_status == 0 )) || _fail "inode allocation failed with an unexpected error"
> > +
> > +# Verify that the filesystem remains usable after the allocation attempt.
> > +$XFS_IO_PROG -c stat "$SCRATCH_MNT" >>"$seqres.full" 2>&1 || \
> > +	_fail "filesystem is not usable after inode allocation"
> > +
> > +echo Silence is golden
> > +status=0
> > +exit
> > diff --git a/tests/xfs/842.out b/tests/xfs/842.out
> > new file mode 100644
> > index 00000000..643d0528
> > --- /dev/null
> > +++ b/tests/xfs/842.out
> > @@ -0,0 +1,2 @@
> > +QA output created by 842
> > +Silence is golden
> > -- 
> > 2.43.0
> > 
> 
> 

      reply	other threads:[~2026-08-26 15:22 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-30 13:45 [PATCH] xfs: Test sparse inode allocation near AGFL limits Matt Fleming
2026-08-26 14:35 ` Brian Foster
2026-08-26 15:22   ` Darrick J. Wong [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260826152252.GZ6072@frogsfrogsfrogs \
    --to=djwong@kernel.org \
    --cc=bfoster@redhat.com \
    --cc=fstests@vger.kernel.org \
    --cc=hch@lst.de \
    --cc=kernel-team@cloudflare.com \
    --cc=linux-xfs@vger.kernel.org \
    --cc=matt@readmodwrite.com \
    --cc=zlang@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox