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 04/27] ext4: add KUnit tests for fs/ext4/xattr.c
Date: Wed, 11 Nov 2026 23:06:15 -0500	[thread overview]
Message-ID: <20261112040638.1576836-5-tytso@mit.edu> (raw)
In-Reply-To: <20261112040638.1576836-1-tytso@mit.edu>

Add the ext4_xattr_test KUnit test suite in fs/ext4/xattr-test.c to
exercise the core extended attribute operations in fs/ext4/xattr.c
without requiring a mounted block device:

- Entry/value sizing macros (EXT4_XATTR_LEN, EXT4_XATTR_SIZE,
  EXT4_XATTR_MIN_LARGE_EA_SIZE), EXT4_INODE_HAS_XATTR_SPACE boundary
  checks, and xattr_sem locking helpers (ext4_write_lock_xattr,
  ext4_write_trylock_xattr, ext4_write_unlock_xattr) preserving
  EXT4_STATE_NO_EXPAND.
- In-inode xattr validation (xattr_check_inode / __xattr_check_inode)
  across 11 parameterized valid and corrupted on-disk layouts (bad
  magic, truncated headers, out-of-bounds names, unterminated entry
  lists, non-zero offset on EA inode entries, and overlapping or
  out-of-bounds value ranges).
- In-inode (ibody) get/set operations (ext4_xattr_get,
  ext4_xattr_ibody_get, ext4_xattr_set_handle, ext4_xattr_set),
  XATTR_CREATE and XATTR_REPLACE flag enforcement, EXT4_STATE_NEW raw
  inode initialization, superblock EXT4_FEATURE_COMPAT_EXT_ATTR update,
  and the identical-value fastpath (ext4_xattr_value_same).
- In-inode value replacement (same padded size vs. shifted offsets),
  zero-length values, entry removal, and EXT4_I(inode)->i_inline_off
  adjustment when system.data is present.
- External xattr block allocation, entry sorting by (e_name_index,
  e_name_len, e_name), migration from ibody to external block,
  EXT4_XATTR_BLOCK_RESERVE enforcement when ea_inode is enabled, and
  external block header and metadata CRC32C checksum verification.
- External xattr block mb_cache deduplication across inodes,
  copy-on-write cloning when modifying a shared block, and
  EXT4_XATTR_REFCOUNT_MAX reuse bit transitions.
- Attribute listing (ext4_listxattr) across ibody and external block
  entries, and user/hurd/trusted xattr handler prefix and mount-option
  filtering.
- Large xattr inode (EXT4_FEATURE_INCOMPAT_EA_INODE) multi-block
  write/read, 64-bit refcount and hash encoding, mb_cache deduplication,
  reference decrement to zero, and ext4_evict_ea_inode() cache cleanup.
- EA inode hash verification (including legacy signed-char name hash
  fallback), ext4_xattr_inode_iget() corruption checks, and Lustre-style
  EA inode compatibility.
- Inode quota usage calculation (ext4_get_inode_usage), journal credit
  estimation (__ext4_xattr_set_credits, ext4_xattr_set_credits), extra
  isize expansion (ext4_expand_extra_isize_ea) for in-place shifts and
  external block eviction, inode xattr deletion
  (ext4_xattr_delete_inode), and error-injection rollback paths.

Export the tested xattr entry points with EXPORT_SYMBOL_FOR_EXT4_TEST
and add KUNIT_STATIC_STUB_REDIRECT hooks for ext4_xattr_getblk,
ext4_sb_bread, ext4_map_blocks, ext4_getblk, ext4_get_inode_loc,
ext4_mark_iloc_dirty, __ext4_iget, and __ext4_new_inode.

Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
 fs/ext4/Makefile        |    2 +-
 fs/ext4/ext4.h          |    2 +
 fs/ext4/ialloc.c        |    9 +
 fs/ext4/inline.c        |    4 +
 fs/ext4/inode.c         |   17 +-
 fs/ext4/super.c         |   11 +-
 fs/ext4/xattr-test.c    | 1801 +++++++++++++++++++++++++++++++++++++++
 fs/ext4/xattr.c         |   30 +-
 fs/ext4/xattr.h         |    2 +
 fs/ext4/xattr_hurd.c    |    4 +
 fs/ext4/xattr_trusted.c |    4 +
 fs/ext4/xattr_user.c    |    4 +
 12 files changed, 1886 insertions(+), 4 deletions(-)
 create mode 100644 fs/ext4/xattr-test.c

diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 74b485dbb229..13040b97d2ff 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -16,7 +16,7 @@ ext4-$(CONFIG_EXT4_FS_POSIX_ACL)	+= acl.o
 ext4-$(CONFIG_EXT4_FS_SECURITY)		+= xattr_security.o
 ext4-test-objs				+= inode-test.o mballoc-test.o \
 					   extents-test.o hash-test.o \
-					   extents_status-test.o
+					   extents_status-test.o xattr-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/ext4.h b/fs/ext4/ext4.h
index 724a27e8be61..b57176b61d80 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -4072,6 +4072,8 @@ extern int ext4_block_write_begin(handle_t *handle, struct folio *folio,
 #if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
 #define EXPORT_SYMBOL_FOR_EXT4_TEST(sym) \
 	EXPORT_SYMBOL_FOR_MODULES(sym, "ext4-test")
+#else
+#define EXPORT_SYMBOL_FOR_EXT4_TEST(sym)
 #endif
 #endif	/* __KERNEL__ */
 
diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c
index a5831fc536db..02f3ef05acaf 100644
--- a/fs/ext4/ialloc.c
+++ b/fs/ext4/ialloc.c
@@ -23,6 +23,7 @@
 #include <linux/bitops.h>
 #include <linux/blkdev.h>
 #include <linux/cred.h>
+#include <kunit/static_stub.h>
 
 #include <asm/byteorder.h>
 
@@ -953,6 +954,10 @@ struct inode *__ext4_new_inode(struct mnt_idmap *idmap,
 	struct ext4_group_info *grp = NULL;
 	bool encrypt = false;
 
+	KUNIT_STATIC_STUB_REDIRECT(__ext4_new_inode, idmap, handle, dir, mode,
+				   qstr, goal, owner, i_flags, handle_type,
+				   line_no, nblocks);
+
 	/* Cannot create files in a deleted directory */
 	if (!dir || !dir->i_nlink)
 		return ERR_PTR(-EPERM);
@@ -1630,3 +1635,7 @@ int ext4_init_inode_table(struct super_block *sb, ext4_group_t group,
 out:
 	return ret;
 }
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_new_inode);
+#endif
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index ceee69a66482..0e662a9f7574 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -2016,3 +2016,7 @@ int ext4_convert_inline_data(struct inode *inode)
 	brelse(iloc.bh);
 	return error;
 }
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_inline_data_nolock);
+#endif
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 289f7fa35090..c8a06c455b3f 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -713,6 +713,8 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode,
 	memcpy(&orig_map, map, sizeof(*map));
 #endif
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_map_blocks, handle, inode, map, flags);
+
 	map->m_flags = 0;
 	ext_debug(inode, "flag 0x%x, max_blocks %u, logical block %lu\n",
 		  flags, map->m_len, (unsigned long) map->m_lblk);
@@ -863,6 +865,7 @@ int ext4_map_blocks(handle_t *handle, struct inode *inode,
 			    map->m_len - 1);
 	return retval;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_map_blocks);
 
 /*
  * Update EXT4_MAP_FLAGS in bh->b_state. For buffer heads attached to pages
@@ -985,6 +988,9 @@ struct buffer_head *ext4_getblk(handle_t *handle, struct inode *inode,
 	bool nowait = map_flags & EXT4_GET_BLOCKS_CACHED_NOWAIT;
 	int err;
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_getblk, handle, inode, block,
+				   map_flags);
+
 	ASSERT((EXT4_SB(inode->i_sb)->s_mount_state & EXT4_FC_REPLAY)
 		    || handle != NULL || create == 0);
 	ASSERT(create == 0 || !nowait);
@@ -1047,6 +1053,7 @@ struct buffer_head *ext4_getblk(handle_t *handle, struct inode *inode,
 	brelse(bh);
 	return ERR_PTR(err);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_getblk);
 
 struct buffer_head *ext4_bread(handle_t *handle, struct inode *inode,
 			       ext4_lblk_t block, int map_flags)
@@ -5051,6 +5058,8 @@ int ext4_get_inode_loc(struct inode *inode, struct ext4_iloc *iloc)
 	ext4_fsblk_t err_blk = 0;
 	int ret;
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_get_inode_loc, inode, iloc);
+
 	ret = __ext4_get_inode_loc(inode->i_sb, inode->i_ino, inode, iloc,
 					&err_blk);
 
@@ -5060,6 +5069,7 @@ int ext4_get_inode_loc(struct inode *inode, struct ext4_iloc *iloc)
 
 	return ret;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_inode_loc);
 
 /*
  * ext4_get_inode_loc_noio() is a best-effort variant of ext4_get_inode_loc().
@@ -5336,6 +5346,8 @@ struct inode *__ext4_iget(struct super_block *sb, unsigned long ino,
 	gid_t i_gid;
 	projid_t i_projid;
 
+	KUNIT_STATIC_STUB_REDIRECT(__ext4_iget, sb, ino, flags, function, line);
+
 	if ((!(flags & EXT4_IGET_SPECIAL) && is_special_ino(sb, ino)) ||
 	    (ino < EXT4_ROOT_INO) ||
 	    (ino > le32_to_cpu(es->s_inodes_count))) {
@@ -5704,6 +5716,7 @@ struct inode *__ext4_iget(struct super_block *sb, unsigned long ino,
 	iget_failed(inode);
 	return ERR_PTR(ret);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_iget);
 
 static void __ext4_update_other_inode_time(struct super_block *sb,
 					   unsigned long orig_ino,
@@ -6464,6 +6477,8 @@ int ext4_mark_iloc_dirty(handle_t *handle,
 	struct super_block *sb = inode->i_sb;
 	int err = 0;
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_mark_iloc_dirty, handle, inode, iloc);
+
 	err = ext4_emergency_state(sb);
 	if (unlikely(err)) {
 		put_bh(iloc->bh);
@@ -6497,6 +6512,7 @@ int ext4_mark_iloc_dirty(handle_t *handle,
 	}
 	return err;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mark_iloc_dirty);
 
 /*
  * On success, We end up with an outstanding reference count against
@@ -6986,5 +7002,4 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_update_reserve_space);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_release_space);
 EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_mark_inode_dirty);
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_map_blocks);
 #endif
diff --git a/fs/ext4/super.c b/fs/ext4/super.c
index bca0dc87d0b7..62ec5323bb25 100644
--- a/fs/ext4/super.c
+++ b/fs/ext4/super.c
@@ -49,6 +49,7 @@
 #include <linux/fs_context.h>
 #include <linux/fs_parser.h>
 #include <linux/fserror.h>
+#include <kunit/static_stub.h>
 
 #include "ext4.h"
 #include "ext4_extents.h"	/* Needed for trace points definition */
