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
next prev 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.