Linux EXT4 FS development
 help / color / mirror / Atom feed
From: "Theodore Ts'o" <tytso@mit.edu>
To: Ext4 Developers List <linux-ext4@vger.kernel.org>
Cc: "Theodore Ts'o" <tytso@mit.edu>
Subject: [PATCH 11/27] ext4: add KUnit tests for fs/ext4/ialloc.c
Date: Wed, 11 Nov 2026 23:06:22 -0500	[thread overview]
Message-ID: <20261112040638.1576836-12-tytso@mit.edu> (raw)
In-Reply-To: <20261112040638.1576836-1-tytso@mit.edu>

Add the ext4_ialloc_test KUnit test suite in fs/ext4/ialloc-test.c (28
test cases, 33 subtests total) to comprehensively exercise inode bitmap
management, group selection heuristics, inode allocation, freeing,
orphan lookup, and lazy inode table initialization in fs/ext4/ialloc.c
using an in-memory VFS superblock, mock block device, and deterministic
block pool without requiring a mounted block device:

- Bitmap padding and asynchronous read completion helpers
  (ext4_mark_bitmap_end and ext4_end_bitmap_read) across unaligned and
  byte-aligned start/end bit boundaries, partial single-byte updates,
  no-op guards, BLK_STS_OK completion, and BLK_STS_IOERR / BIO_QUIET
  failure handling.
- Inode bitmap validation and loading (ext4_validate_inode_bitmap and
  ext4_read_inode_bitmap), covering CRC32C verification, out-of-group
  metadata block overlap detection, EXT4_FC_REPLAY and buffer_verified
  short-circuits, cache hits vs. synchronous disk reads, lazy
  EXT4_BG_INODE_UNINIT bitmap initialization, and fault injection across
  ext4_get_group_desc, ext4_sb_getblk, and ext4_read_bh.
- Inode freeing (ext4_free_inode) across regular files and directories,
  bitmap bit clearing, group descriptor, flex_bg, and percpu counter
  updates, CRC32C recomputation, guard rejections (NULL i_sb, i_count >
  1, i_nlink > 0, reserved ino < EXT4_FIRST_INO, and out-of-range ino),
  double-free corruption marking, pre-corrupt bitmap skips (vs.
  EXT4_FC_REPLAY), and journal get_write_access error handling.
- Orlov directory group allocator (find_group_orlov) across root and
  EXT4_INODE_TOPDIR spreading (hashed qstr and NULL qstr), subdirectory
  co-location, i_last_alloc_group hints, max_dirs / min_inodes /
  min_clusters thresholds, flex_bg constituent group packing, and
  two-pass linear fallback when percpu counters are inflated or all
  groups are exhausted.
- Non-directory group allocator (find_group_other) with and without
  flex_bg, covering parent group preference, last-allocated group
  spreading, quadratic hash probing, cyclic linear search, and modulo
  wraparound.
- Recently-deleted inode buffer-cache checks (recently_deleted) across
  EXT4_BG_INODE_UNINIT, bg_itable_unused bounds, uncached or
  non-uptodate inode table blocks, zero i_dtime, and clean (60s) vs.
  dirty (360s) buffer_dirty() recency windows, plus find_inode_bit
  scanning across non-journaled skipping, journaled (s_journal) bypass,
  and recently_deleted_ino fallback.
- On-disk inode reservation during fast-commit replay
  (ext4_mark_inode_used) across uninitialized EXT4_BG_INODE_UNINIT and
  EXT4_BG_BLOCK_UNINIT groups, overhead cluster calculation,
  bg_itable_unused updates, idempotency on already-set bits, and invalid
  ino, inode/block bitmap read and wait, dirty-metadata, and
  sync_dirty_buffer error paths.
- Extended attribute transaction credit estimation
  (ext4_xattr_credits_for_new_inode) across POSIX default ACL
  inheritance, security xattrs, and encryption contexts.
- Full new-inode allocation (__ext4_new_inode) across regular files,
  directories, symlinks, and special files, flag inheritance and masking
  (EXT4_FL_INHERITED and ext4_mask_flags), EXT4_EA_INODE_FL inline-data
  suppression, UID/GID ownership overrides, SGID inheritance,
  EXT4_INODE_PROJINHERIT project ID propagation, explicit goal and
  s_inode_goal targeting, uninitialized group setup, corrupt/reserved
  bit skipping, EXT4_FC_REPLAY goal allocation,
  ext4_new_inode_start_handle, and error paths (-EPERM, emergency
  -EIO/-EROFS, -ENOMEM, -ENOSPC, duplicate inode hash -EIO,
  write-access and dirty-metadata failures, and ext4_init_security /
  ext4_mark_inode_dirty rollback).
- Orphan inode lookup and validation (ext4_orphan_get), group free-inode
  and used-directory summation (ext4_count_free_inodes and
  ext4_count_dirs, including 64-bit high-16-bit fields), and lazy inode
  table zeroing (ext4_init_inode_table) across already-zeroed, uninit,
  partially used, full, bogus bg_itable_unused, and zeroout error cases.

Route buffer-cache lookups in ialloc.c through ext4_sb_getblk() and
ext4_sb_find_get_block(), extract ext4_itable_zeroout() with
KUNIT_STATIC_STUB_REDIRECT, add KUNIT_STATIC_STUB_REDIRECT hooks in
ext4_read_bh(), __ext4_journal_get_write_access(), and
ext4_init_security(), and export the tested entry points and
static-helper wrappers with EXPORT_SYMBOL_FOR_EXT4_TEST.

Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
 fs/ext4/Makefile         |    2 +-
 fs/ext4/bitmap.c         |    5 +
 fs/ext4/ext4.h           |   21 +
 fs/ext4/ext4_jbd2.c      |    4 +
 fs/ext4/ialloc-test.c    | 2714 ++++++++++++++++++++++++++++++++++++++
 fs/ext4/ialloc.c         |   85 +-
 fs/ext4/super.c          |   17 +
 fs/ext4/xattr_security.c |    4 +
 8 files changed, 2844 insertions(+), 8 deletions(-)
 create mode 100644 fs/ext4/ialloc-test.c

diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 7bd91839c3ce..72198aff5b45 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -18,7 +18,7 @@ ext4-test-objs				+= inode-test.o mballoc-test.o \
 					   extents-test.o hash-test.o \
 					   extents_status-test.o xattr-test.o \
 					   namei-test.o inline-test.o \
-					   fast_commit-test.o
+					   fast_commit-test.o ialloc-test.o
 obj-$(CONFIG_EXT4_KUNIT_TESTS)		+= ext4-test.o
 ext4-$(CONFIG_FS_VERITY)		+= verity.o
 ext4-$(CONFIG_FS_ENCRYPTION)		+= crypto.o
