* [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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox