All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH] xfs: Test sparse inode allocation near AGFL limits
@ 2026-07-30 13:45 Matt Fleming
  0 siblings, 0 replies; only message in thread
From: Matt Fleming @ 2026-07-30 13:45 UTC (permalink / raw)
  To: fstests
  Cc: Zorro Lang, Darrick J. Wong, linux-xfs, Brian Foster,
	Christoph Hellwig, kernel-team

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>
---
 .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
@@ -0,0 +1,285 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/falloc.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#define BLOCK_SIZE 4096ULL
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
+
+static void usage(const char *prog)
+{
+	fprintf(stderr,
+		"usage: %s --selftest\n"
+		"       %s create DIR COUNT\n"
+		"       %s fill FILE\n"
+		"       %s consume FILE BLOCKS\n"
+		"       %s fragment DIR COUNT\n"
+		"       %s punch FILE OFFSET_BLOCKS LENGTH_BLOCKS\n"
+		"       %s trigger DIR\n",
+		prog, prog, prog, prog, prog, prog, prog);
+}
+
+static unsigned long long parse_ull(const char *value, const char *what)
+{
+	char *end;
+	unsigned long long result;
+
+	errno = 0;
+	result = strtoull(value, &end, 0);
+	if (errno || *value == '\0' || *end != '\0') {
+		fprintf(stderr, "invalid %s: %s\n", what, value);
+		exit(EXIT_FAILURE);
+	}
+	return result;
+}
+
+static int create_files(const char *dir, unsigned long long count)
+{
+	char path[4096];
+	unsigned long long i;
+
+	for (i = 0; i < count; i++) {
+		int fd;
+		int len = snprintf(path, sizeof(path), "%s/inode-%08llu", dir, i);
+
+		if (len < 0 || (size_t)len >= sizeof(path)) {
+			fprintf(stderr, "path too long\n");
+			return 1;
+		}
+		fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
+		if (fd < 0) {
+			fprintf(stderr, "create %s failed after %llu files: %s\n",
+				path, i, strerror(errno));
+			return 1;
+		}
+		close(fd);
+		if ((i + 1) % 1024 == 0)
+			printf("repro: created %llu/%llu files\n", i + 1, count);
+	}
+	printf("repro: created %llu files\n", count);
+	return 0;
+}
+
+static int fill_file(const char *path)
+{
+	static const unsigned long long chunks[] = {
+		256ULL << 20, 64ULL << 20, 16ULL << 20, 4ULL << 20,
+		1ULL << 20, 256ULL << 10, 64ULL << 10, 16ULL << 10,
+		BLOCK_SIZE,
+	};
+	struct stat st;
+	off_t offset;
+	int fd;
+	size_t i;
+
+	fd = open(path, O_RDWR | O_CREAT | O_CLOEXEC, 0600);
+	if (fd < 0) {
+		perror("open filler");
+		return 1;
+	}
+	if (fstat(fd, &st) < 0) {
+		perror("fstat filler");
+		close(fd);
+		return 1;
+	}
+	offset = st.st_size;
+
+	for (i = 0; i < ARRAY_SIZE(chunks); i++) {
+		unsigned long long allocated = 0;
+		unsigned long long chunk = chunks[i];
+
+		for (;;) {
+			if (fallocate(fd, 0, offset, (off_t)chunk) == 0) {
+				offset += (off_t)chunk;
+				allocated += chunk;
+				if (allocated >= (256ULL << 20)) {
+					printf("repro: filler size=%" PRIu64 " MiB\n",
+						(uint64_t)offset >> 20);
+					allocated = 0;
+				}
+				continue;
+			}
+			if (errno != ENOSPC) {
+				fprintf(stderr, "fallocate %llu bytes at %jd: %s\n",
+					chunk, (intmax_t)offset, strerror(errno));
+				close(fd);
+				return 1;
+			}
+			break;
+		}
+		printf("repro: allocation stopped at chunk=%llu bytes, size=%" PRIu64
+		       " MiB\n", chunk, (uint64_t)offset >> 20);
+	}
+	if (fsync(fd) < 0)
+		perror("fsync filler");
+	close(fd);
+	return 0;
+}
+
+static int fragment_free_space(const char *dir, unsigned long long count)
+{
+	char path[4096];
+	unsigned long long i;
+	int dirfd;
+
+	for (i = 0; i < count; i++) {
+		int fd;
+
+		snprintf(path, sizeof(path), "%s/inode-%08llu", dir, i);
+		fd = open(path, O_RDWR | O_CLOEXEC);
+		if (fd < 0 || fallocate(fd, 0, 0, BLOCK_SIZE) < 0) {
+			fprintf(stderr, "fragment allocation %s failed: %s\n",
+				path, strerror(errno));
+			if (fd >= 0)
+				close(fd);
+			return 1;
+		}
+		close(fd);
+		if ((i + 1) % 2048 == 0)
+			printf("repro: allocated fragment blocks %llu/%llu\n",
+			       i + 1, count);
+	}
+
+	for (i = 0; i < count; i += 2) {
+		int fd;
+
+		snprintf(path, sizeof(path), "%s/inode-%08llu", dir, i);
+		fd = open(path, O_RDWR | O_CLOEXEC);
+		if (fd < 0 || fallocate(fd, FALLOC_FL_PUNCH_HOLE |
+					      FALLOC_FL_KEEP_SIZE,
+					      0, BLOCK_SIZE) < 0) {
+			fprintf(stderr, "fragment punch %s failed: %s\n",
+				path, strerror(errno));
+			if (fd >= 0)
+				close(fd);
+			return 1;
+		}
+		close(fd);
+	}
+
+	dirfd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+	if (dirfd >= 0) {
+		syncfs(dirfd);
+		close(dirfd);
+	}
+	printf("repro: created %llu one-block free-space fragments\n",
+	       (count + 1) / 2);
+	return 0;
+}
+
+static int consume_blocks(const char *path, unsigned long long blocks)
+{
+	struct stat st;
+	unsigned long long i;
+	off_t offset;
+	int fd = open(path, O_RDWR | O_CLOEXEC);
+
+	if (fd < 0 || fstat(fd, &st) < 0) {
+		perror("open consumer file");
+		if (fd >= 0)
+			close(fd);
+		return 1;
+	}
+	offset = st.st_size;
+	for (i = 0; i < blocks; i++) {
+		if (fallocate(fd, 0, offset, BLOCK_SIZE) < 0) {
+			fprintf(stderr, "one-block allocation failed at %llu/%llu: %s\n",
+				i, blocks, strerror(errno));
+			close(fd);
+			return 1;
+		}
+		offset += BLOCK_SIZE;
+		if ((i + 1) % 512 == 0)
+			printf("repro: consumed %llu/%llu blocks\n", i + 1, blocks);
+	}
+	fsync(fd);
+	close(fd);
+	printf("repro: consumed %llu blocks\n", blocks);
+	return 0;
+}
+
+static int punch_file(const char *path, unsigned long long offset_blocks,
+		      unsigned long long length_blocks)
+{
+	off_t offset = (off_t)(offset_blocks * BLOCK_SIZE);
+	off_t length = (off_t)(length_blocks * BLOCK_SIZE);
+	int fd = open(path, O_RDWR | O_CLOEXEC);
+
+	if (fd < 0) {
+		perror("open filler for punch");
+		return 1;
+	}
+	if (fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
+		      offset, length) < 0) {
+		perror("punch hole");
+		close(fd);
+		return 1;
+	}
+	if (fsync(fd) < 0)
+		perror("fsync punched filler");
+	close(fd);
+	printf("repro: punched %llu blocks at file block %llu\n",
+	       length_blocks, offset_blocks);
+	return 0;
+}
+
+static int trigger_create(const char *dir)
+{
+	char path[4096];
+	int fd;
+
+	if (prctl(PR_SET_NAME, "xfs-ialloc-test", 0, 0, 0) < 0) {
+		perror("prctl PR_SET_NAME");
+		return 1;
+	}
+	if (snprintf(path, sizeof(path), "%s/trigger", dir) >= (int)sizeof(path)) {
+		fprintf(stderr, "trigger path too long\n");
+		return 1;
+	}
+	fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
+	if (fd < 0) {
+		printf("trigger create failed: errno=%d (%s)\n",
+		       errno, strerror(errno));
+		/* Clean ENOSPC is an acceptable result after the bug is fixed. */
+		return errno == ENOSPC ? 0 : 1;
+	}
+	close(fd);
+	printf("trigger create succeeded\n");
+	return 0;
+}
+
+int main(int argc, char **argv)
+{
+	setvbuf(stdout, NULL, _IOLBF, 0);
+
+	if (argc == 2 && strcmp(argv[1], "--selftest") == 0) {
+		puts("selftest: ok");
+		return 0;
+	}
+	if (argc == 4 && strcmp(argv[1], "create") == 0)
+		return create_files(argv[2], parse_ull(argv[3], "file count"));
+	if (argc == 3 && strcmp(argv[1], "fill") == 0)
+		return fill_file(argv[2]);
+	if (argc == 4 && strcmp(argv[1], "consume") == 0)
+		return consume_blocks(argv[2], parse_ull(argv[3], "block count"));
+	if (argc == 4 && strcmp(argv[1], "fragment") == 0)
+		return fragment_free_space(argv[2],
+					   parse_ull(argv[3], "file count"));
+	if (argc == 5 && strcmp(argv[1], "punch") == 0)
+		return punch_file(argv[2], parse_ull(argv[3], "hole offset"),
+				  parse_ull(argv[4], "hole length"));
+	if (argc == 3 && strcmp(argv[1], "trigger") == 0)
+		return trigger_create(argv[2]);
+
+	usage(argv[0]);
+	return 2;
+}
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 }'
+}
+
+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
+}
+
+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"
+
+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
+
+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
+
+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


^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-07-30 13:45 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-30 13:45 [PATCH] xfs: Test sparse inode allocation near AGFL limits Matt Fleming

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.