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

Add a KUnit test suite (ext4_ioctl_test) covering superblock online
tuning, backup superblock propagation, inode data swapping, fileattr
validation, forced shutdown, and ioctl dispatch in fs/ext4/ioctl.c:

- Superblock modification callbacks (ext4_sb_setlabel, ext4_sb_setuuid,
  and ext4_sb_setparams across error behavior, mount counts, 64-bit
  reserved blocks, 32-bit split reserved uid/gid, hash/RAID/encoding
  fields, mount_opts, DIR_INDEX / CSUM_SEED feature edits, and
  FORCE_FSCK).
- Parameter validation and normalization in ext4_validate_tune_sb_params
  (-E2BIG mount_opts, -EOPNOTSUPP flags/feature clears, reserved_blocks
  and hash algorithm bounds, DIR_INDEX hash seed initialization, and
  CASEFOLD encoding defaults).
- Primary and sparse_super backup superblock updates via
  ext4_update_backup_sb, ext4_update_superblocks_fn (including
  EXT4_FLAGS_RESIZING contention and unjournaled >2 backup updates), and
  ext4_update_overhead skip/force semantics.
- Inode swap rollback invariants in swap_inode_data (swapped vs
  preserved flags, extent status cache clearing, double-swap round-trip)
  and metadata_csum seed recalculation in ext4_reset_inode_seed.
- Immutable enforcement, DAX mutual-exclusion rules (including inline
  directories and verity-in-progress), and end-to-end ext4_fileattr_get
  / ext4_fileattr_set flag masking and VFS flag synchronization.
- Forced shutdown state transitions (ext4_force_shutdown) and
  ext4_ioctl / ext4_compat_ioctl dispatch checks.

Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
 fs/ext4/Makefile     |    2 +-
 fs/ext4/ext4.h       |   33 ++
 fs/ext4/ioctl-test.c | 1169 ++++++++++++++++++++++++++++++++++++++++++
 fs/ext4/ioctl.c      |  249 ++++++---
 4 files changed, 1382 insertions(+), 71 deletions(-)
 create mode 100644 fs/ext4/ioctl-test.c

diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index ff489dff825d..c18530508863 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -21,7 +21,7 @@ ext4-test-objs				+= inode-test.o mballoc-test.o \
 					   fast_commit-test.o ialloc-test.o \
 					   balloc-test.o orphan-test.o \
 					   block_validity-test.o super-test.o \
-					   sysfs-test.o
+					   sysfs-test.o ioctl-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 599b595befd8..842fba63f404 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3337,6 +3337,39 @@ int ext4_fileattr_get(struct dentry *dentry, struct file_kattr *fa);
 extern void ext4_reset_inode_seed(struct inode *inode);
 int ext4_update_overhead(struct super_block *sb, bool force);
 int ext4_force_shutdown(struct super_block *sb, u32 flags);
+int ext4_ioctl_sync_bh(struct buffer_head *bh);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+struct ext4_tune_sb_params;
+void ext4_sb_setlabel_test(struct ext4_sb_info *sbi,
+			   struct ext4_super_block *es, const void *arg);
+void ext4_sb_setuuid_test(struct ext4_sb_info *sbi,
+			  struct ext4_super_block *es, const void *arg);
+void ext4_sb_setparams_test(struct ext4_sb_info *sbi,
+			    struct ext4_super_block *es, const void *arg);
+int ext4_validate_tune_sb_params_test(struct super_block *sb,
+				      struct ext4_tune_sb_params *params);
+int ext4_update_primary_sb_test(struct super_block *sb, handle_t *handle,
+				void (*func)(struct ext4_sb_info *,
+					     struct ext4_super_block *,
+					     const void *),
+				const void *arg);
+int ext4_update_backup_sb_test(struct super_block *sb, handle_t *handle,
+			       ext4_group_t grp,
+			       void (*func)(struct ext4_sb_info *,
+					    struct ext4_super_block *,
+					    const void *),
+			       const void *arg);
+int ext4_update_superblocks_fn_test(struct super_block *sb,
+				    void (*func)(struct ext4_sb_info *,
+						 struct ext4_super_block *,
+						 const void *),
+				    const void *arg);
+void ext4_swap_inode_data_test(struct inode *inode1, struct inode *inode2);
+int ext4_ioctl_check_immutable_test(struct inode *inode, __u32 new_projid,
+				    unsigned int flags);
+bool ext4_dax_compatible_test(struct inode *inode, unsigned int oldflags,
+			      unsigned int flags);
+#endif
 
 /* migrate.c */
 extern int ext4_ext_migrate(struct inode *);