@@ -251,7 +252,11 @@ static struct buffer_head *__ext4_sb_bread_gfp(struct super_block *sb,
 struct buffer_head *ext4_sb_bread(struct super_block *sb, sector_t block,
 				   blk_opf_t op_flags)
 {
-	gfp_t gfp = mapping_gfp_constraint(sb->s_bdev->bd_mapping,
+	gfp_t gfp;
+
+	KUNIT_STATIC_STUB_REDIRECT(ext4_sb_bread, sb, block, op_flags);
+
+	gfp = mapping_gfp_constraint(sb->s_bdev->bd_mapping,
 			~__GFP_FS) | __GFP_MOVABLE;
 
 	return __ext4_sb_bread_gfp(sb, block, op_flags, gfp);
@@ -7639,3 +7644,7 @@ MODULE_DESCRIPTION("Fourth Extended Filesystem");
 MODULE_LICENSE("GPL");
 module_init(ext4_init_fs)
 module_exit(ext4_exit_fs)
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_bread);
+#endif
diff --git a/fs/ext4/xattr-test.c b/fs/ext4/xattr-test.c
new file mode 100644
index 000000000000..35be150353a2
--- /dev/null
+++ b/fs/ext4/xattr-test.c
@@ -0,0 +1,1801 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for ext4 extended attributes (fs/ext4/xattr.c).
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/cred.h>
+#include <linux/dcache.h>
+#include <linux/iversion.h>
+#include <linux/mbcache.h>
+#include <linux/quotaops.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/xattr.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "xattr.h"
+
+#define TEST_BLK_SIZE		1024
+#define TEST_BLK_BITS		10
+#define TEST_INODE_SIZE		256
+#define TEST_EXTRA_ISIZE	(sizeof(struct ext4_inode) - \
+				 EXT4_GOOD_OLD_INODE_SIZE)
+#define TEST_FIRST_PBLK		100
+#define TEST_MAX_BLOCKS		16
+#define TEST_MAX_EA_INODES	8
+#define TEST_PRIMARY_INO	12
+#define TEST_SECONDARY_INO	13
+#define TEST_FIRST_EA_INO	20
+#define TEST_CSUM_SEED		0x87654321U
+
+struct mock_block {
+	bool in_use;
+	struct buffer_head bh;
+	char data[TEST_BLK_SIZE] __aligned(8);
+};
+
+struct mock_inode_slot {
+	bool allocated;
+	struct ext4_inode_info ei;
+	struct buffer_head iloc_bh;
+	char raw_buf[TEST_INODE_SIZE] __aligned(8);
+	ext4_fsblk_t lblk_to_pblk[8];
+};
+
+struct xattr_test_ctx {
+	struct super_block sb;
+	struct ext4_sb_info sbi;
+	struct ext4_super_block es;
+	struct block_device bdev;
+	struct address_space bd_mapping;
+	struct buffer_head sb_bh;
+	struct dentry root_dentry;
+
+	struct mock_inode_slot primary;
+	struct mock_inode_slot secondary;
+	struct mock_inode_slot ea_inodes[TEST_MAX_EA_INODES];
+	struct mock_block blocks[TEST_MAX_BLOCKS];
+
+	int fail_get_inode_loc;
+	int fail_mark_iloc_dirty;
+	int fail_dirty_metadata;
+	int fail_alloc_block;
+	int fail_sb_bread;
+	int fail_getblk;
+	int fail_iget;
+	int fail_new_inode;
+	bool fail_xattr_getblk;
+
+	unsigned int alloc_block_calls;
+	unsigned int free_block_calls;
+	ext4_fsblk_t last_freed_block;
+	unsigned int mark_iloc_dirty_calls;
+	unsigned int mark_inode_dirty_calls;
+	unsigned int dirty_metadata_calls;
+};
+
+static const struct super_operations xattr_test_sops = {};
+
+#ifdef CONFIG_QUOTA
+static qsize_t *xattr_test_get_reserved_space(struct inode *inode)
+{
+	return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations xattr_test_qops = {
+	.get_reserved_space = xattr_test_get_reserved_space,
+};
+#endif
+
+static void init_mock_inode_slot(struct xattr_test_ctx *ctx,
+				 struct mock_inode_slot *slot,
+				 unsigned long ino)
+{
+	struct inode *inode = &slot->ei.vfs_inode;
+	struct ext4_inode *raw_inode;
+
+	memset(slot, 0, sizeof(*slot));
+	slot->allocated = true;
+
+	inode->i_sb = &ctx->sb;
+	inode->i_ino = ino;
+	inode->i_mode = S_IFREG | 0644;
+	inode->i_blkbits = TEST_BLK_BITS;
+	inode->i_mapping = &inode->i_data;
+	set_nlink(inode, 1);
+	atomic_set(&inode->i_count, 1);
+
+	spin_lock_init(&inode->i_lock);
+	init_rwsem(&inode->i_rwsem);
+	INIT_LIST_HEAD(&inode->i_io_list);
+	INIT_LIST_HEAD(&inode->i_wb_list);
+	INIT_LIST_HEAD(&inode->i_lru);
+	INIT_HLIST_NODE(&inode->i_hash);
+	/*
+	 * Pre-set all I_DIRTY flags so mark_inode_dirty() from quota helpers
+	 * sees the flags already set and returns without touching global bdi
+	 * writeback lists.
+	 */
+	inode_state_set_raw(inode, I_DIRTY);
+
+	init_rwsem(&slot->ei.xattr_sem);
+	init_rwsem(&slot->ei.i_data_sem);
+	spin_lock_init(&slot->ei.i_raw_lock);
+	spin_lock_init(&slot->ei.i_block_reservation_lock);
+	INIT_LIST_HEAD(&slot->ei.i_orphan);
+	slot->ei.i_extra_isize = TEST_EXTRA_ISIZE;
+	slot->ei.i_csum_seed = TEST_CSUM_SEED ^ (u32)ino;
+
+	slot->iloc_bh.b_data = slot->raw_buf;
+	slot->iloc_bh.b_size = TEST_INODE_SIZE;
+	slot->iloc_bh.b_blocknr = 10 + ino;
+	slot->iloc_bh.b_bdev = &ctx->bdev;
+	atomic_set(&slot->iloc_bh.b_count, 1);
+	set_buffer_uptodate(&slot->iloc_bh);
+	set_buffer_mapped(&slot->iloc_bh);
+
+	raw_inode = (struct ext4_inode *)slot->raw_buf;
+	raw_inode->i_extra_isize = cpu_to_le16(slot->ei.i_extra_isize);
+}
+
+static struct mock_block *alloc_mock_block(struct xattr_test_ctx *ctx)
+{
+	int i;
+
+	for (i = 0; i < TEST_MAX_BLOCKS; i++) {
+		struct mock_block *blk = &ctx->blocks[i];
+
+		if (!blk->in_use) {
+			memset(blk, 0, sizeof(*blk));
+			blk->in_use = true;
+			blk->bh.b_data = blk->data;
+			blk->bh.b_size = TEST_BLK_SIZE;
+			blk->bh.b_blocknr = TEST_FIRST_PBLK + i;
+			blk->bh.b_bdev = &ctx->bdev;
+			atomic_set(&blk->bh.b_count, 1);
+			set_buffer_uptodate(&blk->bh);
+			set_buffer_mapped(&blk->bh);
+			return blk;
+		}
+	}
+	return NULL;
+}
+
+static struct mock_block *find_mock_block(struct xattr_test_ctx *ctx,
+					  ext4_fsblk_t blocknr)
+{
+	int i;
+
+	for (i = 0; i < TEST_MAX_BLOCKS; i++) {
+		if (ctx->blocks[i].in_use &&
+		    ctx->blocks[i].bh.b_blocknr == blocknr)
+			return &ctx->blocks[i];
+	}
+	return NULL;
+}
+
+static int ext4_get_inode_loc_stub(struct inode *inode, struct ext4_iloc *iloc)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct xattr_test_ctx *ctx = test->priv;
+	struct mock_inode_slot *slot;
+
+	if (ctx->fail_get_inode_loc)
+		return ctx->fail_get_inode_loc;
+
+	slot = container_of(EXT4_I(inode), struct mock_inode_slot, ei);
+	get_bh(&slot->iloc_bh);
+	iloc->bh = &slot->iloc_bh;
+	iloc->offset = 0;
+	iloc->block_group = 0;
+	return 0;
+}
+
+static int ext4_mark_iloc_dirty_stub(handle_t *handle, struct inode *inode,
+				     struct ext4_iloc *iloc)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct xattr_test_ctx *ctx = test->priv;
+
+	brelse(iloc->bh);
+	ctx->mark_iloc_dirty_calls++;
+	if (ctx->fail_mark_iloc_dirty)
+		return ctx->fail_mark_iloc_dirty;
+	return 0;
+}
+
+static int __ext4_mark_inode_dirty_stub(handle_t *handle, struct inode *inode,
+					const char *func, unsigned int line)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct xattr_test_ctx *ctx = test->priv;
+
+	ctx->mark_inode_dirty_calls++;
+	return 0;
+}
+
+static int __ext4_handle_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 xattr_test_ctx *ctx = test->priv;
+
+	if (ctx->fail_dirty_metadata)
+		return ctx->fail_dirty_metadata;
+	if (bh)
+		set_buffer_uptodate(bh);
+	ctx->dirty_metadata_calls++;
+	return 0;
+}
+
+static ext4_fsblk_t ext4_mb_new_blocks_stub(handle_t *handle,
+					    struct ext4_allocation_request *ar,
+					    int *errp)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct xattr_test_ctx *ctx = test->priv;
+	struct mock_block *blk;
+
+	if (ctx->fail_alloc_block) {
+		*errp = ctx->fail_alloc_block;
+		return 0;
+	}
+
+	blk = alloc_mock_block(ctx);
+	if (!blk) {
+		*errp = -ENOSPC;
+		return 0;
+	}
+
+	ctx->alloc_block_calls++;
+	*errp = 0;
+	return blk->bh.b_blocknr;
+}
+
+static void ext4_free_blocks_stub(handle_t *handle, struct inode *inode,
+				  struct buffer_head *bh, ext4_fsblk_t block,
+				  unsigned long count, int flags)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct xattr_test_ctx *ctx = test->priv;
+	struct mock_block *blk;
+
+	if (bh) {
+		if (!block)
+			block = bh->b_blocknr;
+		if (flags & EXT4_FREE_BLOCKS_FORGET)
+			brelse(bh);
+	}
+	if (!(flags & EXT4_FREE_BLOCKS_NO_QUOT_UPDATE))
+		dquot_free_block(inode,
+				 EXT4_C2B(EXT4_SB(inode->i_sb), count));
+
+	blk = find_mock_block(ctx, block);
+	if (blk)
+		blk->in_use = false;
+
+	ctx->free_block_calls++;
+	ctx->last_freed_block = block;
+}
+
+static struct buffer_head *ext4_xattr_getblk_stub(struct super_block *sb,
+						  ext4_fsblk_t block)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct xattr_test_ctx *ctx = test->priv;
+	struct mock_block *blk;
+
+	if (ctx->fail_xattr_getblk)
+		return NULL;
+
+	blk = find_mock_block(ctx, block);
+	if (!blk)
+		return NULL;
+
+	get_bh(&blk->bh);
+	return &blk->bh;
+}
+
+static struct buffer_head *ext4_sb_bread_stub(struct super_block *sb,
+					      sector_t block,
+					      blk_opf_t op_flags)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct xattr_test_ctx *ctx = test->priv;
+	struct mock_block *blk;
+
+	if (ctx->fail_sb_bread)
+		return ERR_PTR(ctx->fail_sb_bread);
+
+	blk = find_mock_block(ctx, block);
+	if (!blk)
+		return ERR_PTR(-EIO);
+
+	get_bh(&blk->bh);
+	return &blk->bh;
+}
+
+static int ext4_map_blocks_stub(handle_t *handle, struct inode *inode,
+				struct ext4_map_blocks *map, int flags)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct xattr_test_ctx *ctx = test->priv;
+	struct mock_inode_slot *slot;
+	struct mock_block *blk;
+
+	slot = container_of(EXT4_I(inode), struct mock_inode_slot, ei);
+	if (map->m_lblk >= ARRAY_SIZE(slot->lblk_to_pblk))
+		return -EFSCORRUPTED;
+
+	if (!slot->lblk_to_pblk[map->m_lblk]) {
+		if (!(flags & EXT4_GET_BLOCKS_CREATE))
+			return 0;
+		if (ctx->fail_alloc_block)
+			return ctx->fail_alloc_block;
+		blk = alloc_mock_block(ctx);
+		if (!blk)
+			return -ENOSPC;
+		slot->lblk_to_pblk[map->m_lblk] = blk->bh.b_blocknr;
+		inode->i_blocks += TEST_BLK_SIZE >> 9;
+		map->m_flags = EXT4_MAP_NEW | EXT4_MAP_MAPPED;
+	} else {
+		map->m_flags = EXT4_MAP_MAPPED;
+	}
+
+	map->m_pblk = slot->lblk_to_pblk[map->m_lblk];
+	map->m_len = 1;
+	return 1;
+}
+
+static struct buffer_head *ext4_getblk_stub(handle_t *handle,
+					    struct inode *inode,
+					    ext4_lblk_t block, int map_flags)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct xattr_test_ctx *ctx = test->priv;
+	struct mock_inode_slot *slot;
+	struct mock_block *blk;
+
+	if (ctx->fail_getblk)
+		return ERR_PTR(ctx->fail_getblk);
+
+	slot = container_of(EXT4_I(inode), struct mock_inode_slot, ei);
+	if (block >= ARRAY_SIZE(slot->lblk_to_pblk))
+		return ERR_PTR(-EFSCORRUPTED);
+	if (!slot->lblk_to_pblk[block]) {
+		if (!(map_flags & EXT4_GET_BLOCKS_CREATE))
+			return NULL;
+		blk = alloc_mock_block(ctx);
+		if (!blk)
+			return ERR_PTR(-ENOSPC);
+		slot->lblk_to_pblk[block] = blk->bh.b_blocknr;
+		inode->i_blocks += TEST_BLK_SIZE >> 9;
+	} else {
+		blk = find_mock_block(ctx, slot->lblk_to_pblk[block]);
+		if (!blk)
+			return ERR_PTR(-EIO);
+	}
+
+	get_bh(&blk->bh);
+	return &blk->bh;
+}
+
+static struct inode *__ext4_new_inode_stub(struct mnt_idmap *idmap,
+					   handle_t *handle,
+					   struct inode *dir, umode_t mode,
+					   const struct qstr *qstr,
+					   __u32 goal, uid_t *owner,
+					   __u32 i_flags, int handle_type,
+					   unsigned int line_no, int nblocks)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct xattr_test_ctx *ctx = test->priv;
+	int i;
+
+	if (ctx->fail_new_inode)
+		return ERR_PTR(ctx->fail_new_inode);
+
+	for (i = 0; i < TEST_MAX_EA_INODES; i++) {
+		struct mock_inode_slot *slot = &ctx->ea_inodes[i];
+		struct inode *inode = &slot->ei.vfs_inode;
+
+		if (!slot->allocated) {
+			init_mock_inode_slot(ctx, slot, TEST_FIRST_EA_INO + i);
+			inode->i_mode = mode;
+			slot->ei.i_flags = i_flags;
+			if (owner) {
+				i_uid_write(inode, owner[0]);
+				i_gid_write(inode, owner[1]);
+			}
+			/*
+			 * Set I_NEW for unlock_new_inode() and give the mock
+			 * inode an extra baseline reference (i_count = 2) so
+			 * ext4_put_ea_inode()'s iput_if_not_last() decrements
+			 * i_count to 1 and returns without entering VFS
+			 * eviction.
+			 */
+			inode_state_set_raw(inode, I_DIRTY | I_NEW);
+			atomic_set(&inode->i_count, 2);
+			return inode;
+		}
+	}
+	return ERR_PTR(-ENOSPC);
+}
+
+static struct inode *__ext4_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 xattr_test_ctx *ctx = test->priv;
+	int i;
+
+	if (ctx->fail_iget)
+		return ERR_PTR(ctx->fail_iget);
+
+	for (i = 0; i < TEST_MAX_EA_INODES; i++) {
+		struct mock_inode_slot *slot = &ctx->ea_inodes[i];
+		struct inode *inode = &slot->ei.vfs_inode;
+
+		if (slot->allocated && inode->i_ino == ino) {
+			if ((flags & EXT4_IGET_EA_INODE) &&
+			    !(slot->ei.i_flags & EXT4_EA_INODE_FL))
+				return ERR_PTR(-EFSCORRUPTED);
+			atomic_inc(&inode->i_count);
+			return inode;
+		}
+	}
+	return ERR_PTR(-ENOENT);
+}
+
+static int xattr_test_init(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx;
+	struct super_block *sb;
+	struct ext4_sb_info *sbi;
+
+	ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	test->priv = ctx;
+
+	sb = &ctx->sb;
+	sbi = &ctx->sbi;
+
+	sbi->s_sb = sb;
+	sb->s_fs_info = sbi;
+	sb->s_op = &xattr_test_sops;
+#ifdef CONFIG_QUOTA
+	sb->dq_op = &xattr_test_qops;
+#endif
+	sb->s_blocksize = TEST_BLK_SIZE;
+	sb->s_blocksize_bits = 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_xattr_kunit", sizeof(sb->s_id));
+	init_rwsem(&sb->s_umount);
+
+	ctx->bdev.bd_mapping = &ctx->bd_mapping;
+	bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+	sb->s_bdev = &ctx->bdev;
+
+	sbi->s_es = &ctx->es;
+	ctx->es.s_first_data_block = cpu_to_le32(1);
+	ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+	ctx->es.s_inodes_count = cpu_to_le32(1024);
+	ctx->es.s_first_ino = cpu_to_le32(11);
+	ctx->es.s_inode_size = cpu_to_le16(TEST_INODE_SIZE);
+	ext4_blocks_count_set(&ctx->es, 65536);
+
+	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;
+
+	sbi->s_inode_size = TEST_INODE_SIZE;
+	sbi->s_first_ino = 11;
+	sbi->s_want_extra_isize = TEST_EXTRA_ISIZE;
+	ctx->es.s_min_extra_isize = cpu_to_le16(TEST_EXTRA_ISIZE);
+	sbi->s_blocks_per_group = 8192;
+	sbi->s_clusters_per_group = 8192;
+	sbi->s_cluster_ratio = 1;
+	sbi->s_cluster_bits = 0;
+	sbi->s_groups_count = 8;
+	sbi->s_csum_seed = TEST_CSUM_SEED;
+
+	set_opt(sb, ERRORS_CONT);
+	set_opt(sb, XATTR_USER);
+	spin_lock_init(&sbi->s_bdev_wb_lock);
+	spin_lock_init(&sbi->s_error_lock);
+	spin_lock_init(&sbi->s_md_lock);
+
+	/*
+	 * Use burst = 0 so expected corruption-test error/warning messages
+	 * are suppressed without setting EXT4_FLAGS_SHUTDOWN.
+	 */
+	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);
+
+	init_mock_inode_slot(ctx, &ctx->primary, TEST_PRIMARY_INO);
+	init_mock_inode_slot(ctx, &ctx->secondary, TEST_SECONDARY_INO);
+
+	ctx->root_dentry.d_sb = sb;
+	ctx->root_dentry.d_inode = &ctx->primary.ei.vfs_inode;
+	sb->s_root = &ctx->root_dentry;
+
+	kunit_activate_static_stub(test, ext4_get_inode_loc,
+				   ext4_get_inode_loc_stub);
+	kunit_activate_static_stub(test, ext4_mark_iloc_dirty,
+				   ext4_mark_iloc_dirty_stub);
+	kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+				   __ext4_mark_inode_dirty_stub);
+	kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+				   __ext4_handle_dirty_metadata_stub);
+	kunit_activate_static_stub(test, ext4_mb_new_blocks,
+				   ext4_mb_new_blocks_stub);
+	kunit_activate_static_stub(test, ext4_free_blocks,
+				   ext4_free_blocks_stub);
+	kunit_activate_static_stub(test, ext4_xattr_getblk,
+				   ext4_xattr_getblk_stub);
+	kunit_activate_static_stub(test, ext4_sb_bread,
+				   ext4_sb_bread_stub);
+	kunit_activate_static_stub(test, ext4_map_blocks,
+				   ext4_map_blocks_stub);
+	kunit_activate_static_stub(test, ext4_getblk,
+				   ext4_getblk_stub);
+	kunit_activate_static_stub(test, __ext4_new_inode,
+				   __ext4_new_inode_stub);
+	kunit_activate_static_stub(test, __ext4_iget,
+				   __ext4_iget_stub);
+
+	return 0;
+}
+
+static void xattr_test_exit(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx = test->priv;
+
+	if (!ctx)
+		return;
+
+	ext4_xattr_destroy_cache(ctx->sbi.s_ea_block_cache);
+	ctx->sbi.s_ea_block_cache = NULL;
+	ext4_xattr_destroy_cache(ctx->sbi.s_ea_inode_cache);
+	ctx->sbi.s_ea_inode_cache = NULL;
+}
+
+/*
+ * Test 1: Sizing macros, EXT4_INODE_HAS_XATTR_SPACE, and xattr_sem locking
+ * helpers.
+ */
+static void test_xattr_macros_and_locking(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx = test->priv;
+	struct inode *inode = &ctx->primary.ei.vfs_inode;
+	int save_outer = -1, save_inner = -1;
+	int locked;
+
+	/* EXT4_XATTR_LEN rounds (sizeof(entry) + name_len) to 4-byte boundary */
+	KUNIT_EXPECT_EQ(test, EXT4_XATTR_LEN(0), 16U);
+	KUNIT_EXPECT_EQ(test, EXT4_XATTR_LEN(1), 20U);
+	KUNIT_EXPECT_EQ(test, EXT4_XATTR_LEN(4), 20U);
+	KUNIT_EXPECT_EQ(test, EXT4_XATTR_LEN(5), 24U);
+
+	/* EXT4_XATTR_SIZE rounds value size up to 4-byte boundary */
+	KUNIT_EXPECT_EQ(test, EXT4_XATTR_SIZE(0), 0U);
+	KUNIT_EXPECT_EQ(test, EXT4_XATTR_SIZE(1), 4U);
+	KUNIT_EXPECT_EQ(test, EXT4_XATTR_SIZE(4), 4U);
+	KUNIT_EXPECT_EQ(test, EXT4_XATTR_SIZE(5), 8U);
+
+	/*
+	 * EXT4_XATTR_MIN_LARGE_EA_SIZE(b) is
+	 * b - EXT4_XATTR_LEN(3) - sizeof(struct ext4_xattr_header) - 4.
+	 */
+	KUNIT_EXPECT_EQ(test, EXT4_XATTR_MIN_LARGE_EA_SIZE(1024), 968);
+	KUNIT_EXPECT_EQ(test, EXT4_XATTR_MIN_LARGE_EA_SIZE(4096), 4040);
+
+	/*
+	 * EXT4_INODE_HAS_XATTR_SPACE requires at least
+	 * sizeof(ibody_header) + EXT4_XATTR_PAD = 8 bytes of space after
+	 * EXT4_GOOD_OLD_INODE_SIZE + i_extra_isize.
+	 */
+	KUNIT_EXPECT_TRUE(test, EXT4_INODE_HAS_XATTR_SPACE(inode));
+
+	ctx->sbi.s_inode_size = EXT4_GOOD_OLD_INODE_SIZE;
+	KUNIT_EXPECT_FALSE(test, EXT4_INODE_HAS_XATTR_SPACE(inode));
+	ctx->sbi.s_inode_size = TEST_INODE_SIZE;
+
+	/* Exact boundary: 256 - 128 - 8 = 120 bytes of i_extra_isize fits */
+	EXT4_I(inode)->i_extra_isize = 120;
+	KUNIT_EXPECT_TRUE(test, EXT4_INODE_HAS_XATTR_SPACE(inode));
+
+	/* 124 bytes leaves only 4 bytes (< 8 bytes required) */
+	EXT4_I(inode)->i_extra_isize = 124;
+	KUNIT_EXPECT_FALSE(test, EXT4_INODE_HAS_XATTR_SPACE(inode));
+	EXT4_I(inode)->i_extra_isize = TEST_EXTRA_ISIZE;
+
+	/* Locking helpers and EXT4_STATE_NO_EXPAND state preservation */
+	KUNIT_EXPECT_EQ(test,
+			ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND), 0);
+
+	ext4_write_lock_xattr(inode, &save_outer);
+	locked = ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND);
+	ext4_write_unlock_xattr(inode, &save_outer);
+
+	KUNIT_EXPECT_EQ(test, save_outer, 0);
+	KUNIT_EXPECT_NE(test, locked, 0);
+	KUNIT_EXPECT_EQ(test,
+			ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND), 0);
+
+	/* When EXT4_STATE_NO_EXPAND is already set, unlock must preserve it */
+	ext4_set_inode_state(inode, EXT4_STATE_NO_EXPAND);
+	locked = ext4_write_trylock_xattr(inode, &save_inner);
+	if (locked)
+		ext4_write_unlock_xattr(inode, &save_inner);
+
+	KUNIT_EXPECT_EQ(test, locked, 1);
+	KUNIT_EXPECT_NE(test, save_inner, 0);
+	KUNIT_EXPECT_NE(test,
+			ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND), 0);
+	ext4_clear_inode_state(inode, EXT4_STATE_NO_EXPAND);
+}
+
+/*
+ * Test 2: Parameterized xattr_check_inode() validation across valid and
+ * corrupted in-inode xattr layouts.
+ */
+enum xattr_check_case_id {
+	CHECK_VALID_EMPTY,
+	CHECK_VALID_TWO_ENTRIES,
+	CHECK_BAD_MAGIC,
+	CHECK_END_BEFORE_FIRST,
+	CHECK_ENTRY_HDR_OVERFLOW,
+	CHECK_ENTRY_NAME_OVERFLOW,
+	CHECK_UNTERMINATED_LIST,
+	CHECK_NONZERO_OFFS_WITH_INUM,
+	CHECK_VALUE_SIZE_GT_TOTAL,
+	CHECK_VALUE_OFFS_OVERLAPS_ENTRY,
+	CHECK_VALUE_END_EXCEEDS_TOTAL,
+};
+
+struct xattr_check_param {
+	const char *desc;
+	enum xattr_check_case_id id;
+	int expected_err;
+};
+
+static const struct xattr_check_param xattr_check_params[] = {
+	{ "valid_empty_ibody", CHECK_VALID_EMPTY, 0 },
+	{ "valid_two_entries", CHECK_VALID_TWO_ENTRIES, 0 },
+	{ "bad_magic", CHECK_BAD_MAGIC, -EFSCORRUPTED },
+	{ "end_before_first_entry", CHECK_END_BEFORE_FIRST, -EFSCORRUPTED },
+	{ "entry_header_overflows_end", CHECK_ENTRY_HDR_OVERFLOW, -EFSCORRUPTED },
+	{ "entry_name_overflows_end", CHECK_ENTRY_NAME_OVERFLOW, -EFSCORRUPTED },
+	{ "unterminated_entry_list", CHECK_UNTERMINATED_LIST, -EFSCORRUPTED },
+	{ "nonzero_offs_with_value_inum", CHECK_NONZERO_OFFS_WITH_INUM, -EFSCORRUPTED },
+	{ "value_size_gt_total_size", CHECK_VALUE_SIZE_GT_TOTAL, -EFSCORRUPTED },
+	{ "value_offset_overlaps_entries", CHECK_VALUE_OFFS_OVERLAPS_ENTRY, -EFSCORRUPTED },
+	{ "value_end_exceeds_total_size", CHECK_VALUE_END_EXCEEDS_TOTAL, -EFSCORRUPTED },
+};
+
+static void xattr_check_param_get_desc(const struct xattr_check_param *p,
+				       char *desc)
+{
+	strscpy(desc, p->desc, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(xattr_check, xattr_check_params, xattr_check_param_get_desc);
+
+static void test_xattr_check_inode(struct kunit *test)
+{
+	const struct xattr_check_param *p = test->param_value;
+	struct xattr_test_ctx *ctx = test->priv;
+	struct inode *inode = &ctx->primary.ei.vfs_inode;
+	struct ext4_inode *raw_inode = (struct ext4_inode *)ctx->primary.raw_buf;
+	struct ext4_xattr_ibody_header *hdr = IHDR(inode, raw_inode);
+	struct ext4_xattr_entry *e1, *e2;
+	void *end = ITAIL(inode, raw_inode);
+	size_t total_size = (char *)end - (char *)IFIRST(hdr);
+	int ret;
+
+	memset(hdr, 0, (char *)end - (char *)hdr);
+	hdr->h_magic = cpu_to_le32(EXT4_XATTR_MAGIC);
+	e1 = IFIRST(hdr);
+
+	switch (p->id) {
+	case CHECK_VALID_EMPTY:
+		break;
+	case CHECK_VALID_TWO_ENTRIES:
+		e1->e_name_len = 1;
+		e1->e_name_index = EXT4_XATTR_INDEX_USER;
+		e1->e_name[0] = 'a';
+		e1->e_value_size = cpu_to_le32(4);
+		e1->e_value_offs = cpu_to_le16(total_size - 4);
+
+		e2 = EXT4_XATTR_NEXT(e1);
+		e2->e_name_len = 1;
+		e2->e_name_index = EXT4_XATTR_INDEX_USER;
+		e2->e_name[0] = 'b';
+		e2->e_value_size = cpu_to_le32(4);
+		e2->e_value_offs = cpu_to_le16(total_size - 8);
+		break;
+	case CHECK_BAD_MAGIC:
+		hdr->h_magic = cpu_to_le32(0xdeadbeef);
+		break;
+	case CHECK_END_BEFORE_FIRST:
+		end = (char *)IFIRST(hdr) + 2;
+		break;
+	case CHECK_ENTRY_HDR_OVERFLOW:
+		e1->e_name_len = 1;
+		e1->e_name[0] = 'a';
+		end = (char *)IFIRST(hdr) + EXT4_XATTR_LEN(1) + 8;
+		e2 = EXT4_XATTR_NEXT(e1);
+		e2->e_name_len = 1;
+		break;
+	case CHECK_ENTRY_NAME_OVERFLOW:
+		e1->e_name_len = 100;
+		break;
+	case CHECK_UNTERMINATED_LIST:
+		e1->e_name_len = 1;
+		end = (char *)IFIRST(hdr) + EXT4_XATTR_LEN(1);
+		break;
+	case CHECK_NONZERO_OFFS_WITH_INUM:
+		e1->e_name_len = 1;
+		e1->e_value_inum = cpu_to_le32(20);
+		e1->e_value_offs = cpu_to_le16(total_size - 4);
+		break;
+	case CHECK_VALUE_SIZE_GT_TOTAL:
+		e1->e_name_len = 1;
+		e1->e_value_size = cpu_to_le32(total_size + 4);
+		e1->e_value_offs = cpu_to_le16(total_size - 4);
+		break;
+	case CHECK_VALUE_OFFS_OVERLAPS_ENTRY:
+		e1->e_name_len = 1;
+		e1->e_value_size = cpu_to_le32(4);
+		/* Offset points inside e1 + 4-byte terminator */
+		e1->e_value_offs = cpu_to_le16(EXT4_XATTR_LEN(1));
+		break;
+	case CHECK_VALUE_END_EXCEEDS_TOTAL:
+		e1->e_name_len = 1;
+		e1->e_value_size = cpu_to_le32(8);
+		e1->e_value_offs = cpu_to_le16(total_size - 4);
+		break;
+	}
+
+	ret = xattr_check_inode(inode, hdr, end);
+	KUNIT_EXPECT_EQ_MSG(test, ret, p->expected_err, "case: %s", p->desc);
+}
+
+/*
+ * Test 3: In-inode (ibody) get/set, flags validation, EXT4_STATE_NEW handling,
+ * superblock feature update, and identical-value fastpath.
+ */
+static void test_xattr_ibody_basic_and_flags(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx = test->priv;
+	struct inode *inode = &ctx->primary.ei.vfs_inode;
+	struct ext4_inode *raw_inode = (struct ext4_inode *)ctx->primary.raw_buf;
+	char long_name[260];
+	char buf[32];
+	unsigned int prev_dirty;
+	int ret;
+
+	/* Fresh inode without EXT4_STATE_XATTR returns -ENODATA */
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "missing",
+			     buf, sizeof(buf));
+	KUNIT_EXPECT_EQ(test, ret, -ENODATA);
+
+	/* Parameter validation: NULL name -> -EINVAL, >255 name -> -ERANGE */
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    NULL, "v", 1, 0);
+	KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+	memset(long_name, 'x', sizeof(long_name) - 1);
+	long_name[sizeof(long_name) - 1] = '\0';
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    long_name, "v", 1, 0);
+	KUNIT_EXPECT_EQ(test, ret, -ERANGE);
+
+	/* Forced shutdown check on get and set */
+	set_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "k",
+				       buf, sizeof(buf)),
+			-EIO);
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_set_handle(NULL, inode,
+					      EXT4_XATTR_INDEX_USER, "k",
+					      "v", 1, 0),
+			-EIO);
+	clear_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+
+	/* XATTR_REPLACE on non-existent attribute returns -ENODATA */
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "no_such", "val", 3, XATTR_REPLACE);
+	KUNIT_EXPECT_EQ(test, ret, -ENODATA);
+
+	/* Deleting non-existent attribute without XATTR_REPLACE succeeds (0) */
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "no_such", NULL, 0, 0);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+
+	/*
+	 * Verify EXT4_STATE_NEW causes ext4_xattr_set_handle() to zero the raw
+	 * inode and clear EXT4_STATE_NEW.
+	 */
+	memset(raw_inode, 0x5a, TEST_INODE_SIZE);
+	ext4_set_inode_state(inode, EXT4_STATE_NEW);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_has_feature_xattr(&ctx->sb));
+
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "key1", "hello", 5, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test,
+			ext4_test_inode_state(inode, EXT4_STATE_NEW), 0);
+	KUNIT_EXPECT_NE(test,
+			ext4_test_inode_state(inode, EXT4_STATE_XATTR), 0);
+	KUNIT_EXPECT_EQ(test, IHDR(inode, raw_inode)->h_magic,
+			cpu_to_le32(EXT4_XATTR_MAGIC));
+	/* Setting first xattr enables EXT4_FEATURE_COMPAT_EXT_ATTR on sb */
+	KUNIT_EXPECT_TRUE(test, ext4_has_feature_xattr(&ctx->sb));
+
+	/* XATTR_CREATE on existing attribute returns -EEXIST */
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "key1", "other", 5, XATTR_CREATE);
+	KUNIT_EXPECT_EQ(test, ret, -EEXIST);
+
+	/* Query length (NULL buffer), too small buffer (-ERANGE), and read */
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "key1", NULL, 0);
+	KUNIT_EXPECT_EQ(test, ret, 5);
+
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "key1", buf, 4);
+	KUNIT_EXPECT_EQ(test, ret, -ERANGE);
+
+	memset(buf, 0, sizeof(buf));
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "key1",
+			     buf, sizeof(buf));
+	KUNIT_EXPECT_EQ(test, ret, 5);
+	KUNIT_EXPECT_MEMEQ(test, buf, "hello", 5);
+
+	/* Direct ext4_xattr_ibody_get() check */
+	memset(buf, 0, sizeof(buf));
+	ret = ext4_xattr_ibody_get(inode, EXT4_XATTR_INDEX_USER, "key1",
+				   buf, sizeof(buf));
+	KUNIT_EXPECT_EQ(test, ret, 5);
+	KUNIT_EXPECT_MEMEQ(test, buf, "hello", 5);
+
+	/*
+	 * Setting the exact same value triggers the ext4_xattr_value_same()
+	 * fastpath and skips dirtying the inode location.
+	 */
+	prev_dirty = ctx->mark_iloc_dirty_calls;
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "key1", "hello", 5, XATTR_REPLACE);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, ctx->mark_iloc_dirty_calls, prev_dirty);
+}
+
+/*
+ * Test 4: In-inode (ibody) replacement (same padded size vs different size),
+ * zero-length values, entry removal, and i_inline_off adjustment.
+ */
+static void test_xattr_ibody_replace_and_remove(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx = test->priv;
+	struct inode *inode = &ctx->primary.ei.vfs_inode;
+	struct ext4_inode *raw_inode = (struct ext4_inode *)ctx->primary.raw_buf;
+	struct ext4_xattr_ibody_header *hdr = IHDR(inode, raw_inode);
+	struct ext4_xattr_entry *e;
+	char buf[32];
+	u16 offs_before, offs_after;
+	int ret;
+
+	/* Create two entries in ibody: "a" (7 bytes -> 8 padded) and "b" (4 bytes) */
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "a", "1234567", 7, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "b", "wxyz", 4, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	e = IFIRST(hdr);
+	offs_before = le16_to_cpu(e->e_value_offs);
+
+	/*
+	 * Replace "a" with a 5-byte value (still 8 bytes padded): hits the
+	 * in-place overwrite fastpath without shifting offsets, and zeroes
+	 * the last 3 padding bytes.
+	 */
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "a", "ABCDE", 5, XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	offs_after = le16_to_cpu(e->e_value_offs);
+	KUNIT_EXPECT_EQ(test, offs_after, offs_before);
+	KUNIT_EXPECT_MEMEQ(test, (char *)IFIRST(hdr) + offs_after,
+			   "ABCDE\0\0\0", 8);
+
+	/*
+	 * Replace "a" with a 12-byte value (different padded size): shifts
+	 * "b"'s value and updates offsets while preserving "b"'s content.
+	 */
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "a", "0123456789ab", 12, XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "a",
+				       buf, sizeof(buf)),
+			12);
+	KUNIT_EXPECT_MEMEQ(test, buf, "0123456789ab", 12);
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "b",
+				       buf, sizeof(buf)),
+			4);
+	KUNIT_EXPECT_MEMEQ(test, buf, "wxyz", 4);
+
+	/* Replace "a" with a zero-length value ("", size = 0) */
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "a", "", 0, XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "a",
+				       buf, sizeof(buf)),
+			0);
+
+	/*
+	 * Add system.data (inline data entry) and verify removing an earlier
+	 * entry ("a") shifts system.data's entry header left and updates
+	 * EXT4_I(inode)->i_inline_off.
+	 */
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_SYSTEM,
+				    "data", "inlinedata", 10,
+				    XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	ret = ext4_find_inline_data_nolock(inode);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+	ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+	offs_before = EXT4_I(inode)->i_inline_off;
+	KUNIT_EXPECT_GT(test, offs_before, (u16)0);
+
+	/* Remove "a": system.data entry header moves left by EXT4_XATTR_LEN(1) */
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "a", NULL, 0, XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_inline_off,
+			(u16)(offs_before - EXT4_XATTR_LEN(1)));
+
+	/* Remove "b" and "system.data": emptying ibody clears h_magic and state */
+	ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+	ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_USER,
+				    "b", NULL, 0, XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_NE(test,
+			ext4_test_inode_state(inode, EXT4_STATE_XATTR), 0);
+
+	ret = ext4_xattr_set_handle(NULL, inode, EXT4_XATTR_INDEX_SYSTEM,
+				    "data", NULL, 0,
+				    XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, hdr->h_magic, cpu_to_le32(0));
+	KUNIT_EXPECT_EQ(test,
+			ext4_test_inode_state(inode, EXT4_STATE_XATTR), 0);
+}
+
+/*
+ * Test 5: External xattr block allocation, entry sorting, migration between
+ * ibody and external block, EXT4_XATTR_BLOCK_RESERVE enforcement, and external
+ * block header/CRC32C checksum validation.
+ */
+static void test_xattr_external_block_and_check(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx = test->priv;
+	struct inode *inode = &ctx->primary.ei.vfs_inode;
+	struct mock_block *blk;
+	struct ext4_xattr_header *bhdr;
+	struct ext4_xattr_entry *e1, *e2, *e3;
+	char val100[100];
+	char *val796;
+	char buf[128];
+	int ret;
+
+	ext4_set_feature_metadata_csum(&ctx->sb);
+	memset(val100, 'V', sizeof(val100));
+
+	/*
+	 * Start with a small attribute in ibody ("migr", 4 bytes), then grow
+	 * it to 100 bytes (> 88-byte ibody space) so it migrates from ibody
+	 * to a newly allocated external block.
+	 */
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "migr",
+			     "tiny", 4, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_file_acl, 0ULL);
+
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "migr",
+			     val100, sizeof(val100), XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_NE(test, EXT4_I(inode)->i_file_acl, 0ULL);
+	KUNIT_EXPECT_EQ(test,
+			ext4_test_inode_state(inode, EXT4_STATE_XATTR), 0);
+
+	/*
+	 * Add two more entries into the external block (by temporarily setting
+	 * s_inode_size = 128 so ibody is bypassed) and verify external block
+	 * entries are kept sorted by (e_name_index, e_name_len, e_name).
+	 */
+	ctx->sbi.s_inode_size = EXT4_GOOD_OLD_INODE_SIZE;
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "z",
+			     "1", 1, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "a",
+			     "2", 1, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	ctx->sbi.s_inode_size = TEST_INODE_SIZE;
+
+	blk = find_mock_block(ctx, EXT4_I(inode)->i_file_acl);
+	KUNIT_ASSERT_NOT_NULL(test, blk);
+	bhdr = BHDR(&blk->bh);
+	e1 = BFIRST(&blk->bh);
+	e2 = EXT4_XATTR_NEXT(e1);
+	e3 = EXT4_XATTR_NEXT(e2);
+
+	/* Length 1 ("a", "z") precede length 4 ("migr") */
+	KUNIT_EXPECT_EQ(test, e1->e_name_len, 1);
+	KUNIT_EXPECT_EQ(test, e1->e_name[0], 'a');
+	KUNIT_EXPECT_EQ(test, e2->e_name_len, 1);
+	KUNIT_EXPECT_EQ(test, e2->e_name[0], 'z');
+	KUNIT_EXPECT_EQ(test, e3->e_name_len, 4);
+	KUNIT_EXPECT_MEMEQ(test, e3->e_name, "migr", 4);
+
+	/*
+	 * Verify external block validation and buffer_verified caching:
+	 * 1) Reading sets buffer_verified.
+	 * 2) Corrupting h_checksum while buffer_verified is set still succeeds
+	 *    because ext4_xattr_check_block() trusts buffer_verified.
+	 * 3) Clearing buffer_verified exposes the bad checksum (-EFSBADCRC).
+	 * 4) Corrupting h_blocks or h_magic returns -EFSCORRUPTED.
+	 */
+	clear_buffer_verified(&blk->bh);
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "migr",
+			     buf, sizeof(buf));
+	KUNIT_EXPECT_EQ(test, ret, 100);
+	KUNIT_EXPECT_NE(test, buffer_verified(&blk->bh), 0);
+
+	bhdr->h_checksum ^= cpu_to_le32(1);
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "a",
+			     buf, sizeof(buf));
+	KUNIT_EXPECT_EQ(test, ret, 1);
+
+	clear_buffer_verified(&blk->bh);
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "a",
+			     buf, sizeof(buf));
+	KUNIT_EXPECT_EQ(test, ret, -EFSBADCRC);
+	bhdr->h_checksum ^= cpu_to_le32(1);
+
+	bhdr->h_blocks = cpu_to_le32(2);
+	clear_buffer_verified(&blk->bh);
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "a",
+			     buf, sizeof(buf));
+	KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+	bhdr->h_blocks = cpu_to_le32(1);
+
+	/*
+	 * Verify EXT4_XATTR_BLOCK_RESERVE (128 bytes on 1024B block) when
+	 * EXT4_FEATURE_INCOMPAT_EA_INODE is enabled:
+	 * With "migr" (100B), "a" (4B), and "z" (4B) in the block, min_offs is
+	 * 1024 - 108 = 916 and free space is 820 bytes. Adding "r1" with a
+	 * 796-byte value (816B total <= 820B free) leaves min_offs - new_size
+	 * = 916 - 796 = 120 < 128 (EXT4_XATTR_BLOCK_RESERVE), so with ea_inode
+	 * enabled and EA inode creation disabled it fails with -ENOSPC,
+	 * whereas with ea_inode disabled it fits inline in the block.
+	 */
+	val796 = kunit_kmalloc(test, 796, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, val796);
+	memset(val796, 'R', 796);
+	ctx->sbi.s_inode_size = EXT4_GOOD_OLD_INODE_SIZE;
+	ext4_set_feature_ea_inode(&ctx->sb);
+	ctx->fail_new_inode = -ENOSPC;
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "r1",
+			     val796, 796, XATTR_CREATE);
+	ctx->fail_new_inode = 0;
+	KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+
+	ext4_clear_feature_ea_inode(&ctx->sb);
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "r1",
+			     val796, 796, XATTR_CREATE);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_ASSERT_EQ(test,
+			ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "r1",
+				       NULL, 0, XATTR_REPLACE),
+			0);
+	ctx->sbi.s_inode_size = TEST_INODE_SIZE;
+
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "a",
+			      NULL, 0, XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "z",
+			     NULL, 0, XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	/*
+	 * Removing the last entry ("migr") from the external block frees the
+	 * external xattr block and clears i_file_acl to 0.
+	 */
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "migr",
+			     NULL, 0, XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_file_acl, 0ULL);
+}
+
+/*
+ * Test 6: External xattr block mb_cache deduplication, copy-on-write cloning
+ * when modifying a shared block, and EXT4_XATTR_REFCOUNT_MAX handling.
+ */
+static void test_xattr_block_dedup_and_cow(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx = test->priv;
+	struct inode *inode1 = &ctx->primary.ei.vfs_inode;
+	struct inode *inode2 = &ctx->secondary.ei.vfs_inode;
+	struct mock_block *blk;
+	struct mb_cache_entry *ce;
+	char val100[100];
+	ext4_fsblk_t shared_blk;
+	u32 hash;
+	int ret;
+
+	ctx->sbi.s_ea_block_cache = ext4_xattr_create_cache();
+	KUNIT_ASSERT_NOT_NULL(test, ctx->sbi.s_ea_block_cache);
+
+	memset(val100, 'D', sizeof(val100));
+	ret = ext4_xattr_set(inode1, EXT4_XATTR_INDEX_USER, "shared",
+			     val100, sizeof(val100), XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	shared_blk = EXT4_I(inode1)->i_file_acl;
+	KUNIT_ASSERT_NE(test, shared_blk, 0ULL);
+
+	/* Setting identical xattr on inode2 reuses shared_blk from mb_cache */
+	ret = ext4_xattr_set(inode2, EXT4_XATTR_INDEX_USER, "shared",
+			     val100, sizeof(val100), XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode2)->i_file_acl, shared_blk);
+
+	blk = find_mock_block(ctx, shared_blk);
+	KUNIT_ASSERT_NOT_NULL(test, blk);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(BHDR(&blk->bh)->h_refcount), 2U);
+
+	/*
+	 * Modifying the xattr on inode2 triggers copy-on-write: inode2 gets a
+	 * new block and shared_blk's refcount drops back to 1.
+	 */
+	val100[0] = 'M';
+	ret = ext4_xattr_set(inode2, EXT4_XATTR_INDEX_USER, "shared",
+			     val100, sizeof(val100), XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_NE(test, EXT4_I(inode2)->i_file_acl, shared_blk);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(BHDR(&blk->bh)->h_refcount), 1U);
+
+	/*
+	 * Test EXT4_XATTR_REFCOUNT_MAX (1024) limit:
+	 * Bump shared_blk's h_refcount to EXT4_XATTR_REFCOUNT_MAX (1024),
+	 * then set the same xattr on inode2. Cache lookup must see
+	 * ref >= EXT4_XATTR_REFCOUNT_MAX, clear MBE_REUSABLE_B, and allocate
+	 * a separate block instead of exceeding 1024.
+	 */
+	val100[0] = 'D';
+	BHDR(&blk->bh)->h_refcount = cpu_to_le32(EXT4_XATTR_REFCOUNT_MAX);
+	hash = le32_to_cpu(BHDR(&blk->bh)->h_hash);
+
+	ret = ext4_xattr_set(inode2, EXT4_XATTR_INDEX_USER, "shared",
+			     val100, sizeof(val100), XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_NE(test, EXT4_I(inode2)->i_file_acl, shared_blk);
+
+	ce = mb_cache_entry_get(ctx->sbi.s_ea_block_cache, hash, shared_blk);
+	KUNIT_ASSERT_NOT_NULL(test, ce);
+	KUNIT_EXPECT_FALSE(test, test_bit(MBE_REUSABLE_B, &ce->e_flags));
+	mb_cache_entry_put(ctx->sbi.s_ea_block_cache, ce);
+
+	/*
+	 * Removing the attribute on inode1 releases one reference on
+	 * shared_blk (1024 -> 1023 = EXT4_XATTR_REFCOUNT_MAX - 1), which
+	 * re-enables MBE_REUSABLE_B on the mb_cache entry.
+	 */
+	ret = ext4_xattr_set(inode1, EXT4_XATTR_INDEX_USER, "shared",
+			     NULL, 0, XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(BHDR(&blk->bh)->h_refcount),
+			EXT4_XATTR_REFCOUNT_MAX - 1);
+
+	ce = mb_cache_entry_get(ctx->sbi.s_ea_block_cache, hash, shared_blk);
+	KUNIT_ASSERT_NOT_NULL(test, ce);
+	KUNIT_EXPECT_TRUE(test, test_bit(MBE_REUSABLE_B, &ce->e_flags));
+	mb_cache_entry_put(ctx->sbi.s_ea_block_cache, ce);
+}
+
+/*
+ * Test 7: ext4_listxattr() across ibody and external block, and user/hurd/
+ * trusted handler prefix and mount-option filtering.
+ */
+static void test_xattr_list_and_handlers(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx = test->priv;
+	struct inode *inode = &ctx->primary.ei.vfs_inode;
+	struct dentry *dentry = &ctx->root_dentry;
+	char val100[100];
+	char list_buf[128];
+	char val_buf[16];
+	ssize_t list_len;
+	int ret;
+
+	/* Handler get/set/list via user, hurd, and trusted handlers */
+	ret = ext4_xattr_user_handler.set(&ext4_xattr_user_handler,
+					  &nop_mnt_idmap, dentry, inode,
+					  "ukey", "uval", 4, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	ret = ext4_xattr_user_handler.get(&ext4_xattr_user_handler,
+					  dentry, inode, "ukey",
+					  val_buf, sizeof(val_buf));
+	KUNIT_EXPECT_EQ(test, ret, 4);
+
+	ret = ext4_xattr_hurd_handler.set(&ext4_xattr_hurd_handler,
+					  &nop_mnt_idmap, dentry, inode,
+					  "hkey", "hval", 4, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	ret = ext4_xattr_hurd_handler.get(&ext4_xattr_hurd_handler,
+					  dentry, inode, "hkey",
+					  val_buf, sizeof(val_buf));
+	KUNIT_EXPECT_EQ(test, ret, 4);
+
+	/* Add a large attribute in external block via trusted handler */
+	memset(val100, 'T', sizeof(val100));
+	ret = ext4_xattr_trusted_handler.set(&ext4_xattr_trusted_handler,
+					     &nop_mnt_idmap, dentry, inode,
+					     "tkey", val100, sizeof(val100),
+					     XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	/*
+	 * With XATTR_USER set: "user.ukey\0" (10) + "gnu.hkey\0" (9) +
+	 * (if CAP_SYS_ADMIN) "trusted.tkey\0" (13).
+	 */
+	list_len = ext4_listxattr(dentry, NULL, 0);
+	KUNIT_EXPECT_GE(test, list_len, (ssize_t)19);
+
+	/* Buffer too small by 1 byte returns -ERANGE */
+	KUNIT_EXPECT_EQ(test,
+			ext4_listxattr(dentry, list_buf, list_len - 1),
+			(ssize_t)-ERANGE);
+
+	memset(list_buf, 0, sizeof(list_buf));
+	KUNIT_EXPECT_EQ(test,
+			ext4_listxattr(dentry, list_buf, sizeof(list_buf)),
+			list_len);
+	KUNIT_EXPECT_MEMEQ(test, list_buf, "user.ukey\0gnu.hkey\0", 19);
+
+	/*
+	 * Clearing XATTR_USER mount option disables user and hurd handlers
+	 * (-EOPNOTSUPP on get/set and filtered out of ext4_listxattr).
+	 */
+	clear_opt(&ctx->sb, XATTR_USER);
+	KUNIT_EXPECT_FALSE(test, ext4_xattr_user_handler.list(dentry));
+	KUNIT_EXPECT_FALSE(test, ext4_xattr_hurd_handler.list(dentry));
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_user_handler.get(&ext4_xattr_user_handler,
+						    dentry, inode, "ukey",
+						    val_buf, sizeof(val_buf)),
+			-EOPNOTSUPP);
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_user_handler.set(&ext4_xattr_user_handler,
+						    &nop_mnt_idmap, dentry,
+						    inode, "ukey", "x", 1, 0),
+			-EOPNOTSUPP);
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_hurd_handler.get(&ext4_xattr_hurd_handler,
+						    dentry, inode, "hkey",
+						    val_buf, sizeof(val_buf)),
+			-EOPNOTSUPP);
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_hurd_handler.set(&ext4_xattr_hurd_handler,
+						    &nop_mnt_idmap, dentry,
+						    inode, "hkey", "x", 1, 0),
+			-EOPNOTSUPP);
+	KUNIT_EXPECT_EQ(test, ext4_listxattr(dentry, NULL, 0),
+			capable(CAP_SYS_ADMIN) ? (ssize_t)13 : (ssize_t)0);
+	set_opt(&ctx->sb, XATTR_USER);
+}
+
+/*
+ * Test 8: Large xattr inodes (ea_inode feature) multi-block write/read,
+ * 64-bit refcount & hash encoding, mb_cache deduplication across inodes,
+ * reference decrement to 0, and ext4_evict_ea_inode().
+ */
+static void test_xattr_ea_inode_lifecycle_and_dedup(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx = test->priv;
+	struct inode *inode1 = &ctx->primary.ei.vfs_inode;
+	struct inode *inode2 = &ctx->secondary.ei.vfs_inode;
+	struct inode *ea_inode;
+	struct ext4_inode *raw1 = (struct ext4_inode *)ctx->primary.raw_buf;
+	struct ext4_xattr_entry *e1;
+	char *large_val, *read_buf;
+	const size_t large_len = 1500; /* Spans 2 1024-byte blocks */
+	unsigned int ea_ino;
+	u64 refcount;
+	int i, ret;
+
+	ext4_set_feature_ea_inode(&ctx->sb);
+	ctx->sbi.s_ea_inode_cache = ext4_xattr_create_cache();
+	KUNIT_ASSERT_NOT_NULL(test, ctx->sbi.s_ea_inode_cache);
+	ext4_init_ea_inode_work(&ctx->sbi);
+
+	large_val = kunit_kmalloc(test, large_len, GFP_KERNEL);
+	read_buf = kunit_kzalloc(test, large_len, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, large_val);
+	KUNIT_ASSERT_NOT_NULL(test, read_buf);
+	for (i = 0; i < large_len; i++)
+		large_val[i] = (char)('A' + (i % 26));
+
+	/* Create 1500-byte xattr on inode1: stored in a new EA inode */
+	ret = ext4_xattr_set(inode1, EXT4_XATTR_INDEX_USER, "big",
+			     large_val, large_len, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	e1 = IFIRST(IHDR(inode1, raw1));
+	ea_ino = le32_to_cpu(e1->e_value_inum);
+	KUNIT_ASSERT_EQ(test, ea_ino, (unsigned int)TEST_FIRST_EA_INO);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(e1->e_value_offs), 0U);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(e1->e_value_size),
+			(u32)large_len);
+
+	ea_inode = &ctx->ea_inodes[0].ei.vfs_inode;
+	refcount = ((u64)inode_get_ctime_sec(ea_inode) << 32) |
+		   (u32)inode_peek_iversion_raw(ea_inode);
+	KUNIT_EXPECT_EQ(test, refcount, 1ULL);
+	KUNIT_EXPECT_EQ(test, ea_inode->i_nlink, 1U);
+
+	/* Read back multi-block EA inode value via ext4_xattr_get() */
+	ret = ext4_xattr_get(inode1, EXT4_XATTR_INDEX_USER, "big",
+			     read_buf, large_len);
+	KUNIT_ASSERT_EQ(test, ret, (int)large_len);
+	KUNIT_EXPECT_MEMEQ(test, read_buf, large_val, large_len);
+
+	/*
+	 * Setting the identical 1500-byte value on inode2 deduplicates via
+	 * s_ea_inode_cache and increments ea_inode's refcount to 2.
+	 */
+	ret = ext4_xattr_set(inode2, EXT4_XATTR_INDEX_USER, "big2",
+			     large_val, large_len, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	refcount = ((u64)inode_get_ctime_sec(ea_inode) << 32) |
+		   (u32)inode_peek_iversion_raw(ea_inode);
+	KUNIT_EXPECT_EQ(test, refcount, 2ULL);
+
+	/* Removing from inode1 drops refcount to 1; i_nlink remains 1 */
+	ret = ext4_xattr_set(inode1, EXT4_XATTR_INDEX_USER, "big",
+			     NULL, 0, XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	refcount = ((u64)inode_get_ctime_sec(ea_inode) << 32) |
+		   (u32)inode_peek_iversion_raw(ea_inode);
+	KUNIT_EXPECT_EQ(test, refcount, 1ULL);
+	KUNIT_EXPECT_EQ(test, ea_inode->i_nlink, 1U);
+
+	/*
+	 * Removing from inode2 drops refcount to 0 and clears i_nlink to 0.
+	 * Also verify ext4_evict_ea_inode() removes it from s_ea_inode_cache.
+	 */
+	ret = ext4_xattr_set(inode2, EXT4_XATTR_INDEX_USER, "big2",
+			     NULL, 0, XATTR_REPLACE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	refcount = ((u64)inode_get_ctime_sec(ea_inode) << 32) |
+		   (u32)inode_peek_iversion_raw(ea_inode);
+	KUNIT_EXPECT_EQ(test, refcount, 0ULL);
+	KUNIT_EXPECT_EQ(test, ea_inode->i_nlink, 0U);
+
+	ext4_evict_ea_inode(ea_inode);
+}
+
+/*
+ * Test 9: EA inode hash verification (including legacy signed-char name hash
+ * fallback), ext4_xattr_inode_iget() corruption checks, and Lustre-style
+ * EA inode compatibility.
+ */
+static void test_xattr_ea_inode_validation_and_compat(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx = test->priv;
+	struct inode *inode = &ctx->primary.ei.vfs_inode;
+	struct inode *ea_inode;
+	struct ext4_inode *raw = (struct ext4_inode *)ctx->primary.raw_buf;
+	struct ext4_xattr_entry *e;
+	static const char signed_name[] = "\x80\xfe";
+	const size_t val_len = 1000;
+	char *val, *buf;
+	u32 val_hash, signed_e_hash = 0;
+	__le32 orig_e_hash;
+	int i, ret;
+
+	ext4_set_feature_ea_inode(&ctx->sb);
+	val = kunit_kmalloc(test, val_len, GFP_KERNEL);
+	buf = kunit_kzalloc(test, val_len, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, val);
+	KUNIT_ASSERT_NOT_NULL(test, buf);
+	memset(val, 'S', val_len);
+
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, signed_name,
+			     val, val_len, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	e = IFIRST(IHDR(inode, raw));
+	ea_inode = &ctx->ea_inodes[0].ei.vfs_inode;
+	orig_e_hash = e->e_hash;
+
+	/*
+	 * Compute the legacy signed-char entry hash for "\x80\xfe" and verify
+	 * ext4_xattr_inode_verify_hashes() accepts it as a valid fallback.
+	 */
+	val_hash = (u32)inode_get_atime_sec(ea_inode);
+	for (i = 0; i < (int)strlen(signed_name); i++) {
+		signed_e_hash = (signed_e_hash << 5) ^
+				(signed_e_hash >> 27) ^
+				(signed char)signed_name[i];
+	}
+	signed_e_hash = (signed_e_hash << 16) ^
+			(signed_e_hash >> 16) ^ val_hash;
+	e->e_hash = cpu_to_le32(signed_e_hash);
+
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, signed_name,
+			     buf, val_len);
+	KUNIT_EXPECT_EQ(test, ret, (int)val_len);
+
+	/* Truly corrupted entry hash is rejected with -EFSCORRUPTED */
+	e->e_hash = cpu_to_le32(signed_e_hash ^ 0x12345678U);
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, signed_name,
+			     buf, val_len);
+	KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+	e->e_hash = orig_e_hash;
+
+	/* Corrupted value hash stored in ea_inode->i_atime is rejected */
+	inode_set_atime(ea_inode, val_hash ^ 1, 0);
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, signed_name,
+			     buf, val_len);
+	KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+	inode_set_atime(ea_inode, val_hash, 0);
+
+	/* Size mismatch between ea_inode->i_size and entry->e_value_size */
+	i_size_write(ea_inode, val_len - 1);
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, signed_name,
+			     buf, val_len);
+	KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+	i_size_write(ea_inode, val_len);
+
+	/* Invalid i_nlink (0 or 2) on active EA inode returns -EFSCORRUPTED */
+	set_nlink(ea_inode, 2);
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, signed_name,
+			     buf, val_len);
+	KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+	set_nlink(ea_inode, 1);
+
+	/* Self-referencing e_value_inum == parent->i_ino returns -EFSCORRUPTED */
+	e->e_value_inum = cpu_to_le32(inode->i_ino);
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, signed_name,
+			     buf, val_len);
+	KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+	e->e_value_inum = cpu_to_le32(ea_inode->i_ino);
+
+	/*
+	 * Lustre-style old EA inode compatibility: when e_hash != ea_inode
+	 * hash, but parent ino (stored in i_mtime) and generation (stored in
+	 * i_generation) match parent, ext4_xattr_inode_iget() sets
+	 * EXT4_STATE_LUSTRE_EA_INODE and forces ref_count = 1.
+	 */
+	e->e_hash = 0;
+	inode->i_generation = 42;
+	ea_inode->i_generation = 42;
+	inode_set_mtime(ea_inode, inode->i_ino, 0);
+	inode_set_ctime(ea_inode, 0, 0);
+	inode_set_iversion_raw(ea_inode, 0);
+	/* Even with initial ref_count = 0, Lustre compat sets ref = 1 */
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, signed_name,
+			     buf, val_len);
+	KUNIT_EXPECT_EQ(test, ret, (int)val_len);
+	KUNIT_EXPECT_NE(test,
+			ext4_test_inode_state(ea_inode,
+					      EXT4_STATE_LUSTRE_EA_INODE),
+			0);
+}
+
+/*
+ * Test 10: ext4_get_inode_usage(), credit calculation, handle credit check,
+ * ext4_expand_extra_isize_ea(), and ext4_xattr_delete_inode().
+ */
+static void test_xattr_credits_usage_expand_and_delete(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx = test->priv;
+	struct inode *inode = &ctx->primary.ei.vfs_inode;
+	struct ext4_inode *raw = (struct ext4_inode *)ctx->primary.raw_buf;
+	journal_t fake_journal = {
+		.j_revoke_records_per_block = 64,
+	};
+	handle_t fake_handle = {
+		.h_reserved = 1,
+		.h_journal = &fake_journal,
+		.h_total_credits = 1,
+	};
+	const size_t large_len = 1000;
+	char *large_val;
+	char val28[28];
+	char buf[64];
+	qsize_t usage = 0;
+	int credits_create, credits_replace, credits_out = -1;
+	int ret;
+
+	large_val = kunit_kmalloc(test, large_len, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, large_val);
+	memset(large_val, 'L', large_len);
+
+	/*
+	 * 1. Credit calculation and insufficient handle credits check
+	 */
+	credits_create = __ext4_xattr_set_credits(&ctx->sb, inode, NULL,
+						  100, true);
+	KUNIT_EXPECT_GT(test, credits_create, 0);
+
+	ext4_set_feature_ea_inode(&ctx->sb);
+	credits_create = __ext4_xattr_set_credits(&ctx->sb, inode, NULL,
+						  1500, true);
+	credits_replace = __ext4_xattr_set_credits(&ctx->sb, inode, NULL,
+						   1500, false);
+	KUNIT_EXPECT_GT(test, credits_replace, credits_create);
+
+	/* Without a journal (s_journal == NULL), credits_out is 0 */
+	ret = ext4_xattr_set_credits(inode, 1500, true, &credits_out);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, credits_out, 0);
+
+	/* With s_journal set, ext4_xattr_set_credits() returns credits_create */
+	ctx->sbi.s_journal = &fake_journal;
+	ret = ext4_xattr_set_credits(inode, 1500, true, &credits_out);
+	ctx->sbi.s_journal = NULL;
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, credits_out, credits_create);
+
+	/* Handle with only 1 buffer credit (< credits) fails with -ENOSPC */
+	ret = ext4_xattr_set_handle(&fake_handle, inode, EXT4_XATTR_INDEX_USER,
+				    "k", "v", 1, XATTR_CREATE);
+	KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+
+	/*
+	 * 2. ext4_expand_extra_isize_ea():
+	 * Put "keep" (4B -> 24B total) and "evict" (28B -> 52B total) in
+	 * ibody (88B usable, leaving 12B free).
+	 * First expand i_extra_isize from 32 -> 40 (8 bytes <= 12B free):
+	 * shifts ibody entries in-place without allocating an external block.
+	 * Then expand i_extra_isize from 40 -> 72 (32 bytes > 4B free):
+	 * "keep" (24 + 4 = 28 < 32) is too small alone, so "evict" (52 + 4 >= 32)
+	 * is moved to an external xattr block while "keep" stays in ibody!
+	 */
+	ext4_clear_feature_ea_inode(&ctx->sb);
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "keep",
+			     "1234", 4, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	memset(val28, 'E', sizeof(val28));
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "evict",
+			     val28, sizeof(val28), XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_file_acl, 0ULL);
+
+	/* In-place ibody shift (32 -> 40) */
+	ret = ext4_expand_extra_isize_ea(inode, 40, raw, NULL);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_extra_isize, (u16)40);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_file_acl, 0ULL);
+
+	/* Eviction of "evict" to external block (40 -> 72) */
+	ret = ext4_expand_extra_isize_ea(inode, 72, raw, NULL);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_extra_isize, (u16)72);
+	KUNIT_EXPECT_NE(test, EXT4_I(inode)->i_file_acl, 0ULL);
+
+	/* Both "keep" (in ibody) and "evict" (in external block) remain readable */
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_ibody_get(inode, EXT4_XATTR_INDEX_USER,
+					     "keep", buf, sizeof(buf)),
+			4);
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER,
+				       "evict", buf, sizeof(buf)),
+			28);
+
+	/*
+	 * 3. ext4_get_inode_usage() and ext4_xattr_delete_inode():
+	 * Add a 1000-byte EA inode attribute ("large_ea") and check that
+	 * ext4_get_inode_usage() accounts for 1 (parent inode) + 1 (EA inode)
+	 * = 2 inodes, and ext4_xattr_delete_inode() frees the external block
+	 * and drops the EA inode reference to 0.
+	 */
+	ext4_set_feature_ea_inode(&ctx->sb);
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "large_ea",
+			     large_val, large_len, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	down_read(&EXT4_I(inode)->xattr_sem);
+	ret = ext4_get_inode_usage(inode, &usage);
+	up_read(&EXT4_I(inode)->xattr_sem);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, usage, (qsize_t)2);
+
+	ret = ext4_xattr_delete_inode(NULL, inode, 0);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_file_acl, 0ULL);
+	KUNIT_EXPECT_EQ(test, ctx->ea_inodes[0].ei.vfs_inode.i_nlink, 0U);
+}
+
+/*
+ * Test 11: Error injection and rollback paths across inode location lookup,
+ * external block allocation/getblk/bread/dirty_metadata, and EA inode
+ * creation/write/lookup.
+ */
+static void test_xattr_error_injection(struct kunit *test)
+{
+	struct xattr_test_ctx *ctx = test->priv;
+	struct inode *inode = &ctx->primary.ei.vfs_inode;
+	const size_t large_len = 1000;
+	char *large_val;
+	char val100[100];
+	char buf[128];
+	unsigned int prev_free;
+	int ret;
+
+	large_val = kunit_kmalloc(test, large_len, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, large_val);
+	memset(large_val, 'F', large_len);
+	memset(val100, 'B', sizeof(val100));
+
+	/* 1. ext4_get_inode_loc() failure on set and get */
+	ctx->fail_get_inode_loc = -EIO;
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "k",
+			     "v", 1, XATTR_CREATE);
+	KUNIT_EXPECT_EQ(test, ret, -EIO);
+
+	ext4_set_inode_state(inode, EXT4_STATE_XATTR);
+	ret = ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "k",
+			     buf, sizeof(buf));
+	KUNIT_EXPECT_EQ(test, ret, -EIO);
+	ext4_clear_inode_state(inode, EXT4_STATE_XATTR);
+	ctx->fail_get_inode_loc = 0;
+
+	/* 2. ext4_mark_iloc_dirty() failure on set */
+	ctx->fail_mark_iloc_dirty = -EIO;
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "k",
+			     "v", 1, XATTR_CREATE);
+	ctx->fail_mark_iloc_dirty = 0;
+	KUNIT_EXPECT_EQ(test, ret, -EIO);
+	/* Clean up the entry written to raw_buf before mark_iloc_dirty */
+	KUNIT_ASSERT_EQ(test,
+			ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "k",
+				       NULL, 0, XATTR_REPLACE),
+			0);
+
+	/* 3. External block allocation failure (-ENOSPC) */
+	ctx->fail_alloc_block = -ENOSPC;
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "blk",
+			     val100, sizeof(val100), XATTR_CREATE);
+	ctx->fail_alloc_block = 0;
+	KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_file_acl, 0ULL);
+
+	/*
+	 * 4. ext4_xattr_getblk() failure (-ENOMEM) after block allocation must
+	 * free the newly allocated block via ext4_free_blocks().
+	 */
+	prev_free = ctx->free_block_calls;
+	ctx->fail_xattr_getblk = true;
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "blk",
+			     val100, sizeof(val100), XATTR_CREATE);
+	ctx->fail_xattr_getblk = false;
+	KUNIT_EXPECT_EQ(test, ret, -ENOMEM);
+	KUNIT_EXPECT_EQ(test, ctx->free_block_calls, prev_free + 1);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_file_acl, 0ULL);
+
+	/* 5. ext4_handle_dirty_metadata() failure when creating external block */
+	ctx->fail_dirty_metadata = -EIO;
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "blk",
+			     val100, sizeof(val100), XATTR_CREATE);
+	ctx->fail_dirty_metadata = 0;
+	KUNIT_EXPECT_EQ(test, ret, -EIO);
+	KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_file_acl, 0ULL);
+
+	/* Successfully create external block, then test ext4_sb_bread() failure */
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "blk",
+			     val100, sizeof(val100), XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	ctx->fail_sb_bread = -EIO;
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "blk",
+				       buf, sizeof(buf)),
+			-EIO);
+	KUNIT_EXPECT_EQ(test,
+			ext4_listxattr(&ctx->root_dentry, buf, sizeof(buf)),
+			(ssize_t)-EIO);
+	KUNIT_EXPECT_EQ(test, ext4_xattr_delete_inode(NULL, inode, 0), -EIO);
+	ctx->fail_sb_bread = 0;
+
+	/*
+	 * 6. EA inode block allocation failure in ext4_xattr_inode_write()
+	 * must clean up the newly created EA inode (dropping its i_nlink to 0).
+	 */
+	ext4_set_feature_ea_inode(&ctx->sb);
+	ctx->fail_alloc_block = -ENOSPC;
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "big_err",
+			     large_val, large_len, XATTR_CREATE);
+	ctx->fail_alloc_block = 0;
+	KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+	KUNIT_EXPECT_TRUE(test, ctx->ea_inodes[0].allocated);
+	KUNIT_EXPECT_EQ(test, ctx->ea_inodes[0].ei.vfs_inode.i_nlink, 0U);
+
+	/* 7. Successfully create EA inode, then test __ext4_iget() and ext4_getblk() failure */
+	ret = ext4_xattr_set(inode, EXT4_XATTR_INDEX_USER, "big_ok",
+			     large_val, large_len, XATTR_CREATE);
+	KUNIT_ASSERT_EQ(test, ret, 0);
+
+	ctx->fail_iget = -EIO;
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "big_ok",
+				       large_val, large_len),
+			-EIO);
+	ctx->fail_iget = 0;
+
+	ctx->fail_getblk = -EIO;
+	KUNIT_EXPECT_EQ(test,
+			ext4_xattr_get(inode, EXT4_XATTR_INDEX_USER, "big_ok",
+				       large_val, large_len),
+			-EIO);
+	ctx->fail_getblk = 0;
+}
+
+static struct kunit_case xattr_test_cases[] = {
+	KUNIT_CASE(test_xattr_macros_and_locking),
+	KUNIT_CASE_PARAM(test_xattr_check_inode, xattr_check_gen_params),
+	KUNIT_CASE(test_xattr_ibody_basic_and_flags),
+	KUNIT_CASE(test_xattr_ibody_replace_and_remove),
+	KUNIT_CASE(test_xattr_external_block_and_check),
+	KUNIT_CASE(test_xattr_block_dedup_and_cow),
+	KUNIT_CASE(test_xattr_list_and_handlers),
+	KUNIT_CASE(test_xattr_ea_inode_lifecycle_and_dedup),
+	KUNIT_CASE(test_xattr_ea_inode_validation_and_compat),
+	KUNIT_CASE(test_xattr_credits_usage_expand_and_delete),
+	KUNIT_CASE(test_xattr_error_injection),
+	{}
+};
+
+static struct kunit_suite xattr_test_suite = {
+	.name = "ext4_xattr_test",
+	.init = xattr_test_init,
+	.exit = xattr_test_exit,
+	.test_cases = xattr_test_cases,
+};
+
+kunit_test_suites(&xattr_test_suite);
+
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
index 5c310747b965..f9397a7124fd 100644
--- a/fs/ext4/xattr.c
+++ b/fs/ext4/xattr.c
@@ -57,6 +57,7 @@
 #include <linux/mbcache.h>
 #include <linux/quotaops.h>
 #include <linux/iversion.h>