diff --git a/fs/ext4/bitmap.c b/fs/ext4/bitmap.c
index 87760fabdd2e..f9c22427891f 100644
--- a/fs/ext4/bitmap.c
+++ b/fs/ext4/bitmap.c
@@ -15,6 +15,7 @@ unsigned int ext4_count_free(char *bitmap, unsigned int numchars)
 {
 	return numchars * BITS_PER_BYTE - memweight(bitmap, numchars);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_count_free);
 
 int ext4_inode_bitmap_csum_verify(struct super_block *sb,
 				  struct ext4_group_desc *gdp,
@@ -39,6 +40,7 @@ int ext4_inode_bitmap_csum_verify(struct super_block *sb,
 
 	return provided == calculated;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_bitmap_csum_verify);
 
 void ext4_inode_bitmap_csum_set(struct super_block *sb,
 				struct ext4_group_desc *gdp,
@@ -57,6 +59,7 @@ void ext4_inode_bitmap_csum_set(struct super_block *sb,
 	if (sbi->s_desc_size >= EXT4_BG_INODE_BITMAP_CSUM_HI_END)
 		gdp->bg_inode_bitmap_csum_hi = cpu_to_le16(csum >> 16);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_bitmap_csum_set);
 
 int ext4_block_bitmap_csum_verify(struct super_block *sb,
 				  struct ext4_group_desc *gdp,
@@ -80,6 +83,7 @@ int ext4_block_bitmap_csum_verify(struct super_block *sb,
 
 	return provided == calculated;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_bitmap_csum_verify);
 
 void ext4_block_bitmap_csum_set(struct super_block *sb,
 				struct ext4_group_desc *gdp,
@@ -97,3 +101,4 @@ void ext4_block_bitmap_csum_set(struct super_block *sb,
 	if (sbi->s_desc_size >= EXT4_BG_BLOCK_BITMAP_CSUM_HI_END)
 		gdp->bg_block_bitmap_csum_hi = cpu_to_le16(csum >> 16);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_bitmap_csum_set);
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index c4f685704031..9262ff021bda 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3065,9 +3065,30 @@ extern struct inode * ext4_orphan_get(struct super_block *, unsigned long);
 extern unsigned long ext4_count_free_inodes(struct super_block *);
 extern unsigned long ext4_count_dirs(struct super_block *);
 extern void ext4_mark_bitmap_end(int start_bit, int end_bit, char *bitmap);
+int ext4_itable_zeroout(struct super_block *sb, ext4_fsblk_t blk, int num,
+			int barrier);
 extern int ext4_init_inode_table(struct super_block *sb,
 				 ext4_group_t group, int barrier);
 void ext4_end_bitmap_read(struct bio *bio);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+int ext4_validate_inode_bitmap_test(struct super_block *sb,
+				    struct ext4_group_desc *desc,
+				    ext4_group_t block_group,
+				    struct buffer_head *bh);
+struct buffer_head *ext4_read_inode_bitmap_test(struct super_block *sb,
+						ext4_group_t block_group);
+int ext4_find_group_orlov_test(struct super_block *sb, struct inode *parent,
+			       ext4_group_t *group, umode_t mode,
+			       const struct qstr *qstr);
+int ext4_find_group_other_test(struct super_block *sb, struct inode *parent,
+			       ext4_group_t *group, umode_t mode);
+int ext4_recently_deleted_test(struct super_block *sb, ext4_group_t group,
+			       int ino);
+int ext4_find_inode_bit_test(struct super_block *sb, ext4_group_t group,
+			     struct buffer_head *bitmap, unsigned long *ino);
+int ext4_xattr_credits_for_new_inode_test(struct inode *dir, mode_t mode,
+					  bool encrypt);
+#endif
 
 /* fast_commit.c */
 int ext4_fc_get_buf(journal_t *journal, struct buffer_head **bh);
diff --git a/fs/ext4/ext4_jbd2.c b/fs/ext4/ext4_jbd2.c
index f18a01e6fd13..fa65a6377ca0 100644
--- a/fs/ext4/ext4_jbd2.c
+++ b/fs/ext4/ext4_jbd2.c
@@ -248,6 +248,9 @@ int __ext4_journal_get_write_access(const char *where, unsigned int line,
 {
 	int err;
 
+	KUNIT_STATIC_STUB_REDIRECT(__ext4_journal_get_write_access, where, line,
+				   handle, sb, bh, trigger_type);
+
 	might_sleep();
 
 	if (ext4_handle_valid(handle)) {
@@ -267,6 +270,7 @@ int __ext4_journal_get_write_access(const char *where, unsigned int line,
 		&EXT4_SB(sb)->s_journal_triggers[trigger_type].tr_triggers);
 	return 0;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_journal_get_write_access);
 
 /*
  * The ext4 forget function must perform a revoke if we are freeing data
diff --git a/fs/ext4/ialloc-test.c b/fs/ext4/ialloc-test.c
new file mode 100644
index 000000000000..a3066b788ce6
--- /dev/null
+++ b/fs/ext4/ialloc-test.c
@@ -0,0 +1,2714 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/ialloc.c
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/bio.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/iversion.h>
+#include <linux/jbd2.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/pagemap.h>
+#include <linux/posix_acl.h>
+#include <linux/quotaops.h>
+#include <linux/rcupdate.h>
+#include <linux/slab.h>
+#include <linux/stat.h>
+#include <linux/timekeeping.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "ext4_extents.h"
+#include "xattr.h"
+#include "acl.h"
+
+/*
+ * Multi-group mock filesystem geometry:
+ *  - 4 block groups, 1024-byte blocks (8192 bits per bitmap block)
+ *  - 64 inodes per group (256 total inodes), 256-byte inodes (4 per block)
+ *  - 16 inode table blocks per group, 1024 blocks (clusters) per group
+ *  - s_first_data_block = 1, s_first_ino = 11 (inodes 1..10 reserved in bg 0)
+ *  - sparse_super enabled: groups 0, 1, 3 have backup super + 1 gdb (2 base
+ *    clusters), while group 2 has 0 base clusters (18 total overhead clusters,
+ *    leaving 1024 - 18 = 1006 free clusters after init).
+ */
+#define IALLOC_TEST_NGROUPS		4U
+#define IALLOC_TEST_BLK_SIZE		1024U
+#define IALLOC_TEST_BLK_BITS		10U
+#define IALLOC_TEST_BLOCKS_PER_GRP	1024U
+#define IALLOC_TEST_FIRST_DATA_BLK	1U
+#define IALLOC_TEST_TOTAL_BLKS		(IALLOC_TEST_FIRST_DATA_BLK + \
+					 IALLOC_TEST_NGROUPS * IALLOC_TEST_BLOCKS_PER_GRP)
+#define IALLOC_TEST_INODE_SIZE		256U
+#define IALLOC_TEST_EXTRA_ISIZE		32U
+#define IALLOC_TEST_INODES_PER_BLK	(IALLOC_TEST_BLK_SIZE / IALLOC_TEST_INODE_SIZE)
+#define IALLOC_TEST_INODES_PER_GRP	64U
+#define IALLOC_TEST_ITB_PER_GRP		(IALLOC_TEST_INODES_PER_GRP / \
+					 IALLOC_TEST_INODES_PER_BLK)
+#define IALLOC_TEST_TOTAL_INODES	(IALLOC_TEST_NGROUPS * IALLOC_TEST_INODES_PER_GRP)
+#define IALLOC_TEST_FIRST_INO		11U
+#define IALLOC_TEST_BG0_FREE_INODES	(IALLOC_TEST_INODES_PER_GRP - \
+					 (IALLOC_TEST_FIRST_INO - 1U))
+#define IALLOC_TEST_DEFAULT_FREE_CLU	800U
+#define IALLOC_TEST_CSUM_SEED		0x6d4b29a3U
+
+#define IALLOC_TEST_GDESC_BLK		2ULL
+#define IALLOC_TEST_POOL_SIZE		32U
+#define IALLOC_TEST_MAX_TRACKED_INODES	32U
+
+struct ialloc_test_blk {
+	bool in_use;
+	bool in_cache;
+	bool marked_dirty;
+	ext4_fsblk_t blocknr;
+	struct buffer_head bh;
+	char cache_buf[IALLOC_TEST_BLK_SIZE] __aligned(8);
+	char disk_buf[IALLOC_TEST_BLK_SIZE] __aligned(8);
+};
+
+struct ialloc_test_ctx {
+	struct super_block *sb;
+	struct ext4_sb_info sbi;
+	struct ext4_super_block es;
+	struct blockgroup_lock bgl;
+	struct block_device bdev;
+	struct request_queue bdev_queue;
+	struct inode bd_inode;
+	struct buffer_head sb_bh;
+	struct buffer_head gdesc_bh;
+	char gdesc_buf[IALLOC_TEST_BLK_SIZE] __aligned(8);
+	bool gdesc_marked_dirty;
+
+	struct ext4_group_desc gdps[IALLOC_TEST_NGROUPS];
+	struct ext4_group_info grp_infos[IALLOC_TEST_NGROUPS];
+	struct ext4_group_info *grp_info_ptrs[16];
+	struct ext4_group_info **grp_info_slice[1];
+
+	struct flex_groups flex_grps[IALLOC_TEST_NGROUPS];
+	struct flex_groups *flex_grp_ptrs[IALLOC_TEST_NGROUPS];
+
+	struct dentry root_dentry;
+	struct inode *root_inode;
+
+	struct ext4_inode_info standalone_ei;
+	struct inode *tracked_inodes[IALLOC_TEST_MAX_TRACKED_INODES];
+	unsigned int nr_tracked_inodes;
+	bool evict_skip_free;
+
+	struct ialloc_test_blk pool[IALLOC_TEST_POOL_SIZE];
+
+	/* Fault injection and call tracking */
+	u32 fail_gdp_mask;
+	bool fail_getblk;
+	bool fail_read_eio;
+	bool fail_alloc_inode;
+	bool fail_sync_dirty_buffer;
+	int fail_bbitmap_err;
+	int fail_wait_bbitmap_err;
+	int fail_write_access_err;
+	int fail_write_access_on_call;
+	int fail_dirty_metadata_err;
+	int fail_dirty_metadata_on_call;
+	int fail_mark_inode_dirty_err;
+#ifdef CONFIG_EXT4_FS_SECURITY
+	int fail_init_security_err;
+#endif
+	int fail_itable_zeroout_err;
+	int fail_iget_err;
+	struct inode *iget_inode;
+
+	unsigned int read_bh_calls;
+	unsigned int write_access_calls;
+	unsigned int dirty_metadata_calls;
+	unsigned int zeroout_calls;
+	ext4_fsblk_t last_zeroout_blk;
+	int last_zeroout_num;
+	int last_zeroout_barrier;
+};
+
+static inline ext4_fsblk_t ialloc_test_group_start(ext4_group_t g)
+{
+	return IALLOC_TEST_FIRST_DATA_BLK +
+	       (ext4_fsblk_t)g * IALLOC_TEST_BLOCKS_PER_GRP;
+}
+
+static inline ext4_fsblk_t ialloc_test_bbitmap_blk(ext4_group_t g)
+{
+	return ialloc_test_group_start(g) + 2;
+}
+
+static inline ext4_fsblk_t ialloc_test_ibitmap_blk(ext4_group_t g)
+{
+	return ialloc_test_group_start(g) + 3;
+}
+
+static inline ext4_fsblk_t ialloc_test_itable_blk(ext4_group_t g)
+{
+	return ialloc_test_group_start(g) + 4;
+}
+
+static inline void clear_bitmap_uptodate(struct buffer_head *bh)
+{
+	clear_bit(BH_BITMAP_UPTODATE, &bh->b_state);
+}
+
+static void ialloc_test_init_ei_fields(struct ext4_inode_info *ei)
+{
+	inode_set_iversion(&ei->vfs_inode, 1);
+	ei->i_flags = 0;
+	ext4_clear_state_flags(ei);
+	spin_lock_init(&ei->i_raw_lock);
+	ei->i_prealloc_node = RB_ROOT;
+	atomic_set(&ei->i_prealloc_active, 0);
+	rwlock_init(&ei->i_prealloc_lock);
+	ext4_es_init_tree(&ei->i_es_tree);
+	rwlock_init(&ei->i_es_lock);
+	INIT_LIST_HEAD(&ei->i_es_list);
+	ei->i_es_all_nr = 0;
+	ei->i_es_shk_nr = 0;
+	ei->i_es_shrink_lblk = 0;
+	ei->i_es_seq = 0;
+	ei->i_reserved_data_blocks = 0;
+	spin_lock_init(&ei->i_block_reservation_lock);
+	ext4_init_pending_tree(&ei->i_pending_tree);
+#ifdef CONFIG_QUOTA
+	ei->i_reserved_quota = 0;
+	memset(&ei->i_dquot, 0, sizeof(ei->i_dquot));
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+	ei->i_crypt_info = NULL;
+#endif
+	ei->jinode = NULL;
+	init_rwsem(&ei->xattr_sem);
+	INIT_LIST_HEAD(&ei->i_rsv_conversion_list);
+	spin_lock_init(&ei->i_completed_io_lock);
+	ei->i_sync_tid = 0;
+	ei->i_datasync_tid = 0;
+	INIT_LIST_HEAD(&ei->i_orphan);
+	ext4_fc_init_inode(&ei->vfs_inode);
+	spin_lock_init(&ei->i_fc_lock);
+	ei->i_metadata_bhs = NULL;
+	ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+	ei->i_extra_isize = IALLOC_TEST_EXTRA_ISIZE;
+	ei->i_csum_seed = IALLOC_TEST_CSUM_SEED;
+	ei->i_last_alloc_group = ~0U;
+}
+
+static struct inode *ialloc_test_alloc_inode(struct super_block *sb)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+	struct ext4_inode_info *ei;
+
+	if (ctx && ctx->fail_alloc_inode)
+		return NULL;
+
+	ei = kzalloc_obj(*ei);
+	if (!ei)
+		return NULL;
+
+	init_rwsem(&ei->i_data_sem);
+	inode_init_once(&ei->vfs_inode);
+	ialloc_test_init_ei_fields(ei);
+	return &ei->vfs_inode;
+}
+
+static void ialloc_test_evict_inode(struct inode *inode)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+	truncate_inode_pages_final(&inode->i_data);
+	if (ctx && !ctx->evict_skip_free &&
+	    !inode->i_nlink && !is_bad_inode(inode)) {
+		ext4_free_inode(NULL, inode);
+		return;
+	}
+	ext4_clear_inode(inode);
+}
+
+static void ialloc_test_destroy_inode(struct inode *inode)
+{
+	kfree(ext4_i_metadata_bhs(inode));
+	EXT4_I(inode)->i_metadata_bhs = NULL;
+}
+
+static void ialloc_test_free_inode_mem(struct inode *inode)
+{
+	kfree(EXT4_I(inode));
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **ialloc_test_get_dquots(struct inode *inode)
+{
+	return EXT4_I(inode)->i_dquot;
+}
+
+static qsize_t *ialloc_test_get_reserved_space(struct inode *inode)
+{
+	return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations ialloc_test_qops = {
+	.get_reserved_space = ialloc_test_get_reserved_space,
+};
+#endif
+
+static const struct super_operations ialloc_test_sops = {
+	.alloc_inode	= ialloc_test_alloc_inode,
+	.evict_inode	= ialloc_test_evict_inode,
+	.destroy_inode	= ialloc_test_destroy_inode,
+	.free_inode	= ialloc_test_free_inode_mem,
+	.drop_inode	= inode_just_drop,
+#ifdef CONFIG_QUOTA
+	.get_dquots	= ialloc_test_get_dquots,
+#endif
+};
+
+static int ialloc_test_init_fs_context(struct fs_context *fc)
+{
+	return 0;
+}
+
+static struct file_system_type ialloc_test_fs_type = {
+	.name		 = "ext4_ialloc_kunit",
+	.init_fs_context = ialloc_test_init_fs_context,
+	.kill_sb	 = kill_anon_super,
+};
+
+static struct ialloc_test_blk *ialloc_test_find_blk(struct ialloc_test_ctx *ctx,
+						    ext4_fsblk_t blocknr)
+{
+	unsigned int i;
+
+	for (i = 0; i < IALLOC_TEST_POOL_SIZE; i++) {
+		if (ctx->pool[i].in_use && ctx->pool[i].blocknr == blocknr)
+			return &ctx->pool[i];
+	}
+	return NULL;
+}
+
+static struct ialloc_test_blk *ialloc_test_find_blk_by_bh(
+		struct ialloc_test_ctx *ctx, struct buffer_head *bh)
+{
+	unsigned int i;
+
+	for (i = 0; i < IALLOC_TEST_POOL_SIZE; i++) {
+		if (ctx->pool[i].in_use && &ctx->pool[i].bh == bh)
+			return &ctx->pool[i];
+	}
+	return NULL;
+}
+
+static struct ialloc_test_blk *ialloc_test_setup_blk(struct ialloc_test_ctx *ctx,
+						     ext4_fsblk_t blocknr,
+						     bool in_cache,
+						     bool uptodate)
+{
+	struct ialloc_test_blk *slot = ialloc_test_find_blk(ctx, blocknr);
+	unsigned int i;
+
+	if (!slot) {
+		for (i = 0; i < IALLOC_TEST_POOL_SIZE; i++) {
+			if (!ctx->pool[i].in_use) {
+				slot = &ctx->pool[i];
+				break;
+			}
+		}
+	}
+	if (!slot)
+		return NULL;
+
+	memset(slot, 0, sizeof(*slot));
+	slot->in_use = true;
+	slot->in_cache = in_cache;
+	slot->blocknr = blocknr;
+	slot->bh.b_bdev = &ctx->bdev;
+	slot->bh.b_blocknr = blocknr;
+	slot->bh.b_size = IALLOC_TEST_BLK_SIZE;
+	slot->bh.b_data = slot->cache_buf;
+	atomic_set(&slot->bh.b_count, 1);
+	if (uptodate)
+		set_buffer_uptodate(&slot->bh);
+	return slot;
+}
+
+/* --- Static Stubs --- */
+
+static struct ext4_group_desc *ialloc_test_get_group_desc_stub(
+		struct super_block *sb, ext4_group_t block_group,
+		struct buffer_head **bh)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx || block_group >= EXT4_SB(sb)->s_groups_count)
+		return NULL;
+	if (block_group < 32 && (ctx->fail_gdp_mask & BIT(block_group)))
+		return NULL;
+	if (bh)
+		*bh = &ctx->gdesc_bh;
+	return &ctx->gdps[block_group];
+}
+
+static struct buffer_head *ialloc_test_sb_getblk_stub(struct super_block *sb,
+						      ext4_fsblk_t block)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+	struct ialloc_test_blk *slot;
+
+	if (!ctx || ctx->fail_getblk)
+		return NULL;
+	slot = ialloc_test_find_blk(ctx, block);
+	if (!slot)
+		return NULL;
+	slot->in_cache = true;
+	get_bh(&slot->bh);
+	return &slot->bh;
+}
+
+static struct buffer_head *ialloc_test_sb_find_get_block_stub(
+		struct super_block *sb, ext4_fsblk_t block)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+	struct ialloc_test_blk *slot;
+
+	if (!ctx)
+		return NULL;
+	slot = ialloc_test_find_blk(ctx, block);
+	if (!slot || !slot->in_cache)
+		return NULL;
+	get_bh(&slot->bh);
+	return &slot->bh;
+}
+
+static int ialloc_test_read_bh_stub(struct buffer_head *bh, blk_opf_t op_flags,
+				    bio_end_io_t end_io, bool simu_fail)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+	struct ialloc_test_blk *slot;
+	bool fail;
+
+	if (!ctx) {
+		unlock_buffer(bh);
+		return -EIO;
+	}
+	ctx->read_bh_calls++;
+	slot = ialloc_test_find_blk_by_bh(ctx, bh);
+	fail = ctx->fail_read_eio || simu_fail || !slot;
+
+	if (!fail)
+		memcpy(bh->b_data, slot->disk_buf, bh->b_size);
+
+	if (end_io) {
+		struct bio *bio = bio_alloc(NULL, 0, REQ_OP_READ, GFP_KERNEL);
+
+		if (bio) {
+			bio->bi_private = bh;
+			bio->bi_status = fail ? BLK_STS_IOERR : BLK_STS_OK;
+			end_io(bio);
+			return fail ? -EIO : 0;
+		}
+	}
+
+	if (fail) {
+		clear_buffer_uptodate(bh);
+	} else {
+		set_buffer_uptodate(bh);
+		set_bitmap_uptodate(bh);
+	}
+	unlock_buffer(bh);
+	return fail ? -EIO : 0;
+}
+
+static struct buffer_head *ialloc_test_read_bbitmap_nowait_stub(
+		struct super_block *sb, ext4_group_t block_group,
+		bool ignore_locked)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+	struct ialloc_test_blk *slot;
+
+	if (!ctx)
+		return ERR_PTR(-EIO);
+	if (ctx->fail_bbitmap_err)
+		return ERR_PTR(ctx->fail_bbitmap_err);
+	slot = ialloc_test_find_blk(ctx, ialloc_test_bbitmap_blk(block_group));
+	if (!slot)
+		return ERR_PTR(-ENOMEM);
+	set_buffer_uptodate(&slot->bh);
+	set_bitmap_uptodate(&slot->bh);
+	set_buffer_verified(&slot->bh);
+	get_bh(&slot->bh);
+	return &slot->bh;
+}
+
+static int ialloc_test_wait_bbitmap_stub(struct super_block *sb,
+					 ext4_group_t block_group,
+					 struct buffer_head *bh)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx)
+		return -EIO;
+	return ctx->fail_wait_bbitmap_err;
+}
+
+static int ialloc_test_get_write_access_stub(
+		const char *where, unsigned int line, handle_t *handle,
+		struct super_block *sb, struct buffer_head *bh,
+		enum ext4_journal_trigger_type trigger_type)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx)
+		return 0;
+	ctx->write_access_calls++;
+	if (ctx->fail_write_access_err &&
+	    (!ctx->fail_write_access_on_call ||
+	     ctx->write_access_calls == ctx->fail_write_access_on_call))
+		return ctx->fail_write_access_err;
+	return 0;
+}
+
+static int ialloc_test_dirty_metadata_stub(const char *where, unsigned int line,
+					   handle_t *handle,
+					   struct inode *inode,
+					   struct buffer_head *bh)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+	struct ialloc_test_blk *slot;
+
+	if (!ctx)
+		return 0;
+	ctx->dirty_metadata_calls++;
+	if (ctx->fail_dirty_metadata_err &&
+	    (!ctx->fail_dirty_metadata_on_call ||
+	     ctx->dirty_metadata_calls == ctx->fail_dirty_metadata_on_call))
+		return ctx->fail_dirty_metadata_err;
+
+	if (bh) {
+		set_buffer_uptodate(bh);
+		if (bh == &ctx->gdesc_bh) {
+			ctx->gdesc_marked_dirty = true;
+		} else {
+			slot = ialloc_test_find_blk_by_bh(ctx, bh);
+			if (slot)
+				slot->marked_dirty = true;
+		}
+		if (ctx->fail_sync_dirty_buffer) {
+			clear_buffer_mapped(bh);
+			set_buffer_dirty(bh);
+		} else {
+			clear_buffer_dirty(bh);
+		}
+	}
+	return 0;
+}
+
+static int ialloc_test_mark_inode_dirty_stub(handle_t *handle,
+					     struct inode *inode,
+					     const char *func,
+					     unsigned int line)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx)
+		return 0;
+	return ctx->fail_mark_inode_dirty_err;
+}
+
+#ifdef CONFIG_EXT4_FS_SECURITY
+static int ialloc_test_init_security_stub(handle_t *handle,
+					  struct inode *inode,
+					  struct inode *dir,
+					  const struct qstr *qstr)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx)
+		return 0;
+	return ctx->fail_init_security_err;
+}
+#endif
+
+static int ialloc_test_itable_zeroout_stub(struct super_block *sb,
+					   ext4_fsblk_t blk, int num,
+					   int barrier)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx)
+		return -EIO;
+	ctx->zeroout_calls++;
+	ctx->last_zeroout_blk = blk;
+	ctx->last_zeroout_num = num;
+	ctx->last_zeroout_barrier = barrier;
+	return ctx->fail_itable_zeroout_err;
+}
+
+static struct inode *ialloc_test_iget_stub(struct super_block *sb,
+					   unsigned long ino,
+					   ext4_iget_flags flags,
+					   const char *function,
+					   unsigned int line)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx)
+		return ERR_PTR(-EIO);
+	if (ctx->fail_iget_err)
+		return ERR_PTR(ctx->fail_iget_err);
+	if (!ctx->iget_inode)
+		return ERR_PTR(-ENOENT);
+	ihold(ctx->iget_inode);
+	return ctx->iget_inode;
+}
+
+/* --- Fixture Helpers & Cleanup --- */
+
+static void ialloc_test_track_inode(struct kunit *test,
+				    struct ialloc_test_ctx *ctx,
+				    struct inode *inode)
+{
+	if (inode_state_read_once(inode) & I_NEW)
+		unlock_new_inode(inode);
+	KUNIT_ASSERT_LT(test, ctx->nr_tracked_inodes,
+			IALLOC_TEST_MAX_TRACKED_INODES);
+	ctx->tracked_inodes[ctx->nr_tracked_inodes++] = inode;
+}
+
+static void ialloc_test_untrack_inode(struct ialloc_test_ctx *ctx,
+				      struct inode *inode)
+{
+	unsigned int i;
+
+	for (i = 0; i < ctx->nr_tracked_inodes; i++) {
+		if (ctx->tracked_inodes[i] == inode) {
+			ctx->tracked_inodes[i] =
+				ctx->tracked_inodes[--ctx->nr_tracked_inodes];
+			ctx->tracked_inodes[ctx->nr_tracked_inodes] = NULL;
+			return;
+		}
+	}
+}
+
+static void ialloc_test_sync_ibitmap_csum(struct ialloc_test_ctx *ctx,
+					  ext4_group_t g, bool sync_disk)
+{
+	struct ialloc_test_blk *slot;
+
+	slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(g));
+	if (!slot)
+		return;
+	ext4_inode_bitmap_csum_set(ctx->sb, &ctx->gdps[g], &slot->bh);
+	ext4_group_desc_csum_set(ctx->sb, g, &ctx->gdps[g]);
+	if (sync_disk)
+		memcpy(slot->disk_buf, slot->cache_buf, IALLOC_TEST_BLK_SIZE);
+}
+
+static void ialloc_test_enable_flex_bg(struct ialloc_test_ctx *ctx,
+				       unsigned int log_groups_per_flex)
+{
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = &ctx->sbi;
+	ext4_group_t g, fg;
+
+	ext4_set_feature_flex_bg(sb);
+	sbi->s_log_groups_per_flex = log_groups_per_flex;
+	for (g = 0; g < IALLOC_TEST_NGROUPS; g++) {
+		atomic_set(&ctx->flex_grps[g].free_inodes, 0);
+		atomic_set(&ctx->flex_grps[g].used_dirs, 0);
+		atomic64_set(&ctx->flex_grps[g].free_clusters, 0);
+	}
+	for (g = 0; g < IALLOC_TEST_NGROUPS; g++) {
+		fg = ext4_flex_group(sbi, g);
+		atomic_add(ext4_free_inodes_count(sb, &ctx->gdps[g]),
+			   &ctx->flex_grps[fg].free_inodes);
+		atomic_add(ext4_used_dirs_count(sb, &ctx->gdps[g]),
+			   &ctx->flex_grps[fg].used_dirs);
+		atomic64_add(ext4_free_group_clusters(sb, &ctx->gdps[g]),
+			     &ctx->flex_grps[fg].free_clusters);
+	}
+}
+
+static void ialloc_test_prep_standalone_free_inode(struct ialloc_test_ctx *ctx,
+						   unsigned long ino,
+						   umode_t mode)
+{
+	struct ext4_inode_info *ei = &ctx->standalone_ei;
+	struct inode *inode = &ei->vfs_inode;
+
+	memset(ei, 0, sizeof(*ei));
+	init_rwsem(&ei->i_data_sem);
+	inode_init_once(inode);
+	inode->i_sb = ctx->sb;
+	inode->i_ino = ino;
+	inode->i_mode = mode;
+	inode->i_blkbits = ctx->sb->s_blocksize_bits;
+	inode->i_flags = S_NOQUOTA;
+	inode->i_mapping = &inode->i_data;
+	inode->i_data.host = inode;
+	mapping_set_gfp_mask(&inode->i_data, GFP_HIGHUSER_MOVABLE);
+	init_rwsem(&inode->i_rwsem);
+	init_rwsem(&inode->i_data.invalidate_lock);
+	spin_lock_init(&inode->i_lock);
+	inode_state_assign_raw(inode, I_FREEING);
+	inode->__i_nlink = 0;
+	atomic_set(&inode->i_count, 1);
+	ialloc_test_init_ei_fields(ei);
+}
+
+static void ialloc_test_cleanup(void *data)
+{
+	struct ialloc_test_ctx *ctx = data;
+	handle_t *handle;
+	unsigned int i;
+
+	ctx->evict_skip_free = true;
+	ctx->sbi.s_journal = NULL;
+
+	while ((handle = ext4_journal_current_handle()) != NULL)
+		ext4_journal_stop(handle);
+
+	for (i = 0; i < ctx->nr_tracked_inodes; i++) {
+		struct inode *ti = ctx->tracked_inodes[i];
+
+		if (ti) {
+			if (inode_state_read_once(ti) & I_NEW)
+				unlock_new_inode(ti);
+			spin_lock(&ti->i_lock);
+			inode_state_clear(ti, I_DIRTY_TIME);
+			spin_unlock(&ti->i_lock);
+			set_nlink(ti, 1);
+			iput(ti);
+			ctx->tracked_inodes[i] = NULL;
+		}
+	}
+	ctx->nr_tracked_inodes = 0;
+
+	ctx->sb->s_root = NULL;
+	if (ctx->root_inode) {
+		set_nlink(ctx->root_inode, 2);
+		iput(ctx->root_inode);
+		ctx->root_inode = NULL;
+	}
+
+	deactivate_super(ctx->sb);
+	rcu_barrier();
+
+	percpu_counter_destroy(&ctx->sbi.s_dirs_counter);
+	percpu_counter_destroy(&ctx->sbi.s_freeinodes_counter);
+	percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+
+	kfree(ctx);
+}
+
+static int ialloc_test_init(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx;
+	struct fs_context *fc;
+	struct super_block *sb;
+	struct ext4_sb_info *sbi;
+	ext4_group_t g;
+	unsigned int bit;
+	s64 total_free_inodes = 0;
+	s64 total_free_clusters = 0;
+	int err;
+
+	ctx = kzalloc_obj(*ctx);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	test->priv = ctx;
+
+	fc = fs_context_for_mount(&ialloc_test_fs_type, 0);
+	if (IS_ERR(fc)) {
+		err = PTR_ERR(fc);
+		kfree(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+	sb = sget_fc(fc, NULL, set_anon_super_fc);
+	put_fs_context(fc);
+	if (IS_ERR(sb)) {
+		err = PTR_ERR(sb);
+		kfree(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+	ctx->sb = sb;
+	sbi = &ctx->sbi;
+
+	sbi->s_sb = sb;
+	sb->s_fs_info = sbi;
+	sb->s_op = &ialloc_test_sops;
+#ifdef CONFIG_QUOTA
+	sb->dq_op = &ialloc_test_qops;
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+	sb->s_cop = &ext4_cryptops;
+#endif
+	sb->s_flags |= SB_ACTIVE;
+	sb->s_blocksize = IALLOC_TEST_BLK_SIZE;
+	sb->s_blocksize_bits = IALLOC_TEST_BLK_BITS;
+	sb->s_maxbytes = MAX_LFS_FILESIZE;
+	sb->s_time_gran = 1;
+	sb->s_xattr = ext4_xattr_handlers;
+	strscpy(sb->s_id, "ext4_ialloc_kunit", sizeof(sb->s_id));
+
+	inode_init_once(&ctx->bd_inode);
+	ctx->bd_inode.i_sb = sb;
+	ctx->bd_inode.i_blkbits = IALLOC_TEST_BLK_BITS;
+	ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data;
+	ctx->bd_inode.i_data.host = &ctx->bd_inode;
+	ctx->bdev_queue.limits.logical_block_size = 512;
+	ctx->bdev_queue.limits.dma_alignment = 511;
+	ctx->bdev.bd_queue = &ctx->bdev_queue;
+	ctx->bdev.bd_mapping = &ctx->bd_inode.i_data;
+	bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+	sb->s_bdev = &ctx->bdev;
+
+	bgl_lock_init(&ctx->bgl);
+	sbi->s_blockgroup_lock = &ctx->bgl;
+
+	sbi->s_es = &ctx->es;
+	ctx->es.s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+	ctx->es.s_first_data_block = cpu_to_le32(IALLOC_TEST_FIRST_DATA_BLK);
+	ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+	ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_LINUX);
+	ctx->es.s_inodes_count = cpu_to_le32(IALLOC_TEST_TOTAL_INODES);
+	ctx->es.s_inodes_per_group = cpu_to_le32(IALLOC_TEST_INODES_PER_GRP);
+	ctx->es.s_blocks_per_group = cpu_to_le32(IALLOC_TEST_BLOCKS_PER_GRP);
+	ctx->es.s_clusters_per_group = cpu_to_le32(IALLOC_TEST_BLOCKS_PER_GRP);
+	ctx->es.s_first_ino = cpu_to_le32(IALLOC_TEST_FIRST_INO);
+	ctx->es.s_inode_size = cpu_to_le16(IALLOC_TEST_INODE_SIZE);
+	ctx->es.s_min_extra_isize = cpu_to_le16(IALLOC_TEST_EXTRA_ISIZE);
+	ctx->es.s_want_extra_isize = cpu_to_le16(IALLOC_TEST_EXTRA_ISIZE);
+	ctx->es.s_checksum_type = EXT4_CRC32C_CHKSUM;
+	ext4_blocks_count_set(&ctx->es, IALLOC_TEST_TOTAL_BLKS);
+
+	ext4_set_feature_sparse_super(sb);
+	ext4_set_feature_extents(sb);
+	ext4_set_feature_extra_isize(sb);
+	ext4_set_feature_metadata_csum(sb);
+	ext4_set_feature_64bit(sb);
+	ext4_set_feature_dir_index(sb);
+
+	ctx->sb_bh.b_data = (char *)&ctx->es;
+	ctx->sb_bh.b_size = sizeof(ctx->es);
+	ctx->sb_bh.b_bdev = &ctx->bdev;
+	atomic_set(&ctx->sb_bh.b_count, 1);
+	set_buffer_uptodate(&ctx->sb_bh);
+	sbi->s_sbh = &ctx->sb_bh;
+
+	ctx->gdesc_bh.b_data = ctx->gdesc_buf;
+	ctx->gdesc_bh.b_size = IALLOC_TEST_BLK_SIZE;
+	ctx->gdesc_bh.b_blocknr = IALLOC_TEST_GDESC_BLK;
+	ctx->gdesc_bh.b_bdev = &ctx->bdev;
+	atomic_set(&ctx->gdesc_bh.b_count, 1);
+	set_buffer_uptodate(&ctx->gdesc_bh);
+
+	sbi->s_inode_size = IALLOC_TEST_INODE_SIZE;
+	sbi->s_inodes_per_block = IALLOC_TEST_INODES_PER_BLK;
+	sbi->s_inodes_per_group = IALLOC_TEST_INODES_PER_GRP;
+	sbi->s_itb_per_group = IALLOC_TEST_ITB_PER_GRP;
+	sbi->s_first_ino = IALLOC_TEST_FIRST_INO;
+	sbi->s_want_extra_isize = IALLOC_TEST_EXTRA_ISIZE;
+	sbi->s_blocks_per_group = IALLOC_TEST_BLOCKS_PER_GRP;
+	sbi->s_clusters_per_group = IALLOC_TEST_BLOCKS_PER_GRP;
+	sbi->s_cluster_ratio = 1;
+	sbi->s_cluster_bits = 0;
+	sbi->s_groups_count = IALLOC_TEST_NGROUPS;
+	sbi->s_gdb_count = 1;
+	sbi->s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+	sbi->s_desc_per_block = IALLOC_TEST_BLK_SIZE / EXT4_MIN_DESC_SIZE_64BIT;
+	sbi->s_desc_per_block_bits = ilog2(sbi->s_desc_per_block);
+	sbi->s_bitmap_maxbytes = 0x1ff7ffff000ULL;
+	sbi->s_csum_seed = IALLOC_TEST_CSUM_SEED;
+	sbi->s_hash_seed[0] = 0x12345678U;
+	sbi->s_hash_seed[1] = 0x9abcdef0U;
+	sbi->s_hash_seed[2] = 0x13579bdfU;
+	sbi->s_hash_seed[3] = 0x2468ace0U;
+
+	set_opt(sb, ERRORS_CONT);
+	set_opt(sb, ORDERED_DATA);
+	spin_lock_init(&sbi->s_bdev_wb_lock);
+	spin_lock_init(&sbi->s_error_lock);
+	spin_lock_init(&sbi->s_md_lock);
+	mutex_init(&sbi->s_fc_lock);
+	INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_MAIN]);
+	INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_STAGING]);
+	INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_MAIN]);
+	INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_STAGING]);
+
+	ratelimit_state_init(&sbi->s_err_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+	ratelimit_state_init(&sbi->s_warning_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+	ratelimit_state_init(&sbi->s_msg_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+
+	/* Wire RCU-indexed group_info and flex_groups tables */
+	for (g = 0; g < IALLOC_TEST_NGROUPS; g++) {
+		struct ialloc_test_blk *iblk, *bblk;
+		u32 free_inos = (g == 0) ? IALLOC_TEST_BG0_FREE_INODES :
+					   IALLOC_TEST_INODES_PER_GRP;
+
+		init_rwsem(&ctx->grp_infos[g].alloc_sem);
+		ctx->grp_infos[g].bb_state = 0;
+		ctx->grp_info_ptrs[g] = &ctx->grp_infos[g];
+		ctx->flex_grp_ptrs[g] = &ctx->flex_grps[g];
+
+		ext4_block_bitmap_set(sb, &ctx->gdps[g],
+				      ialloc_test_bbitmap_blk(g));
+		ext4_inode_bitmap_set(sb, &ctx->gdps[g],
+				      ialloc_test_ibitmap_blk(g));
+		ext4_inode_table_set(sb, &ctx->gdps[g],
+				     ialloc_test_itable_blk(g));
+		ext4_free_inodes_set(sb, &ctx->gdps[g], free_inos);
+		ext4_used_dirs_set(sb, &ctx->gdps[g], (g == 0) ? 1U : 0U);
+		ext4_itable_unused_set(sb, &ctx->gdps[g], free_inos);
+		ext4_free_group_clusters_set(sb, &ctx->gdps[g],
+					     IALLOC_TEST_DEFAULT_FREE_CLU);
+
+		iblk = ialloc_test_setup_blk(ctx, ialloc_test_ibitmap_blk(g),
+					     true, true);
+		bblk = ialloc_test_setup_blk(ctx, ialloc_test_bbitmap_blk(g),
+					     true, true);
+		if (!iblk || !bblk) {
+			deactivate_locked_super(sb);
+			kfree(ctx);
+			KUNIT_FAIL_AND_ABORT(test, "block pool setup failed");
+		}
+
+		/* Mark reserved bits 0..9 in group 0 and end padding 64..8191 */
+		if (g == 0) {
+			for (bit = 0; bit < IALLOC_TEST_FIRST_INO - 1; bit++)
+				ext4_set_bit(bit, iblk->cache_buf);
+		}
+		ext4_mark_bitmap_end(IALLOC_TEST_INODES_PER_GRP,
+				     IALLOC_TEST_BLK_SIZE * 8,
+				     iblk->cache_buf);
+		set_bitmap_uptodate(&iblk->bh);
+		set_buffer_verified(&iblk->bh);
+
+		ext4_block_bitmap_csum_set(sb, &ctx->gdps[g], &bblk->bh);
+		memcpy(bblk->disk_buf, bblk->cache_buf, IALLOC_TEST_BLK_SIZE);
+		set_bitmap_uptodate(&bblk->bh);
+		set_buffer_verified(&bblk->bh);
+
+		ialloc_test_sync_ibitmap_csum(ctx, g, true);
+
+		total_free_inodes += free_inos;
+		total_free_clusters += IALLOC_TEST_DEFAULT_FREE_CLU;
+	}
+	ctx->grp_info_slice[0] = ctx->grp_info_ptrs;
+	rcu_assign_pointer(sbi->s_group_info, ctx->grp_info_slice);
+	rcu_assign_pointer(sbi->s_flex_groups, ctx->flex_grp_ptrs);
+
+	err = percpu_counter_init(&sbi->s_freeclusters_counter,
+				  total_free_clusters, GFP_KERNEL);
+	if (err) {
+		deactivate_locked_super(sb);
+		kfree(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+	err = percpu_counter_init(&sbi->s_freeinodes_counter,
+				  total_free_inodes, GFP_KERNEL);
+	if (err) {
+		percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+		deactivate_locked_super(sb);
+		kfree(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+	err = percpu_counter_init(&sbi->s_dirs_counter, 1, GFP_KERNEL);
+	if (err) {
+		percpu_counter_destroy(&ctx->sbi.s_freeinodes_counter);
+		percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+		deactivate_locked_super(sb);
+		kfree(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+
+	/* Create hashed VFS root directory inode (EXT4_ROOT_INO = 2) */
+	ctx->root_inode = new_inode(sb);
+	if (!ctx->root_inode) {
+		percpu_counter_destroy(&ctx->sbi.s_dirs_counter);
+		percpu_counter_destroy(&ctx->sbi.s_freeinodes_counter);
+		percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+		deactivate_locked_super(sb);
+		kfree(ctx);
+		KUNIT_FAIL_AND_ABORT(test, "failed to allocate root_inode");
+	}
+	ctx->root_inode->i_ino = EXT4_ROOT_INO;
+	ctx->root_inode->i_mode = S_IFDIR | 0755;
+	set_nlink(ctx->root_inode, 2);
+	EXT4_I(ctx->root_inode)->i_block_group = 0;
+	insert_inode_locked(ctx->root_inode);
+	unlock_new_inode(ctx->root_inode);
+
+	ctx->root_dentry.d_sb = sb;
+	ctx->root_dentry.d_inode = ctx->root_inode;
+	sb->s_root = &ctx->root_dentry;
+
+	up_write(&sb->s_umount);
+
+	err = kunit_add_action_or_reset(test, ialloc_test_cleanup, ctx);
+	KUNIT_ASSERT_EQ(test, err, 0);
+
+	kunit_activate_static_stub(test, ext4_get_group_desc,
+				   ialloc_test_get_group_desc_stub);
+	kunit_activate_static_stub(test, ext4_sb_getblk,
+				   ialloc_test_sb_getblk_stub);
+	kunit_activate_static_stub(test, ext4_sb_find_get_block,
+				   ialloc_test_sb_find_get_block_stub);
+	kunit_activate_static_stub(test, ext4_read_bh,
+				   ialloc_test_read_bh_stub);
+	kunit_activate_static_stub(test, ext4_read_block_bitmap_nowait,
+				   ialloc_test_read_bbitmap_nowait_stub);
+	kunit_activate_static_stub(test, ext4_wait_block_bitmap,
+				   ialloc_test_wait_bbitmap_stub);
+	kunit_activate_static_stub(test, __ext4_journal_get_write_access,
+				   ialloc_test_get_write_access_stub);
+	kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+				   ialloc_test_dirty_metadata_stub);
+	kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+				   ialloc_test_mark_inode_dirty_stub);
+#ifdef CONFIG_EXT4_FS_SECURITY
+	kunit_activate_static_stub(test, ext4_init_security,
+				   ialloc_test_init_security_stub);
+#endif
+	kunit_activate_static_stub(test, ext4_itable_zeroout,
+				   ialloc_test_itable_zeroout_stub);
+	kunit_activate_static_stub(test, __ext4_iget,
+				   ialloc_test_iget_stub);
+
+	return 0;
+}
+
+/* =========================================================================
+ * 1. Bitmap Helpers: ext4_mark_bitmap_end & ext4_end_bitmap_read
+ * =========================================================================
+ */
+
+struct mark_bitmap_end_param {
+	const char *desc;
+	int start_bit;
+	int end_bit;
+};
+
+static const struct mark_bitmap_end_param mark_bitmap_end_params[] = {
+	{ "unaligned_start_to_aligned_end", 67, 128 },
+	{ "byte_aligned_memset_only", 64, 128 },
+	{ "single_partial_byte_no_memset", 3, 8 },
+	{ "unaligned_within_second_byte", 10, 16 },
+	{ "equal_start_and_end_noop", 64, 64 },
+	{ "start_greater_than_end_noop", 75, 64 },
+};
+
+static void mark_bitmap_end_param_to_desc(const struct mark_bitmap_end_param *p,
+					  char *desc)
+{
+	strscpy(desc, p->desc, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(mark_bitmap_end, mark_bitmap_end_params,
+		  mark_bitmap_end_param_to_desc);
+
+static void ialloc_test_mark_bitmap_end(struct kunit *test)
+{
+	const struct mark_bitmap_end_param *p = test->param_value;
+	char bitmap[24] = {};
+	int bit;
+
+	ext4_mark_bitmap_end(p->start_bit, p->end_bit, bitmap);
+
+	for (bit = 0; bit < (int)sizeof(bitmap) * 8; bit++) {
+		bool expected = (p->start_bit < p->end_bit) &&
+				(bit >= p->start_bit && bit < p->end_bit);
+
+		KUNIT_EXPECT_EQ_MSG(test, (bool)ext4_test_bit(bit, bitmap),
+				    expected, "%s: unexpected value at bit %d",
+				    p->desc, bit);
+	}
+}
+
+static void ialloc_test_end_bitmap_read_ok(struct kunit *test)
+{
+	struct buffer_head bh = {};
+	struct bio *bio;
+
+	atomic_set(&bh.b_count, 1);
+	lock_buffer(&bh);
+
+	bio = bio_alloc(NULL, 0, REQ_OP_READ, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, bio);
+	bio->bi_private = &bh;
+	bio->bi_status = BLK_STS_OK;
+
+	ext4_end_bitmap_read(bio);
+
+	KUNIT_EXPECT_FALSE(test, buffer_locked(&bh));
+	KUNIT_EXPECT_TRUE(test, buffer_uptodate(&bh));
+	KUNIT_EXPECT_TRUE(test, bitmap_uptodate(&bh));
+}
+
+static void ialloc_test_end_bitmap_read_ioerr(struct kunit *test)
+{
+	struct buffer_head bh = {};
+	struct bio *bio;
+
+	atomic_set(&bh.b_count, 1);
+	lock_buffer(&bh);
+
+	bio = bio_alloc(NULL, 0, REQ_OP_READ, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, bio);
+	bio->bi_private = &bh;
+	bio->bi_status = BLK_STS_IOERR;
+	bio_set_flag(bio, BIO_QUIET);
+
+	ext4_end_bitmap_read(bio);
+
+	KUNIT_EXPECT_FALSE(test, buffer_locked(&bh));
+	KUNIT_EXPECT_FALSE(test, buffer_uptodate(&bh));
+	KUNIT_EXPECT_FALSE(test, bitmap_uptodate(&bh));
+	KUNIT_EXPECT_TRUE(test, test_bit(BH_Quiet, &bh.b_state));
+}
+
+/* =========================================================================
+ * 2. Inode Bitmap Validation: ext4_validate_inode_bitmap_test
+ * =========================================================================
+ */
+
+static void ialloc_test_validate_ibitmap_csum(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *slot;
+	s64 free_before;
+	int err;
+
+	slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+	KUNIT_ASSERT_NOT_NULL(test, slot);
+
+	/* 64-bit group descriptor valid CRC */
+	clear_buffer_verified(&slot->bh);
+	err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[1], 1, &slot->bh);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test, (bool)buffer_verified(&slot->bh));
+	KUNIT_EXPECT_FALSE(test,
+			   (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[1]));
+
+	/* 32-bit group descriptor valid CRC (low 16-bit checksum only) */
+	ext4_clear_feature_64bit(sb);
+	ctx->sbi.s_desc_size = EXT4_MIN_DESC_SIZE;
+	ext4_inode_bitmap_csum_set(sb, &ctx->gdps[1], &slot->bh);
+	clear_buffer_verified(&slot->bh);
+	err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[1], 1, &slot->bh);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test, (bool)buffer_verified(&slot->bh));
+
+	/* Restore 64-bit descriptor and corrupt bitmap byte */
+	ext4_set_feature_64bit(sb);
+	ctx->sbi.s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+	ialloc_test_sync_ibitmap_csum(ctx, 1, false);
+	clear_buffer_verified(&slot->bh);
+	slot->cache_buf[0] ^= 0x01;
+	free_before = percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter);
+
+	err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[1], 1, &slot->bh);
+	KUNIT_EXPECT_EQ(test, err, -EFSBADCRC);
+	KUNIT_EXPECT_FALSE(test, (bool)buffer_verified(&slot->bh));
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[1]));
+	KUNIT_EXPECT_EQ(test,
+			percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter),
+			free_before - IALLOC_TEST_INODES_PER_GRP);
+}
+
+static void ialloc_test_validate_ibitmap_short_circuits(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *slot;
+	int err;
+
+	slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(2));
+	KUNIT_ASSERT_NOT_NULL(test, slot);
+
+	/* Corrupt byte, but EXT4_FC_REPLAY skips validation */
+	clear_buffer_verified(&slot->bh);
+	slot->cache_buf[0] ^= 0xff;
+	ctx->sbi.s_mount_state |= EXT4_FC_REPLAY;
+	err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[2], 2, &slot->bh);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_FALSE(test, (bool)buffer_verified(&slot->bh));
+	ctx->sbi.s_mount_state &= ~EXT4_FC_REPLAY;
+
+	/* Already buffer_verified short-circuits before checking CRC */
+	set_buffer_verified(&slot->bh);
+	err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[2], 2, &slot->bh);
+	KUNIT_EXPECT_EQ(test, err, 0);
+
+	/* Out-of-range group (NULL ext4_get_group_info) -> -EFSCORRUPTED */
+	clear_buffer_verified(&slot->bh);
+	err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[2],
+					      IALLOC_TEST_NGROUPS + 1,
+					      &slot->bh);
+	KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+
+	/* Pre-marked IBITMAP_CORRUPT -> -EFSCORRUPTED */
+	set_bit(EXT4_GROUP_INFO_IBITMAP_CORRUPT_BIT,
+		&ctx->grp_infos[2].bb_state);
+	err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[2], 2, &slot->bh);
+	KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+
+#ifdef CONFIG_EXT4_DEBUG
+	slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(3));
+	KUNIT_ASSERT_NOT_NULL(test, slot);
+	clear_buffer_verified(&slot->bh);
+	ctx->sbi.s_simulate_fail = EXT4_SIM_IBITMAP_CRC;
+	err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[3], 3, &slot->bh);
+	KUNIT_EXPECT_EQ(test, err, -EFSBADCRC);
+	KUNIT_EXPECT_EQ(test, ctx->sbi.s_simulate_fail, 0UL);
+#endif
+}
+
+/* =========================================================================
+ * 3. Reading Inode Bitmaps: ext4_read_inode_bitmap_test
+ * =========================================================================
+ */
+
+static void ialloc_test_read_ibitmap_disk_and_cache(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *slot;
+	struct buffer_head *bh;
+
+	slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+	KUNIT_ASSERT_NOT_NULL(test, slot);
+
+	/* Put distinctive valid bitmap on disk and invalidate cache flags */
+	ext4_set_bit(5, slot->disk_buf);
+	memcpy(slot->cache_buf, slot->disk_buf, IALLOC_TEST_BLK_SIZE);
+	ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+	memset(slot->cache_buf, 0, IALLOC_TEST_BLK_SIZE);
+	clear_bitmap_uptodate(&slot->bh);
+	clear_buffer_uptodate(&slot->bh);
+	clear_buffer_verified(&slot->bh);
+
+	bh = ext4_read_inode_bitmap_test(sb, 1);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+	KUNIT_EXPECT_EQ(test, ctx->read_bh_calls, 1U);
+	KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(5, bh->b_data));
+	KUNIT_EXPECT_TRUE(test, (bool)bitmap_uptodate(bh));
+	KUNIT_EXPECT_TRUE(test, (bool)buffer_uptodate(bh));
+	KUNIT_EXPECT_TRUE(test, (bool)buffer_verified(bh));
+	brelse(bh);
+	KUNIT_EXPECT_EQ(test, atomic_read(&slot->bh.b_count), 1);
+
+	/* Second read hits bitmap_uptodate fast path with zero I/O */
+	bh = ext4_read_inode_bitmap_test(sb, 1);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+	KUNIT_EXPECT_EQ(test, ctx->read_bh_calls, 1U);
+	brelse(bh);
+
+	/* buffer_uptodate && !bitmap_uptodate promotes bitmap_uptodate without I/O */
+	clear_bitmap_uptodate(&slot->bh);
+	clear_buffer_verified(&slot->bh);
+	bh = ext4_read_inode_bitmap_test(sb, 1);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+	KUNIT_EXPECT_EQ(test, ctx->read_bh_calls, 1U);
+	KUNIT_EXPECT_TRUE(test, (bool)bitmap_uptodate(bh));
+	KUNIT_EXPECT_TRUE(test, (bool)buffer_verified(bh));
+	brelse(bh);
+	KUNIT_EXPECT_EQ(test, atomic_read(&slot->bh.b_count), 1);
+}
+
+static void ialloc_test_read_ibitmap_uninit(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *slot0, *slot1;
+	struct buffer_head *bh;
+	unsigned int bit;
+
+	/* Group 1 with EXT4_BG_INODE_UNINIT initializes in memory without I/O */
+	slot1 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+	KUNIT_ASSERT_NOT_NULL(test, slot1);
+	memset(slot1->cache_buf, 0xaa, IALLOC_TEST_BLK_SIZE);
+	clear_bitmap_uptodate(&slot1->bh);
+	clear_buffer_uptodate(&slot1->bh);
+	clear_buffer_verified(&slot1->bh);
+	ctx->gdps[1].bg_flags |= cpu_to_le16(EXT4_BG_INODE_UNINIT);
+
+	bh = ext4_read_inode_bitmap_test(sb, 1);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+	KUNIT_EXPECT_EQ(test, ctx->read_bh_calls, 0U);
+	KUNIT_EXPECT_TRUE(test, (bool)bitmap_uptodate(bh));
+	KUNIT_EXPECT_TRUE(test, (bool)buffer_uptodate(bh));
+	KUNIT_EXPECT_TRUE(test, (bool)buffer_verified(bh));
+	for (bit = 0; bit < IALLOC_TEST_INODES_PER_GRP; bit++)
+		KUNIT_EXPECT_FALSE_MSG(test, (bool)ext4_test_bit(bit, bh->b_data),
+				       "expected bit %u cleared", bit);
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)ext4_test_bit(IALLOC_TEST_INODES_PER_GRP,
+					      bh->b_data));
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)ext4_test_bit(IALLOC_TEST_BLK_SIZE * 8 - 1,
+					      bh->b_data));
+	brelse(bh);
+
+	/* Group 0 marked EXT4_BG_INODE_UNINIT is rejected as corrupt */
+	slot0 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(0));
+	KUNIT_ASSERT_NOT_NULL(test, slot0);
+	clear_bitmap_uptodate(&slot0->bh);
+	clear_buffer_uptodate(&slot0->bh);
+	ctx->gdps[0].bg_flags |= cpu_to_le16(EXT4_BG_INODE_UNINIT);
+
+	bh = ext4_read_inode_bitmap_test(sb, 0);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSCORRUPTED);
+	KUNIT_EXPECT_EQ(test, atomic_read(&slot0->bh.b_count), 1);
+}
+
+static void ialloc_test_read_ibitmap_errors(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *slot;
+	struct buffer_head *bh;
+
+	/* NULL group descriptor */
+	ctx->fail_gdp_mask = BIT(1);
+	bh = ext4_read_inode_bitmap_test(sb, 1);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSCORRUPTED);
+	ctx->fail_gdp_mask = 0;
+
+	/* Invalid bitmap_blk <= s_first_data_block (1) */
+	ext4_inode_bitmap_set(sb, &ctx->gdps[1], IALLOC_TEST_FIRST_DATA_BLK);
+	bh = ext4_read_inode_bitmap_test(sb, 1);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSCORRUPTED);
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[1]));
+
+	/* Invalid bitmap_blk >= ext4_blocks_count(es) */
+	ctx->grp_infos[1].bb_state = 0;
+	ext4_inode_bitmap_set(sb, &ctx->gdps[1], IALLOC_TEST_TOTAL_BLKS);
+	bh = ext4_read_inode_bitmap_test(sb, 1);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSCORRUPTED);
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[1]));
+	ctx->grp_infos[1].bb_state = 0;
+	ext4_inode_bitmap_set(sb, &ctx->gdps[1], ialloc_test_ibitmap_blk(1));
+
+	/* ext4_sb_getblk allocation failure */
+	ctx->fail_getblk = true;
+	bh = ext4_read_inode_bitmap_test(sb, 1);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -ENOMEM);
+	ctx->fail_getblk = false;
+
+	/* Disk read I/O failure */
+	slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(2));
+	KUNIT_ASSERT_NOT_NULL(test, slot);
+	clear_bitmap_uptodate(&slot->bh);
+	clear_buffer_uptodate(&slot->bh);
+	ctx->fail_read_eio = true;
+	bh = ext4_read_inode_bitmap_test(sb, 2);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EIO);
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[2]));
+	KUNIT_EXPECT_EQ(test, atomic_read(&slot->bh.b_count), 1);
+	ctx->fail_read_eio = false;
+
+	/* Bad CRC after successful disk read */
+	slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(3));
+	KUNIT_ASSERT_NOT_NULL(test, slot);
+	slot->disk_buf[0] ^= 0x80;
+	clear_bitmap_uptodate(&slot->bh);
+	clear_buffer_uptodate(&slot->bh);
+	clear_buffer_verified(&slot->bh);
+	bh = ext4_read_inode_bitmap_test(sb, 3);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSBADCRC);
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[3]));
+	KUNIT_EXPECT_EQ(test, atomic_read(&slot->bh.b_count), 1);
+}
+
+/* =========================================================================
+ * 4. Freeing Inodes: ext4_free_inode
+ * =========================================================================
+ */
+
+static void ialloc_test_free_inode_reg_and_dir(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *iblk;
+	s64 free_before, dirs_before;
+
+	iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+	KUNIT_ASSERT_NOT_NULL(test, iblk);
+
+	/* Mark bit 2 (ino 67, reg) and bit 3 (ino 68, dir) allocated in group 1 */
+	ext4_set_bit(2, iblk->cache_buf);
+	ext4_set_bit(3, iblk->cache_buf);
+	ext4_free_inodes_set(sb, &ctx->gdps[1], 62);
+	ext4_used_dirs_set(sb, &ctx->gdps[1], 1);
+	percpu_counter_add(&ctx->sbi.s_freeinodes_counter, -2);
+	percpu_counter_add(&ctx->sbi.s_dirs_counter, 1);
+	ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+	ialloc_test_enable_flex_bg(ctx, 1);
+
+	free_before = percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter);
+	dirs_before = percpu_counter_sum_positive(&ctx->sbi.s_dirs_counter);
+
+	/* Free regular file ino 67 (group 1, bit 2, flex_group 0) */
+	ialloc_test_prep_standalone_free_inode(ctx, 67, S_IFREG | 0644);
+	ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+
+	KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(2, iblk->cache_buf));
+	KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[1]), 63U);
+	KUNIT_EXPECT_EQ(test, ext4_used_dirs_count(sb, &ctx->gdps[1]), 1U);
+	KUNIT_EXPECT_EQ(test,
+			percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter),
+			free_before + 1);
+	KUNIT_EXPECT_EQ(test,
+			percpu_counter_sum_positive(&ctx->sbi.s_dirs_counter),
+			dirs_before);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->flex_grps[0].free_inodes),
+			(int)(IALLOC_TEST_BG0_FREE_INODES + 63));
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->flex_grps[0].used_dirs), 2);
+	KUNIT_EXPECT_TRUE(test, iblk->marked_dirty);
+	KUNIT_EXPECT_TRUE(test, ctx->gdesc_marked_dirty);
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)ext4_inode_bitmap_csum_verify(sb, &ctx->gdps[1],
+							      &iblk->bh));
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)ext4_group_desc_csum_verify(sb, 1,
+							    &ctx->gdps[1]));
+
+	/* Free directory ino 68 (group 1, bit 3, flex_group 0) */
+	ialloc_test_prep_standalone_free_inode(ctx, 68, S_IFDIR | 0755);
+	ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+
+	KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(3, iblk->cache_buf));
+	KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[1]), 64U);
+	KUNIT_EXPECT_EQ(test, ext4_used_dirs_count(sb, &ctx->gdps[1]), 0U);
+	KUNIT_EXPECT_EQ(test,
+			percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter),
+			free_before + 2);
+	KUNIT_EXPECT_EQ(test,
+			percpu_counter_sum_positive(&ctx->sbi.s_dirs_counter),
+			dirs_before - 1);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->flex_grps[0].used_dirs), 1);
+}
+
+static void ialloc_test_free_inode_guards(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *iblk0, *iblk1;
+	struct inode *inode = &ctx->standalone_ei.vfs_inode;
+
+	iblk0 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(0));
+	iblk1 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+	KUNIT_ASSERT_NOT_NULL(test, iblk0);
+	KUNIT_ASSERT_NOT_NULL(test, iblk1);
+
+	ext4_set_bit(0, iblk1->cache_buf); /* ino 65 */
+	ext4_free_inodes_set(sb, &ctx->gdps[1], 63);
+	ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+
+	/* NULL i_sb is ignored */
+	ialloc_test_prep_standalone_free_inode(ctx, 65, S_IFREG | 0644);
+	inode->i_sb = NULL;
+	ext4_free_inode(NULL, inode);
+	KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(0, iblk1->cache_buf));
+
+	/* i_count > 1 is rejected before clear_inode */
+	ialloc_test_prep_standalone_free_inode(ctx, 65, S_IFREG | 0644);
+	atomic_set(&inode->i_count, 2);
+	ext4_free_inode(NULL, inode);
+	KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(0, iblk1->cache_buf));
+	KUNIT_EXPECT_FALSE(test, !!(inode_state_read_once(inode) & I_CLEAR));
+
+	/* i_nlink > 0 is rejected before clear_inode */
+	ialloc_test_prep_standalone_free_inode(ctx, 65, S_IFREG | 0644);
+	set_nlink(inode, 1);
+	ext4_free_inode(NULL, inode);
+	KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(0, iblk1->cache_buf));
+	KUNIT_EXPECT_FALSE(test, !!(inode_state_read_once(inode) & I_CLEAR));
+
+	/* Reserved ino < EXT4_FIRST_INO(sb) clears VFS inode then errors out */
+	ialloc_test_prep_standalone_free_inode(ctx, 5, S_IFREG | 0644);
+	ext4_free_inode(NULL, inode);
+	KUNIT_EXPECT_TRUE(test, !!(inode_state_read_once(inode) & I_CLEAR));
+	KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(4, iblk0->cache_buf));
+
+	/* Out-of-range ino > s_inodes_count clears VFS inode then errors out */
+	ialloc_test_prep_standalone_free_inode(ctx, IALLOC_TEST_TOTAL_INODES + 1,
+					       S_IFREG | 0644);
+	ext4_free_inode(NULL, inode);
+	KUNIT_EXPECT_TRUE(test, !!(inode_state_read_once(inode) & I_CLEAR));
+}
+
+static void ialloc_test_free_inode_corrupt_and_journal_errors(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *iblk1, *iblk2, *iblk3;
+
+	/* 1. Double-free (bit already clear) marks group bitmap corrupt */
+	iblk1 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+	KUNIT_ASSERT_NOT_NULL(test, iblk1);
+	ialloc_test_prep_standalone_free_inode(ctx, 66, S_IFREG | 0644);
+	ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[1]));
+	KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[1]), 64U);
+
+	/* 2. Pre-corrupt group skips bitmap clear unless EXT4_FC_REPLAY */
+	iblk2 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(2));
+	KUNIT_ASSERT_NOT_NULL(test, iblk2);
+	ext4_set_bit(0, iblk2->cache_buf); /* ino 129 */
+	ext4_free_inodes_set(sb, &ctx->gdps[2], 63);
+	ialloc_test_sync_ibitmap_csum(ctx, 2, true);
+	set_bit(EXT4_GROUP_INFO_IBITMAP_CORRUPT_BIT,
+		&ctx->grp_infos[2].bb_state);
+
+	ialloc_test_prep_standalone_free_inode(ctx, 129, S_IFREG | 0644);
+	ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+	KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(0, iblk2->cache_buf));
+
+	ctx->sbi.s_mount_state |= EXT4_FC_REPLAY;
+	ialloc_test_prep_standalone_free_inode(ctx, 129, S_IFREG | 0644);
+	ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+	ctx->sbi.s_mount_state &= ~EXT4_FC_REPLAY;
+	KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(0, iblk2->cache_buf));
+	KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[2]), 64U);
+
+	/* 3. Write-access failure on bitmap_bh (1st) vs bh2 (2nd) */
+	iblk3 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(3));
+	KUNIT_ASSERT_NOT_NULL(test, iblk3);
+	ext4_set_bit(0, iblk3->cache_buf); /* ino 193 */
+	ext4_free_inodes_set(sb, &ctx->gdps[3], 63);
+	ialloc_test_sync_ibitmap_csum(ctx, 3, true);
+
+	ctx->write_access_calls = 0;
+	ctx->fail_write_access_err = -EIO;
+	ctx->fail_write_access_on_call = 1;
+	ialloc_test_prep_standalone_free_inode(ctx, 193, S_IFREG | 0644);
+	ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+	KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(0, iblk3->cache_buf));
+	KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[3]), 63U);
+
+	ctx->write_access_calls = 0;
+	ctx->fail_write_access_on_call = 2;
+	iblk3->marked_dirty = false;
+	ialloc_test_prep_standalone_free_inode(ctx, 193, S_IFREG | 0644);
+	ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+	KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(0, iblk3->cache_buf));
+	KUNIT_EXPECT_TRUE(test, iblk3->marked_dirty);
+	KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[3]), 63U);
+	ctx->fail_write_access_err = 0;
+	ctx->fail_write_access_on_call = 0;
+}
+
+/* =========================================================================
+ * 5. Orlov Directory Allocator: ext4_find_group_orlov_test
+ * =========================================================================
+ */
+
+static void ialloc_test_orlov_topdir_spread(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct qstr qname = QSTR_INIT("projects", 8);
+	struct inode *subdir;
+	ext4_group_t grp = 0;
+	int ret;
+
+	/*
+	 * Configure group 2 as the unique above-average group with the lowest
+	 * directory count (0 used_dirs, 64 free_inodes, 900 free_clusters).
+	 */
+	ext4_used_dirs_set(sb, &ctx->gdps[0], 5);
+	ext4_used_dirs_set(sb, &ctx->gdps[1], 4);
+	ext4_used_dirs_set(sb, &ctx->gdps[2], 0);
+	ext4_used_dirs_set(sb, &ctx->gdps[3], 6);
+	ext4_free_group_clusters_set(sb, &ctx->gdps[2], 900);
+
+	ret = ext4_find_group_orlov_test(sb, ctx->root_inode, &grp,
+					 S_IFDIR | 0755, &qname);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 2U);
+
+	/* qstr == NULL uses random start, still converges on best group 2 */
+	grp = 0;
+	ret = ext4_find_group_orlov_test(sb, ctx->root_inode, &grp,
+					 S_IFDIR | 0755, NULL);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 2U);
+
+	/* Non-root directory with EXT4_INODE_TOPDIR also uses topdir spread */
+	subdir = new_inode(sb);
+	KUNIT_ASSERT_NOT_NULL(test, subdir);
+	subdir->i_ino = 65;
+	subdir->i_mode = S_IFDIR | 0755;
+	EXT4_I(subdir)->i_block_group = 1;
+	ext4_set_inode_flag(subdir, EXT4_INODE_TOPDIR);
+	ialloc_test_track_inode(test, ctx, subdir);
+
+	grp = 0;
+	ret = ext4_find_group_orlov_test(sb, subdir, &grp, S_IFDIR | 0755,
+					 &qname);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 2U);
+}
+
+static void ialloc_test_orlov_subdir_and_thresholds(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct inode *parent;
+	ext4_group_t grp = 0;
+	int ret;
+
+	parent = new_inode(sb);
+	KUNIT_ASSERT_NOT_NULL(test, parent);
+	parent->i_ino = 65;
+	parent->i_mode = S_IFDIR | 0755;
+	EXT4_I(parent)->i_block_group = 1;
+	ialloc_test_track_inode(test, ctx, parent);
+
+	/* Healthy parent_group (1) is preferred */
+	ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 1U);
+
+	/* i_last_alloc_group overrides parent_group */
+	EXT4_I(parent)->i_last_alloc_group = 3;
+	ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 3U);
+	EXT4_I(parent)->i_last_alloc_group = ~0U;
+
+	/* Group 1 exceeds max_dirs -> advances cyclically to group 2 */
+	ext4_used_dirs_set(sb, &ctx->gdps[1], 40);
+	ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 2U);
+	ext4_used_dirs_set(sb, &ctx->gdps[1], 0);
+
+	/* Group 1 below min_inodes -> advances to group 2 */
+	ext4_free_inodes_set(sb, &ctx->gdps[1], 2);
+	ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 2U);
+	ext4_free_inodes_set(sb, &ctx->gdps[1], IALLOC_TEST_INODES_PER_GRP);
+
+	/* Group 1 below min_clusters -> advances to group 2 */
+	ext4_free_group_clusters_set(sb, &ctx->gdps[1], 10);
+	ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 2U);
+}
+
+static void ialloc_test_orlov_flex_bg_and_fallback(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct inode *parent;
+	ext4_group_t grp = 0;
+	ext4_group_t g;
+	int ret;
+
+	parent = new_inode(sb);
+	KUNIT_ASSERT_NOT_NULL(test, parent);
+	parent->i_ino = 12;
+	parent->i_mode = S_IFDIR | 0755;
+	EXT4_I(parent)->i_block_group = 0;
+	ialloc_test_track_inode(test, ctx, parent);
+
+	/*
+	 * Flex size 2: flex 0 = {0, 1} has excessive dirs; flex 1 = {2, 3}
+	 * is healthy, with group 2 full (0 free inodes) and group 3 having 64
+	 * free inodes -> packs into first available constituent group 3.
+	 */
+	ext4_used_dirs_set(sb, &ctx->gdps[0], 30);
+	ext4_used_dirs_set(sb, &ctx->gdps[1], 30);
+	ext4_free_inodes_set(sb, &ctx->gdps[2], 0);
+	ext4_free_inodes_set(sb, &ctx->gdps[3], 64);
+	percpu_counter_set(&ctx->sbi.s_dirs_counter, 60);
+	percpu_counter_set(&ctx->sbi.s_freeinodes_counter,
+			   IALLOC_TEST_BG0_FREE_INODES + 64 + 0 + 64);
+	ialloc_test_enable_flex_bg(ctx, 1);
+
+	ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 3U);
+
+	/* Disable flex_bg and test fallback_retry with inflated counter */
+	ctx->sbi.s_log_groups_per_flex = 0;
+	ext4_clear_feature_flex_bg(sb);
+	for (g = 0; g < IALLOC_TEST_NGROUPS; g++) {
+		ext4_used_dirs_set(sb, &ctx->gdps[g], 50);
+		ext4_free_inodes_set(sb, &ctx->gdps[g], (g == 3) ? 1U : 0U);
+	}
+	/* Counter claims 200 free inodes (avefreei = 50), only bg 3 has 1 */
+	percpu_counter_set(&ctx->sbi.s_freeinodes_counter, 200);
+
+	ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 3U);
+
+	/* All groups completely full -> returns -1 */
+	ext4_free_inodes_set(sb, &ctx->gdps[3], 0);
+	ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+	KUNIT_EXPECT_EQ(test, ret, -1);
+}
+
+/* =========================================================================
+ * 6. Non-Directory Group Allocator: ext4_find_group_other_test
+ * =========================================================================
+ */
+
+static void ialloc_test_find_group_other_non_flex(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct inode *parent;
+	ext4_group_t grp = 0;
+	ext4_group_t g;
+	int ret;
+
+	parent = new_inode(sb);
+	KUNIT_ASSERT_NOT_NULL(test, parent);
+	parent->i_ino = 65;
+	parent->i_mode = S_IFDIR | 0755;
+	EXT4_I(parent)->i_block_group = 1;
+	ialloc_test_track_inode(test, ctx, parent);
+
+	/* 1. Parent group 1 has both free inodes and free clusters */
+	ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 1U);
+
+	/*
+	 * 2. Quadratic hash probe when parent group 1 has 0 free inodes:
+	 *    start = (1 + 65) % 4 = 2; step i = 1 -> (2 + 1) % 4 = 3.
+	 */
+	ext4_free_inodes_set(sb, &ctx->gdps[1], 0);
+	ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 3U);
+
+	/*
+	 * 3. Linear fallback when no group has free clusters:
+	 *    starts at parent_group = 1, ++*group checks 2 first.
+	 */
+	for (g = 0; g < IALLOC_TEST_NGROUPS; g++)
+		ext4_free_group_clusters_set(sb, &ctx->gdps[g], 0);
+	ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 2U);
+
+	/* 4. Returns -1 when all groups have 0 free inodes */
+	for (g = 0; g < IALLOC_TEST_NGROUPS; g++)
+		ext4_free_inodes_set(sb, &ctx->gdps[g], 0);
+	ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+	KUNIT_EXPECT_EQ(test, ret, -1);
+}
+
+static void ialloc_test_find_group_other_flex_bg(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct inode *parent;
+	ext4_group_t grp = 0;
+	int ret;
+
+	parent = new_inode(sb);
+	KUNIT_ASSERT_NOT_NULL(test, parent);
+	parent->i_ino = 65;
+	parent->i_mode = S_IFDIR | 0755;
+	EXT4_I(parent)->i_block_group = 1;
+	ialloc_test_track_inode(test, ctx, parent);
+
+	/* Flex size 2: parent in bg 1 (flex 0 = {0, 1}) packs into bg 0 first */
+	ialloc_test_enable_flex_bg(ctx, 1);
+	ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 0U);
+
+	/* Flex 0 full, i_last_alloc_group = 3 (flex 1 = {2, 3}) retries flex 1 -> bg 2 */
+	ext4_free_inodes_set(sb, &ctx->gdps[0], 0);
+	ext4_free_inodes_set(sb, &ctx->gdps[1], 0);
+	ialloc_test_enable_flex_bg(ctx, 1);
+	EXT4_I(parent)->i_last_alloc_group = 3;
+	ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 2U);
+
+	/* Flex 0 full and i_last_alloc_group unset -> Orlov fallback picks bg 2 */
+	EXT4_I(parent)->i_last_alloc_group = ~0U;
+	ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, grp, 2U);
+}
+
+/* =========================================================================
+ * 7. Recently Deleted & Bit Search: recently_deleted & find_inode_bit
+ * =========================================================================
+ */
+
+static void ialloc_test_recently_deleted(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *it_slot;
+	struct ext4_inode *raw;
+	u32 now;
+
+	/* 1. EXT4_BG_INODE_UNINIT returns 0 immediately */
+	ctx->gdps[1].bg_flags |= cpu_to_le16(EXT4_BG_INODE_UNINIT);
+	KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 0), 0);
+	ctx->gdps[1].bg_flags &= ~cpu_to_le16(EXT4_BG_INODE_UNINIT);
+
+	/* 2. Inode >= (64 - itable_unused) was never used -> returns 0 */
+	ext4_itable_unused_set(sb, &ctx->gdps[1], 54); /* 0..9 used */
+	KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 10), 0);
+
+	/* 3. Uncached inode table block -> returns 0 */
+	it_slot = ialloc_test_setup_blk(ctx, ialloc_test_itable_blk(1),
+					false, true);
+	KUNIT_ASSERT_NOT_NULL(test, it_slot);
+	raw = (struct ext4_inode *)(it_slot->cache_buf +
+				    2 * IALLOC_TEST_INODE_SIZE);
+	now = ktime_get_real_seconds();
+	raw->i_dtime = cpu_to_le32(now - 10);
+	KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 0);
+
+	/* 4. Cached but !buffer_uptodate -> returns 0 */
+	it_slot->in_cache = true;
+	clear_buffer_uptodate(&it_slot->bh);
+	KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 0);
+	set_buffer_uptodate(&it_slot->bh);
+
+	/* 5. dtime == 0 -> returns 0 */
+	raw->i_dtime = 0;
+	KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 0);
+
+	/* 6. Clean buffer uses 60s window; dirty buffer extends to 360s */
+	now = ktime_get_real_seconds();
+	raw->i_dtime = cpu_to_le32(now - 20);
+	KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 1);
+
+	raw->i_dtime = cpu_to_le32(now - 120);
+	KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 0);
+
+	set_buffer_dirty(&it_slot->bh);
+	KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 1);
+
+	raw->i_dtime = cpu_to_le32(now - 400);
+	KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 0);
+	clear_buffer_dirty(&it_slot->bh);
+}
+
+static void ialloc_test_find_inode_bit(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *iblk, *it_slot;
+	struct ext4_inode *raw1;
+	unsigned long ino;
+	unsigned int bit;
+	int ret;
+
+	iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+	KUNIT_ASSERT_NOT_NULL(test, iblk);
+	ext4_itable_unused_set(sb, &ctx->gdps[1], 0);
+
+	/* Mark all bits except bit 1 (recently deleted) and bit 6 (old) */
+	for (bit = 0; bit < IALLOC_TEST_INODES_PER_GRP; bit++) {
+		if (bit != 1 && bit != 6)
+			ext4_set_bit(bit, iblk->cache_buf);
+	}
+	it_slot = ialloc_test_setup_blk(ctx, ialloc_test_itable_blk(1),
+					true, true);
+	KUNIT_ASSERT_NOT_NULL(test, it_slot);
+	raw1 = (struct ext4_inode *)(it_slot->cache_buf + IALLOC_TEST_INODE_SIZE);
+	raw1->i_dtime = cpu_to_le32((u32)ktime_get_real_seconds() - 10);
+
+	/* No-journal mode skips recently deleted bit 1 in favor of bit 6 */
+	ino = 0;
+	ret = ext4_find_inode_bit_test(sb, 1, &iblk->bh, &ino);
+	KUNIT_EXPECT_EQ(test, ret, 1);
+	KUNIT_EXPECT_EQ(test, ino, 6UL);
+
+	/* Journaled mode ignores recently_deleted and picks bit 1 directly */
+	ctx->sbi.s_journal = (journal_t *)1UL;
+	ino = 0;
+	ret = ext4_find_inode_bit_test(sb, 1, &iblk->bh, &ino);
+	ctx->sbi.s_journal = NULL;
+	KUNIT_EXPECT_EQ(test, ret, 1);
+	KUNIT_EXPECT_EQ(test, ino, 1UL);
+
+	/* Fallback: when bit 6 is also allocated, returns recently deleted bit 1 */
+	ext4_set_bit(6, iblk->cache_buf);
+	ino = 0;
+	ret = ext4_find_inode_bit_test(sb, 1, &iblk->bh, &ino);
+	KUNIT_EXPECT_EQ(test, ret, 1);
+	KUNIT_EXPECT_EQ(test, ino, 1UL);
+
+	/* Full bitmap returns 0 */
+	ext4_set_bit(1, iblk->cache_buf);
+	ino = 0;
+	ret = ext4_find_inode_bit_test(sb, 1, &iblk->bh, &ino);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+}
+
+/* =========================================================================
+ * 8. Marking Specific Inodes Used: ext4_mark_inode_used
+ * =========================================================================
+ */
+
+static void ialloc_test_mark_inode_used_uninit(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *iblk, *bblk;
+	int err;
+
+	/*
+	 * Configure group 2 with both EXT4_BG_INODE_UNINIT and
+	 * EXT4_BG_BLOCK_UNINIT.  Group 2 has no backup superblock, so overhead
+	 * is 1 (bbitmap) + 1 (ibitmap) + 16 (itable) = 18 clusters -> 1006 free.
+	 */
+	iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(2));
+	bblk = ialloc_test_find_blk(ctx, ialloc_test_bbitmap_blk(2));
+	KUNIT_ASSERT_NOT_NULL(test, iblk);
+	KUNIT_ASSERT_NOT_NULL(test, bblk);
+
+	clear_bitmap_uptodate(&iblk->bh);
+	clear_buffer_uptodate(&iblk->bh);
+	clear_buffer_verified(&iblk->bh);
+	ctx->gdps[2].bg_flags = cpu_to_le16(EXT4_BG_INODE_UNINIT |
+					    EXT4_BG_BLOCK_UNINIT);
+	ext4_free_group_clusters_set(sb, &ctx->gdps[2], 0);
+	ext4_itable_unused_set(sb, &ctx->gdps[2], 64);
+	ext4_free_inodes_set(sb, &ctx->gdps[2], 64);
+	ext4_group_desc_csum_set(sb, 2, &ctx->gdps[2]);
+
+	/* Inode 135 is in group 2, bit 6 ((135 - 1) % 64 == 6) */
+	err = ext4_mark_inode_used(sb, 135);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test,
+			le16_to_cpu(ctx->gdps[2].bg_flags) &
+			(EXT4_BG_INODE_UNINIT | EXT4_BG_BLOCK_UNINIT), 0);
+	KUNIT_EXPECT_EQ(test, ext4_free_group_clusters(sb, &ctx->gdps[2]),
+			1006U);
+	KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(6, iblk->cache_buf));
+	KUNIT_EXPECT_EQ(test, ext4_itable_unused_count(sb, &ctx->gdps[2]), 57U);
+	KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[2]), 63U);
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)ext4_block_bitmap_csum_verify(sb, &ctx->gdps[2],
+							      &bblk->bh));
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)ext4_inode_bitmap_csum_verify(sb, &ctx->gdps[2],
+							      &iblk->bh));
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)ext4_group_desc_csum_verify(sb, 2,
+							    &ctx->gdps[2]));
+
+	/* Idempotent when bit 6 is already set */
+	err = ext4_mark_inode_used(sb, 135);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[2]), 63U);
+
+	/* Lower bit 2 (ino 131) decrements free_inodes without raising itable_unused */
+	err = ext4_mark_inode_used(sb, 131);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(2, iblk->cache_buf));
+	KUNIT_EXPECT_EQ(test, ext4_itable_unused_count(sb, &ctx->gdps[2]), 57U);
+	KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[2]), 62U);
+}
+
+static void ialloc_test_mark_inode_used_errors(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *iblk;
+
+	/* Bounds checks: below EXT4_FIRST_INO and above max_ino */
+	KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 10), -EFSCORRUPTED);
+	KUNIT_EXPECT_EQ(test,
+			ext4_mark_inode_used(sb, IALLOC_TEST_TOTAL_INODES + 1),
+			-EFSCORRUPTED);
+
+	/* Inode bitmap read I/O failure */
+	iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+	KUNIT_ASSERT_NOT_NULL(test, iblk);
+	clear_bitmap_uptodate(&iblk->bh);
+	clear_buffer_uptodate(&iblk->bh);
+	ctx->fail_read_eio = true;
+	KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 65), -EIO);
+	ctx->fail_read_eio = false;
+
+	/* Dirty metadata error on group 2 (ino 129) */
+	ctx->fail_dirty_metadata_err = -EIO;
+	KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 129), -EIO);
+	ctx->fail_dirty_metadata_err = 0;
+
+	/* sync_dirty_buffer I/O error on unmapped dirty buffer (ino 130) */
+	ctx->fail_sync_dirty_buffer = true;
+	KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 130), -EIO);
+	ctx->fail_sync_dirty_buffer = false;
+
+	/* Block bitmap read and wait failures during EXT4_BG_BLOCK_UNINIT init */
+	ctx->gdps[3].bg_flags |= cpu_to_le16(EXT4_BG_BLOCK_UNINIT);
+	ext4_group_desc_csum_set(sb, 3, &ctx->gdps[3]);
+	ctx->fail_bbitmap_err = -EIO;
+	KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 193), -EIO);
+	ctx->fail_bbitmap_err = 0;
+
+	ctx->fail_wait_bbitmap_err = -EFSCORRUPTED;
+	KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 194), -EFSCORRUPTED);
+	ctx->fail_wait_bbitmap_err = 0;
+}
+
+/* =========================================================================
+ * 9. Xattr Credits & Inode Allocation: __ext4_new_inode
+ * =========================================================================
+ */
+
+#ifdef CONFIG_EXT4_FS_POSIX_ACL
+static struct posix_acl *ialloc_test_err_get_inode_acl(struct inode *inode,
+						       int type, bool rcu)
+{
+	return ERR_PTR(-EIO);
+}
+
+static const struct inode_operations ialloc_test_err_acl_iops = {
+	.get_inode_acl = ialloc_test_err_get_inode_acl,
+};
+#endif
+
+static void ialloc_test_xattr_credits_for_new_inode(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	int base, enc_creds, base_ea, enc_ea_creds;
+
+	base = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+						     S_IFREG | 0644, false);
+	KUNIT_ASSERT_GE(test, base, 0);
+	if (!IS_ENABLED(CONFIG_SECURITY))
+		KUNIT_EXPECT_EQ(test, base, 0);
+
+	/* Without ea_inode, encryption xattr adds 7 credits */
+	enc_creds = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+							  S_IFREG | 0644, true);
+	KUNIT_EXPECT_EQ(test, enc_creds, base + 7);
+
+	/* With ea_inode on 1024B blocks, 40B fscrypt context adds 17 credits */
+	ext4_set_feature_ea_inode(sb);
+	base_ea = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+							S_IFREG | 0644, false);
+	enc_ea_creds = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+							     S_IFREG | 0644,
+							     true);
+	KUNIT_EXPECT_EQ(test, enc_ea_creds, base_ea + 17);
+	ext4_clear_feature_ea_inode(sb);
+
+#ifdef CONFIG_EXT4_FS_POSIX_ACL
+	{
+		struct posix_acl *acl;
+		const struct inode_operations *orig_iops;
+		int reg_acl, dir_acl, err_acl;
+
+		acl = posix_acl_alloc(1, GFP_KERNEL);
+		KUNIT_ASSERT_NOT_NULL(test, acl);
+		set_cached_acl(ctx->root_inode, ACL_TYPE_DEFAULT, acl);
+		posix_acl_release(acl);
+
+		reg_acl = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+								S_IFREG | 0644,
+								false);
+		dir_acl = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+								S_IFDIR | 0755,
+								false);
+		KUNIT_EXPECT_EQ(test, reg_acl, base + 7);
+		KUNIT_EXPECT_EQ(test, dir_acl, base + 14);
+		forget_cached_acl(ctx->root_inode, ACL_TYPE_DEFAULT);
+
+		orig_iops = ctx->root_inode->i_op;
+		ctx->root_inode->i_op = &ialloc_test_err_acl_iops;
+		sb->s_flags |= SB_POSIXACL;
+		err_acl = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+								S_IFREG | 0644,
+								false);
+		sb->s_flags &= ~SB_POSIXACL;
+		ctx->root_inode->i_op = orig_iops;
+		KUNIT_EXPECT_EQ(test, err_acl, -EIO);
+	}
+#endif
+}
+
+static void ialloc_test_new_inode_types_and_flags(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	handle_t *nojournal;
+	struct ialloc_test_blk *iblk0;
+	struct inode *reg, *dir, *lnk, *fifo, *ea_ino;
+	__le32 inum_le, gen_le;
+	u32 expected_seed;
+	s64 free_after_reg;
+
+	nojournal = ext4_journal_start_sb(sb, EXT4_HT_INODE, 2);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(nojournal));
+
+	ext4_set_feature_inline_data(sb);
+	ialloc_test_enable_flex_bg(ctx, 1);
+	EXT4_I(ctx->root_inode)->i_flags = EXT4_SYNC_FL | EXT4_NODUMP_FL |
+					   EXT4_DIRSYNC_FL | EXT4_TOPDIR_FL |
+					   EXT4_EXTENTS_FL;
+
+	/* 1. Regular file in group 0 (first free bit is 10 -> ino 11) */
+	reg = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644,
+			     NULL, 0, NULL, 0);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(reg));
+	ialloc_test_track_inode(test, ctx, reg);
+	KUNIT_EXPECT_EQ(test, reg->i_ino, 11UL);
+	KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(reg, EXT4_INODE_EXTENTS));
+	KUNIT_EXPECT_EQ(test, ext_inode_hdr(reg)->eh_magic, EXT4_EXT_MAGIC);
+	KUNIT_EXPECT_TRUE(test, ext4_test_inode_state(reg, EXT4_STATE_NEW));
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_inode_state(reg, EXT4_STATE_MAY_INLINE_DATA));
+	KUNIT_EXPECT_EQ(test,
+			EXT4_I(reg)->i_flags &
+			(EXT4_SYNC_FL | EXT4_NODUMP_FL | EXT4_DIRSYNC_FL |
+			 EXT4_TOPDIR_FL),
+			(u32)(EXT4_SYNC_FL | EXT4_NODUMP_FL));
+	KUNIT_EXPECT_EQ(test, EXT4_I(reg)->i_extra_isize,
+			(u16)IALLOC_TEST_EXTRA_ISIZE);
+
+	inum_le = cpu_to_le32(reg->i_ino);
+	gen_le = cpu_to_le32(reg->i_generation);
+	expected_seed = ext4_chksum(ctx->sbi.s_csum_seed, (u8 *)&inum_le,
+				    sizeof(inum_le));
+	expected_seed = ext4_chksum(expected_seed, (u8 *)&gen_le,
+				    sizeof(gen_le));
+	KUNIT_EXPECT_EQ(test, EXT4_I(reg)->i_csum_seed, expected_seed);
+
+	/*
+	 * 2. Directory inherits DIRSYNC (unlike regular files), still strips
+	 *    non-inheritable TOPDIR, and increments directory counters.
+	 */
+	dir = ext4_new_inode(nojournal, ctx->root_inode, S_IFDIR | 0755,
+			     NULL, 12, NULL, 0);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(dir));
+	ialloc_test_track_inode(test, ctx, dir);
+	KUNIT_EXPECT_EQ(test, dir->i_ino, 12UL);
+	KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(dir, EXT4_INODE_EXTENTS));
+	KUNIT_EXPECT_EQ(test,
+			EXT4_I(dir)->i_flags &
+			(EXT4_SYNC_FL | EXT4_NODUMP_FL | EXT4_DIRSYNC_FL |
+			 EXT4_TOPDIR_FL),
+			(u32)(EXT4_SYNC_FL | EXT4_NODUMP_FL |
+			      EXT4_DIRSYNC_FL));
+	KUNIT_EXPECT_EQ(test, ext4_used_dirs_count(sb, &ctx->gdps[0]), 2U);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->flex_grps[0].used_dirs), 2);
+
+	/* 3. Symlink gets EXT4_INODE_EXTENTS; FIFO does not */
+	lnk = ext4_new_inode(nojournal, ctx->root_inode, S_IFLNK | 0777,
+			     NULL, 13, NULL, 0);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(lnk));
+	ialloc_test_track_inode(test, ctx, lnk);
+	KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(lnk, EXT4_INODE_EXTENTS));
+
+	fifo = ext4_new_inode(nojournal, ctx->root_inode, S_IFIFO | 0600,
+			      NULL, 14, NULL, 0);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fifo));
+	ialloc_test_track_inode(test, ctx, fifo);
+	KUNIT_EXPECT_FALSE(test, ext4_test_inode_flag(fifo, EXT4_INODE_EXTENTS));
+
+	/* 4. EXT4_EA_INODE_FL suppresses EXT4_STATE_MAY_INLINE_DATA */
+	ea_ino = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0600,
+				NULL, 15, NULL, EXT4_EA_INODE_FL);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ea_ino));
+	ialloc_test_track_inode(test, ctx, ea_ino);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(ea_ino,
+						 EXT4_STATE_MAY_INLINE_DATA));
+
+	/* 5. Full lifecycle round-trip: unlink + iput frees ino 11 back */
+	iblk0 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(0));
+	KUNIT_ASSERT_NOT_NULL(test, iblk0);
+	KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(10, iblk0->cache_buf));
+	free_after_reg = percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter);
+
+	ialloc_test_untrack_inode(ctx, reg);
+	clear_nlink(reg);
+	iput(reg);
+
+	KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(10, iblk0->cache_buf));
+	KUNIT_EXPECT_EQ(test,
+			percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter),
+			free_after_reg + 1);
+
+	ext4_journal_stop(nojournal);
+}
+
+static void ialloc_test_new_inode_ownership_and_projid(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	handle_t *nojournal;
+	uid_t explicit_owner[2] = { 1001, 2002 };
+	struct inode *ino_own, *ino_grpid, *ino_sgid;
+
+	nojournal = ext4_journal_start_sb(sb, EXT4_HT_INODE, 2);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(nojournal));
+
+	i_gid_write(ctx->root_inode, 3003);
+	ext4_set_feature_project(sb);
+	EXT4_I(ctx->root_inode)->i_projid = make_kprojid(&init_user_ns, 42);
+	ext4_set_inode_flag(ctx->root_inode, EXT4_INODE_PROJINHERIT);
+
+	/* Explicit owner overrides uid/gid and inherits projid 42 */
+	ino_own = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0640,
+				 NULL, 0, explicit_owner, 0);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ino_own));
+	ialloc_test_track_inode(test, ctx, ino_own);
+	KUNIT_EXPECT_EQ(test, i_uid_read(ino_own), 1001U);
+	KUNIT_EXPECT_EQ(test, i_gid_read(ino_own), 2002U);
+	KUNIT_EXPECT_EQ(test,
+			from_kprojid(&init_user_ns, EXT4_I(ino_own)->i_projid),
+			42U);
+
+	/* Without PROJINHERIT on parent, child gets EXT4_DEF_PROJID (0) and GRPID gid */
+	ext4_clear_inode_flag(ctx->root_inode, EXT4_INODE_PROJINHERIT);
+	set_opt(sb, GRPID);
+	ino_grpid = ext4_new_inode(nojournal, ctx->root_inode, S_IFDIR | 0750,
+				   NULL, 0, NULL, 0);
+	clear_opt(sb, GRPID);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ino_grpid));
+	ialloc_test_track_inode(test, ctx, ino_grpid);
+	KUNIT_EXPECT_EQ(test, i_gid_read(ino_grpid), 3003U);
+	KUNIT_EXPECT_FALSE(test, !!(ino_grpid->i_mode & S_ISGID));
+	KUNIT_EXPECT_EQ(test,
+			from_kprojid(&init_user_ns, EXT4_I(ino_grpid)->i_projid),
+			0U);
+
+	/* Standard inode_init_owner propagates parent's S_ISGID to child dir */
+	ctx->root_inode->i_mode |= S_ISGID;
+	ino_sgid = ext4_new_inode(nojournal, ctx->root_inode, S_IFDIR | 0755,
+				  NULL, 0, NULL, 0);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ino_sgid));
+	ialloc_test_track_inode(test, ctx, ino_sgid);
+	KUNIT_EXPECT_EQ(test, i_gid_read(ino_sgid), 3003U);
+	KUNIT_EXPECT_TRUE(test, !!(ino_sgid->i_mode & S_ISGID));
+
+	ext4_journal_stop(nojournal);
+}
+
+static void ialloc_test_new_inode_goal_uninit_and_corrupt_skip(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	handle_t *nojournal;
+	struct ialloc_test_blk *iblk0, *iblk2;
+	struct inode *ino_goal, *ino_sb_goal, *ino_skip, *ino_started;
+	struct inode *ino_replay;
+
+	nojournal = ext4_journal_start_sb(sb, EXT4_HT_INODE, 2);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(nojournal));
+
+	/* 1. Explicit goal = 140 in uninitialized group 2 (bit 11) */
+	iblk2 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(2));
+	KUNIT_ASSERT_NOT_NULL(test, iblk2);
+	clear_bitmap_uptodate(&iblk2->bh);
+	clear_buffer_uptodate(&iblk2->bh);
+	clear_buffer_verified(&iblk2->bh);
+	ctx->gdps[2].bg_flags = cpu_to_le16(EXT4_BG_INODE_UNINIT |
+					    EXT4_BG_BLOCK_UNINIT);
+	ext4_group_desc_csum_set(sb, 2, &ctx->gdps[2]);
+
+	ino_goal = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644,
+				  NULL, 140, NULL, 0);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ino_goal));
+	ialloc_test_track_inode(test, ctx, ino_goal);
+	KUNIT_EXPECT_EQ(test, ino_goal->i_ino, 140UL);
+	KUNIT_EXPECT_EQ(test,
+			le16_to_cpu(ctx->gdps[2].bg_flags) &
+			(EXT4_BG_INODE_UNINIT | EXT4_BG_BLOCK_UNINIT), 0);
+	KUNIT_EXPECT_EQ(test, ext4_free_group_clusters(sb, &ctx->gdps[2]),
+			1006U);
+	KUNIT_EXPECT_EQ(test, ext4_itable_unused_count(sb, &ctx->gdps[2]), 52U);
+
+	/* 2. sbi->s_inode_goal = 200 (group 3, bit 7) used when goal == 0 */
+	ctx->sbi.s_inode_goal = 200;
+	ino_sb_goal = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644,
+				     NULL, 0, NULL, 0);
+	ctx->sbi.s_inode_goal = 0;
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ino_sb_goal));
+	ialloc_test_track_inode(test, ctx, ino_sb_goal);
+	KUNIT_EXPECT_EQ(test, ino_sb_goal->i_ino, 200UL);
+
+	/*
+	 * 3. Reserved bit 3 (ino 4 < EXT4_FIRST_INO) cleared in group 0:
+	 *    marks group 0 IBITMAP_CORRUPT and advances to group 1 (ino 65).
+	 */
+	iblk0 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(0));
+	KUNIT_ASSERT_NOT_NULL(test, iblk0);
+	ext4_clear_bit(3, iblk0->cache_buf);
+	ialloc_test_sync_ibitmap_csum(ctx, 0, true);
+
+	ino_skip = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644,
+				  NULL, 4, NULL, 0);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ino_skip));
+	ialloc_test_track_inode(test, ctx, ino_skip);
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[0]));
+	KUNIT_EXPECT_EQ(test, ino_skip->i_ino, 65UL);
+
+	ext4_journal_stop(nojournal);
+
+	/* 4. ext4_new_inode_start_handle (handle == NULL) starts handle */
+	KUNIT_ASSERT_NULL(test, ext4_journal_current_handle());
+	ino_started = ext4_new_inode_start_handle(&nop_mnt_idmap,
+						  ctx->root_inode,
+						  S_IFREG | 0644, NULL, 66,
+						  NULL, EXT4_HT_INODE, 2);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ino_started));
+	ialloc_test_track_inode(test, ctx, ino_started);
+	KUNIT_EXPECT_EQ(test, ino_started->i_ino, 66UL);
+	KUNIT_ASSERT_NOT_NULL(test, ext4_journal_current_handle());
+	ext4_journal_stop(ext4_journal_current_handle());
+	KUNIT_EXPECT_NULL(test, ext4_journal_current_handle());
+
+	/*
+	 * 5. EXT4_FC_REPLAY with handle == NULL allocates exact goal ino 15 in
+	 *    group 0 despite IBITMAP_CORRUPT and without starting a handle.
+	 */
+	ext4_set_bit(3, iblk0->cache_buf);
+	ialloc_test_sync_ibitmap_csum(ctx, 0, true);
+	ctx->sbi.s_mount_state |= EXT4_FC_REPLAY;
+	ino_replay = ext4_new_inode(NULL, ctx->root_inode, S_IFREG | 0644,
+				    NULL, 15, NULL, 0);
+	ctx->sbi.s_mount_state &= ~EXT4_FC_REPLAY;
+	KUNIT_ASSERT_FALSE(test, IS_ERR(ino_replay));
+	ialloc_test_track_inode(test, ctx, ino_replay);
+	KUNIT_EXPECT_EQ(test, ino_replay->i_ino, 15UL);
+	KUNIT_EXPECT_NULL(test, ext4_journal_current_handle());
+}
+
+static void ialloc_test_new_inode_errors(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	handle_t *nojournal;
+	struct inode *unlinked_dir, *dup_existing, *res;
+	struct ialloc_test_blk *iblk;
+	ext4_group_t g;
+
+	nojournal = ext4_journal_start_sb(sb, EXT4_HT_INODE, 2);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(nojournal));
+
+	/* 1. NULL dir or unlinked dir -> -EPERM */
+	res = ext4_new_inode(nojournal, NULL, S_IFREG | 0644, NULL, 0, NULL, 0);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EPERM);
+
+	unlinked_dir = new_inode(sb);
+	KUNIT_ASSERT_NOT_NULL(test, unlinked_dir);
+	unlinked_dir->i_ino = 50;
+	unlinked_dir->i_mode = S_IFDIR | 0755;
+	clear_nlink(unlinked_dir);
+	ialloc_test_track_inode(test, ctx, unlinked_dir);
+	res = ext4_new_inode(nojournal, unlinked_dir, S_IFREG | 0644, NULL, 0,
+			     NULL, 0);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EPERM);
+
+	/* 2. Emergency shutdown (-EIO) and emergency RO (-EROFS) */
+	set_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     0, NULL, 0);
+	clear_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+
+	set_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     0, NULL, 0);
+	clear_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EROFS);
+
+	/* 3. VFS alloc_inode failure -> -ENOMEM */
+	ctx->fail_alloc_inode = true;
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     0, NULL, 0);
+	ctx->fail_alloc_inode = false;
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -ENOMEM);
+
+	/* 4. Journal write-access and dirty-metadata failures in group 1 */
+	iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+	KUNIT_ASSERT_NOT_NULL(test, iblk);
+
+	ctx->write_access_calls = 0;
+	ctx->fail_write_access_err = -EIO;
+	ctx->fail_write_access_on_call = 1;
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     70, NULL, 0);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+	KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(5, iblk->cache_buf));
+
+	ctx->write_access_calls = 0;
+	ctx->fail_write_access_on_call = 2;
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     71, NULL, 0);
+	ctx->fail_write_access_err = 0;
+	ctx->fail_write_access_on_call = 0;
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+	KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(6, iblk->cache_buf));
+	ext4_clear_bit(6, iblk->cache_buf);
+	ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+
+	ctx->dirty_metadata_calls = 0;
+	ctx->fail_dirty_metadata_err = -EIO;
+	ctx->fail_dirty_metadata_on_call = 1;
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     71, NULL, 0);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+	ext4_clear_bit(6, iblk->cache_buf);
+	ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+
+	ctx->dirty_metadata_calls = 0;
+	ctx->fail_dirty_metadata_on_call = 2;
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     71, NULL, 0);
+	ctx->fail_dirty_metadata_err = 0;
+	ctx->fail_dirty_metadata_on_call = 0;
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+	ext4_clear_bit(6, iblk->cache_buf);
+	ext4_free_inodes_set(sb, &ctx->gdps[1], 64);
+	ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+
+#ifdef CONFIG_EXT4_FS_SECURITY
+	/* 5. ext4_init_security failure rolls back allocated inode bit */
+	ctx->fail_init_security_err = -EACCES;
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     70, NULL, 0);
+	ctx->fail_init_security_err = 0;
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EACCES);
+	KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(5, iblk->cache_buf));
+	KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[1]), 64U);
+#endif
+
+	/* 6. ext4_mark_inode_dirty failure rolls back allocated inode bit */
+	ctx->fail_mark_inode_dirty_err = -EIO;
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     70, NULL, 0);
+	ctx->fail_mark_inode_dirty_err = 0;
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+	KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(5, iblk->cache_buf));
+	KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[1]), 64U);
+
+	/*
+	 * 7. Doubly-allocated inode (insert_inode_locked collision):
+	 *    Pre-hash ino 140 in VFS cache, leave only bg 2 non-empty with bit 11
+	 *    clear -> marks bg 2 IBITMAP_CORRUPT and returns -EIO.
+	 */
+	dup_existing = new_inode(sb);
+	KUNIT_ASSERT_NOT_NULL(test, dup_existing);
+	dup_existing->i_ino = 140;
+	dup_existing->i_mode = S_IFREG | 0644;
+	KUNIT_ASSERT_EQ(test, insert_inode_locked(dup_existing), 0);
+	ialloc_test_track_inode(test, ctx, dup_existing);
+
+	ext4_free_inodes_set(sb, &ctx->gdps[0], 0);
+	ext4_free_inodes_set(sb, &ctx->gdps[1], 0);
+	ext4_free_inodes_set(sb, &ctx->gdps[3], 0);
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     140, NULL, 0);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[2]));
+
+	/* 8. Descriptor claims free inodes in bg 3, but bitmap is all ones -> -ENOSPC */
+	iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(3));
+	KUNIT_ASSERT_NOT_NULL(test, iblk);
+	memset(iblk->cache_buf, 0xff, IALLOC_TEST_BLK_SIZE);
+	ialloc_test_sync_ibitmap_csum(ctx, 3, true);
+	ext4_free_inodes_set(sb, &ctx->gdps[2], 0);
+	ext4_free_inodes_set(sb, &ctx->gdps[3], 5);
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     193, NULL, 0);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -ENOSPC);
+
+	/* 9. All groups report 0 free inodes -> -ENOSPC before loop */
+	for (g = 0; g < IALLOC_TEST_NGROUPS; g++)
+		ext4_free_inodes_set(sb, &ctx->gdps[g], 0);
+	res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+			     0, NULL, 0);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(res), -ENOSPC);
+
+	ext4_journal_stop(nojournal);
+}
+
+/* =========================================================================
+ * 10. Orphans, Counters, & Lazy Init: ext4_orphan_get, ext4_count_*,
+ *     ext4_init_inode_table
+ * =========================================================================
+ */
+
+static void ialloc_test_orphan_get(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ialloc_test_blk *iblk1;
+	struct inode *orphan, *got;
+
+	/* Bounds checks */
+	KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_orphan_get(sb, 5)), -EFSCORRUPTED);
+	KUNIT_EXPECT_EQ(test,
+			PTR_ERR(ext4_orphan_get(sb, IALLOC_TEST_TOTAL_INODES + 1)),
+			-EFSCORRUPTED);
+
+	/* Clear bit in bitmap -> -EFSCORRUPTED */
+	KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_orphan_get(sb, 65)), -EFSCORRUPTED);
+
+	/* Mark bit 0 (ino 65) allocated in group 1 */
+	iblk1 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+	KUNIT_ASSERT_NOT_NULL(test, iblk1);
+	ext4_set_bit(0, iblk1->cache_buf);
+	ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+
+	/* Bitmap read error propagates */
+	clear_bitmap_uptodate(&iblk1->bh);
+	clear_buffer_uptodate(&iblk1->bh);
+	ctx->fail_read_eio = true;
+	KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_orphan_get(sb, 65)), -EIO);
+	ctx->fail_read_eio = false;
+	set_buffer_uptodate(&iblk1->bh);
+	set_bitmap_uptodate(&iblk1->bh);
+	set_buffer_verified(&iblk1->bh);
+
+	/* ext4_iget error propagates */
+	ctx->fail_iget_err = -EIO;
+	KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_orphan_get(sb, 65)), -EIO);
+	ctx->fail_iget_err = 0;
+
+	/* Prepare mock orphan inode */
+	orphan = new_inode(sb);
+	KUNIT_ASSERT_NOT_NULL(test, orphan);
+	orphan->i_ino = 65;
+	ialloc_test_track_inode(test, ctx, orphan);
+	ctx->iget_inode = orphan;
+
+	/* 1. Valid unlinked orphan (i_nlink == 0, NEXT_ORPHAN <= max_ino) */
+	orphan->i_mode = S_IFREG | 0644;
+	clear_nlink(orphan);
+	orphan->i_blocks = 8;
+	NEXT_ORPHAN(orphan) = 0;
+	got = ext4_orphan_get(sb, 65);
+	KUNIT_ASSERT_PTR_EQ(test, got, orphan);
+	iput(got);
+
+	/* 2. Valid live truncating regular file orphan (i_nlink == 1) */
+	set_nlink(orphan, 1);
+	ext4_set_inode_flag(orphan, EXT4_INODE_EXTENTS);
+	got = ext4_orphan_get(sb, 65);
+	KUNIT_ASSERT_PTR_EQ(test, got, orphan);
+	iput(got);
+
+	/* 3. Live non-truncatable FIFO (i_nlink == 1) -> -EFSCORRUPTED */
+	orphan->i_mode = S_IFIFO | 0600;
+	got = ext4_orphan_get(sb, 65);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(got), -EFSCORRUPTED);
+
+	/* 4. Unlinked orphan with NEXT_ORPHAN > max_ino zeroes i_blocks */
+	orphan->i_mode = S_IFREG | 0644;
+	clear_nlink(orphan);
+	orphan->i_blocks = 24;
+	NEXT_ORPHAN(orphan) = IALLOC_TEST_TOTAL_INODES + 50;
+	got = ext4_orphan_get(sb, 65);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(got), -EFSCORRUPTED);
+	KUNIT_EXPECT_EQ(test, orphan->i_blocks, (blkcnt_t)0);
+
+	/* 5. Unlinked bad_inode zeroes i_blocks and returns -EFSCORRUPTED */
+	NEXT_ORPHAN(orphan) = 0;
+	orphan->i_blocks = 16;
+	make_bad_inode(orphan);
+	got = ext4_orphan_get(sb, 65);
+	KUNIT_EXPECT_EQ(test, PTR_ERR(got), -EFSCORRUPTED);
+	KUNIT_EXPECT_EQ(test, orphan->i_blocks, (blkcnt_t)0);
+}
+
+static void ialloc_test_count_free_inodes_and_dirs(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+
+	/* Include 64-bit high-16-bit values (0x10000 + low) and a NULL gdp */
+	ext4_free_inodes_set(sb, &ctx->gdps[0], 10);
+	ext4_free_inodes_set(sb, &ctx->gdps[1], 0x10020);
+	ext4_free_inodes_set(sb, &ctx->gdps[2], 999); /* masked out below */
+	ext4_free_inodes_set(sb, &ctx->gdps[3], 5);
+
+	ext4_used_dirs_set(sb, &ctx->gdps[0], 2);
+	ext4_used_dirs_set(sb, &ctx->gdps[1], 0x20003);
+	ext4_used_dirs_set(sb, &ctx->gdps[2], 777); /* masked out below */
+	ext4_used_dirs_set(sb, &ctx->gdps[3], 4);
+
+	ctx->fail_gdp_mask = BIT(2);
+
+	KUNIT_EXPECT_EQ(test, ext4_count_free_inodes(sb), 0x1002fUL);
+	KUNIT_EXPECT_EQ(test, ext4_count_dirs(sb), 0x20009UL);
+}
+
+static void ialloc_test_init_inode_table(struct kunit *test)
+{
+	struct ialloc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	int err;
+
+	/* 1. Already EXT4_BG_INODE_ZEROED is a no-op */
+	ctx->gdps[1].bg_flags = cpu_to_le16(EXT4_BG_INODE_ZEROED);
+	err = ext4_init_inode_table(sb, 1, 1);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, ctx->zeroout_calls, 0U);
+
+	/* 2. Uninitialized group 1 zeroes all 16 blocks with barrier = 1 */
+	ctx->gdps[1].bg_flags = cpu_to_le16(EXT4_BG_INODE_UNINIT);
+	err = ext4_init_inode_table(sb, 1, 1);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, ctx->zeroout_calls, 1U);
+	KUNIT_EXPECT_EQ(test, ctx->last_zeroout_blk, ialloc_test_itable_blk(1));
+	KUNIT_EXPECT_EQ(test, ctx->last_zeroout_num, 16);
+	KUNIT_EXPECT_EQ(test, ctx->last_zeroout_barrier, 1);
+	KUNIT_EXPECT_TRUE(test,
+			  !!(le16_to_cpu(ctx->gdps[1].bg_flags) &
+			     EXT4_BG_INODE_ZEROED));
+	KUNIT_EXPECT_TRUE(test,
+			  (bool)ext4_group_desc_csum_verify(sb, 1,
+							    &ctx->gdps[1]));
+
+	/*
+	 * 3. Initialized group 2 with itable_unused = 54 -> used_inos = 10 ->
+	 *    used_blks = DIV_ROUND_UP(10, 4) = 3 -> zeroes remaining 13 blocks.
+	 */
+	ctx->gdps[2].bg_flags = 0;
+	ext4_itable_unused_set(sb, &ctx->gdps[2], 54);
+	err = ext4_init_inode_table(sb, 2, 0);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, ctx->zeroout_calls, 2U);
+	KUNIT_EXPECT_EQ(test, ctx->last_zeroout_blk,
+			ialloc_test_itable_blk(2) + 3);
+	KUNIT_EXPECT_EQ(test, ctx->last_zeroout_num, 13);
+	KUNIT_EXPECT_EQ(test, ctx->last_zeroout_barrier, 0);
+	KUNIT_EXPECT_TRUE(test,
+			  !!(le16_to_cpu(ctx->gdps[2].bg_flags) &
+			     EXT4_BG_INODE_ZEROED));
+
+	/* 4. Full inode table (itable_unused = 0 -> num = 0) skips zeroout, sets ZEROED */
+	ctx->gdps[3].bg_flags = 0;
+	ext4_itable_unused_set(sb, &ctx->gdps[3], 0);
+	err = ext4_init_inode_table(sb, 3, 1);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, ctx->zeroout_calls, 2U);
+	KUNIT_EXPECT_TRUE(test,
+			  !!(le16_to_cpu(ctx->gdps[3].bg_flags) &
+			     EXT4_BG_INODE_ZEROED));
+
+	/* 5. Corrupt itable_unused > inodes_per_group returns 1 without ZEROED */
+	ctx->gdps[3].bg_flags = 0;
+	ext4_itable_unused_set(sb, &ctx->gdps[3], 100);
+	err = ext4_init_inode_table(sb, 3, 0);
+	KUNIT_EXPECT_EQ(test, err, 1);
+	KUNIT_EXPECT_FALSE(test,
+			   !!(le16_to_cpu(ctx->gdps[3].bg_flags) &
+			      EXT4_BG_INODE_ZEROED));
+
+	/* 6. Group 0 with used_inos (4) < EXT4_FIRST_INO (11) returns 1 */
+	ctx->gdps[0].bg_flags = 0;
+	ext4_itable_unused_set(sb, &ctx->gdps[0], 60);
+	err = ext4_init_inode_table(sb, 0, 0);
+	KUNIT_EXPECT_EQ(test, err, 1);
+	KUNIT_EXPECT_FALSE(test,
+			   !!(le16_to_cpu(ctx->gdps[0].bg_flags) &
+			      EXT4_BG_INODE_ZEROED));
+
+	/* 7. Zeroout I/O failure (-EIO) and emergency RO (-EROFS) */
+	ctx->gdps[3].bg_flags = cpu_to_le16(EXT4_BG_INODE_UNINIT);
+	ctx->fail_itable_zeroout_err = -EIO;
+	err = ext4_init_inode_table(sb, 3, 0);
+	ctx->fail_itable_zeroout_err = 0;
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_FALSE(test,
+			   !!(le16_to_cpu(ctx->gdps[3].bg_flags) &
+			      EXT4_BG_INODE_ZEROED));
+
+	set_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+	err = ext4_init_inode_table(sb, 3, 0);
+	clear_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+	KUNIT_EXPECT_EQ(test, err, -EROFS);
+}
+
+static struct kunit_case ialloc_test_cases[] = {
+	KUNIT_CASE_PARAM(ialloc_test_mark_bitmap_end,
+			 mark_bitmap_end_gen_params),
+	KUNIT_CASE(ialloc_test_end_bitmap_read_ok),
+	KUNIT_CASE(ialloc_test_end_bitmap_read_ioerr),
+	KUNIT_CASE(ialloc_test_validate_ibitmap_csum),
+	KUNIT_CASE(ialloc_test_validate_ibitmap_short_circuits),
+	KUNIT_CASE(ialloc_test_read_ibitmap_disk_and_cache),
+	KUNIT_CASE(ialloc_test_read_ibitmap_uninit),
+	KUNIT_CASE(ialloc_test_read_ibitmap_errors),
+	KUNIT_CASE(ialloc_test_free_inode_reg_and_dir),
+	KUNIT_CASE(ialloc_test_free_inode_guards),
+	KUNIT_CASE(ialloc_test_free_inode_corrupt_and_journal_errors),
+	KUNIT_CASE(ialloc_test_orlov_topdir_spread),
+	KUNIT_CASE(ialloc_test_orlov_subdir_and_thresholds),
+	KUNIT_CASE(ialloc_test_orlov_flex_bg_and_fallback),
+	KUNIT_CASE(ialloc_test_find_group_other_non_flex),
+	KUNIT_CASE(ialloc_test_find_group_other_flex_bg),
+	KUNIT_CASE(ialloc_test_recently_deleted),
+	KUNIT_CASE(ialloc_test_find_inode_bit),
+	KUNIT_CASE(ialloc_test_mark_inode_used_uninit),
+	KUNIT_CASE(ialloc_test_mark_inode_used_errors),
+	KUNIT_CASE(ialloc_test_xattr_credits_for_new_inode),
+	KUNIT_CASE(ialloc_test_new_inode_types_and_flags),
+	KUNIT_CASE(ialloc_test_new_inode_ownership_and_projid),
+	KUNIT_CASE(ialloc_test_new_inode_goal_uninit_and_corrupt_skip),
+	KUNIT_CASE(ialloc_test_new_inode_errors),
+	KUNIT_CASE(ialloc_test_orphan_get),
+	KUNIT_CASE(ialloc_test_count_free_inodes_and_dirs),
+	KUNIT_CASE(ialloc_test_init_inode_table),
+	{}
+};
+
+static struct kunit_suite ialloc_test_suite = {
+	.name = "ext4_ialloc_test",
+	.init = ialloc_test_init,
+	.test_cases = ialloc_test_cases,
+};
+
+kunit_test_suites(&ialloc_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 inode allocation and deallocation");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c
index 02546b828836..66215b635932 100644
--- a/fs/ext4/ialloc.c
+++ b/fs/ext4/ialloc.c
@@ -66,6 +66,7 @@ void ext4_mark_bitmap_end(int start_bit, int end_bit, char *bitmap)
 	if (i < end_bit)
 		memset(bitmap + (i >> 3), 0xff, (end_bit - i) >> 3);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mark_bitmap_end);
 
 void ext4_end_bitmap_read(struct bio *bio)
 {
@@ -78,6 +79,7 @@ void ext4_end_bitmap_read(struct bio *bio)
 	}
 	unlock_buffer(bh);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_end_bitmap_read);
 
 static int ext4_validate_inode_bitmap(struct super_block *sb,
 				      struct ext4_group_desc *desc,
@@ -144,7 +146,7 @@ ext4_read_inode_bitmap(struct super_block *sb, ext4_group_t block_group)
 					EXT4_GROUP_INFO_IBITMAP_CORRUPT);
 		return ERR_PTR(-EFSCORRUPTED);
 	}