diff --git a/fs/ext4/ioctl-test.c b/fs/ext4/ioctl-test.c
new file mode 100644
index 000000000000..fa31a9cc5d92
--- /dev/null
+++ b/fs/ext4/ioctl-test.c
@@ -0,0 +1,1169 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/ioctl.c
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/compat.h>
+#include <linux/dcache.h>
+#include <linux/fileattr.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/fscrypt.h>
+#include <linux/fsverity.h>
+#include <linux/iversion.h>
+#include <linux/kernel.h>
+#include <linux/mm.h>
+#include <linux/module.h>
+#include <linux/mount.h>
+#include <linux/part_stat.h>
+#include <linux/percpu.h>
+#include <linux/percpu_counter.h>
+#include <linux/quotaops.h>
+#include <linux/rcupdate.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/timer.h>
+#include <linux/uuid.h>
+#include <linux/workqueue.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "xattr.h"
+
+#define IOCTL_TEST_NGROUPS		8U
+#define IOCTL_TEST_BLK_SIZE		1024U
+#define IOCTL_TEST_BLK_BITS		10U
+#define IOCTL_TEST_BLOCKS_PER_GRP	1024U
+#define IOCTL_TEST_FIRST_DATA_BLK	1U
+#define IOCTL_TEST_TOTAL_BLKS		(IOCTL_TEST_FIRST_DATA_BLK + \
+					 IOCTL_TEST_NGROUPS * \
+					 IOCTL_TEST_BLOCKS_PER_GRP)
+#define IOCTL_TEST_INODE_SIZE		256U
+#define IOCTL_TEST_EXTRA_ISIZE		32U
+#define IOCTL_TEST_INODES_PER_GRP	32U
+#define IOCTL_TEST_TOTAL_INODES		(IOCTL_TEST_NGROUPS * \
+					 IOCTL_TEST_INODES_PER_GRP)
+#define IOCTL_TEST_FIRST_INO		11U
+
+struct ioctl_test_ctx {
+	struct super_block *sb;
+	struct ext4_sb_info sbi;
+	struct ext4_super_block *es;
+	struct blockgroup_lock bgl;
+	struct gendisk bdev_disk;
+	struct block_device bdev;
+	struct request_queue bdev_queue;
+	struct inode bd_inode;
+	struct folio *bh_folio;
+	struct buffer_head sb_bh;
+	u8 primary_sb_buf[IOCTL_TEST_BLK_SIZE];
+	struct buffer_head backup_bhs[IOCTL_TEST_NGROUPS];
+	u8 backup_sb_bufs[IOCTL_TEST_NGROUPS][IOCTL_TEST_BLK_SIZE];
+	struct buffer_head iloc_bh;
+	u8 iloc_buf[IOCTL_TEST_BLK_SIZE];
+	int sync_bh_calls;
+	int sync_bh_err;
+	bool counters_inited;
+};
+
+static struct kunit *active_ioctl_test;
+
+static struct inode *ioctl_test_alloc_inode(struct super_block *sb)
+{
+	struct ext4_inode_info *ei;
+
+	ei = kzalloc_obj(*ei);
+	if (!ei)
+		return NULL;
+
+	init_rwsem(&ei->i_data_sem);
+	inode_init_once(&ei->vfs_inode);
+	inode_set_iversion(&ei->vfs_inode, 1);
+	ei->i_prealloc_node = RB_ROOT;
+	rwlock_init(&ei->i_prealloc_lock);
+	ext4_es_init_tree(&ei->i_es_tree);
+	rwlock_init(&ei->i_es_lock);
+	INIT_LIST_HEAD(&ei->i_es_list);
+	spin_lock_init(&ei->i_raw_lock);
+	spin_lock_init(&ei->i_block_reservation_lock);
+	ext4_init_pending_tree(&ei->i_pending_tree);
+	init_rwsem(&ei->xattr_sem);
+	INIT_LIST_HEAD(&ei->i_rsv_conversion_list);
+	spin_lock_init(&ei->i_completed_io_lock);
+	INIT_LIST_HEAD(&ei->i_orphan);
+	ext4_fc_init_inode(&ei->vfs_inode);
+	spin_lock_init(&ei->i_fc_lock);
+	ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+	ei->i_extra_isize = IOCTL_TEST_EXTRA_ISIZE;
+	ei->i_last_alloc_group = ~0U;
+
+	return &ei->vfs_inode;
+}
+
+static void ioctl_test_free_inode(struct inode *inode)
+{
+	ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+	kfree(EXT4_I(inode));
+}
+
+static void ioctl_test_evict_inode(struct inode *inode)
+{
+	truncate_inode_pages_final(&inode->i_data);
+	clear_inode(inode);
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **ioctl_test_get_dquots(struct inode *inode)
+{
+	return EXT4_I(inode)->i_dquot;
+}
+#endif
+
+static const struct super_operations ioctl_test_sops = {
+	.alloc_inode	= ioctl_test_alloc_inode,
+	.free_inode	= ioctl_test_free_inode,
+	.evict_inode	= ioctl_test_evict_inode,
+#ifdef CONFIG_QUOTA
+	.get_dquots	= ioctl_test_get_dquots,
+#endif
+};
+
+static void ioctl_test_sb_upd_work(struct work_struct *work)
+{
+}
+
+static void ioctl_test_err_timer(struct timer_list *t)
+{
+}
+
+static int ioctl_test_init_fs_context(struct fs_context *fc)
+{
+	return 0;
+}
+
+static struct file_system_type ioctl_test_fs_type = {
+	.name		 = "ext4_ioctl_kunit",
+	.init_fs_context = ioctl_test_init_fs_context,
+	.kill_sb	 = kill_anon_super,
+};
+
+static struct buffer_head *ioctl_sb_bread_stub(struct super_block *sb,
+					       sector_t block,
+					       blk_opf_t op_flags)
+{
+	struct kunit *test = active_ioctl_test;
+	struct ioctl_test_ctx *ctx;
+	ext4_group_t g;
+
+	if (!test)
+		return ERR_PTR(-EIO);
+	ctx = test->priv;
+	if (!ctx || sb != ctx->sb)
+		return ERR_PTR(-EIO);
+
+	for (g = 0; g < IOCTL_TEST_NGROUPS; g++) {
+		ext4_fsblk_t grp_blk = ext4_group_first_block_no(sb, g);
+
+		if ((ext4_fsblk_t)block == grp_blk) {
+			struct buffer_head *bh = &ctx->backup_bhs[g];
+
+			get_bh(bh);
+			return bh;
+		}
+	}
+
+	return ERR_PTR(-ENOENT);
+}
+
+static int ioctl_sync_bh_stub(struct buffer_head *bh)
+{
+	struct kunit *test = active_ioctl_test;
+	struct ioctl_test_ctx *ctx;
+
+	if (!test)
+		return 0;
+	ctx = test->priv;
+	if (!ctx)
+		return 0;
+
+	ctx->sync_bh_calls++;
+	return ctx->sync_bh_err;
+}
+
+static int ioctl_get_inode_loc_stub(struct inode *inode, struct ext4_iloc *iloc)
+{
+	struct kunit *test = active_ioctl_test;
+	struct ioctl_test_ctx *ctx;
+
+	if (!test)
+		return -EIO;
+	ctx = test->priv;
+	if (!ctx)
+		return -EIO;
+
+	get_bh(&ctx->iloc_bh);
+	iloc->bh = &ctx->iloc_bh;
+	iloc->offset = 0;
+	iloc->block_group = 0;
+	return 0;
+}
+
+static int ioctl_mark_iloc_dirty_stub(handle_t *handle, struct inode *inode,
+				      struct ext4_iloc *iloc)
+{
+	brelse(iloc->bh);
+	iloc->bh = NULL;
+	return 0;
+}
+
+static void ioctl_init_mock_bh(struct buffer_head *bh, struct folio *folio,
+			       u8 *data, size_t size, sector_t blocknr)
+{
+	memset(bh, 0, sizeof(*bh));
+	bh->b_folio = folio;
+	bh->b_data = (char *)data;
+	bh->b_size = size;
+	bh->b_blocknr = blocknr;
+	atomic_set(&bh->b_count, 2);
+	set_buffer_uptodate(bh);
+}
+
+static void ioctl_sync_backups_from_primary(struct ioctl_test_ctx *ctx)
+{
+	struct super_block *sb = ctx->sb;
+	ext4_group_t g;
+
+	ext4_superblock_csum_set(sb);
+	for (g = 0; g < IOCTL_TEST_NGROUPS; g++) {
+		struct ext4_super_block *bes;
+
+		bes = (struct ext4_super_block *)ctx->backup_sb_bufs[g];
+		memcpy(bes, ctx->es, sizeof(*bes));
+		bes->s_block_group_nr = cpu_to_le16(g);
+		if (ext4_has_feature_metadata_csum(sb))
+			bes->s_checksum = ext4_superblock_csum(bes);
+	}
+}
+
+static void ioctl_test_cleanup(struct ioctl_test_ctx *ctx)
+{
+	active_ioctl_test = NULL;
+
+	if (ctx->sb) {
+		deactivate_super(ctx->sb);
+		rcu_barrier();
+	}
+
+	cancel_work_sync(&ctx->sbi.s_sb_upd_work);
+	timer_delete_sync(&ctx->sbi.s_err_report);
+
+	if (ctx->counters_inited) {
+		percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+		percpu_counter_destroy(&ctx->sbi.s_dirtyclusters_counter);
+		percpu_counter_destroy(&ctx->sbi.s_freeinodes_counter);
+	}
+
+	if (ctx->bh_folio)
+		folio_put(ctx->bh_folio);
+	free_percpu(ctx->bdev.bd_stats);
+	kfree(ctx);
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(ioctl_test_cleanup_action, ioctl_test_cleanup,
+			    struct ioctl_test_ctx *);
+
+static int ioctl_test_init(struct kunit *test)
+{
+	struct ioctl_test_ctx *ctx;
+	struct fs_context *fc;
+	struct super_block *sb;
+	struct ext4_sb_info *sbi;
+	struct inode *root_inode;
+	ext4_group_t g;
+	int err;
+
+	ctx = kzalloc_obj(*ctx);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	test->priv = ctx;
+	active_ioctl_test = test;
+
+	ctx->bdev.bd_stats = alloc_percpu(struct disk_stats);
+	if (!ctx->bdev.bd_stats) {
+		kfree(ctx);
+		KUNIT_FAIL_AND_ABORT(test, "failed to alloc bd_stats");
+	}
+
+	ctx->bh_folio = folio_alloc(GFP_KERNEL, 0);
+	if (!ctx->bh_folio) {
+		free_percpu(ctx->bdev.bd_stats);
+		kfree(ctx);
+		KUNIT_FAIL_AND_ABORT(test, "failed to alloc bh_folio");
+	}
+
+	fc = fs_context_for_mount(&ioctl_test_fs_type, 0);
+	if (IS_ERR(fc)) {
+		err = PTR_ERR(fc);
+		folio_put(ctx->bh_folio);
+		free_percpu(ctx->bdev.bd_stats);
+		kfree(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+	sb = sget_fc(fc, NULL, set_anon_super_fc);
+	put_fs_context(fc);
+	if (IS_ERR(sb)) {
+		err = PTR_ERR(sb);
+		folio_put(ctx->bh_folio);
+		free_percpu(ctx->bdev.bd_stats);
+		kfree(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+	ctx->sb = sb;
+	sbi = &ctx->sbi;
+
+	err = percpu_counter_init(&sbi->s_freeclusters_counter, 0, GFP_KERNEL);
+	if (err) {
+		up_write(&sb->s_umount);
+		ioctl_test_cleanup(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+	err = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0, GFP_KERNEL);
+	if (err) {
+		percpu_counter_destroy(&sbi->s_freeclusters_counter);
+		up_write(&sb->s_umount);
+		ioctl_test_cleanup(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+	err = percpu_counter_init(&sbi->s_freeinodes_counter, 0, GFP_KERNEL);
+	if (err) {
+		percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
+		percpu_counter_destroy(&sbi->s_freeclusters_counter);
+		up_write(&sb->s_umount);
+		ioctl_test_cleanup(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+	ctx->counters_inited = true;
+
+	INIT_WORK(&sbi->s_sb_upd_work, ioctl_test_sb_upd_work);
+	timer_setup(&sbi->s_err_report, ioctl_test_err_timer, 0);
+	spin_lock_init(&sbi->s_error_lock);
+	spin_lock_init(&sbi->s_md_lock);
+	spin_lock_init(&sbi->s_bdev_wb_lock);
+	spin_lock_init(&sbi->s_es_lock);
+	INIT_LIST_HEAD(&sbi->s_es_list);
+	mutex_init(&sbi->s_fc_lock);
+	INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_MAIN]);
+	INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_STAGING]);
+	INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_MAIN]);
+	INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_STAGING]);
+	ratelimit_state_init(&sbi->s_err_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+	ratelimit_state_init(&sbi->s_warning_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+	ratelimit_state_init(&sbi->s_msg_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+
+	sbi->s_sb = sb;
+	sb->s_fs_info = sbi;
+	sb->s_op = &ioctl_test_sops;
+#ifdef CONFIG_FS_ENCRYPTION
+	sb->s_cop = &ext4_cryptops;
+#endif
+	sb->s_flags |= SB_ACTIVE;
+	sb->s_blocksize = IOCTL_TEST_BLK_SIZE;
+	sb->s_blocksize_bits = IOCTL_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_ioctl_kunit", sizeof(sb->s_id));
+
+	inode_init_once(&ctx->bd_inode);
+	ctx->bd_inode.i_sb = sb;
+	ctx->bd_inode.i_blkbits = IOCTL_TEST_BLK_BITS;
+	ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data;
+	ctx->bd_inode.i_data.host = &ctx->bd_inode;
+	ctx->bdev_queue.limits.logical_block_size = 512;
+	ctx->bdev_queue.limits.dma_alignment = 511;
+	ctx->bdev_disk.part0 = &ctx->bdev;
+	ctx->bdev_disk.queue = &ctx->bdev_queue;
+	ctx->bdev.bd_disk = &ctx->bdev_disk;
+	ctx->bdev.bd_queue = &ctx->bdev_queue;
+	ctx->bdev.bd_mapping = &ctx->bd_inode.i_data;
+	bdev_clear_flag(&ctx->bdev, BD_READ_ONLY);
+	sb->s_bdev = &ctx->bdev;
+
+	bgl_lock_init(&ctx->bgl);
+	sbi->s_blockgroup_lock = &ctx->bgl;
+
+	ctx->es = (struct ext4_super_block *)ctx->primary_sb_buf;
+	ctx->es->s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+	ctx->es->s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+	ctx->es->s_first_ino = cpu_to_le32(IOCTL_TEST_FIRST_INO);
+	ctx->es->s_inode_size = cpu_to_le16(IOCTL_TEST_INODE_SIZE);
+	ctx->es->s_want_extra_isize = cpu_to_le16(IOCTL_TEST_EXTRA_ISIZE);
+	ctx->es->s_min_extra_isize = cpu_to_le16(IOCTL_TEST_EXTRA_ISIZE);
+	ctx->es->s_first_data_block = cpu_to_le32(IOCTL_TEST_FIRST_DATA_BLK);
+	ctx->es->s_blocks_per_group = cpu_to_le32(IOCTL_TEST_BLOCKS_PER_GRP);
+	ctx->es->s_clusters_per_group = cpu_to_le32(IOCTL_TEST_BLOCKS_PER_GRP);
+	ctx->es->s_inodes_per_group = cpu_to_le32(IOCTL_TEST_INODES_PER_GRP);
+	ctx->es->s_inodes_count = cpu_to_le32(IOCTL_TEST_TOTAL_INODES);
+	ext4_blocks_count_set(ctx->es, IOCTL_TEST_TOTAL_BLKS);
+	ctx->es->s_checksum_type = EXT4_CRC32C_CHKSUM;
+	ctx->es->s_feature_incompat = cpu_to_le32(EXT4_FEATURE_INCOMPAT_EXTENTS |
+						  EXT4_FEATURE_INCOMPAT_64BIT);
+	ctx->es->s_feature_ro_compat =
+		cpu_to_le32(EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER |
+			    EXT4_FEATURE_RO_COMPAT_METADATA_CSUM |
+			    EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE);
+	generate_random_uuid(ctx->es->s_uuid);
+
+	ioctl_init_mock_bh(&ctx->sb_bh, ctx->bh_folio, ctx->primary_sb_buf,
+			   IOCTL_TEST_BLK_SIZE, IOCTL_TEST_FIRST_DATA_BLK);
+	for (g = 0; g < IOCTL_TEST_NGROUPS; g++) {
+		ext4_fsblk_t blk = IOCTL_TEST_FIRST_DATA_BLK +
+				   (ext4_fsblk_t)g * IOCTL_TEST_BLOCKS_PER_GRP;
+
+		ioctl_init_mock_bh(&ctx->backup_bhs[g], ctx->bh_folio,
+				   ctx->backup_sb_bufs[g], IOCTL_TEST_BLK_SIZE,
+				   blk);
+	}
+	ioctl_init_mock_bh(&ctx->iloc_bh, ctx->bh_folio, ctx->iloc_buf,
+			   IOCTL_TEST_BLK_SIZE, 20);
+
+	sbi->s_es = ctx->es;
+	sbi->s_sbh = &ctx->sb_bh;
+	sbi->s_blocks_per_group = IOCTL_TEST_BLOCKS_PER_GRP;
+	sbi->s_clusters_per_group = IOCTL_TEST_BLOCKS_PER_GRP;
+	sbi->s_cluster_ratio = 1;
+	sbi->s_cluster_bits = 0;
+	sbi->s_inodes_per_group = IOCTL_TEST_INODES_PER_GRP;
+	sbi->s_groups_count = IOCTL_TEST_NGROUPS;
+	sbi->s_inode_size = IOCTL_TEST_INODE_SIZE;
+	sbi->s_first_ino = IOCTL_TEST_FIRST_INO;
+	sbi->s_want_extra_isize = IOCTL_TEST_EXTRA_ISIZE;
+	sbi->s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+	sbi->s_csum_seed = ext4_chksum(~0, ctx->es->s_uuid,
+				       sizeof(ctx->es->s_uuid));
+
+	ioctl_sync_backups_from_primary(ctx);
+
+	root_inode = new_inode(sb);
+	if (!root_inode) {
+		up_write(&sb->s_umount);
+		ioctl_test_cleanup(ctx);
+		KUNIT_FAIL_AND_ABORT(test, "failed to alloc root_inode");
+	}
+	root_inode->i_ino = EXT4_ROOT_INO;
+	root_inode->i_mode = S_IFDIR | 0755;
+	root_inode->i_flags |= S_NOQUOTA;
+	root_inode->i_op = &simple_dir_inode_operations;
+	set_nlink(root_inode, 2);
+	sb->s_root = d_make_root(root_inode);
+	if (!sb->s_root) {
+		up_write(&sb->s_umount);
+		ioctl_test_cleanup(ctx);
+		KUNIT_FAIL_AND_ABORT(test, "failed to alloc s_root");
+	}
+
+	up_write(&sb->s_umount);
+
+	err = kunit_add_action_or_reset(test, ioctl_test_cleanup_action, ctx);
+	KUNIT_ASSERT_EQ(test, err, 0);
+
+	kunit_activate_static_stub(test, ext4_sb_bread, ioctl_sb_bread_stub);
+	kunit_activate_static_stub(test, ext4_ioctl_sync_bh,
+				   ioctl_sync_bh_stub);
+	kunit_activate_static_stub(test, ext4_get_inode_loc,
+				   ioctl_get_inode_loc_stub);
+	kunit_activate_static_stub(test, ext4_mark_iloc_dirty,
+				   ioctl_mark_iloc_dirty_stub);
+
+	return 0;
+}
+
+/*
+ * Verify ext4_sb_setlabel, ext4_sb_setuuid, and ext4_sb_setparams across all
+ * superblock tuneable fields, split 64-bit / 32-bit fields, feature masks,
+ * and FORCE_FSCK state marking.
+ */
+static void test_sb_callbacks_and_setparams(struct kunit *test)
+{
+	struct ioctl_test_ctx *ctx = test->priv;
+	struct ext4_sb_info *sbi = &ctx->sbi;
+	struct ext4_super_block *es = ctx->es;
+	struct ext4_tune_sb_params params;
+	char label[EXT4_LABEL_MAX] = "ext4_kunit_lbl";
+	u8 uuid[UUID_SIZE];
+	int i;
+
+	ext4_sb_setlabel_test(sbi, es, label);
+	KUNIT_EXPECT_MEMEQ(test, es->s_volume_name, label, EXT4_LABEL_MAX);
+
+	for (i = 0; i < UUID_SIZE; i++)
+		uuid[i] = 0xa0 + i;
+	ext4_sb_setuuid_test(sbi, es, uuid);
+	KUNIT_EXPECT_MEMEQ(test, es->s_uuid, uuid, UUID_SIZE);
+
+	memset(&params, 0, sizeof(params));
+	sbi->s_def_hash_version = DX_HASH_TEA;
+	sbi->s_csum_seed = 0xdeadbeefU;
+	params.set_flags = EXT4_TUNE_FL_ERRORS_BEHAVIOR |
+			   EXT4_TUNE_FL_MNT_COUNT |
+			   EXT4_TUNE_FL_MAX_MNT_COUNT |
+			   EXT4_TUNE_FL_CHECKINTRVAL |
+			   EXT4_TUNE_FL_LAST_CHECK_TIME |
+			   EXT4_TUNE_FL_RESERVED_BLOCKS |
+			   EXT4_TUNE_FL_RESERVED_UID |
+			   EXT4_TUNE_FL_RESERVED_GID |
+			   EXT4_TUNE_FL_DEFAULT_MNT_OPTS |
+			   EXT4_TUNE_FL_DEF_HASH_ALG |
+			   EXT4_TUNE_FL_RAID_STRIDE |
+			   EXT4_TUNE_FL_RAID_STRIPE_WIDTH |
+			   EXT4_TUNE_FL_ENCODING |
+			   EXT4_TUNE_FL_ENCODING_FLAGS |
+			   EXT4_TUNE_FL_MOUNT_OPTS |
+			   EXT4_TUNE_FL_EDIT_FEATURES |
+			   EXT4_TUNE_FL_FORCE_FSCK;
+	params.errors_behavior = EXT4_ERRORS_RO;
+	params.mnt_count = 7;
+	params.max_mnt_count = 42;
+	params.checkinterval = 86400;
+	params.last_check_time = 1700000000ULL;
+	params.reserved_blocks = 0x100000055ULL;
+	params.reserved_uid = 0x12345678U;
+	params.reserved_gid = 0x87654321U;
+	params.default_mnt_opts = EXT4_DEFM_ACL | EXT4_DEFM_XATTR_USER;
+	params.def_hash_alg = DX_HASH_HALF_MD4;
+	params.raid_stride = 16;
+	params.raid_stripe_width = 128;
+	params.encoding = EXT4_ENC_UTF8_12_1;
+	params.encoding_flags = SB_ENC_STRICT_MODE_FL;
+	strscpy(params.mount_opts, "nodelalloc,noatime",
+		sizeof(params.mount_opts));
+	params.set_feature_compat_mask = EXT4_FEATURE_COMPAT_DIR_INDEX;
+	params.set_feature_incompat_mask = EXT4_FEATURE_INCOMPAT_CSUM_SEED;
+	params.set_feature_ro_compat_mask = EXT4_FEATURE_RO_COMPAT_PROJECT;
+
+	ext4_sb_setparams_test(sbi, es, &params);
+
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(es->s_errors), EXT4_ERRORS_RO);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(es->s_mnt_count), 7);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(es->s_max_mnt_count), 42);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_checkinterval), 86400U);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_lastcheck), 1700000000U);
+	KUNIT_EXPECT_EQ(test, ext4_r_blocks_count(es), 0x100000055ULL);
+	KUNIT_EXPECT_EQ(test, ext4_get_resuid(es), 0x12345678U);
+	KUNIT_EXPECT_EQ(test, ext4_get_resgid(es), 0x87654321U);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_default_mount_opts),
+			(u32)(EXT4_DEFM_ACL | EXT4_DEFM_XATTR_USER));
+	/* EDIT_FEATURES + DIR_INDEX overrides s_def_hash_version from sbi */
+	KUNIT_EXPECT_EQ(test, es->s_def_hash_version, DX_HASH_TEA);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(es->s_raid_stride), 16);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_raid_stripe_width), 128U);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(es->s_encoding), EXT4_ENC_UTF8_12_1);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(es->s_encoding_flags),
+			SB_ENC_STRICT_MODE_FL);
+	KUNIT_EXPECT_STREQ(test, (char *)es->s_mount_opts,
+			   "nodelalloc,noatime");
+	KUNIT_EXPECT_NE(test,
+			le32_to_cpu(es->s_feature_compat) &
+			EXT4_FEATURE_COMPAT_DIR_INDEX, 0U);
+	KUNIT_EXPECT_NE(test,
+			le32_to_cpu(es->s_feature_incompat) &
+			EXT4_FEATURE_INCOMPAT_CSUM_SEED, 0U);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_checksum_seed), 0xdeadbeefU);
+	KUNIT_EXPECT_NE(test,
+			le32_to_cpu(es->s_feature_ro_compat) &
+			EXT4_FEATURE_RO_COMPAT_PROJECT, 0U);
+	KUNIT_EXPECT_NE(test, le16_to_cpu(es->s_state) & EXT4_ERROR_FS, 0);
+}
+
+/*
+ * Verify ext4_validate_tune_sb_params rejects malformed mount_opts, invalid
+ * flags, out-of-range parameters, unsupported feature edits, and standalone
+ * encoding updates, while normalizing DIR_INDEX and CASEFOLD enablement.
+ */
+static void test_validate_tune_sb_params(struct kunit *test)
+{
+	struct ioctl_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = &ctx->sbi;
+	struct ext4_super_block *es = ctx->es;
+	struct ext4_tune_sb_params params;
+	uuid_t uu;
+
+	/* Unterminated mount_opts -> -E2BIG */
+	memset(&params, 0, sizeof(params));
+	memset(params.mount_opts, 'a', sizeof(params.mount_opts));
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			-E2BIG);
+
+	/* Unknown set_flags bit -> -EOPNOTSUPP */
+	memset(&params, 0, sizeof(params));
+	params.set_flags = 0x80000000U;
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			-EOPNOTSUPP);
+
+	/* Invalid errors_behavior -> -EINVAL */
+	memset(&params, 0, sizeof(params));
+	params.set_flags = EXT4_TUNE_FL_ERRORS_BEHAVIOR;
+	params.errors_behavior = EXT4_ERRORS_PANIC + 1;
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			-EINVAL);
+
+	/* Reserved blocks > blocks_count / 2 -> -EINVAL, <= half -> 0 */
+	memset(&params, 0, sizeof(params));
+	params.set_flags = EXT4_TUNE_FL_RESERVED_BLOCKS;
+	params.reserved_blocks = ext4_blocks_count(es) / 2 + 1;
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			-EINVAL);
+	params.reserved_blocks = ext4_blocks_count(es) / 2;
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			0);
+
+	/* Hash algorithm validation: SIPHASH and > DX_HASH_LAST rejected */
+	memset(&params, 0, sizeof(params));
+	params.set_flags = EXT4_TUNE_FL_DEF_HASH_ALG;
+	params.def_hash_alg = DX_HASH_SIPHASH;
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			-EINVAL);
+	params.def_hash_alg = DX_HASH_LAST + 1;
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			-EINVAL);
+	params.def_hash_alg = DX_HASH_HALF_MD4;
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			0);
+
+	/* Mutual exclusion of FEATURES and EDIT_FEATURES */
+	memset(&params, 0, sizeof(params));
+	params.set_flags = EXT4_TUNE_FL_FEATURES | EXT4_TUNE_FL_EDIT_FEATURES;
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			-EINVAL);
+
+	/* Clearing an existing feature via FEATURES -> -EOPNOTSUPP */
+	memset(&params, 0, sizeof(params));
+	params.set_flags = EXT4_TUNE_FL_FEATURES;
+	params.feature_compat = le32_to_cpu(es->s_feature_compat);
+	params.feature_incompat = 0;
+	params.feature_ro_compat = le32_to_cpu(es->s_feature_ro_compat);
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			-EOPNOTSUPP);
+
+	/* Enabling DIR_INDEX via FEATURES initializes hash seed & default alg */
+	memset(sbi->s_hash_seed, 0, sizeof(sbi->s_hash_seed));
+	sbi->s_def_hash_version = 0;
+	memset(&params, 0, sizeof(params));
+	params.set_flags = EXT4_TUNE_FL_FEATURES;
+	params.feature_compat = le32_to_cpu(es->s_feature_compat) |
+				EXT4_FEATURE_COMPAT_DIR_INDEX;
+	params.feature_incompat = le32_to_cpu(es->s_feature_incompat);
+	params.feature_ro_compat = le32_to_cpu(es->s_feature_ro_compat) |
+				   EXT4_FEATURE_RO_COMPAT_PROJECT;
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			0);
+	KUNIT_EXPECT_NE(test, params.set_flags & EXT4_TUNE_FL_EDIT_FEATURES,
+			0U);
+	KUNIT_EXPECT_EQ(test, params.set_feature_compat_mask,
+			(u32)EXT4_FEATURE_COMPAT_DIR_INDEX);
+	KUNIT_EXPECT_EQ(test, params.set_feature_ro_compat_mask,
+			(u32)EXT4_FEATURE_RO_COMPAT_PROJECT);
+	memcpy(&uu, sbi->s_hash_seed, UUID_SIZE);
+	KUNIT_EXPECT_FALSE(test, uuid_is_null(&uu));
+	KUNIT_EXPECT_EQ(test, sbi->s_def_hash_version, DX_HASH_HALF_MD4);
+
+	/* Encoding without CASEFOLD -> -EINVAL; with CASEFOLD -> UTF8_12_1 */
+	memset(&params, 0, sizeof(params));
+	params.set_flags = EXT4_TUNE_FL_ENCODING;
+	params.encoding = EXT4_ENC_UTF8_12_1;
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			-EINVAL);
+
+	memset(&params, 0, sizeof(params));
+	params.set_flags = EXT4_TUNE_FL_EDIT_FEATURES;
+	params.set_feature_incompat_mask = EXT4_FEATURE_INCOMPAT_CASEFOLD;
+	KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, &params),
+			0);
+	KUNIT_EXPECT_EQ(test, params.encoding, EXT4_ENC_UTF8_12_1);
+	KUNIT_EXPECT_EQ(test, params.encoding_flags, 0);
+}
+
+/*
+ * Verify ext4_update_backup_sb, ext4_update_superblocks_fn, and
+ * ext4_update_overhead across primary + 4 sparse_super backup groups (1, 3,
+ * 5, 7), covering both journaled (first 2 backups) and unjournaled (remaining
+ * backups) update paths, EXT4_FLAGS_RESIZING contention, and overhead skip
+ * conditions.
+ */
+static void test_update_superblocks_and_overhead(struct kunit *test)
+{
+	struct ioctl_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = &ctx->sbi;
+	static const ext4_group_t backup_grps[] = { 1, 3, 5, 7 };
+	char new_label[EXT4_LABEL_MAX] = "updated_label";
+	int i;
+
+	/* Group 2 has no backup superblock under sparse_super */
+	KUNIT_EXPECT_EQ(test,
+			ext4_update_backup_sb_test(sb, NULL, 2,
+						   ext4_sb_setlabel_test,
+						   new_label),
+			0);
+
+	/* Contention guard when EXT4_FLAGS_RESIZING is already set */
+	set_bit(EXT4_FLAGS_RESIZING, &sbi->s_ext4_flags);
+	KUNIT_EXPECT_EQ(test,
+			ext4_update_superblocks_fn_test(sb,
+							ext4_sb_setlabel_test,
+							new_label),
+			-EBUSY);
+	clear_bit(EXT4_FLAGS_RESIZING, &sbi->s_ext4_flags);
+
+	/* Full superblock update across primary (grp 0) + 4 backups */
+	ctx->sync_bh_calls = 0;
+	KUNIT_EXPECT_EQ(test,
+			ext4_update_superblocks_fn_test(sb,
+							ext4_sb_setlabel_test,
+							new_label),
+			0);
+	KUNIT_EXPECT_FALSE(test,
+			   test_bit(EXT4_FLAGS_RESIZING, &sbi->s_ext4_flags));
+	/* 1 primary + 4 backup superblocks synced */
+	KUNIT_EXPECT_EQ(test, ctx->sync_bh_calls, 5);
+	KUNIT_EXPECT_MEMEQ(test, ctx->es->s_volume_name, new_label,
+			   EXT4_LABEL_MAX);
+	KUNIT_EXPECT_EQ(test, ctx->es->s_checksum,
+			ext4_superblock_csum(ctx->es));
+
+	for (i = 0; i < ARRAY_SIZE(backup_grps); i++) {
+		ext4_group_t g = backup_grps[i];
+		struct ext4_super_block *bes;
+
+		bes = (struct ext4_super_block *)ctx->backup_sb_bufs[g];
+		KUNIT_EXPECT_MEMEQ(test, bes->s_volume_name, new_label,
+				   EXT4_LABEL_MAX);
+		KUNIT_EXPECT_EQ(test, bes->s_checksum,
+				ext4_superblock_csum(bes));
+	}
+
+	/* ext4_update_overhead skip & force behavior */
+	ctx->sync_bh_calls = 0;
+	sbi->s_overhead = 0;
+	KUNIT_EXPECT_EQ(test, ext4_update_overhead(sb, false), 0);
+	KUNIT_EXPECT_EQ(test, ctx->sync_bh_calls, 0);
+
+	sbi->s_overhead = 77;
+	sb->s_flags |= SB_RDONLY;
+	KUNIT_EXPECT_EQ(test, ext4_update_overhead(sb, true), 0);
+	KUNIT_EXPECT_EQ(test, ctx->sync_bh_calls, 0);
+	sb->s_flags &= ~SB_RDONLY;
+
+	set_bit(EXT4_FLAGS_SHUTDOWN, &sbi->s_ext4_flags);
+	KUNIT_EXPECT_EQ(test, ext4_update_overhead(sb, true), 0);
+	KUNIT_EXPECT_EQ(test, ctx->sync_bh_calls, 0);
+	clear_bit(EXT4_FLAGS_SHUTDOWN, &sbi->s_ext4_flags);
+
+	KUNIT_EXPECT_EQ(test, ext4_update_overhead(sb, false), 0);
+	KUNIT_EXPECT_EQ(test, ctx->sync_bh_calls, 5);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es->s_overhead_clusters), 77U);
+
+	/* Matching s_overhead skips when !force, updates when force == true */
+	ctx->sync_bh_calls = 0;
+	KUNIT_EXPECT_EQ(test, ext4_update_overhead(sb, false), 0);
+	KUNIT_EXPECT_EQ(test, ctx->sync_bh_calls, 0);
+	KUNIT_EXPECT_EQ(test, ext4_update_overhead(sb, true), 0);
+	KUNIT_EXPECT_EQ(test, ctx->sync_bh_calls, 5);
+}
+
+/*
+ * Verify swap_inode_data swaps i_data, i_disksize, i_size, i_version,
+ * atime/mtime, and EXT4_FL_SHOULD_SWAP flags while preserving non-swapped
+ * flags, clears both extent-status caches, and restores exact state when
+ * invoked a second time. Also verify ext4_reset_inode_seed.
+ */
+static void test_swap_inode_data_and_seed(struct kunit *test)
+{
+	struct ioctl_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct inode *inode1, *inode2;
+	struct ext4_inode_info *ei1, *ei2;
+	struct timespec64 at1 = { .tv_sec = 1000, .tv_nsec = 111 };
+	struct timespec64 at2 = { .tv_sec = 2000, .tv_nsec = 222 };
+	struct timespec64 mt1 = { .tv_sec = 3000, .tv_nsec = 333 };
+	struct timespec64 mt2 = { .tv_sec = 4000, .tv_nsec = 444 };
+	__le32 orig_data1[EXT4_N_BLOCKS];
+	__le32 orig_data2[EXT4_N_BLOCKS];
+	u32 seed1, seed2;
+	int i;
+
+	inode1 = new_inode(sb);
+	inode2 = new_inode(sb);
+	KUNIT_ASSERT_NOT_NULL(test, inode1);
+	KUNIT_ASSERT_NOT_NULL(test, inode2);
+	ei1 = EXT4_I(inode1);
+	ei2 = EXT4_I(inode2);
+
+	for (i = 0; i < EXT4_N_BLOCKS; i++) {
+		ei1->i_data[i] = cpu_to_le32(0x100 + i);
+		ei2->i_data[i] = cpu_to_le32(0x200 + i);
+	}
+	memcpy(orig_data1, ei1->i_data, sizeof(orig_data1));
+	memcpy(orig_data2, ei2->i_data, sizeof(orig_data2));
+
+	ei1->i_flags = EXT4_EXTENTS_FL | EXT4_HUGE_FILE_FL | EXT4_IMMUTABLE_FL;
+	ei2->i_flags = EXT4_NOATIME_FL;
+	ei1->i_disksize = 0x11000;
+	ei2->i_disksize = 0x22000;
+	i_size_write(inode1, 0x10500);
+	i_size_write(inode2, 0x20500);
+	inode_set_iversion_raw(inode1, 10);
+	inode_set_iversion_raw(inode2, 20);
+	inode_set_atime_to_ts(inode1, at1);
+	inode_set_atime_to_ts(inode2, at2);
+	inode_set_mtime_to_ts(inode1, mt1);
+	inode_set_mtime_to_ts(inode2, mt2);
+
+	ext4_es_insert_extent(inode1, 0, 4, 50, EXTENT_STATUS_WRITTEN, false);
+	ext4_es_insert_extent(inode2, 8, 2, 60, EXTENT_STATUS_WRITTEN, false);
+	KUNIT_EXPECT_EQ(test, ei1->i_es_all_nr, 1U);
+	KUNIT_EXPECT_EQ(test, ei2->i_es_all_nr, 1U);
+
+	ext4_swap_inode_data_test(inode1, inode2);
+
+	KUNIT_EXPECT_MEMEQ(test, ei1->i_data, orig_data2, sizeof(orig_data2));
+	KUNIT_EXPECT_MEMEQ(test, ei2->i_data, orig_data1, sizeof(orig_data1));
+	KUNIT_EXPECT_EQ(test, ei1->i_flags, (u32)EXT4_IMMUTABLE_FL);
+	KUNIT_EXPECT_EQ(test, ei2->i_flags,
+			(u32)(EXT4_NOATIME_FL | EXT4_EXTENTS_FL |
+			      EXT4_HUGE_FILE_FL));
+	KUNIT_EXPECT_EQ(test, ei1->i_disksize, (loff_t)0x22000);
+	KUNIT_EXPECT_EQ(test, ei2->i_disksize, (loff_t)0x11000);
+	KUNIT_EXPECT_EQ(test, i_size_read(inode1), (loff_t)0x20500);
+	KUNIT_EXPECT_EQ(test, i_size_read(inode2), (loff_t)0x10500);
+	KUNIT_EXPECT_EQ(test, inode_peek_iversion_raw(inode1), 20ULL);
+	KUNIT_EXPECT_EQ(test, inode_peek_iversion_raw(inode2), 10ULL);
+	KUNIT_EXPECT_EQ(test, inode_get_atime_sec(inode1), 2000LL);
+	KUNIT_EXPECT_EQ(test, inode_get_mtime_sec(inode1), 4000LL);
+	KUNIT_EXPECT_EQ(test, ei1->i_es_all_nr, 0U);
+	KUNIT_EXPECT_EQ(test, ei2->i_es_all_nr, 0U);
+
+	/* Second swap restores exact pre-swap attributes */
+	ext4_swap_inode_data_test(inode1, inode2);
+	KUNIT_EXPECT_MEMEQ(test, ei1->i_data, orig_data1, sizeof(orig_data1));
+	KUNIT_EXPECT_MEMEQ(test, ei2->i_data, orig_data2, sizeof(orig_data2));
+	KUNIT_EXPECT_EQ(test, ei1->i_flags,
+			(u32)(EXT4_EXTENTS_FL | EXT4_HUGE_FILE_FL |
+			      EXT4_IMMUTABLE_FL));
+	KUNIT_EXPECT_EQ(test, ei2->i_flags, (u32)EXT4_NOATIME_FL);
+	KUNIT_EXPECT_EQ(test, ei1->i_disksize, (loff_t)0x11000);
+	KUNIT_EXPECT_EQ(test, i_size_read(inode1), (loff_t)0x10500);
+
+	/* ext4_reset_inode_seed with and without metadata_csum */
+	inode1->i_ino = 42;
+	inode1->i_generation = 7;
+	ei1->i_csum_seed = 0;
+	ext4_reset_inode_seed(inode1);
+	seed1 = ei1->i_csum_seed;
+	KUNIT_EXPECT_NE(test, seed1, 0U);
+
+	inode1->i_generation = 8;
+	ext4_reset_inode_seed(inode1);
+	seed2 = ei1->i_csum_seed;
+	KUNIT_EXPECT_NE(test, seed2, seed1);
+
+	ext4_clear_feature_metadata_csum(sb);
+	ei1->i_csum_seed = 0x12345678U;
+	ext4_reset_inode_seed(inode1);
+	KUNIT_EXPECT_EQ(test, ei1->i_csum_seed, 0x12345678U);
+
+	iput(inode1);
+	iput(inode2);
+}
+
+/*
+ * Verify ext4_ioctl_check_immutable, dax_compatible, ext4_fileattr_get, and
+ * ext4_fileattr_set flag masking, mode checks, quota-file protection, and
+ * end-to-end VFS inode flag synchronization.
+ */
+static void test_immutable_dax_and_fileattr(struct kunit *test)
+{
+	struct ioctl_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct inode *reg_inode, *dir_inode;
+	struct dentry *reg_dentry, *dir_dentry;
+	struct ext4_inode_info *rei, *dei;
+	struct file_kattr fa;
+
+	reg_inode = new_inode(sb);
+	dir_inode = new_inode(sb);
+	KUNIT_ASSERT_NOT_NULL(test, reg_inode);
+	KUNIT_ASSERT_NOT_NULL(test, dir_inode);
+	reg_inode->i_ino = 100;
+	reg_inode->i_mode = S_IFREG | 0644;
+	dir_inode->i_ino = 101;
+	dir_inode->i_mode = S_IFDIR | 0755;
+	rei = EXT4_I(reg_inode);
+	dei = EXT4_I(dir_inode);
+
+	reg_dentry = d_alloc_anon(sb);
+	dir_dentry = d_alloc_anon(sb);
+	KUNIT_ASSERT_NOT_NULL(test, reg_dentry);
+	KUNIT_ASSERT_NOT_NULL(test, dir_dentry);
+	d_instantiate(reg_dentry, reg_inode);
+	d_instantiate(dir_dentry, dir_inode);
+
+	/* ext4_ioctl_check_immutable */
+	ext4_set_feature_project(sb);
+	rei->i_projid = make_kprojid(&init_user_ns, 10);
+	rei->i_flags = EXT4_IMMUTABLE_FL | EXT4_NOATIME_FL;
+	KUNIT_EXPECT_EQ(test,
+			ext4_ioctl_check_immutable_test(reg_inode, 10,
+					EXT4_IMMUTABLE_FL | EXT4_NOATIME_FL),
+			0);
+	/* Clearing IMMUTABLE_FL is allowed here (CAP_LINUX_IMMUTABLE checked in VFS) */
+	KUNIT_EXPECT_EQ(test,
+			ext4_ioctl_check_immutable_test(reg_inode, 10,
+							EXT4_NOATIME_FL),
+			0);
+	/* Changing other flags or projid while keeping IMMUTABLE_FL -> -EPERM */
+	KUNIT_EXPECT_EQ(test,
+			ext4_ioctl_check_immutable_test(reg_inode, 10,
+							EXT4_IMMUTABLE_FL),
+			-EPERM);
+	KUNIT_EXPECT_EQ(test,
+			ext4_ioctl_check_immutable_test(reg_inode, 11,
+					EXT4_IMMUTABLE_FL | EXT4_NOATIME_FL),
+			-EPERM);
+
+	/* dax_compatible */
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_dax_compatible_test(reg_inode, EXT4_VERITY_FL,
+						    EXT4_DAX_FL));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_dax_compatible_test(reg_inode,
+						    EXT4_INLINE_DATA_FL,
+						    EXT4_DAX_FL));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_dax_compatible_test(reg_inode, EXT4_DAX_FL,
+						    EXT4_JOURNAL_DATA_FL));
+	/* Inline directory MAY toggle DAX_FL */
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_dax_compatible_test(dir_inode,
+						   EXT4_INLINE_DATA_FL,
+						   EXT4_INLINE_DATA_FL |
+						   EXT4_DAX_FL));
+	/* VERITY_IN_PROGRESS blocks enabling DAX_FL */
+	ext4_set_inode_state(reg_inode, EXT4_STATE_VERITY_IN_PROGRESS);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_dax_compatible_test(reg_inode, 0, EXT4_DAX_FL));
+	ext4_clear_inode_state(reg_inode, EXT4_STATE_VERITY_IN_PROGRESS);
+
+	/* ext4_fileattr_get: strips PROJINHERIT on reg file, keeps on dir */
+	rei->i_flags = EXT4_NOATIME_FL | EXT4_PROJINHERIT_FL | EXT4_RESERVED_FL;
+	dei->i_flags = EXT4_NOATIME_FL | EXT4_PROJINHERIT_FL;
+	dei->i_projid = make_kprojid(&init_user_ns, 25);
+
+	memset(&fa, 0, sizeof(fa));
+	KUNIT_EXPECT_EQ(test, ext4_fileattr_get(reg_dentry, &fa), 0);
+	KUNIT_EXPECT_EQ(test, fa.flags, (u32)EXT4_NOATIME_FL);
+	KUNIT_EXPECT_EQ(test, fa.fsx_projid, 10U);
+
+	memset(&fa, 0, sizeof(fa));
+	KUNIT_EXPECT_EQ(test, ext4_fileattr_get(dir_dentry, &fa), 0);
+	KUNIT_EXPECT_EQ(test, fa.flags,
+			(u32)(EXT4_NOATIME_FL | EXT4_PROJINHERIT_FL));
+	KUNIT_EXPECT_EQ(test, fa.fsx_projid, 25U);
+
+	/* ext4_fileattr_set error paths and success path */
+	ext4_clear_feature_project(sb);
+	rei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+	rei->i_flags = 0;
+
+	/* Flag outside EXT4_FL_USER_VISIBLE -> -EOPNOTSUPP */
+	memset(&fa, 0, sizeof(fa));
+	fileattr_fill_flags(&fa, EXT4_RESERVED_FL);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fileattr_set(&nop_mnt_idmap, reg_dentry, &fa),
+			-EOPNOTSUPP);
+
+	/* Directory-only flag on regular file -> -EOPNOTSUPP */
+	memset(&fa, 0, sizeof(fa));
+	fileattr_fill_flags(&fa, EXT4_PROJINHERIT_FL);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fileattr_set(&nop_mnt_idmap, reg_dentry, &fa),
+			-EOPNOTSUPP);
+
+	/* Quota file cannot be modified -> -EPERM */
+	reg_inode->i_flags |= S_NOQUOTA;
+	memset(&fa, 0, sizeof(fa));
+	fileattr_fill_flags(&fa, EXT4_NOATIME_FL);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fileattr_set(&nop_mnt_idmap, reg_dentry, &fa),
+			-EPERM);
+	reg_inode->i_flags &= ~S_NOQUOTA;
+
+	/* CASEFOLD_FL without casefold feature -> -EOPNOTSUPP */
+	memset(&fa, 0, sizeof(fa));
+	fileattr_fill_flags(&fa, EXT4_CASEFOLD_FL);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fileattr_set(&nop_mnt_idmap, dir_dentry, &fa),
+			-EOPNOTSUPP);
+
+	/* End-to-end setflags on regular file updates ei->i_flags & VFS flags */
+	memset(&fa, 0, sizeof(fa));
+	fileattr_fill_flags(&fa, EXT4_NOATIME_FL | EXT4_SYNC_FL |
+				 EXT4_NODUMP_FL);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fileattr_set(&nop_mnt_idmap, reg_dentry, &fa),
+			0);
+	KUNIT_EXPECT_EQ(test,
+			rei->i_flags & (EXT4_NOATIME_FL | EXT4_SYNC_FL |
+					EXT4_NODUMP_FL),
+			(u32)(EXT4_NOATIME_FL | EXT4_SYNC_FL | EXT4_NODUMP_FL));
+	KUNIT_EXPECT_NE(test, reg_inode->i_flags & S_NOATIME, 0U);
+	KUNIT_EXPECT_NE(test, reg_inode->i_flags & S_SYNC, 0U);
+
+	dput(reg_dentry);
+	dput(dir_dentry);
+}
+
+/*
+ * Verify ext4_force_shutdown state transitions and ext4_ioctl /
+ * ext4_compat_ioctl command dispatch for unknown commands, feature-gated
+ * commands, mode checks, FITRIM preconditions, and EXT4_IOC_CLEAR_ES_CACHE.
+ */
+static void test_force_shutdown_and_ioctl_dispatch(struct kunit *test)
+{
+	struct ioctl_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct inode *inode;
+	struct dentry *dentry;
+	struct vfsmount mnt = {
+		.mnt_root	= sb->s_root,
+		.mnt_sb		= sb,
+		.mnt_idmap	= &nop_mnt_idmap,
+	};
+
+	inode = new_inode(sb);
+	KUNIT_ASSERT_NOT_NULL(test, inode);
+	inode->i_ino = 200;
+	inode->i_mode = S_IFREG | 0644;
+	dentry = d_alloc_anon(sb);
+	KUNIT_ASSERT_NOT_NULL(test, dentry);
+	d_instantiate(dentry, inode);
+
+	{
+		struct file filp = {
+			.f_inode	= inode,
+			.f_path		= {
+				.mnt	= &mnt,
+				.dentry	= dentry,
+			},
+			.f_mode		= FMODE_READ | FMODE_WRITE,
+		};
+
+		/* Unknown command dispatch */
+		KUNIT_EXPECT_EQ(test, ext4_ioctl(&filp, 0xffffffffU, 0),
+				-ENOTTY);
+#ifdef CONFIG_COMPAT
+		KUNIT_EXPECT_EQ(test, ext4_compat_ioctl(&filp, 0xffffffffU, 0),
+				-ENOIOCTLCMD);
+#endif
+
+		/* Encryption and verity ioctls return -EOPNOTSUPP when off */
+		KUNIT_EXPECT_EQ(test,
+				ext4_ioctl(&filp, FS_IOC_SET_ENCRYPTION_POLICY,
+					   0),
+				-EOPNOTSUPP);
+		KUNIT_EXPECT_EQ(test,
+				ext4_ioctl(&filp, FS_IOC_GET_ENCRYPTION_POLICY,
+					   0),
+				-EOPNOTSUPP);
+		KUNIT_EXPECT_EQ(test,
+				ext4_ioctl(&filp, FS_IOC_ENABLE_VERITY, 0),
+				-EOPNOTSUPP);
+		KUNIT_EXPECT_EQ(test,
+				ext4_ioctl(&filp, FS_IOC_MEASURE_VERITY, 0),
+				-EOPNOTSUPP);
+#ifdef CONFIG_COMPAT
+		KUNIT_EXPECT_EQ(test,
+				ext4_compat_ioctl(&filp, FS_IOC_ENABLE_VERITY,
+						  0),
+				-EOPNOTSUPP);
+#endif
+
+		/* SETVERSION rejected with -ENOTTY when metadata_csum is on */
+		KUNIT_EXPECT_EQ(test, ext4_ioctl(&filp, EXT4_IOC_SETVERSION, 0),
+				-ENOTTY);
+
+		/* FITRIM: no discard -> -EOPNOTSUPP; NOLOAD+journal -> -EROFS */
+		ctx->bdev_queue.limits.max_discard_sectors = 0;
+		KUNIT_EXPECT_EQ(test, ext4_ioctl(&filp, FITRIM, 0),
+				-EOPNOTSUPP);
+
+		ctx->bdev_queue.limits.max_discard_sectors = 1024;
+		set_opt(sb, NOLOAD);
+		ext4_set_feature_journal(sb);
+		KUNIT_EXPECT_EQ(test, ext4_ioctl(&filp, FITRIM, 0), -EROFS);
+		clear_opt(sb, NOLOAD);
+		ext4_clear_feature_journal(sb);
+
+		/* MOVE_EXT and SWAP_BOOT require FMODE_WRITE -> -EBADF on RO */
+		filp.f_mode = FMODE_READ;
+		KUNIT_EXPECT_EQ(test, ext4_ioctl(&filp, EXT4_IOC_MOVE_EXT, 0),
+				-EBADF);
+		KUNIT_EXPECT_EQ(test, ext4_ioctl(&filp, EXT4_IOC_SWAP_BOOT, 0),
+				-EBADF);
+		filp.f_mode = FMODE_READ | FMODE_WRITE;
+
+		/* EXT4_IOC_CLEAR_ES_CACHE empties inode extent status tree */
+		ext4_es_insert_extent(inode, 0, 8, 120, EXTENT_STATUS_WRITTEN,
+				      false);
+		KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_es_all_nr, 1U);
+		KUNIT_EXPECT_EQ(test,
+				ext4_ioctl(&filp, EXT4_IOC_CLEAR_ES_CACHE, 0),
+				0);
+		KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_es_all_nr, 0U);
+	}
+
+	/* ext4_force_shutdown validation, NOLOGFLUSH state, and idempotency */
+	KUNIT_EXPECT_EQ(test,
+			ext4_force_shutdown(sb, EXT4_GOING_FLAGS_NOLOGFLUSH + 1),
+			-EINVAL);
+	set_opt(sb, DISCARD);
+	KUNIT_EXPECT_EQ(test,
+			ext4_force_shutdown(sb, EXT4_GOING_FLAGS_NOLOGFLUSH),
+			0);
+	KUNIT_EXPECT_TRUE(test, ext4_forced_shutdown(sb));
+	KUNIT_EXPECT_EQ(test, test_opt(sb, DISCARD), 0U);
+	/* Repeat shutdown call is a no-op returning 0 */
+	KUNIT_EXPECT_EQ(test,
+			ext4_force_shutdown(sb, EXT4_GOING_FLAGS_DEFAULT),
+			0);
+
+	dput(dentry);
+}
+
+static struct kunit_case ioctl_test_cases[] = {
+	KUNIT_CASE(test_sb_callbacks_and_setparams),
+	KUNIT_CASE(test_validate_tune_sb_params),
+	KUNIT_CASE(test_update_superblocks_and_overhead),
+	KUNIT_CASE(test_swap_inode_data_and_seed),
+	KUNIT_CASE(test_immutable_dax_and_fileattr),
+	KUNIT_CASE(test_force_shutdown_and_ioctl_dispatch),
+	{}
+};
+
+static struct kunit_suite ioctl_test_suite = {
+	.name = "ext4_ioctl_test",
+	.init = ioctl_test_init,
+	.test_cases = ioctl_test_cases,
+};
+
+kunit_test_suites(&ioctl_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 ioctl operations");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/ioctl.c b/fs/ext4/ioctl.c
index c8387e6a2c6e..61e59c30e761 100644
--- a/fs/ext4/ioctl.c
+++ b/fs/ext4/ioctl.c
@@ -27,6 +27,7 @@
 #include "fsmap.h"
 #include <trace/events/ext4.h>
 #include <linux/fserror.h>
+#include <kunit/static_stub.h>
 
 typedef void ext4_update_sb_callback(struct ext4_sb_info *sbi,
 				     struct ext4_super_block *es,
@@ -55,6 +56,13 @@ static void ext4_sb_setuuid(struct ext4_sb_info *sbi,
 	memcpy(es->s_uuid, (__u8 *)arg, UUID_SIZE);
 }
 
+int ext4_ioctl_sync_bh(struct buffer_head *bh)
+{
+	KUNIT_STATIC_STUB_REDIRECT(ext4_ioctl_sync_bh, bh);
+	return sync_dirty_buffer(bh);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ioctl_sync_bh);
+
 static
 int ext4_update_primary_sb(struct super_block *sb, handle_t *handle,
 			   ext4_update_sb_callback func,
@@ -89,7 +97,7 @@ int ext4_update_primary_sb(struct super_block *sb, handle_t *handle,
 	err = ext4_handle_dirty_metadata(handle, NULL, bh);
 	if (err)
 		goto out_err;
-	err = sync_dirty_buffer(bh);
+	err = ext4_ioctl_sync_bh(bh);
 out_err:
 	ext4_std_error(sb, err);
 	return err;
@@ -167,7 +175,7 @@ static int ext4_update_backup_sb(struct super_block *sb,
 		BUFFER_TRACE(bh, "marking dirty");
 		mark_buffer_dirty(bh);
 	}
-	err = sync_dirty_buffer(bh);
+	err = ext4_ioctl_sync_bh(bh);
 
 out_bh:
 	brelse(bh);
@@ -361,6 +369,7 @@ void ext4_reset_inode_seed(struct inode *inode)
 	csum = ext4_chksum(sbi->s_csum_seed, (__u8 *)&inum, sizeof(inum));
 	ei->i_csum_seed = ext4_chksum(csum, (__u8 *)&gen, sizeof(gen));
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_reset_inode_seed);
 
 /*
  * Swap the information from the given @inode and the inode
@@ -854,6 +863,7 @@ int ext4_force_shutdown(struct super_block *sb, u32 flags)
 	fserror_report_shutdown(sb, GFP_KERNEL);
 	return 0;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_force_shutdown);
 
 static int ext4_ioctl_shutdown(struct super_block *sb, unsigned long arg)
 {
@@ -1007,6 +1017,7 @@ int ext4_fileattr_get(struct dentry *dentry, struct file_kattr *fa)
 
 	return 0;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fileattr_get);
 
 int ext4_fileattr_set(struct mnt_idmap *idmap,
 		      struct dentry *dentry, struct file_kattr *fa)
@@ -1037,6 +1048,7 @@ int ext4_fileattr_set(struct mnt_idmap *idmap,
 out:
 	return err;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fileattr_set);
 
 /* So that the fiemap access checks can't overflow on 32 bit machines. */
 #define FIEMAP_MAX_EXTENTS	(UINT_MAX / sizeof(struct fiemap_extent))
@@ -1387,77 +1399,68 @@ static void ext4_sb_setparams(struct ext4_sb_info *sbi,
 		es->s_state |= cpu_to_le16(EXT4_ERROR_FS);
 }
 
-static int ext4_ioctl_set_tune_sb(struct file *filp,
-				  struct ext4_tune_sb_params __user *in)
+static int ext4_validate_tune_sb_params(struct super_block *sb,
+					struct ext4_tune_sb_params *params)
 {
-	struct ext4_tune_sb_params params;
-	struct super_block *sb = file_inode(filp)->i_sb;
 	struct ext4_sb_info *sbi = EXT4_SB(sb);
 	struct ext4_super_block *es = sbi->s_es;
 	int enabling_casefold = 0;
-	int ret;
-
-	if (!capable(CAP_SYS_ADMIN))
-		return -EPERM;
-
-	if (copy_from_user(&params, in, sizeof(params)))
-		return -EFAULT;
 
-	if (strnlen(params.mount_opts, sizeof(params.mount_opts)) ==
-	    sizeof(params.mount_opts))
+	if (strnlen(params->mount_opts, sizeof(params->mount_opts)) ==
+	    sizeof(params->mount_opts))
 		return -E2BIG;
 
-	if ((params.set_flags & ~TUNE_OPS_SUPPORTED) != 0)
+	if ((params->set_flags & ~TUNE_OPS_SUPPORTED) != 0)
 		return -EOPNOTSUPP;
 
-	if ((params.set_flags & EXT4_TUNE_FL_ERRORS_BEHAVIOR) &&
-	    (params.errors_behavior > EXT4_ERRORS_PANIC))
+	if ((params->set_flags & EXT4_TUNE_FL_ERRORS_BEHAVIOR) &&
+	    (params->errors_behavior > EXT4_ERRORS_PANIC))
 		return -EINVAL;
 
-	if ((params.set_flags & EXT4_TUNE_FL_RESERVED_BLOCKS) &&
-	    (params.reserved_blocks > ext4_blocks_count(sbi->s_es) / 2))
+	if ((params->set_flags & EXT4_TUNE_FL_RESERVED_BLOCKS) &&
+	    (params->reserved_blocks > ext4_blocks_count(sbi->s_es) / 2))
 		return -EINVAL;
-	if ((params.set_flags & EXT4_TUNE_FL_DEF_HASH_ALG) &&
-	    ((params.def_hash_alg > DX_HASH_LAST) ||
-	     (params.def_hash_alg == DX_HASH_SIPHASH)))
+	if ((params->set_flags & EXT4_TUNE_FL_DEF_HASH_ALG) &&
+	    ((params->def_hash_alg > DX_HASH_LAST) ||
+	     (params->def_hash_alg == DX_HASH_SIPHASH)))
 		return -EINVAL;
-	if ((params.set_flags & EXT4_TUNE_FL_FEATURES) &&
-	    (params.set_flags & EXT4_TUNE_FL_EDIT_FEATURES))
+	if ((params->set_flags & EXT4_TUNE_FL_FEATURES) &&
+	    (params->set_flags & EXT4_TUNE_FL_EDIT_FEATURES))
 		return -EINVAL;
 
-	if (params.set_flags & EXT4_TUNE_FL_FEATURES) {
-		params.set_feature_compat_mask =
-			params.feature_compat &
+	if (params->set_flags & EXT4_TUNE_FL_FEATURES) {
+		params->set_feature_compat_mask =
+			params->feature_compat &
 			~le32_to_cpu(es->s_feature_compat);
-		params.set_feature_incompat_mask =
-			params.feature_incompat &
+		params->set_feature_incompat_mask =
+			params->feature_incompat &
 			~le32_to_cpu(es->s_feature_incompat);
-		params.set_feature_ro_compat_mask =
-			params.feature_ro_compat &
+		params->set_feature_ro_compat_mask =
+			params->feature_ro_compat &
 			~le32_to_cpu(es->s_feature_ro_compat);
-		params.clear_feature_compat_mask =
-			~params.feature_compat &
+		params->clear_feature_compat_mask =
+			~params->feature_compat &
 			le32_to_cpu(es->s_feature_compat);
-		params.clear_feature_incompat_mask =
-			~params.feature_incompat &
+		params->clear_feature_incompat_mask =
+			~params->feature_incompat &
 			le32_to_cpu(es->s_feature_incompat);
-		params.clear_feature_ro_compat_mask =
-			~params.feature_ro_compat &
+		params->clear_feature_ro_compat_mask =
+			~params->feature_ro_compat &
 			le32_to_cpu(es->s_feature_ro_compat);
-		params.set_flags |= EXT4_TUNE_FL_EDIT_FEATURES;
+		params->set_flags |= EXT4_TUNE_FL_EDIT_FEATURES;
 	}
-	if (params.set_flags & EXT4_TUNE_FL_EDIT_FEATURES) {
-		if ((params.set_feature_compat_mask &
+	if (params->set_flags & EXT4_TUNE_FL_EDIT_FEATURES) {
+		if ((params->set_feature_compat_mask &
 		     ~EXT4_TUNE_SET_COMPAT_SUPP) ||
-		    (params.set_feature_incompat_mask &
+		    (params->set_feature_incompat_mask &
 		     ~EXT4_TUNE_SET_INCOMPAT_SUPP) ||
-		    (params.set_feature_ro_compat_mask &
+		    (params->set_feature_ro_compat_mask &
 		     ~EXT4_TUNE_SET_RO_COMPAT_SUPP) ||
-		    (params.clear_feature_compat_mask &
+		    (params->clear_feature_compat_mask &
 		     ~EXT4_TUNE_CLEAR_COMPAT_SUPP) ||
-		    (params.clear_feature_incompat_mask &
+		    (params->clear_feature_incompat_mask &
 		     ~EXT4_TUNE_CLEAR_INCOMPAT_SUPP) ||
-		    (params.clear_feature_ro_compat_mask &
+		    (params->clear_feature_ro_compat_mask &
 		     ~EXT4_TUNE_CLEAR_RO_COMPAT_SUPP))
 			return -EOPNOTSUPP;
 
@@ -1465,34 +1468,35 @@ static int ext4_ioctl_set_tune_sb(struct file *filp,
 		 * Filter out the features that are already set from
 		 * the set_mask.
 		 */
-		params.set_feature_compat_mask &=
+		params->set_feature_compat_mask &=
 			~le32_to_cpu(es->s_feature_compat);
-		params.set_feature_incompat_mask &=
+		params->set_feature_incompat_mask &=
 			~le32_to_cpu(es->s_feature_incompat);
-		params.set_feature_ro_compat_mask &=
+		params->set_feature_ro_compat_mask &=
 			~le32_to_cpu(es->s_feature_ro_compat);
-		if ((params.set_feature_incompat_mask &
-		     EXT4_FEATURE_INCOMPAT_CASEFOLD)) {
+		if (params->set_feature_incompat_mask &
+		    EXT4_FEATURE_INCOMPAT_CASEFOLD) {
 			enabling_casefold = 1;
-			if (!(params.set_flags & EXT4_TUNE_FL_ENCODING)) {
-				params.encoding = EXT4_ENC_UTF8_12_1;
-				params.set_flags |= EXT4_TUNE_FL_ENCODING;
+			if (!(params->set_flags & EXT4_TUNE_FL_ENCODING)) {
+				params->encoding = EXT4_ENC_UTF8_12_1;
+				params->set_flags |= EXT4_TUNE_FL_ENCODING;
 			}
-			if (!(params.set_flags & EXT4_TUNE_FL_ENCODING_FLAGS)) {
-				params.encoding_flags = 0;
-				params.set_flags |= EXT4_TUNE_FL_ENCODING_FLAGS;
+			if (!(params->set_flags &
+			      EXT4_TUNE_FL_ENCODING_FLAGS)) {
+				params->encoding_flags = 0;
+				params->set_flags |=
+					EXT4_TUNE_FL_ENCODING_FLAGS;
 			}
 		}
-		if ((params.set_feature_compat_mask &
-		     EXT4_FEATURE_COMPAT_DIR_INDEX)) {
-			uuid_t	uu;
+		if (params->set_feature_compat_mask &
+		    EXT4_FEATURE_COMPAT_DIR_INDEX) {
+			uuid_t uu;
 
 			memcpy(&uu, sbi->s_hash_seed, UUID_SIZE);
 			if (uuid_is_null(&uu))
-				generate_random_uuid((char *)
-						     &sbi->s_hash_seed);
-			if (params.set_flags & EXT4_TUNE_FL_DEF_HASH_ALG)
-				sbi->s_def_hash_version = params.def_hash_alg;
+				generate_random_uuid((char *)&sbi->s_hash_seed);
+			if (params->set_flags & EXT4_TUNE_FL_DEF_HASH_ALG)
+				sbi->s_def_hash_version = params->def_hash_alg;
 			else if (sbi->s_def_hash_version == 0)
 				sbi->s_def_hash_version = DX_HASH_HALF_MD4;
 			if (!(es->s_flags &
@@ -1507,21 +1511,42 @@ static int ext4_ioctl_set_tune_sb(struct file *filp,
 			}
 		}
 	}
-	if (params.set_flags & EXT4_TUNE_FL_ENCODING) {
+	if (params->set_flags & EXT4_TUNE_FL_ENCODING) {
 		if (!enabling_casefold)
 			return -EINVAL;
-		if (params.encoding == 0)
-			params.encoding = EXT4_ENC_UTF8_12_1;
-		else if (params.encoding != EXT4_ENC_UTF8_12_1)
+		if (params->encoding == 0)
+			params->encoding = EXT4_ENC_UTF8_12_1;
+		else if (params->encoding != EXT4_ENC_UTF8_12_1)
 			return -EINVAL;
 	}
-	if (params.set_flags & EXT4_TUNE_FL_ENCODING_FLAGS) {
+	if (params->set_flags & EXT4_TUNE_FL_ENCODING_FLAGS) {
 		if (!enabling_casefold)
 			return -EINVAL;
-		if (params.encoding_flags & ~SB_ENC_SUPP_MASK)
+		if (params->encoding_flags & ~SB_ENC_SUPP_MASK)
 			return -EINVAL;
 	}
 
+	return 0;
+}
+
+static int ext4_ioctl_set_tune_sb(struct file *filp,
+				  struct ext4_tune_sb_params __user *in)
+{
+	struct ext4_tune_sb_params params;
+	struct super_block *sb = file_inode(filp)->i_sb;
+	struct ext4_sb_info *sbi = EXT4_SB(sb);
+	int ret;
+
+	if (!capable(CAP_SYS_ADMIN))
+		return -EPERM;
+
+	if (copy_from_user(&params, in, sizeof(params)))
+		return -EFAULT;
+
+	ret = ext4_validate_tune_sb_params(sb, &params);
+	if (ret)
+		return ret;
+
 	ret = mnt_want_write_file(filp);
 	if (ret)
 		return ret;
@@ -1930,6 +1955,7 @@ long ext4_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
 {
 	return __ext4_ioctl(filp, cmd, arg);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ioctl);
 
 #ifdef CONFIG_COMPAT
 long ext4_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
@@ -2006,6 +2032,7 @@ long ext4_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
 	}
 	return ext4_ioctl(file, cmd, (unsigned long) compat_ptr(arg));
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_compat_ioctl);
 #endif
 
 static void set_overhead(struct ext4_sb_info *sbi,
@@ -2026,3 +2053,85 @@ int ext4_update_overhead(struct super_block *sb, bool force)
 		return 0;
 	return ext4_update_superblocks_fn(sb, set_overhead, &sbi->s_overhead);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_update_overhead);
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+void ext4_sb_setlabel_test(struct ext4_sb_info *sbi,
+			   struct ext4_super_block *es, const void *arg)
+{
+	ext4_sb_setlabel(sbi, es, arg);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_setlabel_test);
+
+void ext4_sb_setuuid_test(struct ext4_sb_info *sbi,
+			  struct ext4_super_block *es, const void *arg)
+{
+	ext4_sb_setuuid(sbi, es, arg);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_setuuid_test);
+
+void ext4_sb_setparams_test(struct ext4_sb_info *sbi,
+			    struct ext4_super_block *es, const void *arg)
+{
+	ext4_sb_setparams(sbi, es, arg);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_setparams_test);
+
+int ext4_validate_tune_sb_params_test(struct super_block *sb,
+				      struct ext4_tune_sb_params *params)
+{
+	return ext4_validate_tune_sb_params(sb, params);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_validate_tune_sb_params_test);
+
+int ext4_update_primary_sb_test(struct super_block *sb, handle_t *handle,
+				void (*func)(struct ext4_sb_info *,
+					     struct ext4_super_block *,
+					     const void *),
+				const void *arg)
+{
+	return ext4_update_primary_sb(sb, handle, func, arg);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_update_primary_sb_test);
+
+int ext4_update_backup_sb_test(struct super_block *sb, handle_t *handle,
+			       ext4_group_t grp,
+			       void (*func)(struct ext4_sb_info *,
+					    struct ext4_super_block *,
+					    const void *),
+			       const void *arg)
+{
+	return ext4_update_backup_sb(sb, handle, grp, func, arg);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_update_backup_sb_test);
+
+int ext4_update_superblocks_fn_test(struct super_block *sb,
+				    void (*func)(struct ext4_sb_info *,
+						 struct ext4_super_block *,
+						 const void *),
+				    const void *arg)
+{
+	return ext4_update_superblocks_fn(sb, func, arg);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_update_superblocks_fn_test);
+
+void ext4_swap_inode_data_test(struct inode *inode1, struct inode *inode2)
+{
+	swap_inode_data(inode1, inode2);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_swap_inode_data_test);
+
+int ext4_ioctl_check_immutable_test(struct inode *inode, __u32 new_projid,
+				    unsigned int flags)
+{
+	return ext4_ioctl_check_immutable(inode, new_projid, flags);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ioctl_check_immutable_test);
+
+bool ext4_dax_compatible_test(struct inode *inode, unsigned int oldflags,
+			      unsigned int flags)
+{
+	return dax_compatible(inode, oldflags, flags);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dax_compatible_test);
+#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 ` [PATCH 04/27] ext4: add KUnit tests for fs/ext4/xattr.c Theodore Ts'o
2026-11-12  4:26   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 05/27] ext4: add KUnit tests for fs/ext4/namei.c Theodore Ts'o
2026-11-12  4:21   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 06/27] ext4: expand KUnit tests for fs/ext4/mballoc.c Theodore Ts'o
2026-11-12  4:26   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 07/27] ext4: add KUnit tests for fs/ext4/inline.c Theodore Ts'o
2026-11-12  4:29   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 08/27] ext4: zero s_fc_replay_state in ext4_fc_replay_cleanup() Theodore Ts'o
2026-11-12  4:16   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 09/27] ext4: add KUnit tests for fs/ext4/fast_commit.c Theodore Ts'o
2026-11-12  4:28   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 10/27] ext4: expand KUnit tests for fs/ext4/inode.c Theodore Ts'o
2026-11-12  4:36   ` sashiko-bot
2026-11-12  4:06 ` [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 ` Theodore Ts'o [this message]
2026-11-12  4:35   ` [PATCH 19/27] ext4: add KUnit tests for fs/ext4/ioctl.c 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-20-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