+#include <kunit/static_stub.h>
 #include "ext4_jbd2.h"
 #include "ext4.h"
 #include "xattr.h"
@@ -107,6 +108,7 @@ const struct xattr_handler * const ext4_xattr_handlers[] = {
 	&ext4_xattr_hurd_handler,
 	NULL
 };
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_handlers);
 
 #define EA_BLOCK_CACHE(inode)	(((struct ext4_sb_info *) \
 				inode->i_sb->s_fs_info)->s_ea_block_cache)
@@ -169,6 +171,14 @@ static void ext4_xattr_block_csum_set(struct inode *inode,
 						bh->b_blocknr, BHDR(bh));
 }
 
+struct buffer_head *ext4_xattr_getblk(struct super_block *sb,
+				      ext4_fsblk_t block)
+{
+	KUNIT_STATIC_STUB_REDIRECT(ext4_xattr_getblk, sb, block);
+	return sb_getblk(sb, block);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_getblk);
+
 static inline const char *ext4_xattr_prefix(int name_index,
 					    struct dentry *dentry)
 {
@@ -315,6 +325,7 @@ __xattr_check_inode(struct inode *inode, struct ext4_xattr_ibody_header *header,
 	return check_xattrs(inode, NULL, IFIRST(header), end, IFIRST(header),
 			    function, line);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(__xattr_check_inode);
 
 static int
 xattr_find_entry(struct inode *inode, struct ext4_xattr_entry **pentry,
@@ -493,6 +504,7 @@ void ext4_evict_ea_inode(struct inode *inode)
 		mb_cache_entry_put(EA_INODE_CACHE(inode), oe);
 	}
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_evict_ea_inode);
 
 static int
 ext4_xattr_inode_verify_hashes(struct inode *ea_inode,
@@ -693,6 +705,7 @@ ext4_xattr_ibody_get(struct inode *inode, int name_index, const char *name,
 	brelse(iloc.bh);
 	return error;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_ibody_get);
 
 /*
  * ext4_xattr_get()
@@ -725,6 +738,7 @@ ext4_xattr_get(struct inode *inode, int name_index, const char *name,
 	up_read(&EXT4_I(inode)->xattr_sem);
 	return error;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_get);
 
 static int
 ext4_xattr_list_entries(struct dentry *dentry, struct ext4_xattr_entry *entry,
@@ -841,6 +855,7 @@ ext4_listxattr(struct dentry *dentry, char *buffer, size_t buffer_size)
 	up_read(&EXT4_I(d_inode(dentry))->xattr_sem);
 	return ret;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_listxattr);
 
 /*
  * If the EXT4_FEATURE_COMPAT_EXT_ATTR feature of this file system is
@@ -912,6 +927,7 @@ int ext4_get_inode_usage(struct inode *inode, qsize_t *usage)
 	brelse(bh);
 	return ret;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_inode_usage);
 
 static inline size_t round_up_cluster(struct inode *inode, size_t length)
 {
@@ -1029,6 +1045,7 @@ int __ext4_xattr_set_credits(struct super_block *sb, struct inode *inode,
 	}
 	return credits;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_xattr_set_credits);
 
 static int ext4_xattr_inode_update_ref(handle_t *handle, struct inode *ea_inode,
 				       int ref_change)
@@ -2136,7 +2153,7 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
 			ea_idebug(inode, "creating block %llu",
 				  (unsigned long long)block);
 
-			new_bh = sb_getblk(sb, block);
+			new_bh = ext4_xattr_getblk(sb, block);
 			if (unlikely(!new_bh)) {
 				error = -ENOMEM;
 getblk_failed:
@@ -2252,6 +2269,7 @@ int ext4_xattr_ibody_find(struct inode *inode, struct ext4_xattr_info *i,
 	}
 	return 0;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_ibody_find);
 
 int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
 				struct ext4_xattr_info *i,
@@ -2302,6 +2320,7 @@ int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
 	ext4_put_ea_inode(ea_inode);
 	return 0;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_ibody_set);
 
 static int ext4_xattr_value_same(struct ext4_xattr_search *s,
 				 struct ext4_xattr_info *i)
@@ -2499,6 +2518,7 @@ ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
 	ext4_write_unlock_xattr(inode, &no_expand);
 	return error;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_set_handle);
 
 int ext4_xattr_set_credits(struct inode *inode, size_t value_len,
 			   bool is_create, int *credits)
@@ -2526,6 +2546,7 @@ int ext4_xattr_set_credits(struct inode *inode, size_t value_len,
 	up_read(&EXT4_I(inode)->xattr_sem);
 	return err;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_set_credits);
 
 /*
  * ext4_xattr_set()
@@ -2574,6 +2595,7 @@ ext4_xattr_set(struct inode *inode, int name_index, const char *name,
 
 	return error;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_set);
 
 /*
  * Shift the EA entries in the inode to create space for the increased
@@ -2865,6 +2887,7 @@ int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
 	}
 	return error;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_expand_extra_isize_ea);
 
 /*
  * ext4_xattr_delete_inode()
@@ -2973,6 +2996,7 @@ int ext4_xattr_delete_inode(handle_t *handle, struct inode *inode,
 	brelse(bh);
 	return error;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_delete_inode);
 
 /*
  * Worker function for deferred EA inode iput.  Processes all inodes queued
@@ -3026,12 +3050,14 @@ void ext4_put_ea_inode(struct inode *inode)
 	 */
 	schedule_delayed_work(&EXT4_SB(inode->i_sb)->s_ea_inode_work, 1);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_put_ea_inode);
 
 void ext4_init_ea_inode_work(struct ext4_sb_info *sbi)
 {
 	init_llist_head(&sbi->s_ea_inode_to_free);
 	INIT_DELAYED_WORK(&sbi->s_ea_inode_work, ext4_ea_inode_work);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_ea_inode_work);
 
 /*
  * ext4_xattr_block_cache_insert()
@@ -3229,10 +3255,12 @@ ext4_xattr_create_cache(void)
 {
 	return mb_cache_create(HASH_BUCKET_BITS);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_create_cache);
 
 void ext4_xattr_destroy_cache(struct mb_cache *cache)
 {
 	if (cache)
 		mb_cache_destroy(cache);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_destroy_cache);
 
diff --git a/fs/ext4/xattr.h b/fs/ext4/xattr.h
index 821dc6a50e51..1a72e68cea0c 100644
--- a/fs/ext4/xattr.h
+++ b/fs/ext4/xattr.h
@@ -229,3 +229,5 @@ static inline void ext4_xattr_inode_set_class(struct inode *ea_inode) { }
 #endif
 
 extern int ext4_get_inode_usage(struct inode *inode, qsize_t *usage);
+extern struct buffer_head *ext4_xattr_getblk(struct super_block *sb,
+					     ext4_fsblk_t block);
diff --git a/fs/ext4/xattr_hurd.c b/fs/ext4/xattr_hurd.c
index 8a5842e4cd95..69163a6eda2c 100644
--- a/fs/ext4/xattr_hurd.c
+++ b/fs/ext4/xattr_hurd.c
@@ -50,3 +50,7 @@ const struct xattr_handler ext4_xattr_hurd_handler = {
 	.get	= ext4_xattr_hurd_get,
 	.set	= ext4_xattr_hurd_set,
 };
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_hurd_handler);
+#endif
diff --git a/fs/ext4/xattr_trusted.c b/fs/ext4/xattr_trusted.c
index 9811eb0ab276..4329c020201b 100644
--- a/fs/ext4/xattr_trusted.c
+++ b/fs/ext4/xattr_trusted.c
@@ -45,3 +45,7 @@ const struct xattr_handler ext4_xattr_trusted_handler = {
 	.get	= ext4_xattr_trusted_get,
 	.set	= ext4_xattr_trusted_set,
 };
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_trusted_handler);
+#endif
diff --git a/fs/ext4/xattr_user.c b/fs/ext4/xattr_user.c
index 4b70bf4e7626..cb043545d411 100644
--- a/fs/ext4/xattr_user.c
+++ b/fs/ext4/xattr_user.c
@@ -48,3 +48,7 @@ const struct xattr_handler ext4_xattr_user_handler = {
 	.get	= ext4_xattr_user_get,
 	.set	= ext4_xattr_user_set,
 };
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_user_handler);
+#endif
-- 
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 ` Theodore Ts'o [this message]
2026-11-12  4:26   ` [PATCH 04/27] ext4: add KUnit tests for fs/ext4/xattr.c 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 ` [PATCH 11/27] ext4: add KUnit tests for fs/ext4/ialloc.c Theodore Ts'o
2026-11-12  4:25   ` 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-5-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