-	bh = sb_getblk(sb, bitmap_blk);
+	bh = ext4_sb_getblk(sb, bitmap_blk);
 	if (unlikely(!bh)) {
 		ext4_warning(sb, "Cannot read inode bitmap - "
 			     "block_group = %u, inode_bitmap = %llu",
@@ -698,7 +700,7 @@ static int recently_deleted(struct super_block *sb, ext4_group_t group, int ino)
 	     ino >= EXT4_INODES_PER_GROUP(sb) - ext4_itable_unused_count(sb, gdp)))
 		return 0;
 
-	bh = sb_find_get_block(sb, ext4_inode_table(sb, gdp) +
+	bh = ext4_sb_find_get_block(sb, ext4_inode_table(sb, gdp) +
 		       (ino / inodes_per_block));
 	if (!bh || !buffer_uptodate(bh))
 		/*
@@ -878,6 +880,7 @@ int ext4_mark_inode_used(struct super_block *sb, int ino)
 	brelse(inode_bitmap_bh);
 	return err;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mark_inode_used);
 
 static int ext4_xattr_credits_for_new_inode(struct inode *dir, mode_t mode,
 					    bool encrypt)
@@ -1383,6 +1386,7 @@ struct inode *__ext4_new_inode(struct mnt_idmap *idmap,
 	brelse(inode_bitmap_bh);
 	return ERR_PTR(err);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_new_inode);
 
 /* Verify that we are loading a valid orphan from disk */
 struct inode *ext4_orphan_get(struct super_block *sb, unsigned long ino)
@@ -1456,6 +1460,7 @@ struct inode *ext4_orphan_get(struct super_block *sb, unsigned long ino)
 	brelse(bitmap_bh);
 	return ERR_PTR(err);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_orphan_get);
 
 unsigned long ext4_count_free_inodes(struct super_block *sb)
 {
@@ -1506,6 +1511,7 @@ unsigned long ext4_count_free_inodes(struct super_block *sb)
 	return desc_count;
 #endif
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_count_free_inodes);
 
 /* Called at mount-time, super-block is locked */
 unsigned long ext4_count_dirs(struct super_block * sb)
@@ -1521,6 +1527,23 @@ unsigned long ext4_count_dirs(struct super_block * sb)
 	}
 	return count;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_count_dirs);
+
+int ext4_itable_zeroout(struct super_block *sb, ext4_fsblk_t blk, int num,
+			int barrier)
+{
+	int ret;
+
+	KUNIT_STATIC_STUB_REDIRECT(ext4_itable_zeroout, sb, blk, num, barrier);
+
+	ret = sb_issue_zeroout(sb, blk, num, GFP_NOFS);
+	if (ret < 0)
+		return ret;
+	if (barrier)
+		blkdev_issue_flush(sb->s_bdev);
+	return ret;
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_itable_zeroout);
 
 /*
  * Zeroes not yet zeroed inode table - just write zeroes through the whole
@@ -1617,11 +1640,9 @@ int ext4_init_inode_table(struct super_block *sb, ext4_group_t group,
 
 	ext4_debug("going to zero out inode table in group %d\n",
 		   group);
-	ret = sb_issue_zeroout(sb, blk, num, GFP_NOFS);
+	ret = ext4_itable_zeroout(sb, blk, num, barrier);
 	if (ret < 0)
 		goto err_out;
-	if (barrier)
-		blkdev_issue_flush(sb->s_bdev);
 
 skip_zeroout:
 	ext4_lock_group(sb, group);
@@ -1640,8 +1661,58 @@ int ext4_init_inode_table(struct super_block *sb, ext4_group_t group,
 out:
 	return ret;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_inode_table);
 
 #if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
-EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_new_inode);
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mark_inode_used);
+int ext4_validate_inode_bitmap_test(struct super_block *sb,
+				    struct ext4_group_desc *desc,
+				    ext4_group_t block_group,
+				    struct buffer_head *bh)
+{
+	return ext4_validate_inode_bitmap(sb, desc, block_group, bh);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_validate_inode_bitmap_test);
+
+struct buffer_head *ext4_read_inode_bitmap_test(struct super_block *sb,
+						ext4_group_t block_group)
+{
+	return ext4_read_inode_bitmap(sb, block_group);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_inode_bitmap_test);
+
+int ext4_find_group_orlov_test(struct super_block *sb, struct inode *parent,
+			       ext4_group_t *group, umode_t mode,
+			       const struct qstr *qstr)
+{
+	return find_group_orlov(sb, parent, group, mode, qstr);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_group_orlov_test);
+
+int ext4_find_group_other_test(struct super_block *sb, struct inode *parent,
+			       ext4_group_t *group, umode_t mode)
+{
+	return find_group_other(sb, parent, group, mode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_group_other_test);
+
+int ext4_recently_deleted_test(struct super_block *sb, ext4_group_t group,
+			       int ino)
+{
+	return recently_deleted(sb, group, ino);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_recently_deleted_test);
+
+int ext4_find_inode_bit_test(struct super_block *sb, ext4_group_t group,
+			     struct buffer_head *bitmap, unsigned long *ino)
+{
+	return find_inode_bit(sb, group, bitmap, ino);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_inode_bit_test);
+
+int ext4_xattr_credits_for_new_inode_test(struct inode *dir, mode_t mode,
+					  bool encrypt)
+{
+	return ext4_xattr_credits_for_new_inode(dir, mode, encrypt);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_credits_for_new_inode_test);
 #endif
diff --git a/fs/ext4/super.c b/fs/ext4/super.c
index 74604a677ca4..88bd28cedd99 100644
--- a/fs/ext4/super.c
+++ b/fs/ext4/super.c
@@ -201,6 +201,9 @@ EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_bh_nowait);
 int ext4_read_bh(struct buffer_head *bh, blk_opf_t op_flags,
 		 bio_end_io_t end_io, bool simu_fail)
 {
+	KUNIT_STATIC_STUB_REDIRECT(ext4_read_bh, bh, op_flags,
+				   end_io, simu_fail);
+
 	BUG_ON(!buffer_locked(bh));
 
 	if (ext4_buffer_uptodate(bh)) {
@@ -215,6 +218,7 @@ int ext4_read_bh(struct buffer_head *bh, blk_opf_t op_flags,
 		return 0;
 	return -EIO;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_bh);
 
 int ext4_read_bh_lock(struct buffer_head *bh, blk_opf_t op_flags, bool wait)
 {
@@ -348,6 +352,7 @@ ext4_fsblk_t ext4_block_bitmap(struct super_block *sb,
 		(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
 		 (ext4_fsblk_t)le32_to_cpu(bg->bg_block_bitmap_hi) << 32 : 0);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_bitmap);
 
 ext4_fsblk_t ext4_inode_bitmap(struct super_block *sb,
 			       struct ext4_group_desc *bg)
@@ -356,6 +361,7 @@ ext4_fsblk_t ext4_inode_bitmap(struct super_block *sb,
 		(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
 		 (ext4_fsblk_t)le32_to_cpu(bg->bg_inode_bitmap_hi) << 32 : 0);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_bitmap);
 
 ext4_fsblk_t ext4_inode_table(struct super_block *sb,
 			      struct ext4_group_desc *bg)
@@ -364,6 +370,7 @@ ext4_fsblk_t ext4_inode_table(struct super_block *sb,
 		(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
 		 (ext4_fsblk_t)le32_to_cpu(bg->bg_inode_table_hi) << 32 : 0);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_table);
 
 __u32 ext4_free_group_clusters(struct super_block *sb,
 			       struct ext4_group_desc *bg)
@@ -380,6 +387,7 @@ __u32 ext4_free_inodes_count(struct super_block *sb,
 		(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
 		 (__u32)le16_to_cpu(READ_ONCE(bg->bg_free_inodes_count_hi)) << 16 : 0);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_inodes_count);
 
 __u32 ext4_used_dirs_count(struct super_block *sb,
 			      struct ext4_group_desc *bg)
@@ -388,6 +396,7 @@ __u32 ext4_used_dirs_count(struct super_block *sb,
 		(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
 		 (__u32)le16_to_cpu(bg->bg_used_dirs_count_hi) << 16 : 0);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_used_dirs_count);
 
 __u32 ext4_itable_unused_count(struct super_block *sb,
 			      struct ext4_group_desc *bg)
@@ -396,6 +405,7 @@ __u32 ext4_itable_unused_count(struct super_block *sb,
 		(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
 		 (__u32)le16_to_cpu(bg->bg_itable_unused_hi) << 16 : 0);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_itable_unused_count);
 
 void ext4_block_bitmap_set(struct super_block *sb,
 			   struct ext4_group_desc *bg, ext4_fsblk_t blk)
@@ -404,6 +414,7 @@ void ext4_block_bitmap_set(struct super_block *sb,
 	if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
 		bg->bg_block_bitmap_hi = cpu_to_le32(blk >> 32);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_bitmap_set);
 
 void ext4_inode_bitmap_set(struct super_block *sb,
 			   struct ext4_group_desc *bg, ext4_fsblk_t blk)
@@ -438,6 +449,7 @@ void ext4_free_inodes_set(struct super_block *sb,
 	if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
 		WRITE_ONCE(bg->bg_free_inodes_count_hi, cpu_to_le16(count >> 16));
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_inodes_set);
 
 void ext4_used_dirs_set(struct super_block *sb,
 			  struct ext4_group_desc *bg, __u32 count)
@@ -446,6 +458,7 @@ void ext4_used_dirs_set(struct super_block *sb,
 	if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
 		bg->bg_used_dirs_count_hi = cpu_to_le16(count >> 16);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_used_dirs_set);
 
 void ext4_itable_unused_set(struct super_block *sb,
 			  struct ext4_group_desc *bg, __u32 count)
@@ -454,6 +467,7 @@ void ext4_itable_unused_set(struct super_block *sb,
 	if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
 		bg->bg_itable_unused_hi = cpu_to_le16(count >> 16);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_itable_unused_set);
 
 static void __ext4_update_tstamp(__le32 *lo, __u8 *hi, time64_t now)
 {
@@ -1152,6 +1166,7 @@ void ext4_mark_group_bitmap_corrupted(struct super_block *sb,
 		}
 	}
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mark_group_bitmap_corrupted);
 
 void ext4_update_dynamic_rev(struct super_block *sb)
 {
@@ -3332,6 +3347,7 @@ int ext4_group_desc_csum_verify(struct super_block *sb, __u32 block_group,
 
 	return 1;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_group_desc_csum_verify);
 
 void ext4_group_desc_csum_set(struct super_block *sb, __u32 block_group,
 			      struct ext4_group_desc *gdp)
@@ -3340,6 +3356,7 @@ void ext4_group_desc_csum_set(struct super_block *sb, __u32 block_group,
 		return;
 	gdp->bg_checksum = ext4_group_desc_csum(sb, block_group, gdp);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_group_desc_csum_set);
 
 /* Called at mount-time, super-block is locked */
 static int ext4_check_descriptors(struct super_block *sb,
diff --git a/fs/ext4/xattr_security.c b/fs/ext4/xattr_security.c
index 776cf11d24ca..9b6c7126a971 100644
--- a/fs/ext4/xattr_security.c
+++ b/fs/ext4/xattr_security.c
@@ -4,6 +4,7 @@
  * Handler for storing security labels as extended attributes.
  */
 
+#include <kunit/static_stub.h>
 #include <linux/string.h>
 #include <linux/fs.h>
 #include <linux/security.h>
@@ -55,9 +56,12 @@ int
 ext4_init_security(handle_t *handle, struct inode *inode, struct inode *dir,
 		   const struct qstr *qstr)
 {
+	KUNIT_STATIC_STUB_REDIRECT(ext4_init_security,
+				   handle, inode, dir, qstr);
 	return security_inode_init_security(inode, dir, qstr,
 					    &ext4_initxattrs, handle);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_security);
 
 const struct xattr_handler ext4_xattr_security_handler = {
 	.prefix	= XATTR_SECURITY_PREFIX,
-- 
2.56.0.542.g3ef89b7020-goog


  parent reply	other threads:[~2026-11-12  4:07 UTC|newest]

Thread overview: 55+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-12  4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
2026-11-12  4:06 ` [PATCH 01/27] ext4: add KUinit tests for fs/ext4/extents_status.c Theodore Ts'o
2026-11-12  4:18   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 02/27] ext4: add KUnit tests for fs/ext4/extents.c Theodore Ts'o
2026-11-12  4:28   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 03/27] ext4: add KUnit tests for ext4_ext_split() leaf and index splitting Theodore Ts'o
2026-11-12  4:15   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 04/27] ext4: add KUnit tests for fs/ext4/xattr.c Theodore Ts'o
2026-11-12  4:26   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 05/27] ext4: add KUnit tests for fs/ext4/namei.c Theodore Ts'o
2026-11-12  4:21   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 06/27] ext4: expand KUnit tests for fs/ext4/mballoc.c Theodore Ts'o
2026-11-12  4:26   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 07/27] ext4: add KUnit tests for fs/ext4/inline.c Theodore Ts'o
2026-11-12  4:29   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 08/27] ext4: zero s_fc_replay_state in ext4_fc_replay_cleanup() Theodore Ts'o
2026-11-12  4:16   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 09/27] ext4: add KUnit tests for fs/ext4/fast_commit.c Theodore Ts'o
2026-11-12  4:28   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 10/27] ext4: expand KUnit tests for fs/ext4/inode.c Theodore Ts'o
2026-11-12  4:36   ` sashiko-bot
2026-11-12  4:06 ` Theodore Ts'o [this message]
2026-11-12  4:25   ` [PATCH 11/27] ext4: add KUnit tests for fs/ext4/ialloc.c sashiko-bot
2026-11-12  4:06 ` [PATCH 12/27] ext4: add KUnit tests for fs/ext4/balloc.c Theodore Ts'o
2026-11-12  4:16   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 13/27] ext4: add KUnit tests for fs/ext4/orphan.c Theodore Ts'o
2026-11-12  4:18   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 14/27] ext4: add KUnit tests for fs/ext4/block_validity.c Theodore Ts'o
2026-11-12  4:21   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 15/27] ext4: check multi-block overlap on left traversal in add_system_zone() Theodore Ts'o
2026-11-12  4:10   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 16/27] ext4: enable full feature coverage in fs/ext4/.kunitconfig Theodore Ts'o
2026-11-12  4:24   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 17/27] ext4: add KUnit tests for fs/ext4/super.c Theodore Ts'o
2026-11-12  4:27   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 18/27] ext4: add KUnit tests for fs/ext4/sysfs.c Theodore Ts'o
2026-11-12  4:32   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 19/27] ext4: add KUnit tests for fs/ext4/ioctl.c Theodore Ts'o
2026-11-12  4:35   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 20/27] ext4: add KUnit tests for fs/ext4/resize.c Theodore Ts'o
2026-11-12  4:34   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 21/27] ext4: add KUnit tests for fs/ext4/migrate.c Theodore Ts'o
2026-11-12  4:37   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 22/27] ext4: add KUnit tests for fs/ext4/indirect.c Theodore Ts'o
2026-11-12  4:46   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 23/27] ext4: expand KUnit tests in fs/ext4/extents-test.c for file range operations Theodore Ts'o
2026-11-12  4:34   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 24/27] ext4: add KUnit tests for fs/ext4/fsmap.c Theodore Ts'o
2026-11-12  4:30   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 25/27] ext4: add KUnit tests for fs/ext4/move_extent.c Theodore Ts'o
2026-11-12  4:36   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 26/27] ext4: add KUnit tests for fs/ext4/mmp.c Theodore Ts'o
2026-11-12  4:29   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 27/27] ext4: add KUnit tests for fs/ext4/dir.c Theodore Ts'o
2026-11-12  4:41   ` sashiko-bot

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=20261112040638.1576836-12-tytso@mit.edu \
    --to=tytso@mit.edu \
    --cc=linux-ext4@vger.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