* [PATCH 00/27] ext4: add lots of KUnit tests!
@ 2026-11-12 4:06 Theodore Ts'o
2026-11-12 4:06 ` [PATCH 01/27] ext4: add KUinit tests for fs/ext4/extents_status.c Theodore Ts'o
` (26 more replies)
0 siblings, 27 replies; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
These unit tests were written using the writing-kunit-tests.md
prompt[1], and then lightly reviewed by a human with only a few
fix-ups needed.
[1] https://drive.google.com/file/d/1MVZIhlG79kTRX_A65phWiZkTPJEtypsL/view
While the LLM was busily generating these unit tests, it also found
around two dozen potential bug fixes[2]. Those will be sent in a
separate series.
[2] https://drive.google.com/file/d/1pnTKAVf9pAoCK27619uP4_3_hpE04SkR/view
Hopefully extending ext4's Kunit coverage will make it easier to
sanity check the flood of AI generated patch submissions. IOW, since
these tests were created Gemeni Pro, I'm hoping that we can use fire
to fight fire. :-)
So as I process the AI slop backlog, my plan is to do trial merges to
determine whether the patches break the unit tests or not. Which to
be fair, might be an indication of breakage of the unit tests, and not
necessarily a bug in the patches under review.
The patches in this series are available at the ext4.git tree on the
branch ext4-kunit-tests[3]. The patch series was originally based on
v7.3-rc4, so you want to try merging with with v7.4-rc2 or later, use
the branch ext4-kinit-tests-merge[4].
[3] https://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4.git/log/?h=ext4-kunit-tests
[4] https://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4.git/log/?h=ext4-kunit-tests-merge
Theodore Ts'o (27):
ext4: add KUinit tests for fs/ext4/extents_status.c
ext4: add KUnit tests for fs/ext4/extents.c
ext4: add KUnit tests for ext4_ext_split() leaf and index splitting
ext4: add KUnit tests for fs/ext4/xattr.c
ext4: add KUnit tests for fs/ext4/namei.c
ext4: expand KUnit tests for fs/ext4/mballoc.c
ext4: add KUnit tests for fs/ext4/inline.c
ext4: zero s_fc_replay_state in ext4_fc_replay_cleanup()
ext4: add KUnit tests for fs/ext4/fast_commit.c
ext4: expand KUnit tests for fs/ext4/inode.c
ext4: add KUnit tests for fs/ext4/ialloc.c
ext4: add KUnit tests for fs/ext4/balloc.c
ext4: add KUnit tests for fs/ext4/orphan.c
ext4: add KUnit tests for fs/ext4/block_validity.c
ext4: check multi-block overlap on left traversal in add_system_zone()
ext4: enable full feature coverage in fs/ext4/.kunitconfig
ext4: add KUnit tests for fs/ext4/super.c
ext4: add KUnit tests for fs/ext4/sysfs.c
ext4: add KUnit tests for fs/ext4/ioctl.c
ext4: add KUnit tests for fs/ext4/resize.c
ext4: add KUnit tests for fs/ext4/migrate.c
ext4: add KUnit tests for fs/ext4/indirect.c
ext4: expand KUnit tests in fs/ext4/extents-test.c for file range
operations
ext4: add KUnit tests for fs/ext4/fsmap.c
ext4: add KUnit tests for fs/ext4/move_extent.c
ext4: add KUnit tests for fs/ext4/mmp.c
ext4: add KUnit tests for fs/ext4/dir.c
fs/ext4/.kunitconfig | 7 +
fs/ext4/Makefile | 12 +-
fs/ext4/balloc-test.c | 2362 ++++++++++++++++++++++
fs/ext4/balloc.c | 11 +-
fs/ext4/bitmap.c | 5 +
fs/ext4/block_validity-test.c | 1595 +++++++++++++++
fs/ext4/block_validity.c | 11 +-
fs/ext4/crypto.c | 4 +
fs/ext4/dir-test.c | 992 ++++++++++
fs/ext4/dir.c | 37 +
fs/ext4/ext4.h | 251 ++-
fs/ext4/ext4_extents.h | 21 +
fs/ext4/ext4_jbd2.c | 17 +
fs/ext4/extents-test.c | 2601 ++++++++++++++++++++++++-
fs/ext4/extents.c | 94 +-
fs/ext4/extents_status-test.c | 1640 ++++++++++++++++
fs/ext4/extents_status.c | 22 +
fs/ext4/fast_commit-test.c | 2668 +++++++++++++++++++++++++
fs/ext4/fast_commit.c | 181 +-
fs/ext4/fsmap-test.c | 787 ++++++++
fs/ext4/fsmap.c | 3 +
fs/ext4/ialloc-test.c | 2714 ++++++++++++++++++++++++++
fs/ext4/ialloc.c | 96 +-
fs/ext4/indirect-test.c | 1270 ++++++++++++
fs/ext4/indirect.c | 18 +-
fs/ext4/inline-test.c | 1839 ++++++++++++++++++
fs/ext4/inline.c | 34 +-
fs/ext4/inode-test.c | 3460 ++++++++++++++++++++++++++++++++-
fs/ext4/inode.c | 191 +-
fs/ext4/ioctl-test.c | 1169 +++++++++++
fs/ext4/ioctl.c | 249 ++-
fs/ext4/mballoc-test.c | 1868 +++++++++++++++++-
fs/ext4/mballoc.c | 214 +-
fs/ext4/mballoc.h | 49 +
fs/ext4/migrate-test.c | 1064 ++++++++++
fs/ext4/migrate.c | 11 +
fs/ext4/mmp-test.c | 661 +++++++
fs/ext4/mmp.c | 117 +-
fs/ext4/move_extent-test.c | 997 ++++++++++
fs/ext4/move_extent.c | 22 +
fs/ext4/namei-test.c | 2314 ++++++++++++++++++++++
fs/ext4/namei.c | 342 ++--
fs/ext4/namei.h | 213 ++
fs/ext4/orphan-test.c | 2364 ++++++++++++++++++++++
fs/ext4/orphan.c | 12 +
fs/ext4/page-io.c | 1 +
fs/ext4/resize-test.c | 1341 +++++++++++++
fs/ext4/resize.c | 102 +-
fs/ext4/super-test.c | 1341 +++++++++++++
fs/ext4/super.c | 276 ++-
fs/ext4/sysfs-test.c | 653 +++++++
fs/ext4/sysfs.c | 55 +
fs/ext4/xattr-test.c | 1813 +++++++++++++++++
fs/ext4/xattr.c | 30 +-
fs/ext4/xattr.h | 2 +
fs/ext4/xattr_hurd.c | 4 +
fs/ext4/xattr_security.c | 4 +
fs/ext4/xattr_trusted.c | 4 +
fs/ext4/xattr_user.c | 4 +
59 files changed, 39827 insertions(+), 412 deletions(-)
create mode 100644 fs/ext4/balloc-test.c
create mode 100644 fs/ext4/block_validity-test.c
create mode 100644 fs/ext4/dir-test.c
create mode 100644 fs/ext4/extents_status-test.c
create mode 100644 fs/ext4/fast_commit-test.c
create mode 100644 fs/ext4/fsmap-test.c
create mode 100644 fs/ext4/ialloc-test.c
create mode 100644 fs/ext4/indirect-test.c
create mode 100644 fs/ext4/inline-test.c
create mode 100644 fs/ext4/ioctl-test.c
create mode 100644 fs/ext4/migrate-test.c
create mode 100644 fs/ext4/mmp-test.c
create mode 100644 fs/ext4/move_extent-test.c
create mode 100644 fs/ext4/namei-test.c
create mode 100644 fs/ext4/namei.h
create mode 100644 fs/ext4/orphan-test.c
create mode 100644 fs/ext4/resize-test.c
create mode 100644 fs/ext4/super-test.c
create mode 100644 fs/ext4/sysfs-test.c
create mode 100644 fs/ext4/xattr-test.c
base-commit: 93f51579e7df248780214094418f205253383cc5
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply [flat|nested] 55+ messages in thread
* [PATCH 01/27] ext4: add KUinit tests for fs/ext4/extents_status.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (25 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
These tests were created with the help of Gemini Pro and verified by a
human.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 3 +-
fs/ext4/extents.c | 5 -
fs/ext4/extents_status-test.c | 1640 +++++++++++++++++++++++++++++++++
fs/ext4/extents_status.c | 19 +
fs/ext4/inode.c | 10 +
5 files changed, 1671 insertions(+), 6 deletions(-)
create mode 100644 fs/ext4/extents_status-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 3f9fc0eb8eca..74b485dbb229 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -15,7 +15,8 @@ ext4-y := balloc.o bitmap.o block_validity.o dir.o ext4_jbd2.o extents.o \
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-test.o hash-test.o \
+ extents_status-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/extents.c b/fs/ext4/extents.c
index 76038b6c3655..6c018bcc0118 100644
--- a/fs/ext4/extents.c
+++ b/fs/ext4/extents.c
@@ -6407,12 +6407,7 @@ EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_split_convert_extents_test);
EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_ext_dirty);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_zeroout);
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_register_shrinker);
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_unregister_shrinker);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_map_create_blocks);
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_init_tree);
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_lookup_extent);
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_insert_extent);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_insert_extent);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_extent);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_issue_zeroout);
diff --git a/fs/ext4/extents_status-test.c b/fs/ext4/extents_status-test.c
new file mode 100644
index 000000000000..59eaac464bdc
--- /dev/null
+++ b/fs/ext4/extents_status-test.c
@@ -0,0 +1,1640 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for ext4 extents status tree and bigalloc pending reservations.
+ */
+
+#include <kunit/test.h>
+#include <kunit/test-bug.h>
+#include <kunit/static_stub.h>
+#include <linux/fs_context.h>
+#include <linux/seq_file.h>
+
+#include "ext4.h"
+#include "ext4_extents.h"
+
+#define ES_MAX_EXPECTED_EXTENTS 8
+
+struct es_test_ctx {
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ struct ext4_inode_info *ei;
+
+ /*
+ * Saved shrinker callbacks so the registered shrinker can be
+ * replaced with no-op stubs to prevent asynchronous background
+ * reclaim (kswapd / direct reclaim) from touching mock extents.
+ */
+ unsigned long (*shrinker_count)(struct shrinker *shrink,
+ struct shrink_control *sc);
+ unsigned long (*shrinker_scan)(struct shrinker *shrink,
+ struct shrink_control *sc);
+
+ /* Tracking for stubbed delalloc reservation callbacks */
+ int da_update_calls;
+ int last_da_used;
+ int total_da_used;
+ int last_da_quota_claim;
+
+ int da_release_calls;
+ int last_da_released;
+ int total_da_released;
+};
+
+struct expected_es {
+ ext4_lblk_t lblk;
+ ext4_lblk_t len;
+ ext4_fsblk_t pblk;
+ unsigned int type;
+};
+
+static void reset_da_stub_stats(struct es_test_ctx *ctx)
+{
+ ctx->da_update_calls = 0;
+ ctx->last_da_used = 0;
+ ctx->total_da_used = 0;
+ ctx->last_da_quota_claim = 0;
+
+ ctx->da_release_calls = 0;
+ ctx->last_da_released = 0;
+ ctx->total_da_released = 0;
+}
+
+static void ext4_da_update_reserve_space_stub(struct inode *inode,
+ int used, int quota_claim)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct es_test_ctx *ctx;
+
+ if (!test || !test->priv)
+ return;
+
+ ctx = test->priv;
+ ctx->da_update_calls++;
+ ctx->last_da_used = used;
+ ctx->total_da_used += used;
+ ctx->last_da_quota_claim = quota_claim;
+}
+
+static void ext4_da_release_space_stub(struct inode *inode, int to_free)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct es_test_ctx *ctx;
+
+ if (!to_free || !test || !test->priv)
+ return;
+
+ ctx = test->priv;
+ ctx->da_release_calls++;
+ ctx->last_da_released = to_free;
+ ctx->total_da_released += to_free;
+}
+
+static struct ext4_ext_path *
+ext4_find_extent_stub(struct inode *inode, ext4_lblk_t block,
+ struct ext4_ext_path *path, int flags)
+{
+ return ERR_PTR(-EOPNOTSUPP);
+}
+
+static unsigned long es_dummy_shrinker_count(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return 0;
+}
+
+static unsigned long es_dummy_shrinker_scan(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return SHRINK_STOP;
+}
+
+static unsigned long es_shrinker_count(struct es_test_ctx *ctx,
+ struct shrink_control *sc)
+{
+ return ctx->shrinker_count(ctx->sbi->s_es_shrinker, sc);
+}
+
+static unsigned long es_shrinker_scan(struct es_test_ctx *ctx,
+ struct shrink_control *sc)
+{
+ return ctx->shrinker_scan(ctx->sbi->s_es_shrinker, sc);
+}
+
+static void es_insert_extent(struct inode *inode, ext4_lblk_t lblk,
+ ext4_lblk_t len, ext4_fsblk_t pblk,
+ unsigned int status, bool delalloc_reserve_used)
+{
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ext4_es_insert_extent(inode, lblk, len, pblk, status,
+ delalloc_reserve_used);
+ up_write(&EXT4_I(inode)->i_data_sem);
+}
+
+static void es_insert_delayed_extent(struct inode *inode, ext4_lblk_t lblk,
+ ext4_lblk_t len, bool lclu_allocated,
+ bool end_allocated)
+{
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ext4_es_insert_delayed_extent(inode, lblk, len, lclu_allocated,
+ end_allocated);
+ up_write(&EXT4_I(inode)->i_data_sem);
+}
+
+static void es_cache_extent(struct inode *inode, ext4_lblk_t lblk,
+ ext4_lblk_t len, ext4_fsblk_t pblk,
+ unsigned int status)
+{
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ext4_es_cache_extent(inode, lblk, len, pblk, status);
+ up_read(&EXT4_I(inode)->i_data_sem);
+}
+
+static void es_remove_extent(struct inode *inode, ext4_lblk_t lblk,
+ ext4_lblk_t len)
+{
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ext4_es_remove_extent(inode, lblk, len);
+ up_write(&EXT4_I(inode)->i_data_sem);
+}
+
+static int es_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static int es_set_anon(struct super_block *sb, struct fs_context *fc)
+{
+ return set_anon_super_fc(sb, fc);
+}
+
+static struct file_system_type es_fs_type = {
+ .name = "extents_status test",
+ .init_fs_context = es_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static void es_init_mock_inode(struct ext4_inode_info *ei,
+ struct super_block *sb, unsigned long ino)
+{
+ struct inode *inode = &ei->vfs_inode;
+
+ memset(ei, 0, sizeof(*ei));
+ inode->i_sb = sb;
+ inode->i_ino = ino;
+ inode->i_blkbits = sb->s_blocksize_bits;
+
+ ext4_es_init_tree(&ei->i_es_tree);
+ ext4_init_pending_tree(&ei->i_pending_tree);
+ rwlock_init(&ei->i_es_lock);
+ INIT_LIST_HEAD(&ei->i_es_list);
+ init_rwsem(&ei->i_data_sem);
+ spin_lock_init(&ei->i_block_reservation_lock);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+}
+
+static void es_cleanup_inode(struct inode *inode)
+{
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+ struct rb_node *node;
+
+ sbi->s_mount_state &= ~EXT4_FC_REPLAY;
+ es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+
+ while ((node = rb_first(&ei->i_pending_tree.root)) != NULL) {
+ struct pending_reservation *pr;
+
+ pr = rb_entry(node, struct pending_reservation, rb_node);
+ ext4_remove_pending(inode, EXT4_C2B(sbi, pr->lclu));
+ }
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(es_cleanup_inode_action, es_cleanup_inode,
+ struct inode *);
+KUNIT_DEFINE_ACTION_WRAPPER(es_unregister_shrinker_action,
+ ext4_es_unregister_shrinker,
+ struct ext4_sb_info *);
+KUNIT_DEFINE_ACTION_WRAPPER(es_deactivate_super_action, deactivate_super,
+ struct super_block *);
+
+static struct inode *es_alloc_test_inode(struct kunit *test,
+ struct super_block *sb,
+ unsigned long ino)
+{
+ struct ext4_inode_info *ei;
+ int ret;
+
+ ei = kunit_kzalloc(test, sizeof(*ei), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ei);
+ es_init_mock_inode(ei, sb, ino);
+
+ ret = kunit_add_action_or_reset(test, es_cleanup_inode_action,
+ &ei->vfs_inode);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ return &ei->vfs_inode;
+}
+
+static void es_set_cluster_bits(struct super_block *sb,
+ unsigned int cluster_bits)
+{
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+
+ sbi->s_cluster_bits = cluster_bits;
+ sbi->s_cluster_ratio = 1U << cluster_bits;
+}
+
+/*
+ * All test resources and deferred cleanup actions are registered with KUnit's
+ * resource stack (test->resources), which unwinds in strict LIFO order after
+ * each test case completes (or aborts).
+ *
+ * The registration order below (and in helpers like es_alloc_test_inode())
+ * MUST be preserved when modifying these tests so that teardown happens in the
+ * reverse order:
+ * 1. kunit_kzalloc() of ctx, sbi, and ctx->ei (freed last)
+ * 2. es_deactivate_super_action(sb) (superblock deactivated after shrinker
+ * and all inodes are torn down)
+ * 3. es_unregister_shrinker_action(sbi) (shrinker and s_es_stats per-CPU
+ * counters destroyed after all inode extent trees are emptied)
+ * 4. kunit_activate_static_stub() for ext4_da_update_reserve_space,
+ * ext4_da_release_space, and ext4_find_extent (stubs must remain active
+ * while es_cleanup_inode() removes delayed extents via
+ * ext4_es_remove_extent())
+ * 5. es_cleanup_inode_action() for ctx->ei and any additional test inodes
+ * allocated via es_alloc_test_inode() (run first during teardown; inside
+ * es_alloc_test_inode(), kunit_kzalloc() must precede
+ * kunit_add_action_or_reset() so the inode is cleaned up before freed)
+ */
+static int es_kunit_init(struct kunit *test)
+{
+ struct es_test_ctx *ctx;
+ struct ext4_sb_info *sbi;
+ struct super_block *sb;
+ struct fs_context *fc;
+ int err;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ if (!ctx)
+ return -ENOMEM;
+
+ sbi = kunit_kzalloc(test, sizeof(*sbi), GFP_KERNEL);
+ if (!sbi)
+ return -ENOMEM;
+
+ ctx->ei = kunit_kzalloc(test, sizeof(*ctx->ei), GFP_KERNEL);
+ if (!ctx->ei)
+ return -ENOMEM;
+
+ fc = fs_context_for_mount(&es_fs_type, 0);
+ if (IS_ERR(fc))
+ return PTR_ERR(fc);
+
+ sb = sget_fc(fc, NULL, es_set_anon);
+ put_fs_context(fc);
+ if (IS_ERR(sb))
+ return PTR_ERR(sb);
+
+ sbi->s_sb = sb;
+ sb->s_fs_info = sbi;
+ sb->s_blocksize = 4096;
+ sb->s_blocksize_bits = 12;
+ strscpy(sb->s_id, "es_kunit", sizeof(sb->s_id));
+
+ es_set_cluster_bits(sb, 0);
+ set_opt(sb, DELALLOC);
+ ratelimit_state_init(&sbi->s_warning_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL,
+ DEFAULT_RATELIMIT_BURST);
+ up_write(&sb->s_umount);
+
+ err = kunit_add_action_or_reset(test, es_deactivate_super_action, sb);
+ if (err)
+ return err;
+
+ err = ext4_es_register_shrinker(sbi);
+ if (err)
+ return err;
+
+ /*
+ * Detach the real callbacks from the globally registered shrinker
+ * before any extents are inserted so background kswapd / direct
+ * reclaim cannot asynchronously evict mock extents during a test.
+ */
+ ctx->shrinker_count = sbi->s_es_shrinker->count_objects;
+ ctx->shrinker_scan = sbi->s_es_shrinker->scan_objects;
+ WRITE_ONCE(sbi->s_es_shrinker->count_objects, es_dummy_shrinker_count);
+ WRITE_ONCE(sbi->s_es_shrinker->scan_objects, es_dummy_shrinker_scan);
+
+ err = kunit_add_action_or_reset(test, es_unregister_shrinker_action,
+ sbi);
+ if (err)
+ return err;
+
+ ctx->sb = sb;
+ ctx->sbi = sbi;
+ test->priv = ctx;
+
+ kunit_activate_static_stub(test, ext4_da_update_reserve_space,
+ ext4_da_update_reserve_space_stub);
+ kunit_activate_static_stub(test, ext4_da_release_space,
+ ext4_da_release_space_stub);
+ kunit_activate_static_stub(test, ext4_find_extent,
+ ext4_find_extent_stub);
+
+ es_init_mock_inode(ctx->ei, sb, 12);
+ err = kunit_add_action_or_reset(test, es_cleanup_inode_action,
+ &ctx->ei->vfs_inode);
+ if (err)
+ return err;
+
+ return 0;
+}
+
+static void assert_es_tree(struct kunit *test, struct inode *inode,
+ const struct expected_es *exp, unsigned int nr_exp)
+{
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ struct extent_status found[ES_MAX_EXPECTED_EXTENTS];
+ struct rb_node *node;
+ unsigned int nr_found = 0;
+ unsigned int all_nr;
+ unsigned int i;
+ ext4_lblk_t prev_end = 0;
+
+ KUNIT_ASSERT_LE(test, nr_exp, (unsigned int)ARRAY_SIZE(found));
+
+ /*
+ * Snapshot the tree under i_es_lock and release the lock before
+ * running any KUnit assertions or expectations. KUNIT_ASSERT_*
+ * aborts the test kthread on failure (which would leak i_es_lock
+ * and deadlock es_cleanup_inode()), and KUNIT_EXPECT_* allocates
+ * failure logs with GFP_KERNEL.
+ */
+ read_lock(&ei->i_es_lock);
+ for (node = rb_first(&ei->i_es_tree.root); node; node = rb_next(node)) {
+ struct extent_status *es =
+ rb_entry(node, struct extent_status, rb_node);
+
+ if (nr_found < ARRAY_SIZE(found))
+ found[nr_found] = *es;
+ nr_found++;
+ }
+ all_nr = ei->i_es_all_nr;
+ read_unlock(&ei->i_es_lock);
+
+ for (i = 0; i < nr_found; i++) {
+ struct extent_status *es = &found[min(i,
+ (unsigned int)ARRAY_SIZE(found) - 1)];
+
+ KUNIT_ASSERT_LT_MSG(test, i, nr_exp,
+ "unexpected extra extent [%u/%u) type 0x%x",
+ es->es_lblk, es->es_len, ext4_es_type(es));
+
+ if (i > 0)
+ KUNIT_EXPECT_GT_MSG(test, es->es_lblk, prev_end,
+ "extent %u not strictly ordered",
+ i);
+
+ KUNIT_EXPECT_EQ_MSG(test, es->es_lblk, exp[i].lblk,
+ "extent %u lblk mismatch", i);
+ KUNIT_EXPECT_EQ_MSG(test, es->es_len, exp[i].len,
+ "extent %u len mismatch", i);
+ KUNIT_EXPECT_EQ_MSG(test, ext4_es_type(es), exp[i].type,
+ "extent %u type mismatch", i);
+
+ if (exp[i].type & (EXTENT_STATUS_WRITTEN |
+ EXTENT_STATUS_UNWRITTEN))
+ KUNIT_EXPECT_EQ_MSG(test, ext4_es_pblock(es),
+ exp[i].pblk,
+ "extent %u pblk mismatch", i);
+
+ prev_end = es->es_lblk + es->es_len - 1;
+ }
+
+ KUNIT_EXPECT_EQ(test, nr_found, nr_exp);
+ KUNIT_EXPECT_EQ(test, all_nr, nr_exp);
+}
+
+/*
+ * Group 1: Bitfield Encoding & Inline Helpers
+ */
+static void test_es_bitfield_written_and_unwritten_encoding(struct kunit *test)
+{
+ struct extent_status es = {};
+ const ext4_fsblk_t sample_pblk = 0x123456789abcULL;
+
+ ext4_es_store_pblock_status(&es, sample_pblk, EXTENT_STATUS_WRITTEN);
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), sample_pblk);
+ KUNIT_EXPECT_EQ(test, ext4_es_show_pblock(&es), sample_pblk);
+ KUNIT_EXPECT_EQ(test, ext4_es_type(&es), EXTENT_STATUS_WRITTEN);
+ KUNIT_EXPECT_EQ(test, ext4_es_status(&es), EXTENT_STATUS_WRITTEN);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_written(&es));
+ KUNIT_EXPECT_FALSE(test, ext4_es_is_unwritten(&es));
+ KUNIT_EXPECT_FALSE(test, ext4_es_is_delayed(&es));
+ KUNIT_EXPECT_FALSE(test, ext4_es_is_hole(&es));
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_mapped(&es));
+ KUNIT_EXPECT_FALSE(test, ext4_es_is_referenced(&es));
+
+ ext4_es_store_pblock_status(&es, sample_pblk, EXTENT_STATUS_UNWRITTEN);
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), sample_pblk);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_unwritten(&es));
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_mapped(&es));
+ KUNIT_EXPECT_FALSE(test, ext4_es_is_written(&es));
+}
+
+static void test_es_bitfield_delayed_and_hole_encoding(struct kunit *test)
+{
+ struct extent_status es = {};
+ /* 59-bit physical block field (64 - 5 status bits) all ones */
+ const ext4_fsblk_t max_59bit_pblk = 0x07ffffffffffffffULL;
+
+ ext4_es_store_pblock_status(&es, ~0ULL, EXTENT_STATUS_DELAYED);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_delayed(&es));
+ KUNIT_EXPECT_FALSE(test, ext4_es_is_mapped(&es));
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), max_59bit_pblk);
+ KUNIT_EXPECT_EQ(test, ext4_es_show_pblock(&es), 0ULL);
+
+ ext4_es_store_pblock_status(&es, 0ULL, EXTENT_STATUS_HOLE);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_hole(&es));
+ KUNIT_EXPECT_FALSE(test, ext4_es_is_mapped(&es));
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), 0ULL);
+}
+
+static void
+test_es_bitfield_referenced_and_pblock_update_preserve_state(struct kunit *test)
+{
+ struct extent_status es = {};
+ const ext4_fsblk_t initial_pblk = 0x123456789abcULL;
+ const ext4_fsblk_t updated_pblk = 0x07ffffffffffffffULL;
+
+ ext4_es_store_pblock_status(&es, initial_pblk, EXTENT_STATUS_WRITTEN);
+
+ /* Setting referenced bit preserves pblk and type */
+ ext4_es_set_referenced(&es);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_referenced(&es));
+ KUNIT_EXPECT_EQ(test, ext4_es_type(&es), EXTENT_STATUS_WRITTEN);
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), initial_pblk);
+ KUNIT_EXPECT_EQ(test, ext4_es_status(&es),
+ EXTENT_STATUS_WRITTEN | EXTENT_STATUS_REFERENCED);
+
+ /* Updating pblock preserves status and referenced bit */
+ ext4_es_store_pblock(&es, updated_pblk);
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), updated_pblk);
+ KUNIT_EXPECT_EQ(test, ext4_es_type(&es), EXTENT_STATUS_WRITTEN);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_referenced(&es));
+
+ /* Clearing referenced bit preserves updated pblk and type */
+ ext4_es_clear_referenced(&es);
+ KUNIT_EXPECT_FALSE(test, ext4_es_is_referenced(&es));
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), updated_pblk);
+ KUNIT_EXPECT_EQ(test, ext4_es_type(&es), EXTENT_STATUS_WRITTEN);
+}
+
+/*
+ * Group 2: Insertion, Lookup, Sequence Numbers, and Merging
+ */
+static void test_es_lookup_empty_tree_returns_miss(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ struct extent_status es;
+ ext4_lblk_t next_lblk = 0xdead;
+ s64 misses_before;
+ int ret;
+
+ misses_before = percpu_counter_sum_positive(
+ &ctx->sbi->s_es_stats.es_stats_cache_misses);
+ ret = ext4_es_lookup_extent(inode, 10, &next_lblk, &es, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, percpu_counter_sum_positive(
+ &ctx->sbi->s_es_stats.es_stats_cache_misses),
+ misses_before + 1);
+}
+
+static void test_es_insert_zero_len_is_noop(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+
+ es_insert_extent(inode, 10, 0, 100, EXTENT_STATUS_WRITTEN, false);
+ es_insert_delayed_extent(inode, 20, 0, false, false);
+ es_cache_extent(inode, 30, 0, 300, EXTENT_STATUS_WRITTEN);
+
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_all_nr, 0U);
+ KUNIT_EXPECT_TRUE(test, RB_EMPTY_ROOT(&ctx->ei->i_es_tree.root));
+}
+
+static void
+test_es_lookup_hits_cache_and_rbtree_with_next_lblk(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ struct extent_status es;
+ ext4_lblk_t next_lblk = 0xdead;
+ u64 seq_after_first, seq_after_second, lookup_seq = 0;
+ s64 hits_before;
+ int ret;
+
+ es_insert_extent(inode, 10, 5, 100, EXTENT_STATUS_WRITTEN, false);
+ seq_after_first = ctx->ei->i_es_seq;
+ es_insert_extent(inode, 20, 5, 200, EXTENT_STATUS_UNWRITTEN, false);
+ seq_after_second = ctx->ei->i_es_seq;
+ KUNIT_EXPECT_GT(test, seq_after_second, seq_after_first);
+
+ /*
+ * Right after inserting [20, 5), tree->cache_es points to [20, 5).
+ * Looking up lblk 22 hits cache_es and sets next_lblk to 0 because
+ * [20, 5) is the last extent in the tree.
+ */
+ hits_before = percpu_counter_sum_positive(
+ &ctx->sbi->s_es_stats.es_stats_cache_hits);
+
+ ret = ext4_es_lookup_extent(inode, 22, &next_lblk, &es, &lookup_seq);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, es.es_lblk, 20U);
+ KUNIT_EXPECT_EQ(test, es.es_len, 5U);
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), 200ULL);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_unwritten(&es));
+ KUNIT_EXPECT_EQ(test, next_lblk, 0U);
+ KUNIT_EXPECT_EQ(test, lookup_seq, seq_after_second);
+
+ /* Lookup start, middle, and end of first extent [10, 5) via rb-tree */
+ ret = ext4_es_lookup_extent(inode, 10, &next_lblk, &es, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, es.es_lblk, 10U);
+ KUNIT_EXPECT_EQ(test, next_lblk, 20U);
+
+ ret = ext4_es_lookup_extent(inode, 12, &next_lblk, &es, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, es.es_lblk, 10U);
+ KUNIT_EXPECT_EQ(test, next_lblk, 20U);
+
+ ret = ext4_es_lookup_extent(inode, 14, &next_lblk, &es, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, es.es_lblk, 10U);
+ KUNIT_EXPECT_EQ(test, next_lblk, 20U);
+
+ KUNIT_EXPECT_EQ(test, percpu_counter_sum_positive(
+ &ctx->sbi->s_es_stats.es_stats_cache_hits),
+ hits_before + 4);
+
+ /* Lookup in gap [15..19] returns 0 */
+ ret = ext4_es_lookup_extent(inode, 15, &next_lblk, &es, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+}
+
+static void test_es_fc_replay_bypasses_tree_operations(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ struct extent_status es;
+ static const struct expected_es expected[] = {
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ };
+
+ es_insert_extent(inode, 10, 5, 100, EXTENT_STATUS_WRITTEN, false);
+
+ ctx->sbi->s_mount_state |= EXT4_FC_REPLAY;
+ KUNIT_EXPECT_EQ(test,
+ ext4_es_lookup_extent(inode, 10, NULL, &es, NULL), 0);
+ ext4_es_find_extent_range(inode, ext4_es_is_written, 0, 20, &es);
+ KUNIT_EXPECT_EQ(test, es.es_len, 0U);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_es_scan_range(inode, ext4_es_is_written,
+ 0, 20));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_es_scan_clu(inode, ext4_es_is_written, 10));
+
+ es_insert_extent(inode, 30, 5, 300, EXTENT_STATUS_WRITTEN, false);
+ es_cache_extent(inode, 40, 5, 400, EXTENT_STATUS_WRITTEN);
+ es_insert_delayed_extent(inode, 50, 5, false, false);
+ es_remove_extent(inode, 10, 5);
+ ctx->sbi->s_mount_state &= ~EXT4_FC_REPLAY;
+
+ assert_es_tree(test, inode, expected, ARRAY_SIZE(expected));
+}
+
+struct es_merge_test_param {
+ const char *desc;
+ unsigned int nr_inserts;
+ struct expected_es inserts[3];
+ unsigned int nr_expected;
+ struct expected_es expected[3];
+};
+
+static const struct es_merge_test_param es_merge_params[] = {
+ {
+ .desc = "left merge contiguous written extents",
+ .nr_inserts = 2,
+ .inserts = {
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 15, .len = 5, .pblk = 105,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ .nr_expected = 1,
+ .expected = {
+ { .lblk = 10, .len = 10, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ },
+ {
+ .desc = "right merge contiguous written extents",
+ .nr_inserts = 2,
+ .inserts = {
+ { .lblk = 15, .len = 5, .pblk = 105,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ .nr_expected = 1,
+ .expected = {
+ { .lblk = 10, .len = 10, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ },
+ {
+ .desc = "three-way bridge merge via try_to_merge_right",
+ .nr_inserts = 3,
+ .inserts = {
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 20, .len = 5, .pblk = 110,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 15, .len = 5, .pblk = 105,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ .nr_expected = 1,
+ .expected = {
+ { .lblk = 10, .len = 15, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ },
+ {
+ .desc = "three-way bridge merge via try_to_merge_left",
+ .nr_inserts = 3,
+ .inserts = {
+ { .lblk = 20, .len = 5, .pblk = 110,
+ .type = EXTENT_STATUS_UNWRITTEN },
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_UNWRITTEN },
+ { .lblk = 15, .len = 5, .pblk = 105,
+ .type = EXTENT_STATUS_UNWRITTEN },
+ },
+ .nr_expected = 1,
+ .expected = {
+ { .lblk = 10, .len = 15, .pblk = 100,
+ .type = EXTENT_STATUS_UNWRITTEN },
+ },
+ },
+ {
+ .desc = "adjacent hole extents merge",
+ .nr_inserts = 2,
+ .inserts = {
+ { .lblk = 10, .len = 5, .pblk = 0,
+ .type = EXTENT_STATUS_HOLE },
+ { .lblk = 15, .len = 5, .pblk = 0,
+ .type = EXTENT_STATUS_HOLE },
+ },
+ .nr_expected = 1,
+ .expected = {
+ { .lblk = 10, .len = 10, .pblk = 0,
+ .type = EXTENT_STATUS_HOLE },
+ },
+ },
+ {
+ .desc = "discontiguous physical blocks do not merge",
+ .nr_inserts = 2,
+ .inserts = {
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 15, .len = 5, .pblk = 200,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ .nr_expected = 2,
+ .expected = {
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 15, .len = 5, .pblk = 200,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ },
+ {
+ .desc = "different status types with contiguous pblk do not merge",
+ .nr_inserts = 2,
+ .inserts = {
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 15, .len = 5, .pblk = 105,
+ .type = EXTENT_STATUS_UNWRITTEN },
+ },
+ .nr_expected = 2,
+ .expected = {
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 15, .len = 5, .pblk = 105,
+ .type = EXTENT_STATUS_UNWRITTEN },
+ },
+ },
+ {
+ .desc = "logically separated extents with 1-block gap do not merge",
+ .nr_inserts = 2,
+ .inserts = {
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 16, .len = 5, .pblk = 106,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ .nr_expected = 2,
+ .expected = {
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 16, .len = 5, .pblk = 106,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ },
+};
+
+static void es_merge_get_desc(const struct es_merge_test_param *param,
+ char *desc)
+{
+ strscpy(desc, param->desc, KUNIT_PARAM_DESC_SIZE);
+}
+KUNIT_ARRAY_PARAM(es_merge, es_merge_params, es_merge_get_desc);
+
+static void test_es_merge_cases(struct kunit *test)
+{
+ const struct es_merge_test_param *param = test->param_value;
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ unsigned int i;
+
+ for (i = 0; i < param->nr_inserts; i++) {
+ const struct expected_es *ins = ¶m->inserts[i];
+
+ es_insert_extent(inode, ins->lblk, ins->len,
+ ins->pblk, ins->type, false);
+ }
+
+ assert_es_tree(test, inode, param->expected, param->nr_expected);
+}
+
+static void test_es_insert_adjacent_delayed_extents_merges(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ static const struct expected_es expected[] = {
+ { .lblk = 10, .len = 10, .pblk = 0,
+ .type = EXTENT_STATUS_DELAYED },
+ };
+
+ es_insert_delayed_extent(inode, 10, 5, false, false);
+ es_insert_delayed_extent(inode, 15, 5, false, false);
+
+ assert_es_tree(test, inode, expected, ARRAY_SIZE(expected));
+}
+
+static void test_es_merge_preserves_referenced(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ struct extent_status es;
+ struct rb_node *node;
+ bool is_ref = false;
+
+ /*
+ * Insert [10, 5) and [20, 5), mark [20, 5) referenced via lookup,
+ * then bridge [15, 5) so [20, 5) is merged into [10, 10) via
+ * ext4_es_try_to_merge_right().
+ */
+ es_insert_extent(inode, 10, 5, 100, EXTENT_STATUS_WRITTEN, false);
+ es_insert_extent(inode, 20, 5, 110, EXTENT_STATUS_WRITTEN, false);
+ KUNIT_ASSERT_EQ(test,
+ ext4_es_lookup_extent(inode, 20, NULL, &es, NULL), 1);
+
+ es_insert_extent(inode, 15, 5, 105, EXTENT_STATUS_WRITTEN, false);
+ KUNIT_ASSERT_EQ(test, ctx->ei->i_es_all_nr, 1U);
+
+ read_lock(&ctx->ei->i_es_lock);
+ node = rb_first(&ctx->ei->i_es_tree.root);
+ if (node)
+ is_ref = ext4_es_is_referenced(rb_entry(node,
+ struct extent_status,
+ rb_node));
+ read_unlock(&ctx->ei->i_es_lock);
+
+ KUNIT_ASSERT_NOT_NULL(test, node);
+ KUNIT_EXPECT_TRUE(test, is_ref);
+}
+
+static void
+test_es_merge_at_max_blocks_boundary_succeeds(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ static const struct expected_es expected[] = {
+ { .lblk = 0, .len = EXT_MAX_BLOCKS, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ };
+
+ es_insert_extent(inode, 0, 0x80000000U, 100,
+ EXTENT_STATUS_WRITTEN, false);
+ es_insert_extent(inode, 0x80000000U, 0x7fffffffU,
+ 100ULL + 0x80000000ULL,
+ EXTENT_STATUS_WRITTEN, false);
+
+ assert_es_tree(test, inode, expected, ARRAY_SIZE(expected));
+}
+
+/*
+ * Group 3: Removal, Trimming, Splitting, and Overwrites
+ */
+struct es_remove_test_param {
+ const char *desc;
+ ext4_lblk_t rem_lblk;
+ ext4_lblk_t rem_len;
+ unsigned int nr_expected;
+ struct expected_es expected[2];
+};
+
+static const struct es_remove_test_param es_remove_params[] = {
+ {
+ .desc = "remove range before extent",
+ .rem_lblk = 0,
+ .rem_len = 5,
+ .nr_expected = 1,
+ .expected = {
+ { .lblk = 10, .len = 10, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ },
+ {
+ .desc = "remove range after extent",
+ .rem_lblk = 20,
+ .rem_len = 5,
+ .nr_expected = 1,
+ .expected = {
+ { .lblk = 10, .len = 10, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ },
+ {
+ .desc = "remove exact extent range",
+ .rem_lblk = 10,
+ .rem_len = 10,
+ .nr_expected = 0,
+ },
+ {
+ .desc = "remove superset of extent range",
+ .rem_lblk = 5,
+ .rem_len = 20,
+ .nr_expected = 0,
+ },
+ {
+ .desc = "prefix trim of extent",
+ .rem_lblk = 10,
+ .rem_len = 4,
+ .nr_expected = 1,
+ .expected = {
+ { .lblk = 14, .len = 6, .pblk = 104,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ },
+ {
+ .desc = "suffix trim of extent",
+ .rem_lblk = 16,
+ .rem_len = 4,
+ .nr_expected = 1,
+ .expected = {
+ { .lblk = 10, .len = 6, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ },
+ {
+ .desc = "middle split of extent into two",
+ .rem_lblk = 13,
+ .rem_len = 4,
+ .nr_expected = 2,
+ .expected = {
+ { .lblk = 10, .len = 3, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 17, .len = 3, .pblk = 107,
+ .type = EXTENT_STATUS_WRITTEN },
+ },
+ },
+};
+
+static void es_remove_get_desc(const struct es_remove_test_param *param,
+ char *desc)
+{
+ strscpy(desc, param->desc, KUNIT_PARAM_DESC_SIZE);
+}
+KUNIT_ARRAY_PARAM(es_remove, es_remove_params, es_remove_get_desc);
+
+static void test_es_remove_geometries(struct kunit *test)
+{
+ const struct es_remove_test_param *param = test->param_value;
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+
+ es_insert_extent(inode, 10, 10, 100, EXTENT_STATUS_WRITTEN, false);
+ es_remove_extent(inode, param->rem_lblk, param->rem_len);
+
+ assert_es_tree(test, inode, param->expected, param->nr_expected);
+}
+
+static void
+test_es_remove_multi_extent_span_trims_and_deletes(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ static const struct expected_es expected[] = {
+ { .lblk = 10, .len = 2, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 43, .len = 2, .pblk = 403,
+ .type = EXTENT_STATUS_UNWRITTEN },
+ };
+
+ es_insert_extent(inode, 10, 5, 100, EXTENT_STATUS_WRITTEN, false);
+ es_insert_extent(inode, 20, 5, 200, EXTENT_STATUS_HOLE, false);
+ es_insert_extent(inode, 30, 5, 300, EXTENT_STATUS_WRITTEN, false);
+ es_insert_extent(inode, 40, 5, 400, EXTENT_STATUS_UNWRITTEN, false);
+
+ /*
+ * Remove [12, 31) covering lblks 12..42 inclusive:
+ * - trims suffix of [10, 5) -> [10, 2)
+ * - completely removes [20, 5) and [30, 5)
+ * - trims prefix of [40, 5) -> [43, 2)
+ */
+ es_remove_extent(inode, 12, 31);
+ assert_es_tree(test, inode, expected, ARRAY_SIZE(expected));
+}
+
+static void test_es_insert_overwrite_split_and_remerge(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ static const struct expected_es split_exp[] = {
+ { .lblk = 10, .len = 3, .pblk = 100,
+ .type = EXTENT_STATUS_UNWRITTEN },
+ { .lblk = 13, .len = 4, .pblk = 103,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 17, .len = 3, .pblk = 107,
+ .type = EXTENT_STATUS_UNWRITTEN },
+ };
+ static const struct expected_es merged_exp[] = {
+ { .lblk = 10, .len = 10, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ };
+
+ es_insert_extent(inode, 10, 10, 100, EXTENT_STATUS_UNWRITTEN, false);
+ es_insert_extent(inode, 13, 4, 103, EXTENT_STATUS_WRITTEN, false);
+ assert_es_tree(test, inode, split_exp, ARRAY_SIZE(split_exp));
+
+ /* Convert the remaining prefix and suffix to WRITTEN; all 3 merge */
+ es_insert_extent(inode, 10, 3, 100, EXTENT_STATUS_WRITTEN, false);
+ es_insert_extent(inode, 17, 3, 107, EXTENT_STATUS_WRITTEN, false);
+ assert_es_tree(test, inode, merged_exp, ARRAY_SIZE(merged_exp));
+}
+
+/*
+ * Group 4: On-Disk Caching & Conflict Detection (ext4_es_cache_extent)
+ */
+static void
+test_es_cache_extent_inserts_and_coalesces_same_status(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ static const struct expected_es initial_exp[] = {
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ };
+ static const struct expected_es coalesced_exp[] = {
+ { .lblk = 8, .len = 10, .pblk = 98,
+ .type = EXTENT_STATUS_WRITTEN },
+ };
+ int warnings_before;
+
+ warnings_before = atomic_read(&ctx->sbi->s_warning_count);
+
+ /* Cache into empty tree */
+ es_cache_extent(inode, 10, 5, 100, EXTENT_STATUS_WRITTEN);
+ assert_es_tree(test, inode, initial_exp, ARRAY_SIZE(initial_exp));
+
+ /* Caching sub-range with identical status is a no-op */
+ es_cache_extent(inode, 11, 2, 101, EXTENT_STATUS_WRITTEN);
+ assert_es_tree(test, inode, initial_exp, ARRAY_SIZE(initial_exp));
+
+ /* Caching larger covering range with same status coalesces */
+ es_cache_extent(inode, 8, 10, 98, EXTENT_STATUS_WRITTEN);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->sbi->s_warning_count),
+ warnings_before);
+ assert_es_tree(test, inode, coalesced_exp, ARRAY_SIZE(coalesced_exp));
+}
+
+static void
+test_es_cache_extent_conflict_covered_range_warns(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ static const struct expected_es expected[] = {
+ { .lblk = 8, .len = 10, .pblk = 98,
+ .type = EXTENT_STATUS_WRITTEN },
+ };
+ int warnings_before;
+
+ es_cache_extent(inode, 8, 10, 98, EXTENT_STATUS_WRITTEN);
+ warnings_before = atomic_read(&ctx->sbi->s_warning_count);
+
+ /* Sub-range [10, 4) covered by [8, 10) with conflicting status */
+ es_cache_extent(inode, 10, 4, 500, EXTENT_STATUS_UNWRITTEN);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->sbi->s_warning_count),
+ warnings_before + 1);
+ assert_es_tree(test, inode, expected, ARRAY_SIZE(expected));
+}
+
+static void
+test_es_cache_extent_conflict_partial_overlap_warns(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ static const struct expected_es expected[] = {
+ { .lblk = 10, .len = 5, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ };
+ int warnings_before;
+
+ es_cache_extent(inode, 10, 5, 100, EXTENT_STATUS_WRITTEN);
+ warnings_before = atomic_read(&ctx->sbi->s_warning_count);
+
+ /*
+ * Range [8, 10) is not covered by [10, 5), so ext4_es_cache_extent()
+ * enters __es_remove_extent() which detects the status conflict and
+ * returns -EINVAL without modifying [10, 5).
+ */
+ es_cache_extent(inode, 8, 10, 500, EXTENT_STATUS_UNWRITTEN);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->sbi->s_warning_count),
+ warnings_before + 1);
+ assert_es_tree(test, inode, expected, ARRAY_SIZE(expected));
+}
+
+static void
+test_es_cache_extent_hole_over_delayed_does_not_warn(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ static const struct expected_es expected[] = {
+ { .lblk = 30, .len = 5, .pblk = 0,
+ .type = EXTENT_STATUS_DELAYED },
+ };
+ int warnings_before;
+
+ es_insert_delayed_extent(inode, 30, 5, false, false);
+ warnings_before = atomic_read(&ctx->sbi->s_warning_count);
+
+ es_cache_extent(inode, 30, 5, 0, EXTENT_STATUS_HOLE);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->sbi->s_warning_count),
+ warnings_before);
+ assert_es_tree(test, inode, expected, ARRAY_SIZE(expected));
+}
+
+/*
+ * Group 5: Range Search & Cluster Scanning
+ */
+static void
+test_es_find_extent_range_skips_nonmatching_and_finds_next(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ struct extent_status es;
+
+ es_insert_extent(inode, 10, 5, 100, EXTENT_STATUS_WRITTEN, false);
+ es_insert_delayed_extent(inode, 20, 5, false, false);
+ es_insert_extent(inode, 30, 5, 300, EXTENT_STATUS_UNWRITTEN, false);
+
+ /*
+ * Prime cache_es to [10, 5) WRITTEN, then search [10, 25] for DELAYED:
+ * cache_es is hit at lblk 10, fails matching_fn(ext4_es_is_delayed),
+ * and walks rb_next to find [20, 5) DELAYED.
+ */
+ KUNIT_ASSERT_EQ(test,
+ ext4_es_lookup_extent(inode, 10, NULL, &es, NULL), 1);
+ ext4_es_find_extent_range(inode, ext4_es_is_delayed, 10, 25, &es);
+ KUNIT_EXPECT_EQ(test, es.es_lblk, 20U);
+ KUNIT_EXPECT_EQ(test, es.es_len, 5U);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_delayed(&es));
+
+ /* Search [10, 18] for DELAYED when starting inside [10, 5) returns 0 */
+ KUNIT_ASSERT_EQ(test,
+ ext4_es_lookup_extent(inode, 10, NULL, &es, NULL), 1);
+ ext4_es_find_extent_range(inode, ext4_es_is_delayed, 10, 18, &es);
+ KUNIT_EXPECT_EQ(test, es.es_len, 0U);
+
+ /*
+ * Search [16, 18] in gap before [20, 5): __es_tree_search(16) returns
+ * the next extent [20, 5) following the range.
+ */
+ ext4_es_find_extent_range(inode, ext4_es_is_delayed, 16, 18, &es);
+ KUNIT_EXPECT_EQ(test, es.es_lblk, 20U);
+ KUNIT_EXPECT_EQ(test, es.es_len, 5U);
+}
+
+static void
+test_es_scan_range_matches_only_overlapping_extents(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+
+ es_insert_delayed_extent(inode, 20, 5, false, false);
+ es_insert_extent(inode, 30, 5, 300, EXTENT_STATUS_UNWRITTEN, false);
+
+ /* Gap [16, 18] before [20, 5) does not overlap */
+ KUNIT_EXPECT_FALSE(test,
+ ext4_es_scan_range(inode, ext4_es_is_delayed,
+ 16, 18));
+
+ /* Ranges overlapping [20, 5) or [30, 5) return true */
+ KUNIT_EXPECT_TRUE(test,
+ ext4_es_scan_range(inode, ext4_es_is_delayed,
+ 16, 20));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_es_scan_range(inode, ext4_es_is_delayed,
+ 22, 23));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_es_scan_range(inode, ext4_es_is_mapped,
+ 34, 40));
+
+ /* Range [35, 40] after [30, 5) does not overlap */
+ KUNIT_EXPECT_FALSE(test,
+ ext4_es_scan_range(inode, ext4_es_is_mapped,
+ 35, 40));
+}
+
+static void test_es_scan_clu_matches_extent_in_same_cluster(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+
+ /* Configure cluster size = 16 blocks (cluster_bits = 4) */
+ es_set_cluster_bits(ctx->sb, 4);
+
+ /* Insert delayed block at the very end of cluster 0 (lblk 15) */
+ es_insert_delayed_extent(inode, 15, 1, false, false);
+
+ KUNIT_EXPECT_TRUE(test,
+ ext4_es_scan_clu(inode, ext4_es_is_delayed, 0));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_es_scan_clu(inode, ext4_es_is_delayed, 8));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_es_scan_clu(inode, ext4_es_is_delayed, 15));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_es_scan_clu(inode, ext4_es_is_delayed, 16));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_es_scan_clu(inode, ext4_es_is_mapped, 0));
+}
+
+/*
+ * Group 6: Delayed Allocation & Bigalloc Pending Reservations
+ */
+static void
+test_es_delalloc_remove_and_overwrite_account_reserved_blocks(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ static const struct expected_es after_rem_exp[] = {
+ { .lblk = 10, .len = 2, .pblk = 0,
+ .type = EXTENT_STATUS_DELAYED },
+ { .lblk = 15, .len = 5, .pblk = 0,
+ .type = EXTENT_STATUS_DELAYED },
+ };
+ static const struct expected_es after_ins_exp[] = {
+ { .lblk = 10, .len = 6, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 16, .len = 4, .pblk = 0,
+ .type = EXTENT_STATUS_DELAYED },
+ };
+
+ /* Delayed extents are must-keep: i_es_shk_nr remains 0 */
+ es_insert_delayed_extent(inode, 10, 10, false, false);
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_all_nr, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_shk_nr, 0U);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->ei->i_es_list));
+
+ /* Removing 3 delayed blocks [12, 3) releases 3 reserved blocks */
+ reset_da_stub_stats(ctx);
+ es_remove_extent(inode, 12, 3);
+ KUNIT_EXPECT_EQ(test, ctx->da_release_calls, 1);
+ KUNIT_EXPECT_EQ(test, ctx->last_da_released, 3);
+ assert_es_tree(test, inode, after_rem_exp, ARRAY_SIZE(after_rem_exp));
+
+ /*
+ * Inserting WRITTEN [10, 6) overwrites [10, 2) delayed, [12, 3) gap,
+ * and [15, 1) delayed -> claims 3 delayed blocks of reservation.
+ */
+ reset_da_stub_stats(ctx);
+ es_insert_extent(inode, 10, 6, 100, EXTENT_STATUS_WRITTEN, true);
+ KUNIT_EXPECT_EQ(test, ctx->da_update_calls, 1);
+ KUNIT_EXPECT_EQ(test, ctx->last_da_used, 3);
+ KUNIT_EXPECT_EQ(test, ctx->last_da_quota_claim, 1);
+ assert_es_tree(test, inode, after_ins_exp, ARRAY_SIZE(after_ins_exp));
+}
+
+static void
+test_es_bigalloc_insert_delayed_creates_and_removes_pending(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+
+ /* Cluster size = 16 blocks: clu 0=[0..15], clu 1=[16..31], clu 2=[32..47] */
+ es_set_cluster_bits(ctx->sb, 4);
+
+ /* Single cluster with lclu_allocated = true */
+ es_insert_delayed_extent(inode, 2, 4, true, false);
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 0));
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 15));
+ KUNIT_EXPECT_FALSE(test, ext4_is_pending(inode, 16));
+
+ /* Multi-cluster [20, 20) spanning clusters 1 and 2, both allocated */
+ es_insert_delayed_extent(inode, 20, 20, true, true);
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 16));
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 32));
+ KUNIT_EXPECT_FALSE(test, ext4_is_pending(inode, 48));
+
+ /* Duplicate pending insertion on cluster 0 is idempotent */
+ es_insert_delayed_extent(inode, 8, 2, true, false);
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 0));
+
+ /* Explicit removal of pending reservation */
+ ext4_remove_pending(inode, 16);
+ KUNIT_EXPECT_FALSE(test, ext4_is_pending(inode, 16));
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 32));
+}
+
+static void
+test_es_bigalloc_revise_pending_single_cluster_halves(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+
+ /* Cluster size = 16 blocks */
+ es_set_cluster_bits(ctx->sb, 4);
+
+ /* Insert delayed extent [0, 32) covering clusters 0 and 1 */
+ es_insert_delayed_extent(inode, 0, 32, false, false);
+
+ /*
+ * 1. Allocate first half of cluster 0: [0, 8) -> WRITTEN.
+ * Delayed blocks [8, 8) remain after [0, 8) in cluster 0 (l_del=true):
+ * __revise_pending inserts pending on cluster 0 and claims 1 cluster.
+ */
+ reset_da_stub_stats(ctx);
+ es_insert_extent(inode, 0, 8, 100, EXTENT_STATUS_WRITTEN, true);
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 0));
+ KUNIT_EXPECT_EQ(test, ctx->da_update_calls, 1);
+ KUNIT_EXPECT_EQ(test, ctx->last_da_used, 1);
+
+ /*
+ * 2. Allocate second half of cluster 0: [8, 8) -> WRITTEN.
+ * No delayed blocks remain in cluster 0; pending reservation on
+ * cluster 0 is removed and 0 additional clusters are claimed.
+ */
+ reset_da_stub_stats(ctx);
+ es_insert_extent(inode, 8, 8, 108, EXTENT_STATUS_WRITTEN, true);
+ KUNIT_EXPECT_FALSE(test, ext4_is_pending(inode, 0));
+ KUNIT_EXPECT_EQ(test, ctx->da_update_calls, 0);
+
+ /*
+ * 3. Allocate second half of cluster 1 first: [24, 8) -> WRITTEN while
+ * [16, 8) remains delayed before it in cluster 1 (f_del=true branch in
+ * __revise_pending): inserts pending on cluster 1 and claims 1 cluster.
+ */
+ reset_da_stub_stats(ctx);
+ es_insert_extent(inode, 24, 8, 224, EXTENT_STATUS_WRITTEN, true);
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 16));
+ KUNIT_EXPECT_EQ(test, ctx->da_update_calls, 1);
+ KUNIT_EXPECT_EQ(test, ctx->last_da_used, 1);
+}
+
+static void
+test_es_bigalloc_revise_pending_multi_cluster_span(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+
+ /* Cluster size = 16 blocks */
+ es_set_cluster_bits(ctx->sb, 4);
+
+ /*
+ * Insert delayed [16, 32) across cluster 1 [16..31] and cluster 2
+ * [32..47], then allocate [20, 24) (lblks 20..43), leaving delayed
+ * [16, 4) in cluster 1 and delayed [44, 4) in cluster 2. Both clusters
+ * get pending reservations and 2 clusters are claimed.
+ */
+ es_insert_delayed_extent(inode, 16, 32, false, false);
+ reset_da_stub_stats(ctx);
+ es_insert_extent(inode, 20, 24, 220, EXTENT_STATUS_WRITTEN, true);
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 16));
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 32));
+ KUNIT_EXPECT_EQ(test, ctx->da_update_calls, 1);
+ KUNIT_EXPECT_EQ(test, ctx->last_da_used, 2);
+}
+
+static void
+test_es_bigalloc_remove_partial_cluster_holds_rsvd_until_empty(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+
+ /* Cluster size = 16 blocks */
+ es_set_cluster_bits(ctx->sb, 4);
+
+ es_insert_delayed_extent(inode, 0, 16, false, false);
+ reset_da_stub_stats(ctx);
+
+ es_remove_extent(inode, 0, 4);
+ KUNIT_EXPECT_EQ(test, ctx->da_release_calls, 0);
+
+ es_remove_extent(inode, 12, 4);
+ KUNIT_EXPECT_EQ(test, ctx->da_release_calls, 0);
+
+ es_remove_extent(inode, 4, 8);
+ KUNIT_EXPECT_EQ(test, ctx->da_release_calls, 1);
+ KUNIT_EXPECT_EQ(test, ctx->last_da_released, 1);
+}
+
+static void
+test_es_bigalloc_remove_fragmented_cluster_releases_once(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+
+ /* Cluster size = 16 blocks */
+ es_set_cluster_bits(ctx->sb, 4);
+
+ /*
+ * Two fragmented delayed extents within cluster 0 ([2, 3) and [10, 3))
+ * share a single cluster reservation and must release 1 cluster, not 2.
+ */
+ es_insert_delayed_extent(inode, 2, 3, false, false);
+ es_insert_delayed_extent(inode, 10, 3, false, false);
+ reset_da_stub_stats(ctx);
+
+ es_remove_extent(inode, 0, 16);
+ KUNIT_EXPECT_EQ(test, ctx->da_release_calls, 1);
+ KUNIT_EXPECT_EQ(test, ctx->last_da_released, 1);
+}
+
+static void
+test_es_bigalloc_remove_pending_cluster_cancels_pending_only(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+
+ /* Cluster size = 16 blocks */
+ es_set_cluster_bits(ctx->sb, 4);
+
+ /*
+ * Removing a delayed extent in a cluster with a pending reservation
+ * cancels the pending reservation and releases 0 clusters (because the
+ * cluster reservation was already claimed when allocated).
+ */
+ es_insert_delayed_extent(inode, 16, 8, true, false);
+ KUNIT_ASSERT_TRUE(test, ext4_is_pending(inode, 16));
+ reset_da_stub_stats(ctx);
+
+ es_remove_extent(inode, 16, 8);
+ KUNIT_EXPECT_EQ(test, ctx->da_release_calls, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_is_pending(inode, 16));
+}
+
+/*
+ * Group 7: Shrinker, Referenced Aging, and Cache Clearing
+ */
+static void
+test_es_clear_inode_es_retains_only_delayed_extents(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ static const struct expected_es only_delayed_exp[] = {
+ { .lblk = 40, .len = 5, .pblk = 0,
+ .type = EXTENT_STATUS_DELAYED },
+ };
+
+ es_insert_extent(inode, 10, 5, 100, EXTENT_STATUS_WRITTEN, false);
+ es_insert_extent(inode, 20, 5, 200, EXTENT_STATUS_UNWRITTEN, false);
+ es_insert_extent(inode, 30, 5, 0, EXTENT_STATUS_HOLE, false);
+ es_insert_delayed_extent(inode, 40, 5, false, false);
+ ext4_set_inode_state(inode, EXT4_STATE_EXT_PRECACHED);
+
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_all_nr, 4U);
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_shk_nr, 3U);
+
+ ext4_clear_inode_es(inode);
+
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(inode,
+ EXT4_STATE_EXT_PRECACHED));
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_shk_nr, 0U);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->ei->i_es_list));
+ assert_es_tree(test, inode, only_delayed_exp,
+ ARRAY_SIZE(only_delayed_exp));
+}
+
+static void
+test_es_shrinker_ages_referenced_and_keeps_delayed(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ struct percpu_counter *shk_cnt = &ctx->sbi->s_es_stats.es_stats_shk_cnt;
+ struct shrink_control sc = { .nr_to_scan = 0 };
+ struct extent_status es;
+ unsigned long count, shrunk;
+ static const struct expected_es after_first_scan[] = {
+ { .lblk = 10, .len = 2, .pblk = 100,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 30, .len = 2, .pblk = 300,
+ .type = EXTENT_STATUS_WRITTEN },
+ { .lblk = 50, .len = 2, .pblk = 0,
+ .type = EXTENT_STATUS_DELAYED },
+ };
+ static const struct expected_es after_second_scan[] = {
+ { .lblk = 50, .len = 2, .pblk = 0,
+ .type = EXTENT_STATUS_DELAYED },
+ };
+
+ /* Insert 4 reclaimable WRITTEN extents and 1 non-reclaimable DELAYED */
+ es_insert_extent(inode, 10, 2, 100, EXTENT_STATUS_WRITTEN, false);
+ es_insert_extent(inode, 20, 2, 200, EXTENT_STATUS_WRITTEN, false);
+ es_insert_extent(inode, 30, 2, 300, EXTENT_STATUS_WRITTEN, false);
+ es_insert_extent(inode, 40, 2, 400, EXTENT_STATUS_WRITTEN, false);
+ es_insert_delayed_extent(inode, 50, 2, false, false);
+
+ /* Mark [10, 2) and [30, 2) as EXTENT_STATUS_REFERENCED via lookup */
+ KUNIT_ASSERT_EQ(test,
+ ext4_es_lookup_extent(inode, 10, NULL, &es, NULL), 1);
+ KUNIT_ASSERT_EQ(test,
+ ext4_es_lookup_extent(inode, 30, NULL, &es, NULL), 1);
+
+ /*
+ * Fold per-CPU counter deltas into the central count so count_objects
+ * (which uses percpu_counter_read_positive) sees all 4 extents on SMP.
+ */
+ percpu_counter_set(shk_cnt, percpu_counter_sum_positive(shk_cnt));
+ count = es_shrinker_count(ctx, &sc);
+ KUNIT_EXPECT_EQ(test, count, 4UL);
+
+ /*
+ * First scan (nr_to_scan = 5):
+ * - [20, 2) and [40, 2) are unreferenced and reclaimed (shrunk = 2)
+ * - [10, 2) and [30, 2) have REFERENCED cleared and survive
+ * - [50, 2) is DELAYED (must_keep) and survives
+ */
+ sc.nr_to_scan = 5;
+ shrunk = es_shrinker_scan(ctx, &sc);
+ KUNIT_EXPECT_EQ(test, shrunk, 2UL);
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_shk_nr, 2U);
+ assert_es_tree(test, inode, after_first_scan,
+ ARRAY_SIZE(after_first_scan));
+
+ /*
+ * Second scan (nr_to_scan = 5):
+ * - [10, 2) and [30, 2) are now unreferenced and reclaimed (shrunk = 2)
+ * - Only [50, 2) DELAYED remains; inode is removed from s_es_list
+ */
+ sc.nr_to_scan = 5;
+ shrunk = es_shrinker_scan(ctx, &sc);
+ KUNIT_EXPECT_EQ(test, shrunk, 2UL);
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_shk_nr, 0U);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->ei->i_es_list));
+ assert_es_tree(test, inode, after_second_scan,
+ ARRAY_SIZE(after_second_scan));
+}
+
+static void
+test_es_shrinker_cursor_advances_and_wraps_around(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ struct shrink_control sc = {};
+ unsigned long shrunk;
+
+ es_insert_extent(inode, 10, 2, 100, EXTENT_STATUS_WRITTEN, false);
+ es_insert_extent(inode, 20, 2, 200, EXTENT_STATUS_WRITTEN, false);
+ es_insert_extent(inode, 30, 2, 300, EXTENT_STATUS_WRITTEN, false);
+
+ sc.nr_to_scan = 1;
+ shrunk = es_shrinker_scan(ctx, &sc);
+ KUNIT_EXPECT_EQ(test, shrunk, 1UL);
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_shrink_lblk, 20U);
+
+ /* Re-insert [10, 2) behind the shrink cursor; next scan wraps to it */
+ es_insert_extent(inode, 10, 2, 100, EXTENT_STATUS_WRITTEN, false);
+ sc.nr_to_scan = 3;
+ shrunk = es_shrinker_scan(ctx, &sc);
+ KUNIT_EXPECT_EQ(test, shrunk, 3UL);
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_all_nr, 0U);
+}
+
+static void
+test_es_shrinker_skips_precached_inode_until_last_resort(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode1 = &ctx->ei->vfs_inode;
+ struct inode *inode2;
+ struct shrink_control sc = {};
+ unsigned long shrunk;
+
+ inode2 = es_alloc_test_inode(test, ctx->sb, 13);
+
+ es_insert_extent(inode1, 10, 2, 100, EXTENT_STATUS_WRITTEN, false);
+ ext4_set_inode_state(inode1, EXT4_STATE_EXT_PRECACHED);
+ es_insert_extent(inode2, 10, 2, 200, EXTENT_STATUS_WRITTEN, false);
+
+ /* First scan skips precached inode1 and reclaims from inode2 */
+ sc.nr_to_scan = 1;
+ shrunk = es_shrinker_scan(ctx, &sc);
+ KUNIT_EXPECT_EQ(test, shrunk, 1UL);
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_shk_nr, 1U);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode2)->i_es_shk_nr, 0U);
+
+ /*
+ * Now only precached inode1 remains on s_es_list; shrinker retries
+ * and reclaims from inode1 as a last resort.
+ */
+ sc.nr_to_scan = 1;
+ shrunk = es_shrinker_scan(ctx, &sc);
+ KUNIT_EXPECT_EQ(test, shrunk, 1UL);
+ KUNIT_EXPECT_EQ(test, ctx->ei->i_es_shk_nr, 0U);
+}
+
+static void test_es_seq_shrinker_info_show_formats_stats(struct kunit *test)
+{
+ struct es_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei->vfs_inode;
+ struct seq_file seq = {};
+ char *buf;
+
+ es_insert_extent(inode, 10, 2, 100, EXTENT_STATUS_WRITTEN, false);
+
+ buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+ seq.buf = buf;
+ seq.size = PAGE_SIZE;
+ seq.private = ctx->sb;
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_seq_es_shrinker_info_show(&seq, SEQ_START_TOKEN),
+ 0);
+ KUNIT_EXPECT_GT(test, seq.count, 0UL);
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "reclaimable objects"));
+}
+
+static struct kunit_case extents_status_test_cases[] = {
+ KUNIT_CASE(test_es_bitfield_written_and_unwritten_encoding),
+ KUNIT_CASE(test_es_bitfield_delayed_and_hole_encoding),
+ KUNIT_CASE(test_es_bitfield_referenced_and_pblock_update_preserve_state),
+ KUNIT_CASE(test_es_lookup_empty_tree_returns_miss),
+ KUNIT_CASE(test_es_insert_zero_len_is_noop),
+ KUNIT_CASE(test_es_lookup_hits_cache_and_rbtree_with_next_lblk),
+ KUNIT_CASE(test_es_fc_replay_bypasses_tree_operations),
+ KUNIT_CASE_PARAM(test_es_merge_cases, es_merge_gen_params),
+ KUNIT_CASE(test_es_insert_adjacent_delayed_extents_merges),
+ KUNIT_CASE(test_es_merge_preserves_referenced),
+ KUNIT_CASE(test_es_merge_at_max_blocks_boundary_succeeds),
+ KUNIT_CASE_PARAM(test_es_remove_geometries, es_remove_gen_params),
+ KUNIT_CASE(test_es_remove_multi_extent_span_trims_and_deletes),
+ KUNIT_CASE(test_es_insert_overwrite_split_and_remerge),
+ KUNIT_CASE(test_es_cache_extent_inserts_and_coalesces_same_status),
+ KUNIT_CASE(test_es_cache_extent_conflict_covered_range_warns),
+ KUNIT_CASE(test_es_cache_extent_conflict_partial_overlap_warns),
+ KUNIT_CASE(test_es_cache_extent_hole_over_delayed_does_not_warn),
+ KUNIT_CASE(test_es_find_extent_range_skips_nonmatching_and_finds_next),
+ KUNIT_CASE(test_es_scan_range_matches_only_overlapping_extents),
+ KUNIT_CASE(test_es_scan_clu_matches_extent_in_same_cluster),
+ KUNIT_CASE(test_es_delalloc_remove_and_overwrite_account_reserved_blocks),
+ KUNIT_CASE(test_es_bigalloc_insert_delayed_creates_and_removes_pending),
+ KUNIT_CASE(test_es_bigalloc_revise_pending_single_cluster_halves),
+ KUNIT_CASE(test_es_bigalloc_revise_pending_multi_cluster_span),
+ KUNIT_CASE(test_es_bigalloc_remove_partial_cluster_holds_rsvd_until_empty),
+ KUNIT_CASE(test_es_bigalloc_remove_fragmented_cluster_releases_once),
+ KUNIT_CASE(test_es_bigalloc_remove_pending_cluster_cancels_pending_only),
+ KUNIT_CASE(test_es_clear_inode_es_retains_only_delayed_extents),
+ KUNIT_CASE(test_es_shrinker_ages_referenced_and_keeps_delayed),
+ KUNIT_CASE(test_es_shrinker_cursor_advances_and_wraps_around),
+ KUNIT_CASE(test_es_shrinker_skips_precached_inode_until_last_resort),
+ KUNIT_CASE(test_es_seq_shrinker_info_show_formats_stats),
+ {}
+};
+
+static struct kunit_suite extents_status_test_suite = {
+ .name = "ext4_extents_status_test",
+ .init = es_kunit_init,
+ .test_cases = extents_status_test_cases,
+};
+
+kunit_test_suites(&extents_status_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 extents status tree and pending reservations");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/extents_status.c b/fs/ext4/extents_status.c
index 6e4a191e8219..64bac0c4c1fa 100644
--- a/fs/ext4/extents_status.c
+++ b/fs/ext4/extents_status.c
@@ -2390,3 +2390,22 @@ static int __revise_pending(struct inode *inode, ext4_lblk_t lblk,
out:
return (ret < 0) ? ret : pendings;
}
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_init_tree);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_insert_extent);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_cache_extent);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_remove_extent);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_find_extent_range);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_lookup_extent);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_scan_range);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_scan_clu);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_register_shrinker);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_unregister_shrinker);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_seq_es_shrinker_info_show);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_pending_tree);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_remove_pending);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_is_pending);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_insert_delayed_extent);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_clear_inode_es);
+#endif
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 26f0f9714f03..966a760f64e6 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -350,6 +350,9 @@ void ext4_da_update_reserve_space(struct inode *inode,
struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
struct ext4_inode_info *ei = EXT4_I(inode);
+ KUNIT_STATIC_STUB_REDIRECT(ext4_da_update_reserve_space,
+ inode, used, quota_claim);
+
spin_lock(&ei->i_block_reservation_lock);
trace_ext4_da_update_reserve_space(inode, used, quota_claim);
if (unlikely(used > ei->i_reserved_data_blocks)) {
@@ -1646,6 +1649,8 @@ void ext4_da_release_space(struct inode *inode, int to_free)
struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
struct ext4_inode_info *ei = EXT4_I(inode);
+ KUNIT_STATIC_STUB_REDIRECT(ext4_da_release_space, inode, to_free);
+
if (!to_free)
return; /* Nothing to release, exit */
@@ -6973,3 +6978,8 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
sb_end_pagefault(inode->i_sb);
return ret;
}
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_update_reserve_space);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_release_space);
+#endif
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 02/27] ext4: add KUnit tests for fs/ext4/extents.c
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:06 ` 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
` (24 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Expand the ext4_extents_test KUnit test suite in fs/ext4/extents-test.c
to cover the core extent tree operations in fs/ext4/extents.c:
- 48-bit physical block encoding/decoding (ext4_ext_pblock,
ext4_ext_store_pblock, ext4_idx_pblock, ext4_idx_store_pblock) and
unwritten extent length encoding (ext4_ext_mark_unwritten,
ext4_ext_is_unwritten, ext4_ext_get_actual_len,
ext4_ext_mark_initialized).
- Extent tree initialization (ext4_ext_tree_init) and header/entry
validation (ext4_ext_check_inode, __ext4_ext_check) across 13
parameterized corruption cases (magic, depth, max, entries, empty
non-root index, zero-length extent, logical block wrap-around,
out-of-bounds physical blocks, overlapping/unsorted extents and
indexes, and parent/leaf logical block mismatch).
- Metadata checksum calculation and verification on external extent
blocks when EXT4_FEATURE_RO_COMPAT_METADATA_CSUM is enabled.
- Root and multi-level (depth >= 1) extent tree lookup and navigation
(ext4_find_extent, ext4_ext_search_left, ext4_ext_search_right,
ext4_ext_next_allocated_block, ext4_ext_next_leaf_block) and extent
status precaching (ext4_ext_precache) using in-memory simulated
metadata buffer_heads via an ext4_ext_getblk() KUnit static stub.
- Extent merge rules (ext4_can_extents_be_merged) and insertion
(ext4_ext_insert_extent), including forward/backward/three-way
merging, nomerge flag handling, tree depth growth
(ext4_ext_grow_indepth), node splitting (ext4_ext_split), and tree
collapse back into the root inode (ext4_ext_try_to_merge_up).
- Extent space removal and truncation (ext4_ext_remove_space,
ext4_ext_truncate) across tail truncates, partial trims, middle hole
punches, multi-level index removal (ext4_ext_rm_idx), and bigalloc
partial-cluster nofree and pending cluster rereservation
(ext4_rereserve_cluster).
- Block mapping and allocation (ext4_ext_map_blocks), including hole
lookup, delayed-extent hole adjustment, QUERY_LAST_IN_LEAF, block
allocation, overlap clamping, ENOSPC handling, unwritten-to-written
conversion with neighbor merging, and bigalloc implied cluster
allocation (ext4_map_cluster_from_extent).
- Extent shifting (ext4_ext_shift_extents) for FALLOC_FL_COLLAPSE_RANGE
and FALLOC_FL_INSERT_RANGE, extent swapping (ext4_swap_extents),
cluster mapping queries (ext4_clu_mapped), JBD2 transaction credit
calculations (ext4_ext_calc_credits_for_single_extent,
ext4_ext_index_trans_blocks), and fast-commit replay helpers
(ext4_ext_replay_update_ex, ext4_ext_replay_shrink_inode,
ext4_ext_replay_set_iblocks).
- Fix ext4_ext_path memory leaks in the existing test_split_convert()
helpers and isolate the mock extent status shrinker from background
reclaim.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/ext4_extents.h | 21 +
fs/ext4/ext4_jbd2.c | 8 +
fs/ext4/extents-test.c | 1886 ++++++++++++++++++++++++++++++++++++++--
fs/ext4/extents.c | 83 +-
fs/ext4/inode.c | 5 +
fs/ext4/mballoc.c | 7 +
6 files changed, 1938 insertions(+), 72 deletions(-)
diff --git a/fs/ext4/ext4_extents.h b/fs/ext4/ext4_extents.h
index ebaf7cc42430..40e6b3a56f71 100644
--- a/fs/ext4/ext4_extents.h
+++ b/fs/ext4/ext4_extents.h
@@ -268,6 +268,8 @@ extern int __ext4_ext_dirty(const char *where, unsigned int line,
handle_t *handle, struct inode *inode,
struct ext4_ext_path *path);
extern int ext4_ext_zeroout(struct inode *inode, struct ext4_extent *ex);
+extern struct buffer_head *ext4_ext_getblk(struct inode *inode,
+ ext4_fsblk_t pblk, gfp_t gfp_flags);
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
extern int ext4_ext_space_root_idx_test(struct inode *inode, int check);
extern struct ext4_ext_path *ext4_split_convert_extents_test(
@@ -275,6 +277,25 @@ extern struct ext4_ext_path *ext4_split_convert_extents_test(
struct ext4_map_blocks *map,
struct ext4_ext_path *path,
int flags, unsigned int *allocated);
+extern int ext4_ext_check_test(struct inode *inode,
+ struct ext4_extent_header *eh, int depth,
+ ext4_fsblk_t pblk, ext4_lblk_t lblk);
+extern void ext4_extent_block_csum_set_test(struct inode *inode,
+ struct ext4_extent_header *eh);
+extern int ext4_can_extents_be_merged_test(struct inode *inode,
+ struct ext4_extent *ex1,
+ struct ext4_extent *ex2);
+extern int ext4_ext_search_left_test(struct inode *inode,
+ struct ext4_ext_path *path,
+ ext4_lblk_t *logical, ext4_fsblk_t *phys);
+extern int ext4_ext_search_right_test(struct inode *inode,
+ struct ext4_ext_path *path,
+ ext4_lblk_t *logical, ext4_fsblk_t *phys,
+ struct ext4_extent *ret_ex, int flags);
+extern ext4_lblk_t ext4_ext_next_leaf_block_test(struct ext4_ext_path *path);
+extern int ext4_ext_shift_extents_test(struct inode *inode, handle_t *handle,
+ ext4_lblk_t start, ext4_lblk_t shift,
+ enum SHIFT_DIRECTION direction);
#endif
#endif /* _EXT4_EXTENTS */
diff --git a/fs/ext4/ext4_jbd2.c b/fs/ext4/ext4_jbd2.c
index 53ddedb52a6f..d6ffb6f08606 100644
--- a/fs/ext4/ext4_jbd2.c
+++ b/fs/ext4/ext4_jbd2.c
@@ -4,6 +4,7 @@
*/
#include "ext4_jbd2.h"
+#include <kunit/static_stub.h>
#include <trace/events/ext4.h>
@@ -379,6 +380,9 @@ int __ext4_handle_dirty_metadata(const char *where, unsigned int line,
{
int err = 0;
+ KUNIT_STATIC_STUB_REDIRECT(__ext4_handle_dirty_metadata, where, line,
+ handle, inode, bh);
+
might_sleep();
set_buffer_meta(bh);
@@ -431,3 +435,7 @@ int __ext4_handle_dirty_metadata(const char *where, unsigned int line,
}
return err;
}
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_handle_dirty_metadata);
+#endif
diff --git a/fs/ext4/extents-test.c b/fs/ext4/extents-test.c
index c3836ecb89f9..705648186218 100644
--- a/fs/ext4/extents-test.c
+++ b/fs/ext4/extents-test.c
@@ -3,60 +3,75 @@
* Written by Ojaswin Mujoo <ojaswin@linux.ibm.com> (IBM)
*
* These Kunit tests are designed to test the functionality of
- * extent split and conversion in ext4.
- *
- * Currently, ext4 can split extents in 2 ways:
- * 1. By splitting the extents in the extent tree and optionally converting them
- * to written or unwritten based on flags passed.
- * 2. In case 1 encounters an error, ext4 instead zerooes out the unwritten
- * areas of the extent and marks the complete extent written.
- *
- * The primary function that handles this is ext4_split_convert_extents().
- *
- * We test both of the methods of split. The behavior we try to enforce is:
- * 1. When passing EXT4_GET_BLOCKS_CONVERT flag to ext4_split_convert_extents(),
- * the split extent should be converted to initialized.
- * 2. When passing EXT4_GET_BLOCKS_CONVERT_UNWRITTEN flag to
- * ext4_split_convert_extents(), the split extent should be converted to
- * uninitialized.
- * 3. In case we use the zeroout method, then we should correctly write zeroes
- * to the unwritten areas of the extent and we should not corrupt/leak any
- * data.
- *
- * Enforcing 1 and 2 is straight forward, we just setup a minimal inode with
- * extent tree, call ext4_split_convert_extents() and check the final state of
- * the extent tree.
- *
- * For zeroout testing, we maintain a separate buffer which represents the disk
- * data corresponding to the extents. We then override ext4's zeroout functions
- * to instead write zeroes to our buffer. Then, we override
- * ext4_ext_insert_extent() to return -ENOSPC, which triggers the zeroout.
- * Finally, we check the state of the extent tree and zeroout buffer to confirm
- * everything went well.
+ * fs/ext4/extents.c, including extent tree initialization and validation,
+ * binary search and multi-level navigation, extent insertion, merging,
+ * splitting, tree depth growth and collapse, space removal and bigalloc
+ * partial cluster handling, block mapping, unwritten-to-written conversion,
+ * zeroout fallback, extent shifting, extent swapping, cluster mapping,
+ * transaction credit calculation, and fast-commit replay helpers.
*/
#include <kunit/test.h>
#include <kunit/static_stub.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
#include <linux/fs_context.h>
#include <linux/gfp_types.h>
#include <linux/stddef.h>
#include "ext4.h"
#include "ext4_extents.h"
+#include "ext4_jbd2.h"
#define EXT_DATA_PBLK 100
#define EXT_DATA_LBLK 10
#define EXT_DATA_LEN 3
+#define EXT_MAX_META_BLOCKS 16
+#define EXT_META_BASE_PBLK 500
+#define EXT_ALLOC_BASE_PBLK 1000
+#define EXT_TOTAL_BLOCKS 0x100000ULL
+
+struct kunit_meta_block {
+ ext4_fsblk_t pblk;
+ struct buffer_head bh;
+ char data[4096];
+ bool in_use;
+};
+
struct kunit_ctx {
/*
- * Ext4 inode which has only 1 unwrit extent
+ * Ext4 inode initialized with 1 extent by default
*/
struct ext4_inode_info *k_ei;
+ /*
+ * Optional secondary inode (used for swap_extents tests)
+ */
+ struct ext4_inode_info *k_ei2;
/*
* Represents the underlying data area (used for zeroout testing)
*/
char *k_data;
+ /*
+ * In-memory pool of external extent tree blocks (leaf/index blocks)
+ */
+ struct kunit_meta_block *meta_blocks;
+ /*
+ * Mock block allocator and free tracking
+ */
+ ext4_fsblk_t next_alloc_pblk;
+ unsigned int alloc_calls;
+ ext4_fsblk_t last_alloc_pblk;
+ unsigned int last_alloc_len;
+ int fail_alloc_err;
+
+ unsigned int free_calls;
+ ext4_fsblk_t last_free_pblk;
+ unsigned long last_free_count;
+ int last_free_flags;
+ unsigned long total_freed_blocks;
+
+ bool fail_getblk;
} k_ctx;
/*
@@ -126,6 +141,31 @@ struct kunit_ext_test_param {
struct kunit_ext_data_state exp_data_state[3];
};
+static unsigned long ext_dummy_shrinker_count(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return 0;
+}
+
+static unsigned long ext_dummy_shrinker_scan(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return SHRINK_STOP;
+}
+
+static const struct super_operations ext_dummy_sops = {};
+
+#ifdef CONFIG_QUOTA
+static qsize_t *ext_get_reserved_space(struct inode *inode)
+{
+ return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations ext_dummy_qops = {
+ .get_reserved_space = ext_get_reserved_space,
+};
+#endif
+
static int ext_init_fs_context(struct fs_context *fc)
{
return 0;
@@ -142,33 +182,201 @@ static struct file_system_type ext_fs_type = {
.kill_sb = kill_anon_super,
};
+static void ext_init_mock_inode(struct ext4_inode_info *ei,
+ struct super_block *sb, unsigned long ino)
+{
+ struct inode *inode = &ei->vfs_inode;
+
+ memset(ei, 0, sizeof(*ei));
+ inode->i_sb = sb;
+ inode->i_ino = ino;
+ inode->i_mode = S_IFREG | 0644;
+ inode->i_blkbits = sb->s_blocksize_bits;
+ inode->i_flags |= S_NOQUOTA;
+ inode->i_mapping = &inode->i_data;
+
+ spin_lock_init(&inode->i_lock);
+ init_rwsem(&inode->i_rwsem);
+ init_rwsem(&inode->i_data.invalidate_lock);
+ inode_state_set_raw(inode, I_DIRTY_SYNC | I_DIRTY_DATASYNC);
+
+ ext4_es_init_tree(&ei->i_es_tree);
+ ext4_init_pending_tree(&ei->i_pending_tree);
+ rwlock_init(&ei->i_es_lock);
+ INIT_LIST_HEAD(&ei->i_es_list);
+ init_rwsem(&ei->i_data_sem);
+ spin_lock_init(&ei->i_block_reservation_lock);
+ rwlock_init(&ei->i_prealloc_lock);
+ ei->i_prealloc_node = RB_ROOT;
+ ei->i_csum_seed = 0x12345678;
+
+ ei->i_disksize = (EXT_DATA_LBLK + EXT_DATA_LEN + 10)
+ << sb->s_blocksize_bits;
+ inode->i_size = ei->i_disksize;
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+}
+
+static void ext_cleanup_inode(struct inode *inode)
+{
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+ struct rb_node *node;
+
+ down_write(&ei->i_data_sem);
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ up_write(&ei->i_data_sem);
+
+ while ((node = rb_first(&ei->i_pending_tree.root)) != NULL) {
+ struct pending_reservation *pr;
+
+ pr = rb_entry(node, struct pending_reservation, rb_node);
+ ext4_remove_pending(inode, EXT4_C2B(sbi, pr->lclu));
+ }
+}
+
static void extents_kunit_exit(struct kunit *test)
{
struct ext4_sb_info *sbi;
+ struct super_block *sb;
if (!k_ctx.k_ei)
return;
- sbi = k_ctx.k_ei->vfs_inode.i_sb->s_fs_info;
+ sb = k_ctx.k_ei->vfs_inode.i_sb;
+ sbi = sb->s_fs_info;
+
+ if (k_ctx.k_ei2) {
+ ext_cleanup_inode(&k_ctx.k_ei2->vfs_inode);
+ kfree(k_ctx.k_ei2);
+ }
+ ext_cleanup_inode(&k_ctx.k_ei->vfs_inode);
+
+ percpu_counter_destroy(&sbi->s_freeclusters_counter);
+ percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
ext4_es_unregister_shrinker(sbi);
- deactivate_super(sbi->s_sb);
+ deactivate_super(sb);
+
+ kfree(k_ctx.meta_blocks);
+ kfree(sbi->s_es);
+ kfree(sb->s_bdev->bd_mapping);
+ kfree(sb->s_bdev);
kfree(sbi);
kfree(k_ctx.k_ei);
kfree(k_ctx.k_data);
+ memset(&k_ctx, 0, sizeof(k_ctx));
}
static int __ext4_ext_dirty_stub(const char *where, unsigned int line,
handle_t *handle, struct inode *inode,
struct ext4_ext_path *path)
{
+ if (path && path->p_bh) {
+ ext4_extent_block_csum_set_test(inode,
+ ext_block_hdr(path->p_bh));
+ set_buffer_verified(path->p_bh);
+ }
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)
+{
+ if (bh)
+ set_buffer_uptodate(bh);
+ return 0;
+}
+
+static int __ext4_mark_inode_dirty_stub(handle_t *handle, struct inode *inode,
+ const char *func, unsigned int line)
+{
+ return 0;
+}
+
+static struct buffer_head *ext4_ext_getblk_stub(struct inode *inode,
+ ext4_fsblk_t pblk,
+ gfp_t gfp_flags)
+{
+ struct kunit_meta_block *slot = NULL;
+ int i;
+
+ if (k_ctx.fail_getblk || !k_ctx.meta_blocks)
+ return NULL;
+
+ for (i = 0; i < EXT_MAX_META_BLOCKS; i++) {
+ if (k_ctx.meta_blocks[i].in_use &&
+ k_ctx.meta_blocks[i].pblk == pblk) {
+ slot = &k_ctx.meta_blocks[i];
+ break;
+ }
+ }
+
+ if (!slot) {
+ for (i = 0; i < EXT_MAX_META_BLOCKS; i++) {
+ if (!k_ctx.meta_blocks[i].in_use) {
+ slot = &k_ctx.meta_blocks[i];
+ break;
+ }
+ }
+ if (!slot)
+ return NULL;
+
+ memset(slot, 0, sizeof(*slot));
+ slot->in_use = true;
+ slot->pblk = pblk;
+ slot->bh.b_data = slot->data;
+ slot->bh.b_size = inode->i_sb->s_blocksize;
+ slot->bh.b_blocknr = pblk;
+ slot->bh.b_bdev = inode->i_sb->s_bdev;
+ atomic_set(&slot->bh.b_count, 1);
+ set_buffer_uptodate(&slot->bh);
+ set_buffer_mapped(&slot->bh);
+ }
+
+ get_bh(&slot->bh);
+ return &slot->bh;
+}
+
+static ext4_fsblk_t ext4_mb_new_blocks_stub(handle_t *handle,
+ struct ext4_allocation_request *ar,
+ int *errp)
+{
+ struct ext4_sb_info *sbi = EXT4_SB(ar->inode->i_sb);
+ ext4_fsblk_t pblk;
+
+ k_ctx.alloc_calls++;
+ if (k_ctx.fail_alloc_err) {
+ *errp = k_ctx.fail_alloc_err;
+ return 0;
+ }
+
+ pblk = k_ctx.next_alloc_pblk;
+ k_ctx.next_alloc_pblk += EXT4_C2B(sbi, ar->len);
+ k_ctx.last_alloc_pblk = pblk;
+ k_ctx.last_alloc_len = ar->len;
+ *errp = 0;
+ return pblk;
+}
+
+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)
+{
+ k_ctx.free_calls++;
+ k_ctx.last_free_pblk = block;
+ k_ctx.last_free_count = count;
+ k_ctx.last_free_flags = flags;
+ k_ctx.total_freed_blocks += count;
+}
+
static struct ext4_ext_path *
ext4_ext_insert_extent_stub(handle_t *handle, struct inode *inode,
struct ext4_ext_path *path,
struct ext4_extent *newext, int gb_flags)
{
+ ext4_free_ext_path(path);
return ERR_PTR(-ENOSPC);
}
@@ -227,10 +435,11 @@ static int extents_kunit_init(struct kunit *test)
struct super_block *sb;
struct fs_context *fc;
struct ext4_sb_info *sbi = NULL;
- struct kunit_ext_test_param *param =
- (struct kunit_ext_test_param *)(test->param_value);
int err;
+ memset(&k_ctx, 0, sizeof(k_ctx));
+ k_ctx.next_alloc_pblk = EXT_ALLOC_BASE_PBLK;
+
sbi = kzalloc_obj(struct ext4_sb_info);
if (sbi == NULL)
return -ENOMEM;
@@ -249,16 +458,76 @@ static int extents_kunit_init(struct kunit *test)
sbi->s_sb = sb;
sb->s_fs_info = sbi;
-
+ sb->s_op = &ext_dummy_sops;
+#ifdef CONFIG_QUOTA
+ sb->dq_op = &ext_dummy_qops;
+#endif
sb->s_blocksize = 4096;
sb->s_blocksize_bits = 12;
+ sb->s_maxbytes = MAX_LFS_FILESIZE;
+ strscpy(sb->s_id, "extents_kunit", sizeof(sb->s_id));
+
+ sb->s_bdev = kzalloc_obj(struct block_device);
+ if (!sb->s_bdev) {
+ err = -ENOMEM;
+ goto out_deactivate;
+ }
+ bdev_set_flag(sb->s_bdev, BD_READ_ONLY);
+ sb->s_bdev->bd_mapping = kzalloc_obj(struct address_space);
+ if (!sb->s_bdev->bd_mapping) {
+ err = -ENOMEM;
+ goto out_bdev;
+ }
- if (!param || !param->disable_zeroout)
- sbi->s_extent_max_zeroout_kb = 32;
+ sbi->s_es = kzalloc_obj(struct ext4_super_block);
+ if (!sbi->s_es) {
+ err = -ENOMEM;
+ goto out_bdev_mapping;
+ }
+ sbi->s_es->s_first_data_block = cpu_to_le32(1);
+ sbi->s_es->s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ ext4_blocks_count_set(sbi->s_es, EXT_TOTAL_BLOCKS);
+ set_opt(sb, ERRORS_CONT);
+ sbi->s_blocks_per_group = 32768;
+ sbi->s_clusters_per_group = 32768;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_cluster_bits = 0;
+ sbi->s_desc_per_block = 64;
+ sbi->s_groups_count = 32;
+ sbi->s_extent_max_zeroout_kb = 32;
+ spin_lock_init(&sbi->s_bdev_wb_lock);
+ spin_lock_init(&sbi->s_error_lock);
+ spin_lock_init(&sbi->s_md_lock);
+ ratelimit_state_init(&sbi->s_warning_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL,
+ DEFAULT_RATELIMIT_BURST);
+
+ err = percpu_counter_init(&sbi->s_freeclusters_counter, 100000,
+ GFP_KERNEL);
+ if (err)
+ goto out_es;
+ err = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0,
+ GFP_KERNEL);
+ if (err)
+ goto out_freeclusters;
err = ext4_es_register_shrinker(sbi);
if (err)
- goto out_deactivate;
+ goto out_dirtyclusters;
+
+ /*
+ * Isolate the mock shrinker from asynchronous background reclaim.
+ */
+ sbi->s_es_shrinker->count_objects = ext_dummy_shrinker_count;
+ sbi->s_es_shrinker->scan_objects = ext_dummy_shrinker_scan;
+
+ k_ctx.meta_blocks = kcalloc(EXT_MAX_META_BLOCKS,
+ sizeof(struct kunit_meta_block),
+ GFP_KERNEL);
+ if (!k_ctx.meta_blocks) {
+ err = -ENOMEM;
+ goto out;
+ }
/* setup the mock inode */
k_ctx.k_ei = kzalloc_obj(struct ext4_inode_info);
@@ -268,19 +537,7 @@ static int extents_kunit_init(struct kunit *test)
}
ei = k_ctx.k_ei;
inode = &ei->vfs_inode;
-
- ext4_es_init_tree(&ei->i_es_tree);
- rwlock_init(&ei->i_es_lock);
- INIT_LIST_HEAD(&ei->i_es_list);
- ei->i_es_all_nr = 0;
- ei->i_es_shk_nr = 0;
- ei->i_es_shrink_lblk = 0;
-
- ei->i_disksize = (EXT_DATA_LBLK + EXT_DATA_LEN + 10)
- << sb->s_blocksize_bits;
- ei->i_flags = 0;
- ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
- inode->i_sb = sb;
+ ext_init_mock_inode(ei, sb, 12);
k_ctx.k_data = kzalloc(EXT_DATA_LEN * 4096, GFP_KERNEL);
if (k_ctx.k_data == NULL) {
@@ -293,9 +550,6 @@ static int extents_kunit_init(struct kunit *test)
*/
memset(k_ctx.k_data, 'X', EXT_DATA_LEN * 4096);
- /* create a tree with depth 0 */
- eh = (struct ext4_extent_header *)k_ctx.k_ei->i_data;
-
/* Fill extent header */
eh = ext_inode_hdr(&k_ctx.k_ei->vfs_inode);
eh->eh_depth = 0;
@@ -307,25 +561,34 @@ static int extents_kunit_init(struct kunit *test)
/*
* add 1 extent in leaf node covering:
- * - lblks: [EXT_DATA_LBLK, EXT_DATA_LBLK * + EXT_DATA_LEN)
+ * - lblks: [EXT_DATA_LBLK, EXT_DATA_LBLK + EXT_DATA_LEN)
* - pblks: [EXT_DATA_PBLK, EXT_DATA_PBLK + EXT_DATA_LEN)
*/
EXT_FIRST_EXTENT(eh)->ee_block = cpu_to_le32(EXT_DATA_LBLK);
EXT_FIRST_EXTENT(eh)->ee_len = cpu_to_le16(EXT_DATA_LEN);
ext4_ext_store_pblock(EXT_FIRST_EXTENT(eh), EXT_DATA_PBLK);
- if (!param || param->is_unwrit_at_start)
- ext4_ext_mark_unwritten(EXT_FIRST_EXTENT(eh));
+ ext4_ext_mark_unwritten(EXT_FIRST_EXTENT(eh));
+ down_write(&ei->i_data_sem);
ext4_es_insert_extent(inode, EXT_DATA_LBLK, EXT_DATA_LEN, EXT_DATA_PBLK,
- ext4_ext_is_unwritten(EXT_FIRST_EXTENT(eh)) ?
- EXTENT_STATUS_UNWRITTEN :
- EXTENT_STATUS_WRITTEN,
- 0);
+ EXTENT_STATUS_UNWRITTEN, 0);
+ up_write(&ei->i_data_sem);
/* Add stubs */
kunit_activate_static_stub(test, __ext4_ext_dirty,
__ext4_ext_dirty_stub);
- kunit_activate_static_stub(test, ext4_ext_zeroout, ext4_ext_zeroout_stub);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ __ext4_handle_dirty_metadata_stub);
+ kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+ __ext4_mark_inode_dirty_stub);
+ kunit_activate_static_stub(test, ext4_ext_getblk,
+ ext4_ext_getblk_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_ext_zeroout,
+ ext4_ext_zeroout_stub);
kunit_activate_static_stub(test, ext4_issue_zeroout,
ext4_issue_zeroout_stub);
up_write(&sb->s_umount);
@@ -333,13 +596,23 @@ static int extents_kunit_init(struct kunit *test)
return 0;
out:
- kfree(k_ctx.k_ei);
- k_ctx.k_ei = NULL;
-
kfree(k_ctx.k_data);
k_ctx.k_data = NULL;
-
+ kfree(k_ctx.k_ei);
+ k_ctx.k_ei = NULL;
+ kfree(k_ctx.meta_blocks);
+ k_ctx.meta_blocks = NULL;
ext4_es_unregister_shrinker(sbi);
+out_dirtyclusters:
+ percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
+out_freeclusters:
+ percpu_counter_destroy(&sbi->s_freeclusters_counter);
+out_es:
+ kfree(sbi->s_es);
+out_bdev_mapping:
+ kfree(sb->s_bdev->bd_mapping);
+out_bdev:
+ kfree(sb->s_bdev);
out_deactivate:
deactivate_locked_super(sb);
kfree(sbi);
@@ -392,12 +665,24 @@ static void test_split_convert(struct kunit *test)
{
struct ext4_ext_path *path;
struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
struct ext4_extent *ex;
struct ext4_map_blocks map;
const struct kunit_ext_test_param *param =
(const struct kunit_ext_test_param *)(test->param_value);
int blkbits = inode->i_sb->s_blocksize_bits;
+ if (param->disable_zeroout)
+ EXT4_SB(inode->i_sb)->s_extent_max_zeroout_kb = 0;
+
+ if (!param->is_unwrit_at_start) {
+ ext4_ext_mark_initialized(EXT_FIRST_EXTENT(eh));
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ext4_es_insert_extent(inode, EXT_DATA_LBLK, EXT_DATA_LEN,
+ EXT_DATA_PBLK, EXTENT_STATUS_WRITTEN, 0);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ }
+
if (param->is_zeroout_test)
/*
* Force zeroout by making ext4_ext_insert_extent return ENOSPC
@@ -406,6 +691,7 @@ static void test_split_convert(struct kunit *test)
ext4_ext_insert_extent_stub);
path = ext4_find_extent(inode, EXT_DATA_LBLK, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
ex = path->p_ext;
KUNIT_EXPECT_EQ(test, EXT_DATA_LBLK, le32_to_cpu(ex->ee_block));
KUNIT_EXPECT_EQ(test, EXT_DATA_LEN, ext4_ext_get_actual_len(ex));
@@ -423,15 +709,21 @@ static void test_split_convert(struct kunit *test)
case TEST_SPLIT_CONVERT:
path = ext4_split_convert_extents_test(NULL, inode, &map,
path, param->split_flags, NULL);
+ if (!IS_ERR(path))
+ ext4_free_ext_path(path);
break;
case TEST_CREATE_BLOCKS:
+ ext4_free_ext_path(path);
ext4_map_create_blocks_helper(test, inode, &map, param->split_flags);
break;
default:
+ ext4_free_ext_path(path);
KUNIT_FAIL(test, "param->type %d not support.", param->type);
+ return;
}
path = ext4_find_extent(inode, EXT_DATA_LBLK, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
ex = path->p_ext;
for (int i = 0; i < param->nr_exp_ext; i++) {
@@ -494,6 +786,8 @@ static void test_split_convert(struct kunit *test)
ex = ex + 1;
}
+ ext4_free_ext_path(path);
+
if (!param->is_zeroout_test)
return;
@@ -512,8 +806,1439 @@ static void test_split_convert(struct kunit *test)
"# corruption in byte range [%lld, %lld)",
off, len);
}
+}
+
+/*
+ * Helper to reset an inode's extent tree and extent status cache to empty.
+ */
+static void ext_reset_tree(struct inode *inode)
+{
+ struct ext4_inode_info *ei = EXT4_I(inode);
+
+ down_write(&ei->i_data_sem);
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ memset(ei->i_data, 0, sizeof(ei->i_data));
+ ext4_ext_tree_init(NULL, inode);
+ up_write(&ei->i_data_sem);
+}
+
+/*
+ * Helper to insert an extent into @inode's extent tree under i_data_sem.
+ */
+static int ext_insert_one(struct inode *inode, ext4_lblk_t lblk,
+ unsigned short len, ext4_fsblk_t pblk,
+ bool unwritten, int gb_flags)
+{
+ struct ext4_ext_path *path;
+ struct ext4_extent newex = {};
+
+ newex.ee_block = cpu_to_le32(lblk);
+ newex.ee_len = cpu_to_le16(len);
+ ext4_ext_store_pblock(&newex, pblk);
+ if (unwritten)
+ ext4_ext_mark_unwritten(&newex);
+
+ down_write(&EXT4_I(inode)->i_data_sem);
+ path = ext4_find_extent(inode, lblk, NULL, EXT4_EX_NOCACHE);
+ if (IS_ERR(path)) {
+ up_write(&EXT4_I(inode)->i_data_sem);
+ return PTR_ERR(path);
+ }
+ path = ext4_ext_insert_extent(NULL, inode, path, &newex,
+ gb_flags | EXT4_EX_NOCACHE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ if (IS_ERR(path))
+ return PTR_ERR(path);
+
+ ext4_free_ext_path(path);
+ return 0;
+}
+
+/*
+ * Verify 48-bit physical block number store and load helpers for both
+ * leaf extents (ext4_extent) and index entries (ext4_extent_idx).
+ */
+static void test_ext_pblock_48bit(struct kunit *test)
+{
+ struct ext4_extent ex = {};
+ struct ext4_extent_idx idx = {};
+ const ext4_fsblk_t pblk_48 = 0x123456789abcULL;
+ const ext4_fsblk_t pblk_max = 0xffffffffffffULL;
+
+ ext4_ext_store_pblock(&ex, 0);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex), 0ULL);
+
+ ext4_ext_store_pblock(&ex, 0xffffffffULL);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex), 0xffffffffULL);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ex.ee_start_hi), 0);
+
+ ext4_ext_store_pblock(&ex, pblk_48);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex.ee_start_lo), 0x56789abcU);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ex.ee_start_hi), 0x1234U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex), pblk_48);
+
+ ext4_ext_store_pblock(&ex, pblk_max);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex), pblk_max);
+
+ ext4_idx_store_pblock(&idx, pblk_48);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(idx.ei_leaf_lo), 0x56789abcU);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(idx.ei_leaf_hi), 0x1234U);
+ KUNIT_EXPECT_EQ(test, ext4_idx_pblock(&idx), pblk_48);
+
+ ext4_idx_store_pblock(&idx, pblk_max);
+ KUNIT_EXPECT_EQ(test, ext4_idx_pblock(&idx), pblk_max);
+}
+
+/*
+ * Verify extent length encoding and unwritten bit manipulation at boundaries:
+ * - EXT_INIT_MAX_LEN (32768 = 0x8000) is the maximum initialized extent length
+ * and must NOT be treated as unwritten.
+ * - EXT_UNWRITTEN_MAX_LEN (32767 = 0x7fff) is the maximum unwritten extent
+ * length (stored on disk as 0xffff).
+ */
+static void test_ext_unwritten_len_encoding(struct kunit *test)
+{
+ struct ext4_extent ex = {};
+
+ ex.ee_len = cpu_to_le16(1);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 0);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex), 1);
+
+ ext4_ext_mark_unwritten(&ex);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 1);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex), 1);
+
+ ext4_ext_mark_initialized(&ex);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 0);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex), 1);
+
+ ex.ee_len = cpu_to_le16(EXT_UNWRITTEN_MAX_LEN);
+ ext4_ext_mark_unwritten(&ex);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 1);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex),
+ EXT_UNWRITTEN_MAX_LEN);
+
+ ext4_ext_mark_initialized(&ex);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 0);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex),
+ EXT_UNWRITTEN_MAX_LEN);
+
+ ex.ee_len = cpu_to_le16(EXT_INIT_MAX_LEN);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 0);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex), EXT_INIT_MAX_LEN);
+}
+
+/*
+ * Verify ext4_ext_tree_init() initializes the in-inode extent header and
+ * ext4_ext_check_inode() accepts valid empty and populated root headers.
+ */
+static void test_ext_tree_init_and_check_inode(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+
+ memset(k_ctx.k_ei->i_data, 0xff, sizeof(k_ctx.k_ei->i_data));
+ ext4_ext_tree_init(NULL, inode);
+
+ KUNIT_EXPECT_EQ(test, eh->eh_magic, EXT4_EXT_MAGIC);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_depth), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_max), 4);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(eh->eh_generation), 0U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_check_inode(inode), 0);
+
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 5, 100, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, ext4_ext_check_inode(inode), 0);
+}
+
+enum ext_check_corrupt_type {
+ CORRUPT_BAD_MAGIC,
+ CORRUPT_BAD_DEPTH,
+ CORRUPT_ZERO_MAX,
+ CORRUPT_EXCESS_MAX,
+ CORRUPT_ENTRIES_GT_MAX,
+ CORRUPT_EMPTY_NONROOT_INDEX,
+ CORRUPT_ZERO_LEN_EXTENT,
+ CORRUPT_LBLK_OVERFLOW,
+ CORRUPT_PBLK_TOO_LOW,
+ CORRUPT_PBLK_TOO_HIGH,
+ CORRUPT_UNSORTED_EXTENTS,
+ CORRUPT_UNSORTED_INDEXES,
+ CORRUPT_NONROOT_LBLK_MISMATCH,
+};
+
+struct ext_check_corrupt_param {
+ const char *desc;
+ enum ext_check_corrupt_type type;
+};
+
+static const struct ext_check_corrupt_param ext_check_corrupt_params[] = {
+ { "invalid eh_magic", CORRUPT_BAD_MAGIC },
+ { "mismatched eh_depth", CORRUPT_BAD_DEPTH },
+ { "eh_max == 0", CORRUPT_ZERO_MAX },
+ { "eh_max exceeds root capacity", CORRUPT_EXCESS_MAX },
+ { "eh_entries exceeds eh_max", CORRUPT_ENTRIES_GT_MAX },
+ { "empty index block at depth > 0", CORRUPT_EMPTY_NONROOT_INDEX },
+ { "extent with ee_len == 0", CORRUPT_ZERO_LEN_EXTENT },
+ { "extent logical block 32-bit wrap-around", CORRUPT_LBLK_OVERFLOW },
+ { "extent physical block <= first_data_block", CORRUPT_PBLK_TOO_LOW },
+ { "extent physical block past blocks_count", CORRUPT_PBLK_TOO_HIGH },
+ { "overlapping/unsorted leaf extents", CORRUPT_UNSORTED_EXTENTS },
+ { "unsorted index entries", CORRUPT_UNSORTED_INDEXES },
+ { "non-root leaf first ee_block != parent ei_block",
+ CORRUPT_NONROOT_LBLK_MISMATCH },
+};
+
+static void ext_check_corrupt_get_desc(struct kunit *test, const void *p,
+ char *desc)
+{
+ const struct ext_check_corrupt_param *param = p;
+
+ strscpy(desc, param->desc, KUNIT_PARAM_DESC_SIZE);
+}
+
+static int ext_check_corrupt_param_init(struct kunit *test)
+{
+ kunit_register_params_array(test, ext_check_corrupt_params,
+ ARRAY_SIZE(ext_check_corrupt_params),
+ ext_check_corrupt_get_desc);
+ return 0;
+}
+
+/*
+ * Verify __ext4_ext_check() detects and rejects all classes of corrupt
+ * extent headers, leaf extents, and index entries with -EFSCORRUPTED.
+ */
+static void test_ext_check_corruptions(struct kunit *test)
+{
+ const struct ext_check_corrupt_param *param = test->param_value;
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ struct ext4_extent *ex;
+ struct ext4_extent_idx *ix;
+ struct {
+ struct ext4_extent_header eh;
+ struct ext4_extent ex;
+ } nonroot_leaf;
+ int depth = 0;
+ ext4_fsblk_t pblk = 0;
+ ext4_lblk_t lblk = 0;
+ int err;
+
+ ext_reset_tree(inode);
+ eh->eh_entries = cpu_to_le16(1);
+ ex = EXT_FIRST_EXTENT(eh);
+ ex->ee_block = cpu_to_le32(10);
+ ex->ee_len = cpu_to_le16(4);
+ ext4_ext_store_pblock(ex, 100);
+
+ switch (param->type) {
+ case CORRUPT_BAD_MAGIC:
+ eh->eh_magic = cpu_to_le16(0xdead);
+ break;
+ case CORRUPT_BAD_DEPTH:
+ eh->eh_depth = cpu_to_le16(1);
+ depth = 0;
+ break;
+ case CORRUPT_ZERO_MAX:
+ eh->eh_max = 0;
+ break;
+ case CORRUPT_EXCESS_MAX:
+ eh->eh_max = cpu_to_le16(10);
+ break;
+ case CORRUPT_ENTRIES_GT_MAX:
+ eh->eh_entries = cpu_to_le16(5);
+ break;
+ case CORRUPT_EMPTY_NONROOT_INDEX:
+ eh->eh_depth = cpu_to_le16(1);
+ eh->eh_entries = 0;
+ depth = 1;
+ break;
+ case CORRUPT_ZERO_LEN_EXTENT:
+ ex->ee_len = 0;
+ break;
+ case CORRUPT_LBLK_OVERFLOW:
+ ex->ee_block = cpu_to_le32(0xfffffff0U);
+ ex->ee_len = cpu_to_le16(32);
+ break;
+ case CORRUPT_PBLK_TOO_LOW:
+ ext4_ext_store_pblock(ex, 1);
+ break;
+ case CORRUPT_PBLK_TOO_HIGH:
+ ext4_ext_store_pblock(ex, EXT_TOTAL_BLOCKS);
+ break;
+ case CORRUPT_UNSORTED_EXTENTS:
+ eh->eh_entries = cpu_to_le16(2);
+ ex[1].ee_block = cpu_to_le32(12);
+ ex[1].ee_len = cpu_to_le16(4);
+ ext4_ext_store_pblock(&ex[1], 200);
+ break;
+ case CORRUPT_UNSORTED_INDEXES:
+ eh->eh_depth = cpu_to_le16(1);
+ eh->eh_entries = cpu_to_le16(2);
+ depth = 1;
+ ix = EXT_FIRST_INDEX(eh);
+ ix[0].ei_block = cpu_to_le32(20);
+ ext4_idx_store_pblock(&ix[0], 500);
+ ix[1].ei_block = cpu_to_le32(10);
+ ext4_idx_store_pblock(&ix[1], 501);
+ break;
+ case CORRUPT_NONROOT_LBLK_MISMATCH:
+ eh->eh_depth = cpu_to_le16(1);
+ memset(&nonroot_leaf, 0, sizeof(nonroot_leaf));
+ nonroot_leaf.eh.eh_magic = EXT4_EXT_MAGIC;
+ nonroot_leaf.eh.eh_depth = 0;
+ nonroot_leaf.eh.eh_entries = cpu_to_le16(1);
+ nonroot_leaf.eh.eh_max = cpu_to_le16(340);
+ nonroot_leaf.ex.ee_block = cpu_to_le32(10);
+ nonroot_leaf.ex.ee_len = cpu_to_le16(4);
+ ext4_ext_store_pblock(&nonroot_leaf.ex, 100);
+ eh = &nonroot_leaf.eh;
+ depth = 0;
+ pblk = 500;
+ lblk = 20;
+ break;
+ }
+
+ /* Suppress __ext4_error_inode() log spam during expected failure */
+ set_bit(EXT4_FLAGS_SHUTDOWN, &sbi->s_ext4_flags);
+ err = ext4_ext_check_test(inode, eh, depth, pblk, lblk);
+ clear_bit(EXT4_FLAGS_SHUTDOWN, &sbi->s_ext4_flags);
+
+ KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+}
+
+/*
+ * Verify metadata checksum calculation and verification on external extent
+ * tree blocks when EXT4_FEATURE_RO_COMPAT_METADATA_CSUM is enabled.
+ */
+static void test_ext_block_csum_verify(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+ struct buffer_head *bh;
+ struct ext4_extent_header *eh;
+ struct ext4_extent *ex;
+ struct ext4_extent_tail *et;
+ int max_entries;
+ int err;
+
+ ext_reset_tree(inode);
+ ext_inode_hdr(inode)->eh_depth = cpu_to_le16(1);
+ ext4_set_feature_metadata_csum(inode->i_sb);
+ bh = ext4_ext_getblk_stub(inode, EXT_META_BASE_PBLK, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+
+ max_entries = (inode->i_sb->s_blocksize -
+ sizeof(struct ext4_extent_header) -
+ sizeof(struct ext4_extent_tail)) /
+ sizeof(struct ext4_extent);
+
+ eh = ext_block_hdr(bh);
+ eh->eh_magic = EXT4_EXT_MAGIC;
+ eh->eh_depth = 0;
+ eh->eh_entries = cpu_to_le16(1);
+ eh->eh_max = cpu_to_le16(max_entries);
+
+ ex = EXT_FIRST_EXTENT(eh);
+ ex->ee_block = cpu_to_le32(10);
+ ex->ee_len = cpu_to_le16(5);
+ ext4_ext_store_pblock(ex, 100);
+
+ ext4_extent_block_csum_set_test(inode, eh);
+ err = ext4_ext_check_test(inode, eh, 0, EXT_META_BASE_PBLK, 10);
+ KUNIT_EXPECT_EQ(test, err, 0);
+
+ /* Flip a bit in the tail checksum and verify -EFSBADCRC */
+ et = find_ext4_extent_tail(eh);
+ et->et_checksum ^= cpu_to_le32(0x1);
+
+ set_bit(EXT4_FLAGS_SHUTDOWN, &sbi->s_ext4_flags);
+ err = ext4_ext_check_test(inode, eh, 0, EXT_META_BASE_PBLK, 10);
+ clear_bit(EXT4_FLAGS_SHUTDOWN, &sbi->s_ext4_flags);
+
+ KUNIT_EXPECT_EQ(test, err, -EFSBADCRC);
+ brelse(bh);
+}
+
+/*
+ * Verify ext4_find_extent() binary search on an empty tree and a multi-extent
+ * root leaf, as well as extent status caching vs EXT4_EX_NOCACHE.
+ */
+static void test_find_extent_root_binsearch(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_ext_path *path;
+ struct extent_status es;
+
+ ext_reset_tree(inode);
+
+ /* Empty tree returns NULL p_ext */
+ path = ext4_find_extent(inode, 10, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_PTR_EQ(test, path[0].p_ext, NULL);
+ ext4_free_ext_path(path);
+
+ /* Populate 3 separated extents: [10, 15), [20, 25), [30, 35) */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 5, 100, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 20, 5, 200, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 30, 5, 300, false, 0), 0);
+
+ /* Lookup before first extent (lblk 5) -> points to first extent */
+ path = ext4_find_extent(inode, 5, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_ASSERT_NOT_NULL(test, path[0].p_ext);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[0].p_ext->ee_block), 10U);
+ ext4_free_ext_path(path);
+
+ /* Lookup inside first extent (lblk 12) */
+ path = ext4_find_extent(inode, 12, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[0].p_ext->ee_block), 10U);
+ ext4_free_ext_path(path);
+
+ /* Lookup in hole between 1st and 2nd extent (lblk 17) -> 1st extent */
+ path = ext4_find_extent(inode, 17, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[0].p_ext->ee_block), 10U);
+ ext4_free_ext_path(path);
+
+ /* Lookup exact start of 2nd extent (lblk 20) */
+ path = ext4_find_extent(inode, 20, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[0].p_ext->ee_block), 20U);
+ ext4_free_ext_path(path);
+
+ /* Lookup past last extent (lblk 50) -> 3rd extent */
+ path = ext4_find_extent(inode, 50, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[0].p_ext->ee_block), 30U);
+ ext4_free_ext_path(path);
+
+ /* EXT4_EX_NOCACHE left i_es_tree empty */
+ KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 10, NULL, &es, NULL),
+ 0);
+
+ /* Lookup without EXT4_EX_NOCACHE populates extents and holes in es */
+ down_read(&EXT4_I(inode)->i_data_sem);
+ path = ext4_find_extent(inode, 20, NULL, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ ext4_free_ext_path(path);
+
+ /*
+ * Verify inter-extent hole [15, 20) and extents are cached in es
+ * (ext4_cache_extents only caches holes where prev != 0).
+ */
+ KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 5, NULL, &es, NULL),
+ 0);
+ KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 17, NULL, &es, NULL),
+ 1);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_hole(&es));
+ KUNIT_EXPECT_EQ(test, es.es_lblk, 15U);
+ KUNIT_EXPECT_EQ(test, es.es_len, 5U);
+
+ KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 22, NULL, &es, NULL),
+ 1);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_written(&es));
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), 200ULL);
+}
+
+/*
+ * Helper to construct a depth-1 tree with two external leaf blocks:
+ * Leaf 0 (pblk 500): [10, 15) -> 1000 (written), [20, 25) -> 1100 (written)
+ * Leaf 1 (pblk 501): [40, 45) -> 2000 (unwritten), [50, 55) -> 2100 (written)
+ */
+static void setup_depth1_two_leaf_tree(struct kunit *test, struct inode *inode)
+{
+ struct ext4_extent_header *root_eh = ext_inode_hdr(inode);
+ struct ext4_extent_idx *idx;
+ struct buffer_head *bh0, *bh1;
+ struct ext4_extent_header *eh0, *eh1;
+ struct ext4_extent *ex;
+ int max_leaf = (inode->i_sb->s_blocksize -
+ sizeof(struct ext4_extent_header)) /
+ sizeof(struct ext4_extent);
+
+ ext_reset_tree(inode);
+
+ root_eh->eh_depth = cpu_to_le16(1);
+ root_eh->eh_entries = cpu_to_le16(2);
+ root_eh->eh_max = cpu_to_le16(ext4_ext_space_root_idx_test(inode, 0));
+
+ idx = EXT_FIRST_INDEX(root_eh);
+ idx[0].ei_block = cpu_to_le32(10);
+ ext4_idx_store_pblock(&idx[0], EXT_META_BASE_PBLK);
+ idx[1].ei_block = cpu_to_le32(40);
+ ext4_idx_store_pblock(&idx[1], EXT_META_BASE_PBLK + 1);
+
+ bh0 = ext4_ext_getblk_stub(inode, EXT_META_BASE_PBLK, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, bh0);
+ eh0 = ext_block_hdr(bh0);
+ eh0->eh_magic = EXT4_EXT_MAGIC;
+ eh0->eh_depth = 0;
+ eh0->eh_entries = cpu_to_le16(2);
+ eh0->eh_max = cpu_to_le16(max_leaf);
+ ex = EXT_FIRST_EXTENT(eh0);
+ ex[0].ee_block = cpu_to_le32(10);
+ ex[0].ee_len = cpu_to_le16(5);
+ ext4_ext_store_pblock(&ex[0], 1000);
+ ex[1].ee_block = cpu_to_le32(20);
+ ex[1].ee_len = cpu_to_le16(5);
+ ext4_ext_store_pblock(&ex[1], 1100);
+ brelse(bh0);
+
+ bh1 = ext4_ext_getblk_stub(inode, EXT_META_BASE_PBLK + 1, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, bh1);
+ eh1 = ext_block_hdr(bh1);
+ eh1->eh_magic = EXT4_EXT_MAGIC;
+ eh1->eh_depth = 0;
+ eh1->eh_entries = cpu_to_le16(2);
+ eh1->eh_max = cpu_to_le16(max_leaf);
+ ex = EXT_FIRST_EXTENT(eh1);
+ ex[0].ee_block = cpu_to_le32(40);
+ ex[0].ee_len = cpu_to_le16(5);
+ ext4_ext_store_pblock(&ex[0], 2000);
+ ext4_ext_mark_unwritten(&ex[0]);
+ ex[1].ee_block = cpu_to_le32(50);
+ ex[1].ee_len = cpu_to_le16(5);
+ ext4_ext_store_pblock(&ex[1], 2100);
+ brelse(bh1);
+}
+
+/*
+ * Verify multi-level (depth 1) extent lookup and tree navigation helpers:
+ * ext4_ext_next_allocated_block(), ext4_ext_next_leaf_block(),
+ * ext4_ext_search_left(), and ext4_ext_search_right() across leaf boundaries.
+ */
+static void test_find_extent_multilevel_and_navigation(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_ext_path *path;
+ struct ext4_extent ret_ex;
+ ext4_lblk_t logical;
+ ext4_fsblk_t phys;
+ int ret;
+
+ setup_depth1_two_leaf_tree(test, inode);
+
+ /* Lookup first extent [10, 15) in leaf 0 */
+ path = ext4_find_extent(inode, 10, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, path->p_depth, 1);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[1].p_ext->ee_block), 10U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_next_allocated_block(path), 20U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_next_leaf_block_test(path), 40U);
+ ext4_free_ext_path(path);
+
+ /*
+ * Lookup lblk 30 (in hole between leaf 0 and leaf 1):
+ * path[1].p_ext points to [20, 25) (last extent in leaf 0).
+ */
+ path = ext4_find_extent(inode, 30, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[1].p_ext->ee_block), 20U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_next_allocated_block(path), 40U);
+
+ /* Search left from lblk 30 finds last block of [20, 25) -> 24 @ 1104 */
+ logical = 30;
+ ret = ext4_ext_search_left_test(inode, path, &logical, &phys);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, logical, 24U);
+ KUNIT_EXPECT_EQ(test, phys, 1104ULL);
+
+ /* Search right from lblk 30 crosses into leaf 1 -> [40, 45) @ 2000 */
+ logical = 30;
+ ret = ext4_ext_search_right_test(inode, path, &logical, &phys, &ret_ex,
+ EXT4_EX_NOCACHE);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, logical, 40U);
+ KUNIT_EXPECT_EQ(test, phys, 2000ULL);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ret_ex.ee_block), 40U);
+ ext4_free_ext_path(path);
+
+ /* Lookup last extent [50, 55) in leaf 1 */
+ path = ext4_find_extent(inode, 52, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[1].p_ext->ee_block), 50U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_next_allocated_block(path),
+ EXT_MAX_BLOCKS);
+ KUNIT_EXPECT_EQ(test, ext4_ext_next_leaf_block_test(path),
+ EXT_MAX_BLOCKS);
+ ext4_free_ext_path(path);
+}
+
+/*
+ * Verify ext4_ext_precache() walks all leaf blocks of a multi-level extent
+ * tree, caches all extents and holes into the extent status tree, and sets
+ * EXT4_STATE_EXT_PRECACHED.
+ */
+static void test_ext_precache(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct extent_status es;
+ int err;
+
+ setup_depth1_two_leaf_tree(test, inode);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(inode,
+ EXT4_STATE_EXT_PRECACHED));
+
+ err = ext4_ext_precache(inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_state(inode,
+ EXT4_STATE_EXT_PRECACHED));
+
+ /* Verify cached inter-extent hole [15, 20) and extents from both leaves */
+ KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 17, NULL, &es, NULL),
+ 1);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_hole(&es));
+ KUNIT_EXPECT_EQ(test, es.es_lblk, 15U);
+ KUNIT_EXPECT_EQ(test, es.es_len, 5U);
+
+ KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 10, NULL, &es, NULL),
+ 1);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_written(&es));
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), 1000ULL);
+
+ KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 40, NULL, &es, NULL),
+ 1);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_unwritten(&es));
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), 2000ULL);
+
+ KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 50, NULL, &es, NULL),
+ 1);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_written(&es));
+ KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), 2100ULL);
+}
+
+/*
+ * Verify ext4_can_extents_be_merged() rules for written/unwritten state,
+ * logical and physical contiguity, and maximum length limits.
+ */
+static void test_can_extents_be_merged(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent ex1 = {}, ex2 = {};
+
+ /* Contiguous written extents [10, 15) @ 100 and [15, 20) @ 105 */
+ ex1.ee_block = cpu_to_le32(10);
+ ex1.ee_len = cpu_to_le16(5);
+ ext4_ext_store_pblock(&ex1, 100);
+ ex2.ee_block = cpu_to_le32(15);
+ ex2.ee_len = cpu_to_le16(5);
+ ext4_ext_store_pblock(&ex2, 105);
+ KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+ 1);
+
+ /* Mismatched written vs unwritten cannot merge */
+ ext4_ext_mark_unwritten(&ex2);
+ KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+ 0);
+
+ /* Both unwritten and contiguous can merge */
+ ext4_ext_mark_unwritten(&ex1);
+ KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+ 1);
+
+ /* Non-contiguous physical blocks cannot merge */
+ ext4_ext_mark_initialized(&ex1);
+ ext4_ext_mark_initialized(&ex2);
+ ext4_ext_store_pblock(&ex2, 200);
+ KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+ 0);
+
+ /* Non-contiguous logical blocks cannot merge */
+ ext4_ext_store_pblock(&ex2, 105);
+ ex2.ee_block = cpu_to_le32(16);
+ KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+ 0);
+
+ /* Written max length boundary (EXT_INIT_MAX_LEN = 32768) */
+ ex1.ee_block = cpu_to_le32(0);
+ ex1.ee_len = cpu_to_le16(32760);
+ ext4_ext_store_pblock(&ex1, 100);
+ ex2.ee_block = cpu_to_le32(32760);
+ ex2.ee_len = cpu_to_le16(8);
+ ext4_ext_store_pblock(&ex2, 100 + 32760);
+ KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+ 1);
+ ex2.ee_len = cpu_to_le16(9);
+ KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+ 0);
+
+ /* Unwritten max length boundary (EXT_UNWRITTEN_MAX_LEN = 32767) */
+ ex1.ee_len = cpu_to_le16(32760);
+ ext4_ext_mark_unwritten(&ex1);
+ ex2.ee_len = cpu_to_le16(7);
+ ext4_ext_mark_unwritten(&ex2);
+ KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+ 1);
+ ex2.ee_len = cpu_to_le16(8);
+ ext4_ext_mark_unwritten(&ex2);
+ KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+ 0);
+}
+
+/*
+ * Verify ext4_ext_insert_extent() appending, prepending, 3-way bridge merging,
+ * and EXT4_GET_BLOCKS_SPLIT_NOMERGE suppression.
+ */
+static void test_insert_extent_merge_and_nomerge(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ struct ext4_extent *ex;
+
+ ext_reset_tree(inode);
+
+ /* Insert [10, 15) @ 100 and [20, 25) @ 110 */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 5, 100, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 20, 5, 110, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 2);
+
+ /* Append [15, 18) @ 105 -> merges with [10, 15) into [10, 18) */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 15, 3, 105, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 2);
+ ex = EXT_FIRST_EXTENT(eh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 8);
+
+ /* Insert [18, 20) @ 108 -> bridges [10, 18) and [20, 25) into [10, 25) */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 18, 2, 108, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 15);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[0]), 100ULL);
+
+ /* Insert contiguous [25, 30) @ 115 with SPLIT_NOMERGE -> stays separate */
+ KUNIT_ASSERT_EQ(test,
+ ext_insert_one(inode, 25, 5, 115, false,
+ EXT4_GET_BLOCKS_SPLIT_NOMERGE),
+ 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 2);
+}
+
+/*
+ * Verify tree growth (depth 0 -> 1 via ext4_ext_grow_indepth), parent index
+ * update when inserting a new lowest lblk (ext4_ext_correct_indexes), and
+ * tree collapse back into the inode root (depth 1 -> 0 via
+ * ext4_ext_try_to_merge_up) when merging reduces leaf entries to <= 4.
+ */
+static void test_insert_extent_grow_and_merge_up(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ struct ext4_extent *ex;
+
+ ext_reset_tree(inode);
+
+ /*
+ * Insert 4 non-contiguous extents that fill the root leaf (eh_max = 4):
+ * [10, 12) @ 100, [14, 16) @ 104, [18, 20) @ 108, [22, 24) @ 112.
+ */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 2, 100, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 14, 2, 104, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 18, 2, 108, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 22, 2, 112, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, ext_depth(inode), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 4);
+
+ /*
+ * Insert 5th extent [26, 28) @ 116: triggers ext4_ext_grow_indepth(),
+ * moving the leaf to a newly allocated external block and increasing
+ * tree depth to 1.
+ */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 26, 2, 116, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, ext_depth(inode), 1);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT_FIRST_INDEX(eh)->ei_block), 10U);
+ KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 1U);
+
+ /*
+ * Prepend [8, 10) @ 98 to [10, 12) @ 100: modifies first extent in the
+ * leaf and triggers ext4_ext_correct_indexes(), updating root ei_block
+ * from 10 to 8.
+ */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 8, 2, 98, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT_FIRST_INDEX(eh)->ei_block), 8U);
+
+ /*
+ * Bridge two gaps ([12, 14) @ 102 and [16, 18) @ 106): leaf entries
+ * shrink from 5 -> 4 -> 3. Once <= 4 (max_root), ext4_ext_try_to_merge_up()
+ * copies the extents back into the inode root and frees the leaf block!
+ */
+ k_ctx.free_calls = 0;
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 12, 2, 102, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, ext_depth(inode), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 4);
+ KUNIT_EXPECT_EQ(test, k_ctx.free_calls, 1U);
+
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 16, 2, 106, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, ext_depth(inode), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+ ex = EXT_FIRST_EXTENT(eh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 8U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 12);
+}
+
+/*
+ * Verify ext4_ext_remove_space() for middle-of-extent hole punching, full
+ * extent removal with left-scooting, and ext4_ext_truncate() tail removal.
+ */
+static void test_remove_space_tail_and_hole_punch(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ struct ext4_extent *ex;
+ int err;
+
+ ext_reset_tree(inode);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 10, 100, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 30, 10, 200, false, 0), 0);
+
+ /*
+ * Punch hole [12, 17] inside [10, 20): splits [10, 20) into [10, 12)
+ * and [18, 20), leaving 3 extents total.
+ */
+ down_write(&EXT4_I(inode)->i_data_sem);
+ err = ext4_ext_remove_space(inode, 12, 17);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+ ex = EXT_FIRST_EXTENT(eh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 2);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[1].ee_block), 18U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[1]), 2);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[1]), 108ULL);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[2].ee_block), 30U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[2]), 10);
+
+ /*
+ * Punch out the entire middle extent [18, 19]: scoots [30, 40) into
+ * slot 1 and reduces eh_entries to 2.
+ */
+ down_write(&EXT4_I(inode)->i_data_sem);
+ err = ext4_ext_remove_space(inode, 18, 19);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 2);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[1].ee_block), 30U);
+
+ /*
+ * Truncate file to 11 blocks (i_size = 11 * 4096): removes [30, 40)
+ * and trims [10, 12) down to [10, 11).
+ */
+ inode->i_size = 11ULL << inode->i_sb->s_blocksize_bits;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ err = ext4_ext_truncate(NULL, inode);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 1);
+}
+
+/*
+ * Verify ext4_ext_remove_space() on a depth-1 tree removes all leaf extents,
+ * removes empty index entries via ext4_ext_rm_idx(), frees the leaf metadata
+ * blocks, and resets the root header to eh_depth == 0.
+ */
+static void test_remove_space_multilevel_collapse(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ int err;
+
+ setup_depth1_two_leaf_tree(test, inode);
+ k_ctx.free_calls = 0;
+ k_ctx.total_freed_blocks = 0;
+
+ down_write(&EXT4_I(inode)->i_data_sem);
+ err = ext4_ext_remove_space(inode, 0, EXT_MAX_BLOCKS - 1);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ext_depth(inode), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_max), 4);
+ /* 4 data extents (5 blocks each = 20) + 2 leaf metadata blocks = 22 */
+ KUNIT_EXPECT_EQ(test, k_ctx.total_freed_blocks, 22UL);
+}
+
+/*
+ * Verify bigalloc partial cluster handling in ext4_ext_remove_space():
+ * 1. When a cluster is shared with a remaining extent (state == nofree),
+ * the physical cluster is not freed.
+ * 2. When the entire unaligned extent is removed and has a pending reservation
+ * in i_pending_tree, the cluster is freed with RERESERVE_CLUSTER and
+ * ext4_rereserve_cluster() re-increments i_reserved_data_blocks and removes
+ * the pending reservation.
+ */
+static void test_remove_space_bigalloc_partial_cluster(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ int err;
+
+ sbi->s_cluster_bits = 2;
+ sbi->s_cluster_ratio = 4;
+
+ /*
+ * Case 1: [0, 2) @ 100 and [2, 4) @ 102 share physical cluster 25
+ * (blocks 100..103). Punching [0, 1] must NOT free cluster 25.
+ */
+ ext_reset_tree(inode);
+ KUNIT_ASSERT_EQ(test,
+ ext_insert_one(inode, 0, 2, 100, false,
+ EXT4_GET_BLOCKS_SPLIT_NOMERGE),
+ 0);
+ KUNIT_ASSERT_EQ(test,
+ ext_insert_one(inode, 2, 2, 102, false,
+ EXT4_GET_BLOCKS_SPLIT_NOMERGE),
+ 0);
+
+ k_ctx.free_calls = 0;
+ k_ctx.total_freed_blocks = 0;
+ down_write(&ei->i_data_sem);
+ err = ext4_ext_remove_space(inode, 0, 1);
+ up_write(&ei->i_data_sem);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ /*
+ * ext4_remove_blocks() is called with NOFREE_FIRST_CLUSTER |
+ * NOFREE_LAST_CLUSTER, and partial cluster 25 is marked nofree.
+ */
+ KUNIT_EXPECT_EQ(test, k_ctx.free_calls, 1U);
+ KUNIT_EXPECT_TRUE(test,
+ (k_ctx.last_free_flags &
+ EXT4_FREE_BLOCKS_NOFREE_FIRST_CLUSTER) != 0);
+
+ /*
+ * Case 2: Single unaligned extent [1, 3) @ 101 in cluster 0 (pclu 25)
+ * with a pending reservation on cluster 0. Truncating from 1 frees
+ * cluster 25 with RERESERVE_CLUSTER and calls ext4_rereserve_cluster().
+ */
+ ext_reset_tree(inode);
+ set_opt(inode->i_sb, DELALLOC);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 1, 2, 101, true, 0), 0);
+
+ down_write(&ei->i_data_sem);
+ /* Insert delayed extent at lblk 0 in already-allocated cluster 0 */
+ ext4_es_insert_delayed_extent(inode, 0, 1, true, false);
+ up_write(&ei->i_data_sem);
+ KUNIT_ASSERT_TRUE(test, ext4_is_pending(inode, 0));
+
+ ei->i_reserved_data_blocks = 0;
+ k_ctx.free_calls = 0;
+ down_write(&ei->i_data_sem);
+ err = ext4_ext_remove_space(inode, 1, EXT_MAX_BLOCKS - 1);
+ up_write(&ei->i_data_sem);
+
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, k_ctx.last_free_pblk, 100ULL);
+ KUNIT_EXPECT_EQ(test, k_ctx.last_free_count, 4UL);
+ KUNIT_EXPECT_TRUE(test,
+ (k_ctx.last_free_flags &
+ EXT4_FREE_BLOCKS_RERESERVE_CLUSTER) != 0);
+ KUNIT_EXPECT_EQ(test, ei->i_reserved_data_blocks, 1U);
+ KUNIT_EXPECT_FALSE(test, ext4_is_pending(inode, 0));
+}
+
+/*
+ * Verify ext4_ext_map_blocks() hole lookup, delayed-extent hole truncation
+ * (ext4_ext_determine_insert_hole), and EXT4_GET_BLOCKS_QUERY_LAST_IN_LEAF.
+ */
+static void test_map_blocks_lookup_hole_and_query_last_in_leaf(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_map_blocks map = {};
+ int ret;
+
+ ext_reset_tree(inode);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 5, 100, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 20, 5, 200, false, 0), 0);
+
+ /* Hole lookup at [2, 22) returns 0 and trims m_len to [2, 10) (len 8) */
+ map.m_lblk = 2;
+ map.m_len = 20;
+ map.m_flags = 0;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ext_map_blocks(NULL, inode, &map, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, map.m_pblk, 0ULL);
+ KUNIT_EXPECT_EQ(test, map.m_len, 8U);
+
+ /*
+ * Insert a delayed extent at [5, 7) in the es tree: hole lookup at
+ * [2, 22) must stop at the delayed extent -> [2, 5) (len 3).
+ */
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ ext4_es_insert_delayed_extent(inode, 5, 2, false, false);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ map.m_lblk = 2;
+ map.m_len = 20;
+ map.m_flags = 0;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_EX_NOCACHE);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, map.m_len, 3U);
+
+ /* QUERY_LAST_IN_LEAF on first extent [10, 15) -> flag NOT set */
+ map.m_lblk = 10;
+ map.m_len = 5;
+ map.m_flags = 0;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ext_map_blocks(NULL, inode, &map,
+ EXT4_GET_BLOCKS_QUERY_LAST_IN_LEAF);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 5);
+ KUNIT_EXPECT_FALSE(test,
+ (map.m_flags & EXT4_MAP_QUERY_LAST_IN_LEAF) != 0);
+
+ /* QUERY_LAST_IN_LEAF on last extent [20, 25) -> flag IS set */
+ map.m_lblk = 20;
+ map.m_len = 5;
+ map.m_flags = 0;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ext_map_blocks(NULL, inode, &map,
+ EXT4_GET_BLOCKS_QUERY_LAST_IN_LEAF);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 5);
+ KUNIT_EXPECT_TRUE(test,
+ (map.m_flags & EXT4_MAP_QUERY_LAST_IN_LEAF) != 0);
+}
+
+/*
+ * Verify ext4_ext_map_blocks() block allocation with overlap clamping
+ * (ext4_ext_check_overlap) and freeing of newly allocated blocks when
+ * ext4_ext_insert_extent() fails with -ENOSPC.
+ */
+static void test_map_blocks_alloc_overlap_and_enospc(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_map_blocks map = {};
+ int ret;
+
+ ext_reset_tree(inode);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 15, 5, 200, false, 0), 0);
+
+ /*
+ * Request allocation for [10, 20) (len 10): ext4_ext_check_overlap()
+ * clamps the allocation to [10, 15) (len 5) to avoid overlapping [15, 20).
+ */
+ k_ctx.alloc_calls = 0;
+ map.m_lblk = 10;
+ map.m_len = 10;
+ map.m_flags = 0;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ KUNIT_EXPECT_EQ(test, ret, 5);
+ KUNIT_EXPECT_EQ(test, map.m_len, 5U);
+ KUNIT_EXPECT_EQ(test, map.m_pblk, EXT_ALLOC_BASE_PBLK);
+ KUNIT_EXPECT_TRUE(test,
+ (map.m_flags & (EXT4_MAP_NEW | EXT4_MAP_MAPPED)) ==
+ (EXT4_MAP_NEW | EXT4_MAP_MAPPED));
+ KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 1U);
+ KUNIT_EXPECT_EQ(test, k_ctx.last_alloc_len, 5U);
+
+ /*
+ * Force ext4_ext_insert_extent() to return -ENOSPC and verify that
+ * ext4_ext_map_blocks() frees the newly allocated blocks.
+ */
+ kunit_activate_static_stub(test, ext4_ext_insert_extent,
+ ext4_ext_insert_extent_stub);
+ k_ctx.free_calls = 0;
+ map.m_lblk = 30;
+ map.m_len = 4;
+ map.m_flags = 0;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ext_map_blocks(NULL, inode, &map,
+ EXT4_GET_BLOCKS_CREATE |
+ EXT4_GET_BLOCKS_DELALLOC_RESERVE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+ KUNIT_EXPECT_EQ(test, k_ctx.free_calls, 1U);
+ KUNIT_EXPECT_EQ(test, k_ctx.last_free_pblk, k_ctx.last_alloc_pblk);
+ KUNIT_EXPECT_EQ(test, k_ctx.last_free_count, 4UL);
+ KUNIT_EXPECT_EQ(test, k_ctx.last_free_flags,
+ EXT4_FREE_BLOCKS_NO_QUOT_UPDATE);
+}
+
+/*
+ * Verify ext4_ext_convert_to_initialized() fastpath block transfers to
+ * abutting left (ex - 1) and right (ex + 1) initialized neighbors without
+ * splitting into new extent entries.
+ */
+static void test_convert_to_initialized_abut_neighbors(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ struct ext4_extent *ex;
+ struct ext4_map_blocks map = {};
+ int ret;
+
+ EXT4_SB(inode->i_sb)->s_extent_max_zeroout_kb = 0;
+ ext_reset_tree(inode);
+
+ /*
+ * Setup: [10, 12) @ 100 (written), [12, 16) @ 102 (unwritten),
+ * [16, 18) @ 106 (written).
+ */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 2, 100, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 12, 4, 102, true, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 16, 2, 106, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+ /*
+ * Convert head of unwritten extent [12, 14): transfers 2 blocks to
+ * left neighbor [10, 12) -> [10, 14) written, [14, 16) unwritten.
+ */
+ map.m_lblk = 12;
+ map.m_len = 2;
+ map.m_flags = 0;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 2);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+ ex = EXT_FIRST_EXTENT(eh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 4);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex[0]), 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[1].ee_block), 14U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[1]), 2);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex[1]), 1);
+
+ /*
+ * Convert tail of unwritten extent [15, 16): transfers 1 block to
+ * right neighbor [16, 18) -> [14, 15) unwritten, [15, 18) written.
+ */
+ map.m_lblk = 15;
+ map.m_len = 1;
+ map.m_flags = 0;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[1].ee_block), 14U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[1]), 1);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex[1]), 1);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[2].ee_block), 15U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[2]), 3);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[2]), 105ULL);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex[2]), 0);
+}
+
+/*
+ * Verify bigalloc get_implied_cluster_alloc() in ext4_ext_map_blocks() when
+ * an existing extent to the left or right already maps the same logical
+ * cluster, avoiding a duplicate physical cluster allocation.
+ */
+static void test_map_blocks_bigalloc_implied_cluster(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+ struct ext4_map_blocks map = {};
+ int ret;
+
+ sbi->s_cluster_bits = 2;
+ sbi->s_cluster_ratio = 4;
+
+ /*
+ * Case 1: Left extent [8, 9) @ 100 in logical cluster 2 (lblks 8..11).
+ * Allocating [10, 12) reuses physical cluster 25 (pblks 102..103)
+ * without calling ext4_mb_new_blocks().
+ */
+ ext_reset_tree(inode);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 8, 1, 100, false, 0), 0);
+
+ k_ctx.alloc_calls = 0;
+ map.m_lblk = 10;
+ map.m_len = 2;
+ map.m_flags = 0;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ KUNIT_EXPECT_EQ(test, ret, 2);
+ KUNIT_EXPECT_EQ(test, map.m_pblk, 102ULL);
+ KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 0U);
+
+ /*
+ * Case 2: Right extent [10, 12) @ 102 in logical cluster 2 (lblks 8..11).
+ * Allocating [8, 10) searches right, finds [10, 12), and reuses
+ * physical cluster 25 (pblks 100..101) without calling ext4_mb_new_blocks().
+ */
+ ext_reset_tree(inode);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 2, 102, false, 0), 0);
+
+ k_ctx.alloc_calls = 0;
+ map.m_lblk = 8;
+ map.m_len = 2;
+ map.m_flags = 0;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ KUNIT_EXPECT_EQ(test, ret, 2);
+ KUNIT_EXPECT_EQ(test, map.m_pblk, 100ULL);
+ KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 0U);
+}
+
+/*
+ * Verify ext4_ext_shift_extents() for SHIFT_LEFT (collapse range) with
+ * neighbor merging, SHIFT_RIGHT (insert range), and -EINVAL boundary checks.
+ */
+static void test_ext_shift_extents(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ struct ext4_extent *ex;
+ int err;
+
+ ext_reset_tree(inode);
+ /* [10, 15) @ 100 and [20, 25) @ 105 (physically contiguous!) */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 5, 100, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 20, 5, 105, false, 0), 0);
+
+ /* Shifting [20, 25) left by 6 overlaps [10, 15) -> -EINVAL */
+ down_write(&EXT4_I(inode)->i_data_sem);
+ err = ext4_ext_shift_extents_test(inode, NULL, 20, 6, SHIFT_LEFT);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, err, -EINVAL);
+
+ /* Shifting [20, 25) left by 5 merges with [10, 15) into [10, 20) @ 100 */
+ down_write(&EXT4_I(inode)->i_data_sem);
+ err = ext4_ext_shift_extents_test(inode, NULL, 20, 5, SHIFT_LEFT);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+ ex = EXT_FIRST_EXTENT(eh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 10);
+
+ /* Shifting [10, 20) right by 4 moves it to [14, 24) @ 100 */
+ down_write(&EXT4_I(inode)->i_data_sem);
+ err = ext4_ext_shift_extents_test(inode, NULL, 10, 4, SHIFT_RIGHT);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 14U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 10);
+
+ /* Shifting right past EXT_MAX_BLOCKS -> -EINVAL */
+ down_write(&EXT4_I(inode)->i_data_sem);
+ err = ext4_ext_shift_extents_test(inode, NULL, 14, EXT_MAX_BLOCKS,
+ SHIFT_RIGHT);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, err, -EINVAL);
+}
+
+/*
+ * Verify ext4_swap_extents() splits unaligned boundaries on both inodes,
+ * swaps physical block mappings for the requested subrange, and marks
+ * the donor inode's swapped extent unwritten when requested.
+ */
+static void test_swap_extents(struct kunit *test)
+{
+ struct inode *inode1 = &k_ctx.k_ei->vfs_inode;
+ struct inode *inode2;
+ struct ext4_extent_header *eh1, *eh2;
+ struct ext4_extent *ex1, *ex2;
+ int replaced, err = 0;
+
+ k_ctx.k_ei2 = kzalloc_obj(struct ext4_inode_info);
+ KUNIT_ASSERT_NOT_NULL(test, k_ctx.k_ei2);
+ ext_init_mock_inode(k_ctx.k_ei2, inode1->i_sb, 13);
+ inode2 = &k_ctx.k_ei2->vfs_inode;
+
+ ext_reset_tree(inode1);
+ ext_reset_tree(inode2);
+
+ /* inode1: [10, 20) @ 100; inode2: [10, 20) @ 200 */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode1, 10, 10, 100, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode2, 10, 10, 200, false, 0), 0);
+
+ inode_lock(inode1);
+ inode_lock(inode2);
+ down_write(&EXT4_I(inode1)->i_data_sem);
+ down_write(&EXT4_I(inode2)->i_data_sem);
+
+ replaced = ext4_swap_extents(NULL, inode1, inode2, 12, 12, 5, 1, &err);
+
+ up_write(&EXT4_I(inode2)->i_data_sem);
+ up_write(&EXT4_I(inode1)->i_data_sem);
+ inode_unlock(inode2);
+ inode_unlock(inode1);
+
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, replaced, 5);
+
+ eh1 = ext_inode_hdr(inode1);
+ eh2 = ext_inode_hdr(inode2);
+ KUNIT_ASSERT_EQ(test, le16_to_cpu(eh1->eh_entries), 3);
+ KUNIT_ASSERT_EQ(test, le16_to_cpu(eh2->eh_entries), 3);
+
+ ex1 = EXT_FIRST_EXTENT(eh1);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex1[0].ee_block), 10U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex1[0]), 100ULL);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex1[1].ee_block), 12U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex1[1]), 5);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex1[1]), 202ULL);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex1[1]), 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex1[2].ee_block), 17U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex1[2]), 107ULL);
+
+ ex2 = EXT_FIRST_EXTENT(eh2);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex2[0].ee_block), 10U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex2[0]), 200ULL);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex2[1].ee_block), 12U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex2[1]), 5);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex2[1]), 102ULL);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex2[1]), 1);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex2[2].ee_block), 17U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex2[2]), 207ULL);
+}
+
+/*
+ * Verify ext4_clu_mapped() across inline-data inodes, empty trees, clusters
+ * mapped at the start or end of a cluster, and unmapped clusters.
+ */
+static void test_clu_mapped(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+
+ sbi->s_cluster_bits = 2;
+ sbi->s_cluster_ratio = 4;
+
+ ext_reset_tree(inode);
+ /* Empty tree -> unmapped */
+ KUNIT_EXPECT_EQ(test, ext4_clu_mapped(inode, 0), 0);
+
+ /*
+ * Insert [0, 2) @ 100 (in lclu 0 = lblks 0..3) and
+ * [6, 8) @ 106 (in lclu 1 = lblks 4..7).
+ */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 0, 2, 100, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 6, 2, 106, false, 0), 0);
+
+ /* Inline-data state returns 0 immediately */
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ KUNIT_EXPECT_EQ(test, ext4_clu_mapped(inode, 0), 0);
+ ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+
+ /* lclu 0 (lblk 0) is mapped directly by [0, 2) */
+ KUNIT_EXPECT_EQ(test, ext4_clu_mapped(inode, 0), 1);
+ /* lclu 1 (lblk 4) is mapped by next allocated extent [6, 8) */
+ KUNIT_EXPECT_EQ(test, ext4_clu_mapped(inode, 1), 1);
+ /* lclu 2 (lblk 8..11) is unmapped */
+ KUNIT_EXPECT_EQ(test, ext4_clu_mapped(inode, 2), 0);
+}
+
+/*
+ * Verify ext4_ext_index_trans_blocks() and
+ * ext4_ext_calc_credits_for_single_extent() credit calculations.
+ */
+static void test_ext_credits_calculation(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ struct ext4_ext_path *path;
+ int credits_with_space, credits_full;
+
+ /* 1 extent vs multiple extents */
+ KUNIT_EXPECT_EQ(test, ext4_ext_index_trans_blocks(inode, 1),
+ EXT4_MAX_EXTENT_DEPTH * 2 + 1);
+ KUNIT_EXPECT_EQ(test, ext4_ext_index_trans_blocks(inode, 2),
+ EXT4_MAX_EXTENT_DEPTH * 3 + 2);
+
+ /* Inline data inode needs only 1 index block credit */
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ EXT4_I(inode)->i_inline_off = 1;
+ KUNIT_EXPECT_EQ(test, ext4_ext_index_trans_blocks(inode, 2), 1);
+ EXT4_I(inode)->i_inline_off = 0;
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+
+ ext_reset_tree(inode);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 2, 100, false, 0), 0);
+
+ path = ext4_find_extent(inode, 10, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ /* Leaf has free space (eh_entries = 1 < eh_max = 4) */
+ credits_with_space =
+ ext4_ext_calc_credits_for_single_extent(inode, 1, path);
+
+ /* Mark leaf full (eh_entries == eh_max) */
+ eh->eh_entries = eh->eh_max;
+ credits_full = ext4_ext_calc_credits_for_single_extent(inode, 1, path);
+ eh->eh_entries = cpu_to_le16(1);
+ ext4_free_ext_path(path);
+
+ KUNIT_EXPECT_LT(test, credits_with_space, credits_full);
+ KUNIT_EXPECT_EQ(test, credits_full,
+ ext4_ext_calc_credits_for_single_extent(inode, 1, NULL));
+}
+
+/*
+ * Verify fast-commit replay extent helpers:
+ * - ext4_ext_replay_update_ex() splits and updates extent state/pblk
+ * - ext4_ext_replay_shrink_inode() merges adjacent compatible extents
+ * - ext4_ext_replay_set_iblocks() counts data and extent tree blocks and
+ * updates inode->i_blocks.
+ */
+static void test_ext_fast_commit_replay(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ struct ext4_extent *ex;
+ int err;
- return;
+ ext_reset_tree(inode);
+ /* Start with unwritten extent [10, 20) @ 100 */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 10, 100, true, 0), 0);
+
+ /* Replay update converting middle [12, 16) to written @ 102 */
+ err = ext4_ext_replay_update_ex(inode, 12, 4, 0, 102);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+ /* Replay update converting [10, 12) @ 100 and [16, 20) @ 106 to written */
+ KUNIT_ASSERT_EQ(test, ext4_ext_replay_update_ex(inode, 10, 2, 0, 100),
+ 0);
+ KUNIT_ASSERT_EQ(test, ext4_ext_replay_update_ex(inode, 16, 4, 0, 106),
+ 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+ /* Shrink inode merges the 3 contiguous written extents into [10, 20) */
+ ext4_ext_replay_shrink_inode(inode, 25);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+ ex = EXT_FIRST_EXTENT(eh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 10);
+ KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex[0]), 0);
+
+ /* Recalculate i_blocks for the 10-block extent (10 * 8 = 80 sectors) */
+ inode->i_size = 30ULL << inode->i_sb->s_blocksize_bits;
+ inode->i_blocks = 0;
+ err = ext4_ext_replay_set_iblocks(inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, inode->i_blocks,
+ 10ULL << (inode->i_sb->s_blocksize_bits - 9));
}
static const struct kunit_ext_test_param test_split_convert_params[] = {
@@ -1049,6 +2774,31 @@ static struct kunit_case extents_test_cases[] = {
test_convert_initialized_param_init, NULL),
KUNIT_CASE_PARAM_WITH_INIT(test_split_convert, kunit_array_gen_params,
test_handle_unwritten_init, NULL),
+ KUNIT_CASE(test_ext_pblock_48bit),
+ KUNIT_CASE(test_ext_unwritten_len_encoding),
+ KUNIT_CASE(test_ext_tree_init_and_check_inode),
+ KUNIT_CASE_PARAM_WITH_INIT(test_ext_check_corruptions,
+ kunit_array_gen_params,
+ ext_check_corrupt_param_init, NULL),
+ KUNIT_CASE(test_ext_block_csum_verify),
+ KUNIT_CASE(test_find_extent_root_binsearch),
+ KUNIT_CASE(test_find_extent_multilevel_and_navigation),
+ KUNIT_CASE(test_ext_precache),
+ KUNIT_CASE(test_can_extents_be_merged),
+ KUNIT_CASE(test_insert_extent_merge_and_nomerge),
+ KUNIT_CASE(test_insert_extent_grow_and_merge_up),
+ KUNIT_CASE(test_remove_space_tail_and_hole_punch),
+ KUNIT_CASE(test_remove_space_multilevel_collapse),
+ KUNIT_CASE(test_remove_space_bigalloc_partial_cluster),
+ KUNIT_CASE(test_map_blocks_lookup_hole_and_query_last_in_leaf),
+ KUNIT_CASE(test_map_blocks_alloc_overlap_and_enospc),
+ KUNIT_CASE(test_convert_to_initialized_abut_neighbors),
+ KUNIT_CASE(test_map_blocks_bigalloc_implied_cluster),
+ KUNIT_CASE(test_ext_shift_extents),
+ KUNIT_CASE(test_swap_extents),
+ KUNIT_CASE(test_clu_mapped),
+ KUNIT_CASE(test_ext_credits_calculation),
+ KUNIT_CASE(test_ext_fast_commit_replay),
{}
};
diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
index 6c018bcc0118..efbee7aaa95d 100644
--- a/fs/ext4/extents.c
+++ b/fs/ext4/extents.c
@@ -547,6 +547,13 @@ static void ext4_cache_extents(struct inode *inode,
}
}
+struct buffer_head *ext4_ext_getblk(struct inode *inode, ext4_fsblk_t pblk,
+ gfp_t gfp_flags)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_ext_getblk, inode, pblk, gfp_flags);
+ return sb_getblk_gfp(inode->i_sb, pblk, gfp_flags);
+}
+
static struct buffer_head *
__read_extent_tree_block(const char *function, unsigned int line,
struct inode *inode, struct ext4_extent_idx *idx,
@@ -561,7 +568,7 @@ __read_extent_tree_block(const char *function, unsigned int line,
gfp_flags |= __GFP_NOFAIL;
pblk = ext4_idx_pblock(idx);
- bh = sb_getblk_gfp(inode->i_sb, pblk, gfp_flags);
+ bh = ext4_ext_getblk(inode, pblk, gfp_flags);
if (unlikely(!bh))
return ERR_PTR(-ENOMEM);
@@ -1124,7 +1131,7 @@ static int ext4_ext_split(handle_t *handle, struct inode *inode,
err = -EFSCORRUPTED;
goto cleanup;
}
- bh = sb_getblk_gfp(inode->i_sb, newblock, __GFP_MOVABLE | GFP_NOFS);
+ bh = ext4_ext_getblk(inode, newblock, __GFP_MOVABLE | GFP_NOFS);
if (unlikely(!bh)) {
err = -ENOMEM;
goto cleanup;
@@ -1203,7 +1210,7 @@ static int ext4_ext_split(handle_t *handle, struct inode *inode,
while (k--) {
oldblock = newblock;
newblock = ablocks[--a];
- bh = sb_getblk(inode->i_sb, newblock);
+ bh = ext4_ext_getblk(inode, newblock, __GFP_MOVABLE | GFP_NOFS);
if (unlikely(!bh)) {
err = -ENOMEM;
goto cleanup;
@@ -1332,7 +1339,7 @@ static int ext4_ext_grow_indepth(handle_t *handle, struct inode *inode,
if (newblock == 0)
return err;
- bh = sb_getblk_gfp(inode->i_sb, newblock, __GFP_MOVABLE | GFP_NOFS);
+ bh = ext4_ext_getblk(inode, newblock, __GFP_MOVABLE | GFP_NOFS);
if (unlikely(!bh))
return -ENOMEM;
lock_buffer(bh);
@@ -6405,11 +6412,79 @@ struct ext4_ext_path *ext4_split_convert_extents_test(handle_t *handle,
}
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_split_convert_extents_test);
+int ext4_ext_check_test(struct inode *inode, struct ext4_extent_header *eh,
+ int depth, ext4_fsblk_t pblk, ext4_lblk_t lblk)
+{
+ return __ext4_ext_check(__func__, __LINE__, inode, eh, depth, pblk,
+ lblk);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_check_test);
+
+void ext4_extent_block_csum_set_test(struct inode *inode,
+ struct ext4_extent_header *eh)
+{
+ ext4_extent_block_csum_set(inode, eh);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_extent_block_csum_set_test);
+
+int ext4_can_extents_be_merged_test(struct inode *inode,
+ struct ext4_extent *ex1,
+ struct ext4_extent *ex2)
+{
+ return ext4_can_extents_be_merged(inode, ex1, ex2);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_can_extents_be_merged_test);
+
+int ext4_ext_search_left_test(struct inode *inode, struct ext4_ext_path *path,
+ ext4_lblk_t *logical, ext4_fsblk_t *phys)
+{
+ return ext4_ext_search_left(inode, path, logical, phys);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_search_left_test);
+
+int ext4_ext_search_right_test(struct inode *inode, struct ext4_ext_path *path,
+ ext4_lblk_t *logical, ext4_fsblk_t *phys,
+ struct ext4_extent *ret_ex, int flags)
+{
+ return ext4_ext_search_right(inode, path, logical, phys, ret_ex, flags);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_search_right_test);
+
+ext4_lblk_t ext4_ext_next_leaf_block_test(struct ext4_ext_path *path)
+{
+ return ext4_ext_next_leaf_block(path);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_next_leaf_block_test);
+
+int ext4_ext_shift_extents_test(struct inode *inode, handle_t *handle,
+ ext4_lblk_t start, ext4_lblk_t shift,
+ enum SHIFT_DIRECTION direction)
+{
+ return ext4_ext_shift_extents(inode, handle, start, shift, direction);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_shift_extents_test);
+
EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_ext_dirty);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_getblk);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_zeroout);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_map_create_blocks);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_insert_extent);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_extent);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_issue_zeroout);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_map_query_blocks);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_ext_path);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_tree_init);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_check_inode);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_precache);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_next_allocated_block);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_calc_credits_for_single_extent);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_index_trans_blocks);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_remove_space);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_truncate);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_map_blocks);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_swap_extents);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_clu_mapped);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_replay_update_ex);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_replay_shrink_inode);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_replay_set_iblocks);
#endif
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 966a760f64e6..289f7fa35090 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -6699,6 +6699,9 @@ int __ext4_mark_inode_dirty(handle_t *handle, struct inode *inode,
struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
int err;
+ KUNIT_STATIC_STUB_REDIRECT(__ext4_mark_inode_dirty, handle, inode,
+ func, line);
+
might_sleep();
trace_ext4_mark_inode_dirty(inode, _RET_IP_);
err = ext4_reserve_inode_write(handle, inode, &iloc);
@@ -6982,4 +6985,6 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
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/mballoc.c b/fs/ext4/mballoc.c
index 06171a11db12..0f73fb826dbf 100644
--- a/fs/ext4/mballoc.c
+++ b/fs/ext4/mballoc.c
@@ -6239,6 +6239,8 @@ ext4_fsblk_t ext4_mb_new_blocks(handle_t *handle,
int retries = 0;
u64 seq;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_mb_new_blocks, handle, ar, errp);
+
might_sleep();
sb = ar->inode->i_sb;
sbi = EXT4_SB(sb);
@@ -6697,6 +6699,9 @@ void ext4_free_blocks(handle_t *handle, struct inode *inode,
unsigned int overflow;
struct ext4_sb_info *sbi;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_free_blocks, handle, inode, bh, block,
+ count, flags);
+
sbi = EXT4_SB(sb);
if (bh) {
@@ -7281,4 +7286,6 @@ EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_block_bitmap_nowait);
EXPORT_SYMBOL_FOR_EXT4_TEST(mb_set_bits);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_init_inode);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_mark_context);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_new_blocks);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_blocks);
#endif
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 03/27] ext4: add KUnit tests for ext4_ext_split() leaf and index splitting
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:06 ` [PATCH 02/27] ext4: add KUnit tests for fs/ext4/extents.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (23 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Commit 7831b3664109 ("ext4: add KUnit tests for fs/ext4/extents.c")
updated both block-allocation paths in ext4_ext_split() to call
ext4_ext_getblk() and noted node splitting in the commit message, but
none of the test cases in fs/ext4/extents-test.c actually reached
ext4_ext_split(). In test_insert_extent_grow_and_merge_up(), inserting a
5th extent into a full depth-0 root triggers ext4_ext_grow_indepth(),
moving the 4 extents into an external leaf block with eh_max == 340 and
returning from ext4_ext_create_new_leaf() without splitting.
Add two KUnit test cases to exercise ext4_ext_split() across leaf and
intermediate index levels:
- test_insert_extent_leaf_split(): fills a depth-1 external leaf block
to capacity (340 extents), inserts in the middle to trigger a leaf
split that moves tail extents into a new leaf block, and then fills
the second leaf and appends past its end to exercise the m == 0
leaf-addition path.
- test_insert_extent_index_split(): exercises depth 1 -> 2 tree growth
followed by leaf splitting via the repeat loop in
ext4_ext_create_new_leaf(), and then clamps the depth-1 index block's
eh_max to exercise simultaneous leaf and intermediate index block
splitting (k == 1) in ext4_ext_split().
Fixes: 7831b3664109 ("ext4: add KUnit tests for fs/ext4/extents.c")
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/extents-test.c | 206 +++++++++++++++++++++++++++++++++++++++--
fs/ext4/move_extent.c | 5 +
2 files changed, 203 insertions(+), 8 deletions(-)
diff --git a/fs/ext4/extents-test.c b/fs/ext4/extents-test.c
index 705648186218..47ddeb94e2bf 100644
--- a/fs/ext4/extents-test.c
+++ b/fs/ext4/extents-test.c
@@ -1584,6 +1584,198 @@ static void test_insert_extent_grow_and_merge_up(struct kunit *test)
KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 12);
}
+/*
+ * Verify ext4_ext_split() at depth 1 both when splitting a full leaf in the
+ * middle (moving tail extents to a newly allocated leaf block) and when
+ * appending past the end of a full leaf (m == 0).
+ */
+static void test_insert_extent_leaf_split(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ struct ext4_ext_path *path;
+ int i;
+
+ ext_reset_tree(inode);
+
+ /*
+ * Insert 340 non-contiguous extents [i * 4, i * 4 + 2) @ 2000 + i * 4
+ * to grow from depth 0 to depth 1 and completely fill the external
+ * 4096-byte leaf block (eh_max = 340).
+ */
+ for (i = 0; i < 340; i++) {
+ KUNIT_ASSERT_EQ_MSG(test,
+ ext_insert_one(inode, i * 4, 2,
+ 2000 + i * 4, false, 0),
+ 0, "i=%d", i);
+ }
+ KUNIT_EXPECT_EQ(test, ext_depth(inode), 1);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+
+ /*
+ * Insert [402, 403) @ 9000 in the middle of the full leaf between
+ * [400, 402) and [404, 406): triggers ext4_ext_split() with m = 239,
+ * moving [404..1358) into a newly allocated second leaf block.
+ */
+ k_ctx.alloc_calls = 0;
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 402, 1, 9000, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ext_depth(inode), 1);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 2);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT_FIRST_INDEX(eh)[0].ei_block), 0U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT_FIRST_INDEX(eh)[1].ei_block),
+ 404U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_check_inode(inode), 0);
+
+ path = ext4_find_extent(inode, 402, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(path[1].p_hdr->eh_entries), 102);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[1].p_ext->ee_block), 402U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(path[1].p_ext), 9000ULL);
+ ext4_free_ext_path(path);
+
+ path = ext4_find_extent(inode, 404, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(path[1].p_hdr->eh_entries), 239);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[1].p_ext->ee_block), 404U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(path[1].p_ext), 2404ULL);
+ ext4_free_ext_path(path);
+
+ /*
+ * Fill the second leaf to capacity (239 + 101 = 340 entries), then
+ * append [1764, 1766) @ 3764 past its end to exercise ext4_ext_split()
+ * with m == 0 (adding a new leaf without moving existing entries).
+ */
+ for (i = 340; i < 441; i++) {
+ KUNIT_ASSERT_EQ_MSG(test,
+ ext_insert_one(inode, i * 4, 2,
+ 2000 + i * 4, false, 0),
+ 0, "i=%d", i);
+ }
+ k_ctx.alloc_calls = 0;
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 1764, 2, 3764, false, 0),
+ 0);
+ KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ext_depth(inode), 1);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT_FIRST_INDEX(eh)[2].ei_block),
+ 1764U);
+
+ path = ext4_find_extent(inode, 1764, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(path[1].p_hdr->eh_entries), 1);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[1].p_ext->ee_block), 1764U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(path[1].p_ext), 3764ULL);
+ ext4_free_ext_path(path);
+}
+
+/*
+ * Verify multi-level tree growth from depth 1 -> 2 followed by leaf split
+ * (ext4_ext_create_new_leaf repeat loop), and intermediate index block
+ * splitting in ext4_ext_split() when both leaf and depth-1 index are full.
+ */
+static void test_insert_extent_index_split(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ struct ext4_extent_idx *idx;
+ struct ext4_ext_path *path;
+ struct buffer_head *bh;
+ int i;
+
+ ext_reset_tree(inode);
+
+ /*
+ * Build a full depth-1 root index (eh_entries = eh_max = 4) pointing
+ * to 4 full leaf blocks (eh_entries = eh_max = 2).
+ */
+ eh->eh_depth = cpu_to_le16(1);
+ eh->eh_entries = cpu_to_le16(4);
+ eh->eh_max = cpu_to_le16(ext4_ext_space_root_idx_test(inode, 0));
+ idx = EXT_FIRST_INDEX(eh);
+
+ for (i = 0; i < 4; i++) {
+ struct ext4_extent_header *leaf_eh;
+ struct ext4_extent *ex;
+ ext4_lblk_t lblk = 10 + i * 10;
+
+ idx[i].ei_block = cpu_to_le32(lblk);
+ ext4_idx_store_pblock(&idx[i], EXT_META_BASE_PBLK + i);
+
+ bh = ext4_ext_getblk_stub(inode, EXT_META_BASE_PBLK + i,
+ GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ leaf_eh = ext_block_hdr(bh);
+ leaf_eh->eh_magic = EXT4_EXT_MAGIC;
+ leaf_eh->eh_depth = 0;
+ leaf_eh->eh_entries = cpu_to_le16(2);
+ leaf_eh->eh_max = cpu_to_le16(2);
+ ex = EXT_FIRST_EXTENT(leaf_eh);
+ ex[0].ee_block = cpu_to_le32(lblk);
+ ex[0].ee_len = cpu_to_le16(2);
+ ext4_ext_store_pblock(&ex[0], 2000 + i * 10);
+ ex[1].ee_block = cpu_to_le32(lblk + 4);
+ ex[1].ee_len = cpu_to_le16(2);
+ ext4_ext_store_pblock(&ex[1], 2004 + i * 10);
+ brelse(bh);
+ }
+
+ /*
+ * Inserting [48, 50) @ 2038 finds both leaf 3 and the root index full:
+ * ext4_ext_grow_indepth() grows depth 1 -> 2, and the repeat loop in
+ * ext4_ext_create_new_leaf() calls ext4_ext_split() at depth 1.
+ */
+ k_ctx.alloc_calls = 0;
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 48, 2, 2038, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 2U);
+ KUNIT_EXPECT_EQ(test, ext_depth(inode), 2);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+
+ /*
+ * Clamp the depth-1 index block's eh_max to its current 5 entries so
+ * inserting [12, 13) @ 9000 into full leaf 0 splits both leaf 0 and
+ * the intermediate depth-1 index block (depth = 2, at = 0, k = 1).
+ */
+ bh = ext4_ext_getblk_stub(inode, ext4_idx_pblock(EXT_FIRST_INDEX(eh)),
+ GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ KUNIT_ASSERT_EQ(test, le16_to_cpu(ext_block_hdr(bh)->eh_entries), 5);
+ ext_block_hdr(bh)->eh_max = cpu_to_le16(5);
+ brelse(bh);
+
+ k_ctx.alloc_calls = 0;
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 12, 1, 9000, false, 0), 0);
+ KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 2U);
+ KUNIT_EXPECT_EQ(test, ext_depth(inode), 2);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 2);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT_FIRST_INDEX(eh)[0].ei_block),
+ 10U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT_FIRST_INDEX(eh)[1].ei_block),
+ 14U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_check_inode(inode), 0);
+
+ path = ext4_find_extent(inode, 12, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, path->p_depth, 2);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[2].p_ext->ee_block), 12U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(path[2].p_ext), 9000ULL);
+ ext4_free_ext_path(path);
+
+ path = ext4_find_extent(inode, 14, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, path->p_depth, 2);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[2].p_ext->ee_block), 14U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(path[2].p_ext), 2004ULL);
+ ext4_free_ext_path(path);
+
+ path = ext4_find_extent(inode, 48, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+ KUNIT_EXPECT_EQ(test, path->p_depth, 2);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(path[2].p_ext->ee_block), 48U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(path[2].p_ext), 2038ULL);
+ ext4_free_ext_path(path);
+}
+
/*
* Verify ext4_ext_remove_space() for middle-of-extent hole punching, full
* extent removal with left-scooting, and ext4_ext_truncate() tail removal.
@@ -2073,17 +2265,13 @@ static void test_swap_extents(struct kunit *test)
KUNIT_ASSERT_EQ(test, ext_insert_one(inode1, 10, 10, 100, false, 0), 0);
KUNIT_ASSERT_EQ(test, ext_insert_one(inode2, 10, 10, 200, false, 0), 0);
- inode_lock(inode1);
- inode_lock(inode2);
- down_write(&EXT4_I(inode1)->i_data_sem);
- down_write(&EXT4_I(inode2)->i_data_sem);
+ lock_two_nondirectories(inode1, inode2);
+ ext4_double_down_write_data_sem(inode1, inode2);
replaced = ext4_swap_extents(NULL, inode1, inode2, 12, 12, 5, 1, &err);
- up_write(&EXT4_I(inode2)->i_data_sem);
- up_write(&EXT4_I(inode1)->i_data_sem);
- inode_unlock(inode2);
- inode_unlock(inode1);
+ ext4_double_up_write_data_sem(inode1, inode2);
+ unlock_two_nondirectories(inode1, inode2);
KUNIT_EXPECT_EQ(test, err, 0);
KUNIT_EXPECT_EQ(test, replaced, 5);
@@ -2787,6 +2975,8 @@ static struct kunit_case extents_test_cases[] = {
KUNIT_CASE(test_can_extents_be_merged),
KUNIT_CASE(test_insert_extent_merge_and_nomerge),
KUNIT_CASE(test_insert_extent_grow_and_merge_up),
+ KUNIT_CASE(test_insert_extent_leaf_split),
+ KUNIT_CASE(test_insert_extent_index_split),
KUNIT_CASE(test_remove_space_tail_and_hole_punch),
KUNIT_CASE(test_remove_space_multilevel_collapse),
KUNIT_CASE(test_remove_space_bigalloc_partial_cluster),
diff --git a/fs/ext4/move_extent.c b/fs/ext4/move_extent.c
index 3329b7ad5dbd..d1405e763912 100644
--- a/fs/ext4/move_extent.c
+++ b/fs/ext4/move_extent.c
@@ -656,3 +656,8 @@ int ext4_move_extents(struct file *o_filp, struct file *d_filp, __u64 orig_blk,
unlock_two_nondirectories(orig_inode, donor_inode);
return ret;
}
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_double_down_write_data_sem);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_double_up_write_data_sem);
+#endif
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 04/27] ext4: add KUnit tests for fs/ext4/xattr.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (2 preceding siblings ...)
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:06 ` 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
` (22 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
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
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 05/27] ext4: add KUnit tests for fs/ext4/namei.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (3 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 04/27] ext4: add KUnit tests for fs/ext4/xattr.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (21 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add the ext4_namei_test KUnit test suite in fs/ext4/namei-test.c and
shared internal HTree definitions in fs/ext4/namei.h to exercise
directory lookup, HTree indexing, and namespace operations in
fs/ext4/namei.c without requiring a mounted block device:
- Directory leaf tail initialization (ext4_initialize_dirent_tail),
metadata CRC32C checksum calculation and verification
(ext4_dirblock_csum_verify, ext4_handle_dirty_dirblock), and
corrupted tail/checksum detection.
- HTree root and interior node limit calculations (dx_root_limit,
dx_node_limit), entry accessors (dx_get_block, dx_set_block,
dx_get_hash, dx_set_hash, dx_get_count, dx_get_limit, dx_set_count,
dx_set_limit), and index block checksum verification
(ext4_dx_csum_set, ext4_dx_csum_verify).
- Directory block reading (__ext4_read_dirblock) across 10
parameterized cases covering valid leaf and index blocks, unmapped
holes (EITHER vs. block 0 vs. DIRENT_HTREE), reads past i_size,
ext4_bread -EIO propagation, leaf block found when expecting INDEX,
and corrupted leaf/index checksums (-EFSBADCRC).
- New directory initialization (ext4_init_new_dir, ext4_init_dirblock
with and without inline directory conversion data) and directory
block appending (ext4_append), including s_max_dir_size_kb limit
enforcement (-ENOSPC) and pre-allocated logical block detection
(-EFSCORRUPTED).
- Directory entry destination search (ext4_find_dest_de), insertion
(ext4_insert_dentry), linear search (ext4_search_dir), and deletion
(ext4_delete_entry, ext4_generic_delete_entry) for both first-entry
inode zeroing and predecessor rec_len merging.
- Directory emptiness checks (ext4_empty_dir) across fresh, populated,
deleted, multi-block with unmapped holes, and corrupted '.'/'..'
directories.
- HTree leaf splitting and index helpers (dx_make_map, dx_sort_map
exercising both combsort and bubble-sort phases, dx_move_dirents,
dx_pack_dirents, and dx_insert_block).
- HTree root validation (ext4_check_dx_root), multi-level index probing
(dx_probe), and continuation block traversal (ext4_htree_next_block)
across 14 parameterized valid and corrupted on-disk layouts (invalid
'.'/'..' entries, bad hash_version, SipHash without hash-in-dirent,
non-zero unused_flags, excessive indirect_levels, limit mismatch,
zero entry count, and tree cycle detection).
- End-to-end HTree conversion (make_indexed_dir), leaf splitting
(do_split / ext4_dx_add_entry), indexed lookup (ext4_dx_find_entry),
and readdir red-black tree population (ext4_htree_fill_tree).
- HTree index level growth (splitting a full dx_root into a 2-level
tree and splitting a full interior dx_node), max HTree level -ENOSPC
enforcement, and corrupted dx_root fallback in __ext4_find_entry and
__ext4_add_entry.
- Inline directory lifecycle through namei.c entry points
(ext4_init_new_dir, ext4_empty_dir, ext4_add_entry, ext4_find_entry,
ext4_delete_entry, and ext4_htree_fill_tree).
- Directory link count helpers (ext4_inc_count, ext4_dec_count
including EXT4_LINK_MAX wrap to 1), VFS lookup (ext4_lookup) and
parent resolution (ext4_get_parent) error paths (-ENAMETOOLONG,
self-linked directory entry, invalid inode number, and -ESTALE to
-EFSCORRUPTED translation).
- Hard link creation (__ext4_link, ext4_link including 0-nlink tmpfile
orphan removal, EXT4_INODE_PROJINHERIT -EXDEV, and EXT4_LINK_MAX
-EMLINK), unlinking (__ext4_unlink, ext4_unlink including orphan
addition and EXT4_FC_REPLAY inode mismatch handling), and directory
removal (ext4_rmdir).
- Same-directory rename, overwrite rename, cross-directory subdirectory
rename, RENAME_EXCHANGE (ext4_cross_rename), project quota
inheritance -EXDEV checks, 0-nlink target -EFSCORRUPTED check, slow
symlink block initialization (ext4_init_symlink_block), and
case-folded (EXT4_INODE_CASEFOLD) directory lookup and strict UTF-8
validation.
Export the tested functions with EXPORT_SYMBOL_FOR_EXT4_TEST and add
KUNIT_STATIC_STUB_REDIRECT hooks for ext4_orphan_add and
ext4_orphan_del.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 3 +-
fs/ext4/crypto.c | 4 +
fs/ext4/dir.c | 4 +
fs/ext4/namei-test.c | 2304 ++++++++++++++++++++++++++++++++++++++++++
fs/ext4/namei.c | 338 ++++---
fs/ext4/namei.h | 213 ++++
fs/ext4/orphan.c | 7 +
7 files changed, 2708 insertions(+), 165 deletions(-)
create mode 100644 fs/ext4/namei-test.c
create mode 100644 fs/ext4/namei.h
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 13040b97d2ff..8fe7b512ebb4 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -16,7 +16,8 @@ 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 xattr-test.o
+ extents_status-test.o xattr-test.o \
+ namei-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/crypto.c b/fs/ext4/crypto.c
index 1a0fccb084ef..5f0b0f288c91 100644
--- a/fs/ext4/crypto.c
+++ b/fs/ext4/crypto.c
@@ -37,6 +37,7 @@ int ext4_fname_setup_filename(struct inode *dir, const struct qstr *iname,
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fname_setup_filename);
int ext4_fname_prepare_lookup(struct inode *dir, struct dentry *dentry,
struct ext4_filename *fname)
@@ -55,6 +56,7 @@ int ext4_fname_prepare_lookup(struct inode *dir, struct dentry *dentry,
ext4_fname_free_filename(fname);
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fname_prepare_lookup);
void ext4_fname_free_filename(struct ext4_filename *fname)
{
@@ -69,6 +71,7 @@ void ext4_fname_free_filename(struct ext4_filename *fname)
ext4_fname_free_ci_filename(fname);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fname_free_filename);
static bool uuid_is_zero(__u8 u[16])
{
@@ -246,3 +249,4 @@ const struct fscrypt_operations ext4_cryptops = {
.empty_dir = ext4_empty_dir,
.has_stable_inodes = ext4_has_stable_inodes,
};
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_cryptops);
diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
index 8d7b81e6948e..4214034b5aba 100644
--- a/fs/ext4/dir.c
+++ b/fs/ext4/dir.c
@@ -126,6 +126,7 @@ int __ext4_check_dir_entry(const char *function, unsigned int line,
return 1;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_check_dir_entry);
static int ext4_readdir(struct file *file, struct dir_context *ctx)
{
@@ -475,6 +476,7 @@ void ext4_htree_free_dir_info(struct dir_private_info *p)
free_rb_tree_fname(&p->root);
kfree(p);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_htree_free_dir_info);
/*
* Given a directory entry, enter it into the fname rb tree.
@@ -535,6 +537,7 @@ int ext4_htree_store_dirent(struct file *dir_file, __u32 hash,
rb_insert_color(&new_fn->rb_hash, &info->root);
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_htree_store_dirent);
@@ -683,6 +686,7 @@ int ext4_check_all_de(struct inode *dir, struct buffer_head *bh, void *buf,
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_check_all_de);
static int ext4_dir_open(struct inode *inode, struct file *file)
{
diff --git a/fs/ext4/namei-test.c b/fs/ext4/namei-test.c
new file mode 100644
index 000000000000..df5affef1289
--- /dev/null
+++ b/fs/ext4/namei-test.c
@@ -0,0 +1,2304 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for ext4 directory lookup, HTree indexing, and namespace
+ * operations (fs/ext4/namei.c).
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/iversion.h>
+#include <linux/quotaops.h>
+#include <linux/rbtree.h>
+#include <linux/rcupdate.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/unicode.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "xattr.h"
+#include "namei.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 64
+#define TEST_MAX_LBLOCKS 16
+#define TEST_MAX_INODES 8
+
+#define TEST_ROOT_INO EXT4_ROOT_INO
+#define TEST_DIR_INO 12
+#define TEST_DIR2_INO 13
+#define TEST_SUBDIR_INO 14
+#define TEST_FILE1_INO 15
+#define TEST_FILE2_INO 16
+#define TEST_FILE3_INO 17
+#define TEST_CSUM_SEED 0x12345678U
+
+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[TEST_MAX_LBLOCKS];
+};
+
+struct namei_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 mock_inode_slot inodes[TEST_MAX_INODES];
+ struct mock_block blocks[TEST_MAX_BLOCKS];
+
+ int fail_map_blocks;
+ int force_map_blocks_ret;
+ int fail_getblk;
+ int fail_get_inode_loc;
+ int fail_mark_iloc_dirty;
+ int fail_dirty_metadata;
+ int fail_iget;
+
+ unsigned long last_iget_ino;
+ unsigned int orphan_add_calls;
+ unsigned long last_orphan_add_ino;
+ unsigned int orphan_del_calls;
+ unsigned long last_orphan_del_ino;
+ unsigned int dirty_metadata_calls;
+ unsigned int mark_inode_dirty_calls;
+};
+
+static const struct super_operations namei_test_sops = {};
+
+#ifdef CONFIG_QUOTA
+static qsize_t *namei_test_get_reserved_space(struct inode *inode)
+{
+ return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations namei_test_qops = {
+ .get_reserved_space = namei_test_get_reserved_space,
+};
+#endif
+
+static void init_mock_inode_slot(struct namei_test_ctx *ctx,
+ struct mock_inode_slot *slot,
+ unsigned long ino, umode_t mode)
+{
+ 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 = mode;
+ inode->i_blkbits = TEST_BLK_BITS;
+ inode->i_mapping = &inode->i_data;
+ set_nlink(inode, S_ISDIR(mode) ? 2 : 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() is a fastpath
+ * no-op that does not touch superblock/bdi writeback lists.
+ */
+ inode_state_set_raw(inode, I_DIRTY);
+
+ init_rwsem(&slot->ei.xattr_sem);
+ init_rwsem(&slot->ei.i_data_sem);
+ INIT_LIST_HEAD(&slot->ei.i_orphan);
+ INIT_LIST_HEAD(&slot->ei.i_fc_list);
+ spin_lock_init(&slot->ei.i_fc_lock);
+ spin_lock_init(&slot->ei.i_raw_lock);
+
+ slot->ei.i_extra_isize = TEST_EXTRA_ISIZE;
+ slot->ei.i_csum_seed = TEST_CSUM_SEED ^ (u32)ino;
+ slot->ei.i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+
+ slot->iloc_bh.b_data = slot->raw_buf;
+ slot->iloc_bh.b_size = TEST_INODE_SIZE;
+ slot->iloc_bh.b_blocknr = 10 + ino;
+ atomic_set(&slot->iloc_bh.b_count, 1);
+ set_buffer_uptodate(&slot->iloc_bh);
+
+ raw_inode = (struct ext4_inode *)slot->raw_buf;
+ raw_inode->i_extra_isize = cpu_to_le16(TEST_EXTRA_ISIZE);
+}
+
+static struct mock_inode_slot *find_inode_slot_by_ino(struct namei_test_ctx *ctx,
+ unsigned long ino)
+{
+ int i;
+
+ for (i = 0; i < TEST_MAX_INODES; i++) {
+ if (ctx->inodes[i].allocated &&
+ ctx->inodes[i].ei.vfs_inode.i_ino == ino)
+ return &ctx->inodes[i];
+ }
+ return NULL;
+}
+
+static struct mock_inode_slot *find_inode_slot(struct namei_test_ctx *ctx,
+ struct inode *inode)
+{
+ return find_inode_slot_by_ino(ctx, inode->i_ino);
+}
+
+static struct inode *get_mock_inode(struct namei_test_ctx *ctx,
+ unsigned long ino)
+{
+ struct mock_inode_slot *slot = find_inode_slot_by_ino(ctx, ino);
+
+ return slot ? &slot->ei.vfs_inode : NULL;
+}
+
+static struct mock_block *get_mock_block(struct namei_test_ctx *ctx,
+ ext4_fsblk_t pblk)
+{
+ unsigned long idx;
+
+ if (pblk < TEST_FIRST_PBLK)
+ return NULL;
+ idx = pblk - TEST_FIRST_PBLK;
+ if (idx >= TEST_MAX_BLOCKS)
+ return NULL;
+ return &ctx->blocks[idx];
+}
+
+static ext4_fsblk_t alloc_mock_pblk(struct namei_test_ctx *ctx)
+{
+ int i;
+
+ for (i = 0; i < TEST_MAX_BLOCKS; i++) {
+ if (!ctx->blocks[i].in_use) {
+ ctx->blocks[i].in_use = true;
+ memset(ctx->blocks[i].data, 0, TEST_BLK_SIZE);
+ set_buffer_uptodate(&ctx->blocks[i].bh);
+ clear_buffer_verified(&ctx->blocks[i].bh);
+ return TEST_FIRST_PBLK + i;
+ }
+ }
+ return 0;
+}
+
+static struct buffer_head *map_and_get_dir_bh(struct namei_test_ctx *ctx,
+ struct inode *inode,
+ ext4_lblk_t lblk)
+{
+ struct mock_inode_slot *slot = find_inode_slot(ctx, inode);
+ struct mock_block *blk;
+ ext4_fsblk_t pblk;
+
+ if (!slot || lblk >= TEST_MAX_LBLOCKS)
+ return NULL;
+ pblk = slot->lblk_to_pblk[lblk];
+ if (!pblk) {
+ pblk = alloc_mock_pblk(ctx);
+ if (!pblk)
+ return NULL;
+ slot->lblk_to_pblk[lblk] = pblk;
+ }
+ blk = get_mock_block(ctx, pblk);
+ return blk ? &blk->bh : NULL;
+}
+
+static void init_mock_dentry(struct dentry *dentry, struct dentry *parent,
+ const char *name, struct inode *inode,
+ struct super_block *sb)
+{
+ memset(dentry, 0, sizeof(*dentry));
+ spin_lock_init(&dentry->d_lock);
+ seqcount_spinlock_init(&dentry->d_seq, &dentry->d_lock);
+ INIT_HLIST_NODE(&dentry->d_sib);
+ INIT_HLIST_HEAD(&dentry->d_children);
+ INIT_HLIST_BL_NODE(&dentry->d_hash);
+ dentry->d_parent = parent ? parent : dentry;
+ dentry->d_sb = sb;
+ dentry->__d_name.name = (const unsigned char *)name;
+ dentry->__d_name.len = strlen(name);
+ if (inode) {
+ dentry->d_inode = inode;
+ dentry->d_flags = S_ISDIR(inode->i_mode) ?
+ DCACHE_DIRECTORY_TYPE : DCACHE_REGULAR_TYPE;
+ } else {
+ dentry->d_flags = DCACHE_MISS_TYPE;
+ }
+}
+
+/* --- KUnit Static Stubs --- */
+
+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 namei_test_ctx *ctx = test->priv;
+ struct mock_inode_slot *slot;
+ ext4_fsblk_t pblk;
+
+ if (ctx->fail_map_blocks)
+ return ctx->fail_map_blocks;
+ if (ctx->force_map_blocks_ret)
+ return ctx->force_map_blocks_ret;
+
+ slot = find_inode_slot(ctx, inode);
+ if (!slot || map->m_lblk >= TEST_MAX_LBLOCKS)
+ return -EFSCORRUPTED;
+
+ pblk = slot->lblk_to_pblk[map->m_lblk];
+ if (pblk) {
+ map->m_pblk = pblk;
+ map->m_len = 1;
+ map->m_flags = EXT4_MAP_MAPPED;
+ return 1;
+ }
+
+ if (!(flags & EXT4_GET_BLOCKS_CREATE)) {
+ map->m_pblk = 0;
+ map->m_flags = 0;
+ return 0;
+ }
+
+ pblk = alloc_mock_pblk(ctx);
+ if (!pblk)
+ return -ENOSPC;
+
+ slot->lblk_to_pblk[map->m_lblk] = pblk;
+ map->m_pblk = pblk;
+ map->m_len = 1;
+ map->m_flags = EXT4_MAP_MAPPED | EXT4_MAP_NEW;
+ 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 namei_test_ctx *ctx = test->priv;
+ struct mock_inode_slot *slot;
+ struct mock_block *blk;
+ ext4_fsblk_t pblk;
+
+ if (ctx->fail_getblk)
+ return ERR_PTR(ctx->fail_getblk);
+
+ slot = find_inode_slot(ctx, inode);
+ if (!slot || block >= TEST_MAX_LBLOCKS)
+ return ERR_PTR(-EFSCORRUPTED);
+
+ pblk = slot->lblk_to_pblk[block];
+ if (!pblk) {
+ if (!(map_flags & EXT4_GET_BLOCKS_CREATE))
+ return NULL;
+ pblk = alloc_mock_pblk(ctx);
+ if (!pblk)
+ return ERR_PTR(-ENOSPC);
+ slot->lblk_to_pblk[block] = pblk;
+ }
+
+ blk = get_mock_block(ctx, pblk);
+ if (!blk)
+ return ERR_PTR(-EFSCORRUPTED);
+
+ get_bh(&blk->bh);
+ return &blk->bh;
+}
+
+static int ext4_get_inode_loc_stub(struct inode *inode, struct ext4_iloc *iloc)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct namei_test_ctx *ctx = test->priv;
+ struct mock_inode_slot *slot;
+
+ if (ctx->fail_get_inode_loc)
+ return ctx->fail_get_inode_loc;
+
+ slot = find_inode_slot(ctx, inode);
+ if (!slot)
+ return -ENOENT;
+
+ 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 namei_test_ctx *ctx = test->priv;
+
+ brelse(iloc->bh);
+ iloc->bh = NULL;
+ 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 namei_test_ctx *ctx = test->priv;
+
+ ctx->mark_inode_dirty_calls++;
+ if (ctx->fail_mark_iloc_dirty)
+ return ctx->fail_mark_iloc_dirty;
+ 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 namei_test_ctx *ctx = test->priv;
+
+ ctx->dirty_metadata_calls++;
+ if (ctx->fail_dirty_metadata)
+ return ctx->fail_dirty_metadata;
+ return 0;
+}
+
+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 namei_test_ctx *ctx = test->priv;
+
+ ctx->last_iget_ino = ino;
+ if (ctx->fail_iget)
+ return ERR_PTR(ctx->fail_iget);
+ return ERR_PTR(-ENOENT);
+}
+
+static int ext4_orphan_add_stub(handle_t *handle, struct inode *inode)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct namei_test_ctx *ctx = test->priv;
+
+ ctx->orphan_add_calls++;
+ ctx->last_orphan_add_ino = inode->i_ino;
+ if (list_empty(&EXT4_I(inode)->i_orphan))
+ list_add(&EXT4_I(inode)->i_orphan, &ctx->sbi.s_orphan);
+ return 0;
+}
+
+static int ext4_orphan_del_stub(handle_t *handle, struct inode *inode)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct namei_test_ctx *ctx = test->priv;
+
+ ctx->orphan_del_calls++;
+ ctx->last_orphan_del_ino = inode->i_ino;
+ list_del_init(&EXT4_I(inode)->i_orphan);
+ return 0;
+}
+
+static int namei_test_init(struct kunit *test)
+{
+ struct namei_test_ctx *ctx;
+ int i;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+ test->priv = ctx;
+
+ ctx->sb.s_blocksize = TEST_BLK_SIZE;
+ ctx->sb.s_blocksize_bits = TEST_BLK_BITS;
+ ctx->sb.s_magic = EXT4_SUPER_MAGIC;
+ ctx->sb.s_maxbytes = MAX_LFS_FILESIZE;
+ ctx->sb.s_time_gran = 1;
+ ctx->sb.s_op = &namei_test_sops;
+ ctx->sb.s_fs_info = &ctx->sbi;
+ ctx->sb.s_bdev = &ctx->bdev;
+#ifdef CONFIG_FS_ENCRYPTION
+ ctx->sb.s_cop = &ext4_cryptops;
+#endif
+#ifdef CONFIG_QUOTA
+ ctx->sb.dq_op = &namei_test_qops;
+#endif
+ ctx->bdev.bd_mapping = &ctx->bd_mapping;
+ bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+ init_rwsem(&ctx->sb.s_umount);
+ strscpy(ctx->sb.s_id, "ext4_namei_kunit", sizeof(ctx->sb.s_id));
+
+ ctx->sbi.s_sb = &ctx->sb;
+ ctx->sbi.s_es = &ctx->es;
+ ctx->sbi.s_sbh = &ctx->sb_bh;
+ ctx->sbi.s_inode_size = TEST_INODE_SIZE;
+ ctx->sbi.s_first_ino = EXT4_GOOD_OLD_FIRST_INO;
+ ctx->sbi.s_want_extra_isize = TEST_EXTRA_ISIZE;
+ ctx->sbi.s_def_hash_version = DX_HASH_HALF_MD4;
+ ctx->sbi.s_hash_unsigned = 3;
+ ctx->sbi.s_hash_seed[0] = 0x11223344U;
+ ctx->sbi.s_hash_seed[1] = 0x55667788U;
+ ctx->sbi.s_hash_seed[2] = 0x99aabbccU;
+ ctx->sbi.s_hash_seed[3] = 0xddeeff00U;
+ ctx->sbi.s_csum_seed = TEST_CSUM_SEED;
+ set_opt(&ctx->sb, ERRORS_CONT);
+ spin_lock_init(&ctx->sbi.s_bdev_wb_lock);
+ spin_lock_init(&ctx->sbi.s_error_lock);
+ spin_lock_init(&ctx->sbi.s_md_lock);
+ mutex_init(&ctx->sbi.s_orphan_lock);
+ INIT_LIST_HEAD(&ctx->sbi.s_orphan);
+ ratelimit_state_init(&ctx->sbi.s_err_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+ ratelimit_state_init(&ctx->sbi.s_warning_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+ ratelimit_state_init(&ctx->sbi.s_msg_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+
+ ctx->es.s_inodes_count = cpu_to_le32(1024);
+ ctx->es.s_first_ino = cpu_to_le32(EXT4_GOOD_OLD_FIRST_INO);
+ ctx->es.s_inode_size = cpu_to_le16(TEST_INODE_SIZE);
+ ctx->es.s_min_extra_isize = cpu_to_le16(TEST_EXTRA_ISIZE);
+ ctx->es.s_want_extra_isize = cpu_to_le16(TEST_EXTRA_ISIZE);
+ ctx->es.s_feature_incompat = cpu_to_le32(EXT4_FEATURE_INCOMPAT_FILETYPE);
+ ctx->es.s_feature_compat = cpu_to_le32(EXT4_FEATURE_COMPAT_DIR_INDEX);
+
+ ctx->sb_bh.b_data = (char *)&ctx->es;
+ ctx->sb_bh.b_size = TEST_BLK_SIZE;
+ ctx->sb_bh.b_blocknr = 1;
+ atomic_set(&ctx->sb_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->sb_bh);
+
+ for (i = 0; i < TEST_MAX_BLOCKS; i++) {
+ struct buffer_head *bh = &ctx->blocks[i].bh;
+
+ bh->b_data = ctx->blocks[i].data;
+ bh->b_size = TEST_BLK_SIZE;
+ bh->b_blocknr = TEST_FIRST_PBLK + i;
+ atomic_set(&bh->b_count, 1);
+ set_buffer_uptodate(bh);
+ }
+
+ init_mock_inode_slot(ctx, &ctx->inodes[0], TEST_ROOT_INO,
+ S_IFDIR | 0755);
+ init_mock_inode_slot(ctx, &ctx->inodes[1], TEST_DIR_INO,
+ S_IFDIR | 0755);
+ init_mock_inode_slot(ctx, &ctx->inodes[2], TEST_DIR2_INO,
+ S_IFDIR | 0755);
+ init_mock_inode_slot(ctx, &ctx->inodes[3], TEST_SUBDIR_INO,
+ S_IFDIR | 0755);
+ init_mock_inode_slot(ctx, &ctx->inodes[4], TEST_FILE1_INO,
+ S_IFREG | 0644);
+ init_mock_inode_slot(ctx, &ctx->inodes[5], TEST_FILE2_INO,
+ S_IFREG | 0644);
+ init_mock_inode_slot(ctx, &ctx->inodes[6], TEST_FILE3_INO,
+ S_IFLNK | 0777);
+
+ 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_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_iget, ext4_iget_stub);
+ kunit_activate_static_stub(test, ext4_orphan_add, ext4_orphan_add_stub);
+ kunit_activate_static_stub(test, ext4_orphan_del, ext4_orphan_del_stub);
+
+ return 0;
+}
+
+static void namei_test_exit(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+
+ if (!ctx)
+ return;
+#if IS_ENABLED(CONFIG_UNICODE)
+ utf8_unload(ctx->sb.s_encoding);
+ ctx->sb.s_encoding = NULL;
+#endif
+}
+
+/*
+ * Test 1: Directory leaf tail initialization, checksum computation, and
+ * verification (ext4_initialize_dirent_tail, ext4_dirblock_csum_verify,
+ * ext4_handle_dirty_dirblock).
+ */
+static void test_namei_dirent_tail_and_csum(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct buffer_head *bh;
+ struct ext4_dir_entry_tail *t;
+ int err;
+
+ bh = map_and_get_dir_bh(ctx, dir, 0);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+
+ /* Without METADATA_CSUM, verification always succeeds */
+ KUNIT_EXPECT_EQ(test, ext4_dirblock_csum_verify(dir, bh), 1);
+
+ /* Enable METADATA_CSUM */
+ ctx->es.s_feature_ro_compat |=
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+
+ /* Uninitialized tail fails verification */
+ KUNIT_EXPECT_EQ(test, ext4_dirblock_csum_verify(dir, bh), 0);
+
+ /* Initialize directory block and tail, then compute csum */
+ err = ext4_init_dirblock(NULL, dir, bh, TEST_ROOT_INO, NULL, 0);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ t = EXT4_DIRENT_TAIL(bh->b_data, TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, t->det_reserved_zero1, 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(t->det_rec_len),
+ sizeof(struct ext4_dir_entry_tail));
+ KUNIT_EXPECT_EQ(test, t->det_reserved_zero2, 0);
+ KUNIT_EXPECT_EQ(test, t->det_reserved_ft, EXT4_FT_DIR_CSUM);
+ KUNIT_EXPECT_EQ(test, ext4_dirblock_csum_verify(dir, bh), 1);
+
+ /* Flip a bit in the directory data -> checksum mismatch */
+ bh->b_data[0] ^= 0x01;
+ KUNIT_EXPECT_EQ(test, ext4_dirblock_csum_verify(dir, bh), 0);
+ bh->b_data[0] ^= 0x01;
+ KUNIT_EXPECT_EQ(test, ext4_dirblock_csum_verify(dir, bh), 1);
+
+ /* Corrupt tail reserved_ft -> get_dirent_tail() rejects it */
+ t->det_reserved_ft = EXT4_FT_REG_FILE;
+ KUNIT_EXPECT_EQ(test, ext4_dirblock_csum_verify(dir, bh), 0);
+ ext4_initialize_dirent_tail(bh, TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, ext4_handle_dirty_dirblock(NULL, dir, bh), 0);
+ KUNIT_EXPECT_EQ(test, ext4_dirblock_csum_verify(dir, bh), 1);
+}
+
+/*
+ * Test 2: HTree dx_entry accessors, dx_root_limit / dx_node_limit, and
+ * dx_root / dx_node metadata checksum verification (ext4_dx_csum_set_test,
+ * ext4_dx_csum_verify_test).
+ */
+static void test_namei_dx_helpers_and_csum(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct buffer_head *root_bh, *node_bh;
+ struct dx_root *root;
+ struct dx_node *node;
+ struct dx_entry *entries;
+
+ root_bh = map_and_get_dir_bh(ctx, dir, 0);
+ node_bh = map_and_get_dir_bh(ctx, dir, 1);
+ KUNIT_ASSERT_NOT_NULL(test, root_bh);
+ KUNIT_ASSERT_NOT_NULL(test, node_bh);
+
+ /* Check limits without METADATA_CSUM (1024B block) */
+ /* Root: (1024 - 12 - 12 - 8) / 8 = 124 */
+ KUNIT_EXPECT_EQ(test,
+ dx_root_limit(dir, sizeof(struct dx_root_info)), 124U);
+ /* Node: (1024 - 8) / 8 = 127 */
+ KUNIT_EXPECT_EQ(test, dx_node_limit(dir), 127U);
+
+ /* Enable METADATA_CSUM -> 1 fewer entry for 8-byte dx_tail */
+ ctx->es.s_feature_ro_compat |=
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+ KUNIT_EXPECT_EQ(test,
+ dx_root_limit(dir, sizeof(struct dx_root_info)), 123U);
+ KUNIT_EXPECT_EQ(test, dx_node_limit(dir), 126U);
+
+ /* Set up a valid dx_root in root_bh */
+ root = (struct dx_root *)root_bh->b_data;
+ root->dot.inode = cpu_to_le32(dir->i_ino);
+ root->dot.rec_len = ext4_rec_len_to_disk(12, TEST_BLK_SIZE);
+ root->dot.name_len = 1;
+ root->dot.file_type = EXT4_FT_DIR;
+ strscpy(root->dot_name, ".", sizeof(root->dot_name));
+
+ root->dotdot.inode = cpu_to_le32(TEST_ROOT_INO);
+ root->dotdot.rec_len = ext4_rec_len_to_disk(TEST_BLK_SIZE - 12,
+ TEST_BLK_SIZE);
+ root->dotdot.name_len = 2;
+ root->dotdot.file_type = EXT4_FT_DIR;
+ strscpy(root->dotdot_name, "..", sizeof(root->dotdot_name));
+
+ root->info.info_length = sizeof(struct dx_root_info);
+ root->info.hash_version = DX_HASH_HALF_MD4;
+ entries = root->entries;
+ dx_set_limit(entries, dx_root_limit(dir, sizeof(struct dx_root_info)));
+ dx_set_count(entries, 2);
+ dx_set_block(entries + 0, 0xf0000001U); /* high 4 bits masked */
+ dx_set_hash(entries + 1, 0x80001234U);
+ dx_set_block(entries + 1, 2);
+
+ KUNIT_EXPECT_EQ(test, dx_get_limit(entries), 123U);
+ KUNIT_EXPECT_EQ(test, dx_get_count(entries), 2U);
+ KUNIT_EXPECT_EQ(test, dx_get_block(entries + 0), 1U);
+ KUNIT_EXPECT_EQ(test, dx_get_hash(entries + 1), 0x80001234U);
+ KUNIT_EXPECT_EQ(test, dx_get_block(entries + 1), 2U);
+
+ ext4_dx_csum_set_test(dir, (struct ext4_dir_entry *)root_bh->b_data);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dx_csum_verify_test(dir,
+ (struct ext4_dir_entry *)root_bh->b_data), 1);
+
+ /* Corrupt hash -> csum verify fails */
+ dx_set_hash(entries + 1, 0x80001235U);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dx_csum_verify_test(dir,
+ (struct ext4_dir_entry *)root_bh->b_data), 0);
+ dx_set_hash(entries + 1, 0x80001234U);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dx_csum_verify_test(dir,
+ (struct ext4_dir_entry *)root_bh->b_data), 1);
+
+ /* Overflow limit -> no space for csum fails */
+ dx_set_limit(entries, 200);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dx_csum_verify_test(dir,
+ (struct ext4_dir_entry *)root_bh->b_data), 0);
+
+ /* Set up a valid dx_node in node_bh */
+ node = (struct dx_node *)node_bh->b_data;
+ node->fake.rec_len = ext4_rec_len_to_disk(TEST_BLK_SIZE, TEST_BLK_SIZE);
+ dx_set_limit(node->entries, dx_node_limit(dir));
+ dx_set_count(node->entries, 2);
+ dx_set_block(node->entries + 0, 3);
+ dx_set_hash(node->entries + 1, 0x40000000U);
+ dx_set_block(node->entries + 1, 4);
+
+ ext4_dx_csum_set_test(dir, (struct ext4_dir_entry *)node_bh->b_data);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dx_csum_verify_test(dir,
+ (struct ext4_dir_entry *)node_bh->b_data), 1);
+
+ /* Corrupt fake.rec_len -> get_dx_countlimit returns NULL */
+ node->fake.rec_len = ext4_rec_len_to_disk(12, TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dx_csum_verify_test(dir,
+ (struct ext4_dir_entry *)node_bh->b_data), 0);
+}
+
+/*
+ * Test 3: Parameterized tests for __ext4_read_dirblock (ext4_read_dirblock_test).
+ */
+enum read_dirblock_case_id {
+ RDB_VALID_LEAF,
+ RDB_VALID_INDEX,
+ RDB_HOLE_ALLOWED_EITHER,
+ RDB_PAST_ISIZE,
+ RDB_BREAD_EIO,
+ RDB_HOLE_AT_BLOCK0,
+ RDB_HOLE_FOR_HTREE_LEAF,
+ RDB_LEAF_INSTEAD_OF_INDEX,
+ RDB_BAD_LEAF_CSUM,
+ RDB_BAD_INDEX_CSUM,
+};
+
+struct read_dirblock_param {
+ const char *desc;
+ enum read_dirblock_case_id id;
+ ext4_lblk_t block;
+ enum dirblock_type type;
+ int expected_err;
+ bool expect_null;
+};
+
+static const struct read_dirblock_param read_dirblock_params[] = {
+ {
+ .desc = "valid leaf block with metadata_csum",
+ .id = RDB_VALID_LEAF,
+ .block = 0,
+ .type = DIRENT,
+ .expected_err = 0,
+ },
+ {
+ .desc = "valid dx_root index block with metadata_csum",
+ .id = RDB_VALID_INDEX,
+ .block = 0,
+ .type = INDEX,
+ .expected_err = 0,
+ },
+ {
+ .desc = "hole at block > 0 with EITHER returns NULL",
+ .id = RDB_HOLE_ALLOWED_EITHER,
+ .block = 1,
+ .type = EITHER,
+ .expected_err = 0,
+ .expect_null = true,
+ },
+ {
+ .desc = "reading block past i_size returns -EFSCORRUPTED",
+ .id = RDB_PAST_ISIZE,
+ .block = 2,
+ .type = EITHER,
+ .expected_err = -EFSCORRUPTED,
+ },
+ {
+ .desc = "ext4_bread -EIO error propagates",
+ .id = RDB_BREAD_EIO,
+ .block = 0,
+ .type = DIRENT,
+ .expected_err = -EIO,
+ },
+ {
+ .desc = "hole at block 0 returns -EFSCORRUPTED",
+ .id = RDB_HOLE_AT_BLOCK0,
+ .block = 0,
+ .type = EITHER,
+ .expected_err = -EFSCORRUPTED,
+ },
+ {
+ .desc = "hole for DIRENT_HTREE returns -EFSCORRUPTED",
+ .id = RDB_HOLE_FOR_HTREE_LEAF,
+ .block = 1,
+ .type = DIRENT_HTREE,
+ .expected_err = -EFSCORRUPTED,
+ },
+ {
+ .desc = "leaf block found when expecting INDEX returns -EFSCORRUPTED",
+ .id = RDB_LEAF_INSTEAD_OF_INDEX,
+ .block = 1,
+ .type = INDEX,
+ .expected_err = -EFSCORRUPTED,
+ },
+ {
+ .desc = "corrupted leaf checksum returns -EFSBADCRC",
+ .id = RDB_BAD_LEAF_CSUM,
+ .block = 0,
+ .type = DIRENT,
+ .expected_err = -EFSBADCRC,
+ },
+ {
+ .desc = "corrupted index checksum returns -EFSBADCRC",
+ .id = RDB_BAD_INDEX_CSUM,
+ .block = 0,
+ .type = INDEX,
+ .expected_err = -EFSBADCRC,
+ },
+};
+
+static void read_dirblock_get_desc(const struct read_dirblock_param *p,
+ char *desc)
+{
+ strscpy(desc, p->desc, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(read_dirblock, read_dirblock_params, read_dirblock_get_desc);
+
+static void test_namei_read_dirblock(struct kunit *test)
+{
+ const struct read_dirblock_param *p = test->param_value;
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct buffer_head *bh, *res;
+ struct dx_root *root;
+
+ ctx->es.s_feature_ro_compat |=
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+ dir->i_size = 2 * TEST_BLK_SIZE;
+
+ switch (p->id) {
+ case RDB_VALID_LEAF:
+ bh = map_and_get_dir_bh(ctx, dir, 0);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ KUNIT_ASSERT_EQ(test,
+ ext4_init_dirblock(NULL, dir, bh, TEST_ROOT_INO,
+ NULL, 0), 0);
+ clear_buffer_verified(bh);
+ break;
+ case RDB_VALID_INDEX:
+ case RDB_BAD_INDEX_CSUM:
+ ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
+ bh = map_and_get_dir_bh(ctx, dir, 0);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ root = (struct dx_root *)bh->b_data;
+ root->dot.inode = cpu_to_le32(dir->i_ino);
+ root->dot.rec_len = ext4_rec_len_to_disk(12, TEST_BLK_SIZE);
+ root->dot.name_len = 1;
+ strscpy(root->dot_name, ".", sizeof(root->dot_name));
+ root->dotdot.inode = cpu_to_le32(TEST_ROOT_INO);
+ root->dotdot.rec_len = ext4_rec_len_to_disk(TEST_BLK_SIZE - 12,
+ TEST_BLK_SIZE);
+ root->dotdot.name_len = 2;
+ strscpy(root->dotdot_name, "..", sizeof(root->dotdot_name));
+ root->info.info_length = sizeof(struct dx_root_info);
+ root->info.hash_version = DX_HASH_HALF_MD4;
+ dx_set_limit(root->entries,
+ dx_root_limit(dir, sizeof(struct dx_root_info)));
+ dx_set_count(root->entries, 1);
+ dx_set_block(root->entries, 1);
+ ext4_dx_csum_set_test(dir, (struct ext4_dir_entry *)bh->b_data);
+ if (p->id == RDB_BAD_INDEX_CSUM)
+ dx_set_block(root->entries, 99);
+ clear_buffer_verified(bh);
+ break;
+ case RDB_HOLE_ALLOWED_EITHER:
+ case RDB_PAST_ISIZE:
+ case RDB_HOLE_AT_BLOCK0:
+ case RDB_HOLE_FOR_HTREE_LEAF:
+ break;
+ case RDB_BREAD_EIO:
+ ctx->fail_getblk = -EIO;
+ break;
+ case RDB_LEAF_INSTEAD_OF_INDEX:
+ ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
+ bh = map_and_get_dir_bh(ctx, dir, 1);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ KUNIT_ASSERT_EQ(test,
+ ext4_init_dirblock(NULL, dir, bh, TEST_ROOT_INO,
+ NULL, 0), 0);
+ clear_buffer_verified(bh);
+ break;
+ case RDB_BAD_LEAF_CSUM:
+ bh = map_and_get_dir_bh(ctx, dir, 0);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ KUNIT_ASSERT_EQ(test,
+ ext4_init_dirblock(NULL, dir, bh, TEST_ROOT_INO,
+ NULL, 0), 0);
+ bh->b_data[0] ^= 0xff;
+ clear_buffer_verified(bh);
+ break;
+ }
+
+ res = ext4_read_dirblock_test(dir, p->block, p->type);
+ if (p->expected_err) {
+ KUNIT_EXPECT_TRUE(test, IS_ERR(res));
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), p->expected_err);
+ } else if (p->expect_null) {
+ KUNIT_EXPECT_NULL(test, res);
+ } else {
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(res));
+ KUNIT_EXPECT_TRUE(test, buffer_verified(res));
+ brelse(res);
+ }
+}
+
+/*
+ * Test 4: Directory initialization and block appending (ext4_init_new_dir,
+ * ext4_append_test, s_max_dir_size_kb limit, already-allocated block check,
+ * and ext4_init_dirblock with inline_buf conversion data).
+ */
+static void test_namei_append_and_init_dir(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *root_dir = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct buffer_head *bh;
+ struct ext4_dir_entry_2 *de;
+ char inline_entries[64];
+ ext4_lblk_t lblk = 0;
+ int err;
+
+ ctx->es.s_feature_ro_compat |=
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+
+ err = ext4_init_new_dir(NULL, root_dir, dir);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, dir->i_nlink, 2U);
+ KUNIT_EXPECT_EQ(test, dir->i_size, (loff_t)TEST_BLK_SIZE);
+
+ /* Append a second block (lblk 1) */
+ bh = ext4_append_test(NULL, dir, &lblk);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(bh));
+ KUNIT_EXPECT_EQ(test, lblk, 1U);
+ KUNIT_EXPECT_EQ(test, dir->i_size, (loff_t)(2 * TEST_BLK_SIZE));
+ brelse(bh);
+
+ /* Enforce s_max_dir_size_kb limit -> -ENOSPC */
+ ctx->sbi.s_max_dir_size_kb = 2;
+ bh = ext4_append_test(NULL, dir, &lblk);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -ENOSPC);
+ ctx->sbi.s_max_dir_size_kb = 0;
+
+ /* Pre-allocated logical block at append target -> -EFSCORRUPTED */
+ ctx->force_map_blocks_ret = 1;
+ bh = ext4_append_test(NULL, dir, &lblk);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSCORRUPTED);
+ ctx->force_map_blocks_ret = 0;
+
+ /* Test ext4_init_dirblock with non-NULL inline_buf */
+ memset(inline_entries, 0, sizeof(inline_entries));
+ de = (struct ext4_dir_entry_2 *)inline_entries;
+ de->inode = cpu_to_le32(TEST_FILE1_INO);
+ de->name_len = 3;
+ de->file_type = EXT4_FT_REG_FILE;
+ memcpy(de->name, "foo", 3);
+ de->rec_len = ext4_rec_len_to_disk(sizeof(inline_entries),
+ TEST_BLK_SIZE);
+
+ bh = map_and_get_dir_bh(ctx, dir, 1);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ err = ext4_init_dirblock(NULL, dir, bh, TEST_ROOT_INO,
+ inline_entries, sizeof(inline_entries));
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ext4_dirblock_csum_verify(dir, bh), 1);
+
+ /* Verify '.', '..', and 'foo' in the initialized block */
+ de = (struct ext4_dir_entry_2 *)bh->b_data;
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), dir->i_ino);
+ de = ext4_next_entry(de, TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), TEST_ROOT_INO);
+ de = ext4_next_entry(de, TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), TEST_FILE1_INO);
+ KUNIT_EXPECT_EQ(test, de->name_len, 3);
+ KUNIT_EXPECT_MEMEQ(test, de->name, "foo", 3);
+ KUNIT_EXPECT_EQ(test, ext4_rec_len_from_disk(de->rec_len, TEST_BLK_SIZE),
+ TEST_BLK_SIZE - 24 -
+ (int)sizeof(struct ext4_dir_entry_tail));
+}
+
+/*
+ * Test 5: Directory entry destination search, insertion, linear lookup, and
+ * deletion (ext4_find_dest_de, ext4_insert_dentry, ext4_search_dir,
+ * ext4_delete_entry_test, ext4_generic_delete_entry).
+ */
+static void test_namei_search_insert_delete_dentry(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *root_dir = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+ struct buffer_head *bh;
+ struct ext4_dir_entry_2 *de = NULL, *res_de = NULL;
+ struct ext4_filename fname1, fname2;
+ struct qstr q1 = QSTR_INIT("alpha.txt", 9);
+ struct qstr q2 = QSTR_INIT("beta.txt", 8);
+ int csum_size = sizeof(struct ext4_dir_entry_tail);
+ int buf_size = TEST_BLK_SIZE - csum_size;
+ int err;
+
+ ctx->es.s_feature_ro_compat |=
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir), 0);
+ bh = map_and_get_dir_bh(ctx, dir, 0);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+
+ KUNIT_ASSERT_EQ(test, ext4_fname_setup_filename(dir, &q1, 0, &fname1), 0);
+ KUNIT_ASSERT_EQ(test, ext4_fname_setup_filename(dir, &q2, 0, &fname2), 0);
+
+ /* Find space and insert alpha.txt */
+ err = ext4_find_dest_de(dir, bh, bh->b_data, buf_size, &fname1, &de);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_ASSERT_NOT_NULL(test, de);
+ ext4_insert_dentry(dir, file1, de, TEST_BLK_SIZE, &fname1);
+ KUNIT_ASSERT_EQ(test, ext4_handle_dirty_dirblock(NULL, dir, bh), 0);
+
+ /* Duplicate insert of alpha.txt must return -EEXIST */
+ err = ext4_find_dest_de(dir, bh, bh->b_data, buf_size, &fname1, &de);
+ KUNIT_EXPECT_EQ(test, err, -EEXIST);
+
+ /* Insert beta.txt */
+ err = ext4_find_dest_de(dir, bh, bh->b_data, buf_size, &fname2, &de);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ ext4_insert_dentry(dir, file2, de, TEST_BLK_SIZE, &fname2);
+ KUNIT_ASSERT_EQ(test, ext4_handle_dirty_dirblock(NULL, dir, bh), 0);
+
+ /* Search for both entries via ext4_search_dir */
+ err = ext4_search_dir(bh, bh->b_data, buf_size, dir, &fname1, 0,
+ &res_de);
+ KUNIT_ASSERT_EQ(test, err, 1);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(res_de->inode), TEST_FILE1_INO);
+ KUNIT_EXPECT_EQ(test, res_de->file_type, EXT4_FT_REG_FILE);
+
+ /* Delete alpha.txt via ext4_delete_entry_test (merges into '..') */
+ err = ext4_delete_entry_test(NULL, dir, res_de, bh);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ext4_dirblock_csum_verify(dir, bh), 1);
+
+ /* alpha.txt is gone, beta.txt is still present */
+ KUNIT_EXPECT_EQ(test,
+ ext4_search_dir(bh, bh->b_data, buf_size, dir, &fname1, 0,
+ &res_de), 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_search_dir(bh, bh->b_data, buf_size, dir, &fname2, 0,
+ &res_de), 1);
+
+ /*
+ * Test deleting the very first entry in a buffer (pde == NULL),
+ * which zeroes de->inode while preserving rec_len.
+ */
+ de = (struct ext4_dir_entry_2 *)bh->b_data;
+ err = ext4_generic_delete_entry(dir, de, bh, bh->b_data,
+ TEST_BLK_SIZE, csum_size);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), 0U);
+ KUNIT_EXPECT_EQ(test, ext4_rec_len_from_disk(de->rec_len, TEST_BLK_SIZE),
+ 12);
+
+ /* Deleting a pointer not in the buffer returns -ENOENT */
+ err = ext4_generic_delete_entry(dir,
+ (struct ext4_dir_entry_2 *)(bh->b_data + 500),
+ bh, bh->b_data, TEST_BLK_SIZE, csum_size);
+ KUNIT_EXPECT_EQ(test, err, -ENOENT);
+
+ ext4_fname_free_filename(&fname1);
+ ext4_fname_free_filename(&fname2);
+}
+
+/*
+ * Test 6: Directory emptiness validation (ext4_empty_dir) across fresh,
+ * non-empty, deleted, corrupt, and multi-block directories.
+ */
+static void test_namei_empty_dir(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *root_dir = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct dentry parent_dentry, file_dentry;
+ struct buffer_head *bh, *bh1;
+ struct ext4_dir_entry_2 *de = NULL;
+ ext4_lblk_t lblk;
+
+ init_mock_dentry(&parent_dentry, NULL, "dir", dir, &ctx->sb);
+ init_mock_dentry(&file_dentry, &parent_dentry, "item", file1, &ctx->sb);
+
+ /* Freshly initialized directory is empty */
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir), 0);
+ KUNIT_EXPECT_TRUE(test, ext4_empty_dir(dir));
+
+ /* Truncated i_size < 24 bytes is rejected as non-empty (corrupt) */
+ dir->i_size = 12;
+ KUNIT_EXPECT_FALSE(test, ext4_empty_dir(dir));
+ dir->i_size = TEST_BLK_SIZE;
+
+ /* Add an entry -> not empty */
+ KUNIT_ASSERT_EQ(test, ext4_add_entry_test(NULL, &file_dentry, file1), 0);
+ KUNIT_EXPECT_FALSE(test, ext4_empty_dir(dir));
+
+ /* Delete the entry -> empty again */
+ bh = ext4_find_entry_test(dir, &file_dentry.d_name, &de, NULL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(bh));
+ KUNIT_ASSERT_EQ(test, ext4_delete_entry_test(NULL, dir, de, bh), 0);
+ brelse(bh);
+ KUNIT_EXPECT_TRUE(test, ext4_empty_dir(dir));
+
+ /* Multi-block directory with a hole at block 1 and an empty block 2 */
+ dir->i_size = 2 * TEST_BLK_SIZE; /* block 1 is unmapped (hole) */
+ bh1 = ext4_append_test(NULL, dir, &lblk); /* appends block 2 */
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(bh1));
+ KUNIT_EXPECT_EQ(test, lblk, 2U);
+ de = (struct ext4_dir_entry_2 *)bh1->b_data;
+ de->inode = 0;
+ de->rec_len = ext4_rec_len_to_disk(TEST_BLK_SIZE, TEST_BLK_SIZE);
+ KUNIT_EXPECT_TRUE(test, ext4_empty_dir(dir));
+
+ /* Active entry in block 2 makes directory non-empty */
+ de->inode = cpu_to_le32(TEST_FILE1_INO);
+ de->name_len = 1;
+ de->name[0] = 'x';
+ KUNIT_EXPECT_FALSE(test, ext4_empty_dir(dir));
+ de->inode = 0;
+ brelse(bh1);
+
+ /* Corrupt '..' name in block 0 -> returns false */
+ bh = map_and_get_dir_bh(ctx, dir, 0);
+ de = ext4_next_entry((struct ext4_dir_entry_2 *)bh->b_data,
+ TEST_BLK_SIZE);
+ de->name[1] = 'x';
+ KUNIT_EXPECT_FALSE(test, ext4_empty_dir(dir));
+ de->name[1] = '.';
+
+ /* Corrupt '.' inode number in block 0 -> returns false */
+ de = (struct ext4_dir_entry_2 *)bh->b_data;
+ de->inode = cpu_to_le32(999);
+ KUNIT_EXPECT_FALSE(test, ext4_empty_dir(dir));
+}
+
+/*
+ * Test 7: HTree leaf splitting helpers: dx_make_map_test, dx_sort_map_test
+ * (exercising both combsort and bubble sort phases), dx_move_dirents_test,
+ * dx_pack_dirents_test, and dx_insert_block_test.
+ */
+static void test_namei_dx_map_pack_and_move(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *root_dir = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct buffer_head *bh, *bh2;
+ struct dx_hash_info hinfo = {};
+ struct dx_map_entry *map;
+ struct ext4_dir_entry_2 *de, *last_de;
+ struct dx_frame frame;
+ struct dx_entry entries[8];
+ int i, count;
+
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir), 0);
+ bh = map_and_get_dir_bh(ctx, dir, 0);
+ bh2 = map_and_get_dir_bh(ctx, dir, 1);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ KUNIT_ASSERT_NOT_NULL(test, bh2);
+
+ /* Insert 16 entries into block 0 so count >= 18 (triggers combsort) */
+ for (i = 0; i < 16; i++) {
+ char name[16];
+ struct qstr q;
+ struct ext4_filename fname;
+
+ snprintf(name, sizeof(name), "entry_%02d", i);
+ q.name = (const unsigned char *)name;
+ q.len = strlen(name);
+ KUNIT_ASSERT_EQ(test,
+ ext4_fname_setup_filename(dir, &q, 0, &fname), 0);
+ KUNIT_ASSERT_EQ(test,
+ ext4_find_dest_de(dir, bh, bh->b_data, TEST_BLK_SIZE,
+ &fname, &de), 0);
+ ext4_insert_dentry(dir, file1, de, TEST_BLK_SIZE, &fname);
+ ext4_fname_free_filename(&fname);
+ }
+
+ hinfo.hash_version = DX_HASH_HALF_MD4 + ctx->sbi.s_hash_unsigned;
+ hinfo.seed = ctx->sbi.s_hash_seed;
+
+ map = (struct dx_map_entry *)(bh2->b_data + TEST_BLK_SIZE);
+ count = dx_make_map_test(dir, bh, &hinfo, map);
+ /* 2 ('.' and '..') + 16 = 18 entries */
+ KUNIT_ASSERT_EQ(test, count, 18);
+
+ map -= count;
+ dx_sort_map_test(map, count);
+ for (i = 1; i < count; i++)
+ KUNIT_EXPECT_LE(test, map[i - 1].hash, map[i].hash);
+
+ /* Move the second half (9 entries) to bh2 and pack the first half */
+ last_de = dx_move_dirents_test(dir, bh->b_data, bh2->b_data,
+ map + 9, 9, TEST_BLK_SIZE);
+ KUNIT_ASSERT_NOT_NULL(test, last_de);
+ last_de->rec_len = ext4_rec_len_to_disk(
+ bh2->b_data + TEST_BLK_SIZE - (char *)last_de, TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test,
+ ext4_check_all_de(dir, bh2, bh2->b_data, TEST_BLK_SIZE), 0);
+
+ last_de = dx_pack_dirents_test(dir, bh->b_data, TEST_BLK_SIZE);
+ KUNIT_ASSERT_NOT_NULL(test, last_de);
+ last_de->rec_len = ext4_rec_len_to_disk(
+ bh->b_data + TEST_BLK_SIZE - (char *)last_de, TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test,
+ ext4_check_all_de(dir, bh, bh->b_data, TEST_BLK_SIZE), 0);
+
+ /* Test dx_insert_block_test shifting entries in a dx_frame */
+ memset(entries, 0, sizeof(entries));
+ dx_set_limit(entries, 8);
+ dx_set_count(entries, 2);
+ dx_set_block(entries + 0, 1);
+ dx_set_hash(entries + 1, 0x80000000U);
+ dx_set_block(entries + 1, 2);
+
+ frame.entries = entries;
+ frame.at = entries + 0;
+ dx_insert_block_test(&frame, 0x40000000U, 3);
+ KUNIT_EXPECT_EQ(test, dx_get_count(entries), 3U);
+ KUNIT_EXPECT_EQ(test, dx_get_hash(entries + 1), 0x40000000U);
+ KUNIT_EXPECT_EQ(test, dx_get_block(entries + 1), 3U);
+ KUNIT_EXPECT_EQ(test, dx_get_hash(entries + 2), 0x80000000U);
+ KUNIT_EXPECT_EQ(test, dx_get_block(entries + 2), 2U);
+}
+
+/*
+ * Test 8: Parameterized tests for ext4_check_dx_root_test, dx_probe_test, and
+ * ext4_htree_next_block_test across valid 1-level / 2-level trees and 10
+ * on-disk corruption cases.
+ */
+enum dx_probe_case_id {
+ DXP_VALID_1LEVEL,
+ DXP_VALID_2LEVEL_AND_NEXT,
+ DXP_DOT_BAD_NAMELEN,
+ DXP_DOT_BAD_NAME,
+ DXP_DOT_BAD_RECLEN,
+ DXP_DOTDOT_BAD_NAMELEN,
+ DXP_DOTDOT_BAD_NAME,
+ DXP_BAD_HASH_VERSION,
+ DXP_SIPHASH_WITHOUT_IN_DIRENT,
+ DXP_UNUSED_FLAGS_SET,
+ DXP_INDIRECT_LEVEL_TOO_DEEP,
+ DXP_BAD_ROOT_LIMIT,
+ DXP_ZERO_COUNT,
+ DXP_TREE_CYCLE,
+};
+
+struct dx_probe_param {
+ const char *desc;
+ enum dx_probe_case_id id;
+ bool check_dx_root_only;
+ bool expect_ok;
+};
+
+static const struct dx_probe_param dx_probe_params[] = {
+ {
+ .desc = "valid 1-level dx_root binary search probe",
+ .id = DXP_VALID_1LEVEL,
+ .expect_ok = true,
+ },
+ {
+ .desc = "valid 2-level htree probe and ext4_htree_next_block",
+ .id = DXP_VALID_2LEVEL_AND_NEXT,
+ .expect_ok = true,
+ },
+ {
+ .desc = "ext4_check_dx_root rejects dot name_len != 1",
+ .id = DXP_DOT_BAD_NAMELEN,
+ .check_dx_root_only = true,
+ .expect_ok = false,
+ },
+ {
+ .desc = "ext4_check_dx_root rejects dot name != '.'",
+ .id = DXP_DOT_BAD_NAME,
+ .check_dx_root_only = true,
+ .expect_ok = false,
+ },
+ {
+ .desc = "ext4_check_dx_root rejects dot rec_len >= blocksize",
+ .id = DXP_DOT_BAD_RECLEN,
+ .check_dx_root_only = true,
+ .expect_ok = false,
+ },
+ {
+ .desc = "ext4_check_dx_root rejects dotdot name_len != 2",
+ .id = DXP_DOTDOT_BAD_NAMELEN,
+ .check_dx_root_only = true,
+ .expect_ok = false,
+ },
+ {
+ .desc = "ext4_check_dx_root rejects dotdot name != '..'",
+ .id = DXP_DOTDOT_BAD_NAME,
+ .check_dx_root_only = true,
+ .expect_ok = false,
+ },
+ {
+ .desc = "dx_probe rejects unrecognized hash_version",
+ .id = DXP_BAD_HASH_VERSION,
+ .expect_ok = false,
+ },
+ {
+ .desc = "dx_probe rejects DX_HASH_SIPHASH when not hash_in_dirent",
+ .id = DXP_SIPHASH_WITHOUT_IN_DIRENT,
+ .expect_ok = false,
+ },
+ {
+ .desc = "dx_probe rejects non-zero unused_flags & 1",
+ .id = DXP_UNUSED_FLAGS_SET,
+ .expect_ok = false,
+ },
+ {
+ .desc = "dx_probe rejects indirect_levels >= ext4_dir_htree_level",
+ .id = DXP_INDIRECT_LEVEL_TOO_DEEP,
+ .expect_ok = false,
+ },
+ {
+ .desc = "dx_probe rejects root limit mismatch",
+ .id = DXP_BAD_ROOT_LIMIT,
+ .expect_ok = false,
+ },
+ {
+ .desc = "dx_probe rejects count == 0",
+ .id = DXP_ZERO_COUNT,
+ .expect_ok = false,
+ },
+ {
+ .desc = "dx_probe detects tree cycle pointing back to block 0",
+ .id = DXP_TREE_CYCLE,
+ .expect_ok = false,
+ },
+};
+
+static void dx_probe_get_desc(const struct dx_probe_param *p, char *desc)
+{
+ strscpy(desc, p->desc, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(dx_probe, dx_probe_params, dx_probe_get_desc);
+
+static void setup_valid_dx_root(struct inode *dir, struct buffer_head *bh)
+{
+ struct dx_root *root = (struct dx_root *)bh->b_data;
+
+ memset(root, 0, TEST_BLK_SIZE);
+ root->dot.inode = cpu_to_le32(dir->i_ino);
+ root->dot.rec_len = ext4_rec_len_to_disk(12, TEST_BLK_SIZE);
+ root->dot.name_len = 1;
+ root->dot.file_type = EXT4_FT_DIR;
+ strscpy(root->dot_name, ".", sizeof(root->dot_name));
+
+ root->dotdot.inode = cpu_to_le32(TEST_ROOT_INO);
+ root->dotdot.rec_len = ext4_rec_len_to_disk(TEST_BLK_SIZE - 12,
+ TEST_BLK_SIZE);
+ root->dotdot.name_len = 2;
+ root->dotdot.file_type = EXT4_FT_DIR;
+ strscpy(root->dotdot_name, "..", sizeof(root->dotdot_name));
+
+ root->info.info_length = sizeof(struct dx_root_info);
+ root->info.hash_version = DX_HASH_HALF_MD4;
+ dx_set_limit(root->entries,
+ dx_root_limit(dir, sizeof(struct dx_root_info)));
+ dx_set_count(root->entries, 2);
+ dx_set_block(root->entries + 0, 1);
+ dx_set_hash(root->entries + 1, 0x80000000U);
+ dx_set_block(root->entries + 1, 2);
+}
+
+static void test_namei_check_dx_root_and_probe(struct kunit *test)
+{
+ const struct dx_probe_param *p = test->param_value;
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct buffer_head *bh0, *bh1;
+ struct dx_root *root;
+ struct dx_node *node;
+ struct dx_frame frames[EXT4_HTREE_LEVEL], *frame;
+ struct dx_hash_info hinfo = {};
+ __u32 start_hash = 0;
+
+ ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
+ dir->i_size = 4 * TEST_BLK_SIZE;
+ bh0 = map_and_get_dir_bh(ctx, dir, 0);
+ KUNIT_ASSERT_NOT_NULL(test, bh0);
+ setup_valid_dx_root(dir, bh0);
+ root = (struct dx_root *)bh0->b_data;
+
+ switch (p->id) {
+ case DXP_VALID_1LEVEL:
+ root->dotdot.rec_len = ext4_rec_len_to_disk(12, TEST_BLK_SIZE);
+ KUNIT_EXPECT_TRUE(test, ext4_check_dx_root_test(dir, root));
+ root->dotdot.rec_len = ext4_rec_len_to_disk(TEST_BLK_SIZE - 12,
+ TEST_BLK_SIZE);
+ break;
+ case DXP_VALID_2LEVEL_AND_NEXT:
+ root->info.indirect_levels = 1;
+ bh1 = map_and_get_dir_bh(ctx, dir, 1);
+ KUNIT_ASSERT_NOT_NULL(test, bh1);
+ node = (struct dx_node *)bh1->b_data;
+ node->fake.rec_len = ext4_rec_len_to_disk(TEST_BLK_SIZE,
+ TEST_BLK_SIZE);
+ dx_set_limit(node->entries, dx_node_limit(dir));
+ dx_set_count(node->entries, 2);
+ dx_set_block(node->entries + 0, 2);
+ /* Continuation hash (low bit 1) matching 0x40000000 */
+ dx_set_hash(node->entries + 1, 0x40000001U);
+ dx_set_block(node->entries + 1, 3);
+ break;
+ case DXP_DOT_BAD_NAMELEN:
+ root->dotdot.rec_len = ext4_rec_len_to_disk(12, TEST_BLK_SIZE);
+ root->dot.name_len = 2;
+ break;
+ case DXP_DOT_BAD_NAME:
+ root->dotdot.rec_len = ext4_rec_len_to_disk(12, TEST_BLK_SIZE);
+ root->dot_name[0] = 'x';
+ break;
+ case DXP_DOT_BAD_RECLEN:
+ root->dotdot.rec_len = ext4_rec_len_to_disk(12, TEST_BLK_SIZE);
+ root->dot.rec_len = ext4_rec_len_to_disk(TEST_BLK_SIZE,
+ TEST_BLK_SIZE);
+ break;
+ case DXP_DOTDOT_BAD_NAMELEN:
+ root->dotdot.rec_len = ext4_rec_len_to_disk(12, TEST_BLK_SIZE);
+ root->dotdot.name_len = 1;
+ break;
+ case DXP_DOTDOT_BAD_NAME:
+ root->dotdot.rec_len = ext4_rec_len_to_disk(12, TEST_BLK_SIZE);
+ root->dotdot_name[1] = 'x';
+ break;
+ case DXP_BAD_HASH_VERSION:
+ root->info.hash_version = 99;
+ break;
+ case DXP_SIPHASH_WITHOUT_IN_DIRENT:
+ root->info.hash_version = DX_HASH_SIPHASH;
+ break;
+ case DXP_UNUSED_FLAGS_SET:
+ root->info.unused_flags = 1;
+ break;
+ case DXP_INDIRECT_LEVEL_TOO_DEEP:
+ root->info.indirect_levels = EXT4_HTREE_LEVEL_COMPAT;
+ break;
+ case DXP_BAD_ROOT_LIMIT:
+ dx_set_limit(root->entries, 50);
+ break;
+ case DXP_ZERO_COUNT:
+ dx_set_count(root->entries, 0);
+ break;
+ case DXP_TREE_CYCLE:
+ dx_set_block(root->entries + 0, 0);
+ break;
+ }
+
+ if (p->check_dx_root_only) {
+ KUNIT_EXPECT_EQ(test, ext4_check_dx_root_test(dir, root),
+ p->expect_ok);
+ return;
+ }
+
+ hinfo.hash = 0x40000000U;
+ frame = dx_probe_test(NULL, dir, &hinfo, frames);
+ if (!p->expect_ok) {
+ KUNIT_EXPECT_TRUE(test, IS_ERR(frame));
+ KUNIT_EXPECT_EQ(test, PTR_ERR(frame), ERR_BAD_DX_DIR);
+ return;
+ }
+
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(frame));
+ if (p->id == DXP_VALID_1LEVEL) {
+ KUNIT_EXPECT_PTR_EQ(test, frame, &frames[0]);
+ KUNIT_EXPECT_EQ(test, dx_get_block(frame->at), 1U);
+ } else if (p->id == DXP_VALID_2LEVEL_AND_NEXT) {
+ KUNIT_EXPECT_PTR_EQ(test, frame, &frames[1]);
+ KUNIT_EXPECT_EQ(test, dx_get_block(frame->at), 2U);
+
+ /* Advance to next leaf block matching continuation hash */
+ KUNIT_EXPECT_EQ(test,
+ ext4_htree_next_block_test(dir, 0x40000000U, frame,
+ frames, &start_hash), 1);
+ KUNIT_EXPECT_EQ(test, start_hash, 0x40000001U);
+ KUNIT_EXPECT_EQ(test, dx_get_block(frame->at), 3U);
+
+ /* Non-matching even hash stops traversal */
+ KUNIT_EXPECT_EQ(test,
+ ext4_htree_next_block_test(dir, 0x40000000U, frame,
+ frames, NULL), 0);
+ }
+ dx_release_test(frames);
+}
+
+/*
+ * Test 9: End-to-end HTree directory conversion (make_indexed_dir), multiple
+ * leaf splits (ext4_dx_add_entry / do_split), indexed lookup
+ * (ext4_dx_find_entry), and rb-tree population (ext4_htree_fill_tree).
+ */
+static void test_namei_add_find_and_htree_split(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *root_dir = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct dentry parent_dentry, child_dentry;
+ struct dir_private_info *dir_info;
+ struct file dir_file = {};
+ __u32 curr_hash = 0, next_hash = 0;
+ int i, err, total_loaded = 0;
+
+ ctx->es.s_feature_ro_compat |=
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir), 0);
+ init_mock_dentry(&parent_dentry, NULL, "dir", dir, &ctx->sb);
+
+ /*
+ * Insert 60 entries into a 1024-byte block directory.
+ * Each entry takes 20 bytes (name_len = 10 -> rec_len = 20), so ~48
+ * entries fit in block 0 before make_indexed_dir converts it into an
+ * HTree and subsequent insertions split leaves via ext4_dx_add_entry.
+ */
+ for (i = 0; i < 60; i++) {
+ char name[16];
+
+ snprintf(name, sizeof(name), "file_%05d", i);
+ init_mock_dentry(&child_dentry, &parent_dentry, name, file1,
+ &ctx->sb);
+ err = ext4_add_entry_test(NULL, &child_dentry, file1);
+ KUNIT_ASSERT_EQ_MSG(test, err, 0, "failed at entry %d", i);
+ }
+
+ /* Verify the directory was converted to an HTree index */
+ KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(dir, EXT4_INODE_INDEX));
+ KUNIT_EXPECT_GE(test, dir->i_size, (loff_t)(3 * TEST_BLK_SIZE));
+
+ /* Adding a duplicate entry must return -EEXIST */
+ init_mock_dentry(&child_dentry, &parent_dentry, "file_00025", file1,
+ &ctx->sb);
+ KUNIT_EXPECT_EQ(test,
+ ext4_add_entry_test(NULL, &child_dentry, file1),
+ -EEXIST);
+
+ /* Look up all 60 entries via ext4_find_entry_test (ext4_dx_find_entry) */
+ for (i = 0; i < 60; i++) {
+ char name[16];
+ struct qstr q;
+ struct ext4_dir_entry_2 *de = NULL;
+ struct buffer_head *bh;
+
+ snprintf(name, sizeof(name), "file_%05d", i);
+ q.name = (const unsigned char *)name;
+ q.len = strlen(name);
+ bh = ext4_find_entry_test(dir, &q, &de, NULL);
+ KUNIT_ASSERT_FALSE_MSG(test, IS_ERR_OR_NULL(bh),
+ "missing entry %s", name);
+ KUNIT_ASSERT_NOT_NULL(test, de);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), TEST_FILE1_INO);
+ brelse(bh);
+ }
+
+ /* Populate readdir rb-tree via ext4_htree_fill_tree */
+ dir_file.f_inode = dir;
+ while (1) {
+ int cnt;
+
+ dir_info = kzalloc_obj(*dir_info);
+ KUNIT_ASSERT_NOT_NULL(test, dir_info);
+ dir_file.private_data = dir_info;
+ cnt = ext4_htree_fill_tree(&dir_file, curr_hash, 0, &next_hash);
+ KUNIT_ASSERT_GE(test, cnt, 0);
+ total_loaded += cnt;
+ ext4_htree_free_dir_info(dir_info);
+ if (next_hash == ~0U)
+ break;
+ curr_hash = next_hash;
+ }
+ /* 60 files + '.' + '..' = 62 total entries */
+ KUNIT_EXPECT_EQ(test, total_loaded, 62);
+}
+
+/*
+ * Test 10: HTree index level growth (splitting full dx_root into a 2-level
+ * tree with indirect_levels == 1), max htree level -ENOSPC enforcement, and
+ * corrupted dx_root fallback in __ext4_find_entry and __ext4_add_entry.
+ */
+static void test_namei_dx_index_level_growth_and_fallback(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *root_dir = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct dentry parent_dentry, child_dentry;
+ struct buffer_head *bh0, *bh1, *found_bh;
+ struct dx_root *root;
+ struct ext4_dir_entry_2 *de = NULL;
+ unsigned int root_limit;
+ int i, err;
+
+ ctx->es.s_feature_ro_compat |=
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir), 0);
+ init_mock_dentry(&parent_dentry, NULL, "dir", dir, &ctx->sb);
+
+ /* Set up a 1-level HTree where root->entries is completely full */
+ ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
+ dir->i_size = 2 * TEST_BLK_SIZE;
+ bh0 = map_and_get_dir_bh(ctx, dir, 0);
+ bh1 = map_and_get_dir_bh(ctx, dir, 1);
+ KUNIT_ASSERT_NOT_NULL(test, bh0);
+ KUNIT_ASSERT_NOT_NULL(test, bh1);
+
+ setup_valid_dx_root(dir, bh0);
+ root = (struct dx_root *)bh0->b_data;
+ root_limit = dx_root_limit(dir, sizeof(struct dx_root_info));
+ dx_set_limit(root->entries, root_limit);
+ dx_set_count(root->entries, root_limit);
+ /* Point every index entry at leaf block 1 in ascending hash order */
+ for (i = 0; i < root_limit; i++) {
+ if (i > 0)
+ dx_set_hash(root->entries + i, (u32)i * 0x02000000U);
+ dx_set_block(root->entries + i, 1);
+ }
+ ext4_dx_csum_set_test(dir, (struct ext4_dir_entry *)bh0->b_data);
+
+ /* Fill leaf block 1 completely so adding one more entry splits it */
+ KUNIT_ASSERT_EQ(test,
+ ext4_init_dirblock(NULL, dir, bh1, TEST_ROOT_INO, NULL, 0), 0);
+ for (i = 0; i < 60; i++) {
+ char name[16];
+ struct qstr q;
+ struct ext4_filename fname;
+
+ snprintf(name, sizeof(name), "leaf_%05d", i);
+ q.name = (const unsigned char *)name;
+ q.len = strlen(name);
+ KUNIT_ASSERT_EQ(test,
+ ext4_fname_setup_filename(dir, &q, 0, &fname), 0);
+ err = ext4_find_dest_de(dir, bh1, bh1->b_data,
+ TEST_BLK_SIZE -
+ sizeof(struct ext4_dir_entry_tail),
+ &fname, &de);
+ if (err == 0)
+ ext4_insert_dentry(dir, file1, de, TEST_BLK_SIZE,
+ &fname);
+ ext4_fname_free_filename(&fname);
+ if (err == -ENOSPC)
+ break;
+ }
+ KUNIT_ASSERT_EQ(test, ext4_handle_dirty_dirblock(NULL, dir, bh1), 0);
+
+ /*
+ * Adding "trigger_split_entry" finds leaf block 1 full and root index
+ * full (count == limit), triggering the add_level branch in
+ * ext4_dx_add_entry (indirect_levels: 0 -> 1).
+ */
+ init_mock_dentry(&child_dentry, &parent_dentry, "trigger_split_entry",
+ file1, &ctx->sb);
+ err = ext4_add_entry_test(NULL, &child_dentry, file1);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, root->info.indirect_levels, 1);
+ KUNIT_EXPECT_EQ(test, dx_get_count(root->entries), 1U);
+
+ /* Verify the newly inserted entry can be found in the 2-level tree */
+ found_bh = ext4_find_entry_test(dir, &child_dentry.d_name, &de, NULL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(found_bh));
+ brelse(found_bh);
+
+ /*
+ * Test interior dx_node split (!add_level in ext4_dx_add_entry) and
+ * max htree level (-ENOSPC when levels == ext4_dir_htree_level(sb)):
+ * Block 2 is the new interior dx_node created by the root split above.
+ * Point all node_limit entries in block 2 to leaf block 1 and refill
+ * leaf block 1 so the next insertion splits the interior dx_node.
+ */
+ {
+ struct buffer_head *bh2 = map_and_get_dir_bh(ctx, dir, 2);
+ struct dx_node *node;
+ unsigned int node_limit = dx_node_limit(dir);
+
+ KUNIT_ASSERT_NOT_NULL(test, bh2);
+ node = (struct dx_node *)bh2->b_data;
+ dx_set_limit(node->entries, node_limit);
+ dx_set_count(node->entries, node_limit);
+ for (i = 0; i < node_limit; i++) {
+ if (i > 0)
+ dx_set_hash(node->entries + i,
+ (u32)i * 0x01000000U);
+ dx_set_block(node->entries + i, 1);
+ }
+ ext4_dx_csum_set_test(dir,
+ (struct ext4_dir_entry *)bh2->b_data);
+
+ /* Refill leaf block 1 to capacity */
+ for (i = 60; i < 120; i++) {
+ char name[16];
+ struct qstr q;
+ struct ext4_filename fname;
+
+ snprintf(name, sizeof(name), "leaf_%05d", i);
+ q.name = (const unsigned char *)name;
+ q.len = strlen(name);
+ KUNIT_ASSERT_EQ(test,
+ ext4_fname_setup_filename(dir, &q, 0, &fname),
+ 0);
+ err = ext4_find_dest_de(dir, bh1, bh1->b_data,
+ TEST_BLK_SIZE -
+ sizeof(struct ext4_dir_entry_tail),
+ &fname, &de);
+ if (err == 0)
+ ext4_insert_dentry(dir, file1, de,
+ TEST_BLK_SIZE, &fname);
+ ext4_fname_free_filename(&fname);
+ if (err == -ENOSPC)
+ break;
+ }
+ KUNIT_ASSERT_EQ(test,
+ ext4_handle_dirty_dirblock(NULL, dir, bh1), 0);
+
+ /*
+ * First test max htree level check: if both root (level 0) and
+ * interior node (level 1) are full when LARGEDIR is disabled
+ * (max level 2), ext4_dx_add_entry returns -ENOSPC.
+ */
+ dx_set_count(root->entries, root_limit);
+ for (i = 0; i < root_limit; i++) {
+ if (i > 0)
+ dx_set_hash(root->entries + i,
+ (u32)i * 0x02000000U);
+ dx_set_block(root->entries + i, 2);
+ }
+ ext4_dx_csum_set_test(dir,
+ (struct ext4_dir_entry *)bh0->b_data);
+ init_mock_dentry(&child_dentry, &parent_dentry,
+ "max_level_enospc", file1, &ctx->sb);
+ KUNIT_EXPECT_EQ(test,
+ ext4_add_entry_test(NULL, &child_dentry, file1),
+ -ENOSPC);
+
+ /*
+ * Restore root count to 1 and insert again: now root has room
+ * while interior node at block 2 is full, exercising the
+ * !add_level interior node split in ext4_dx_add_entry.
+ */
+ dx_set_count(root->entries, 1);
+ dx_set_block(root->entries + 0, 2);
+ ext4_dx_csum_set_test(dir,
+ (struct ext4_dir_entry *)bh0->b_data);
+ init_mock_dentry(&child_dentry, &parent_dentry,
+ "interior_node_split", file1, &ctx->sb);
+ KUNIT_ASSERT_EQ(test,
+ ext4_add_entry_test(NULL, &child_dentry, file1),
+ 0);
+ KUNIT_EXPECT_EQ(test, root->info.indirect_levels, 1);
+ KUNIT_EXPECT_EQ(test, dx_get_count(root->entries), 2U);
+ }
+
+ /*
+ * Now test corrupted dx_root handling:
+ * 1) With METADATA_CSUM enabled, __ext4_add_entry refuses to clear
+ * EXT4_INODE_INDEX and returns -EFSCORRUPTED.
+ */
+ root->info.hash_version = 99;
+ ext4_dx_csum_set_test(dir, (struct ext4_dir_entry *)bh0->b_data);
+ init_mock_dentry(&child_dentry, &parent_dentry, "after_corrupt",
+ file1, &ctx->sb);
+ KUNIT_EXPECT_EQ(test,
+ ext4_add_entry_test(NULL, &child_dentry, file1),
+ -EFSCORRUPTED);
+
+ /*
+ * 2) __ext4_find_entry falls back to linear block scan on
+ * ERR_BAD_DX_DIR and still finds "leaf_00000" in block 1!
+ */
+ init_mock_dentry(&child_dentry, &parent_dentry, "leaf_00000",
+ file1, &ctx->sb);
+ found_bh = ext4_find_entry_test(dir, &child_dentry.d_name, &de, NULL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(found_bh));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), TEST_FILE1_INO);
+ brelse(found_bh);
+
+ /*
+ * 3) Without METADATA_CSUM, __ext4_add_entry clears EXT4_INODE_INDEX
+ * on ERR_BAD_DX_DIR and falls back to linear append.
+ */
+ ctx->es.s_feature_ro_compat &=
+ ~cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+ init_mock_dentry(&child_dentry, &parent_dentry, "fallback_new_entry",
+ file1, &ctx->sb);
+ KUNIT_EXPECT_EQ(test,
+ ext4_add_entry_test(NULL, &child_dentry, file1), 0);
+ KUNIT_EXPECT_FALSE(test, ext4_test_inode_flag(dir, EXT4_INODE_INDEX));
+}
+
+/*
+ * Test 11: Inline directory lifecycle through namei.c entry points
+ * (ext4_init_new_dir, ext4_empty_dir, ext4_add_entry_test,
+ * ext4_find_entry_test, ext4_delete_entry_test, ext4_htree_fill_tree).
+ */
+static void test_namei_inline_dir_lifecycle(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *root_dir = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct dentry parent_dentry, child_dentry;
+ struct buffer_head *bh;
+ struct ext4_dir_entry_2 *de = NULL;
+ struct dir_private_info *dir_info;
+ struct file dir_file = {};
+ __u32 next_hash = 0;
+ int inlined = 0;
+
+ ctx->es.s_feature_incompat |=
+ cpu_to_le32(EXT4_FEATURE_INCOMPAT_INLINE_DATA);
+ ext4_set_inode_state(dir, EXT4_STATE_MAY_INLINE_DATA);
+
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir), 0);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(dir));
+ KUNIT_EXPECT_TRUE(test, ext4_empty_dir(dir));
+
+ init_mock_dentry(&parent_dentry, NULL, "idir", dir, &ctx->sb);
+ init_mock_dentry(&child_dentry, &parent_dentry, "tiny", file1,
+ &ctx->sb);
+
+ /* Add an entry into the inline directory */
+ KUNIT_ASSERT_EQ(test,
+ ext4_add_entry_test(NULL, &child_dentry, file1), 0);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(dir));
+ KUNIT_EXPECT_FALSE(test, ext4_empty_dir(dir));
+
+ /* Find the inline entry */
+ bh = ext4_find_entry_test(dir, &child_dentry.d_name, &de, &inlined);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(bh));
+ KUNIT_EXPECT_EQ(test, inlined, 1);
+ KUNIT_ASSERT_NOT_NULL(test, de);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), TEST_FILE1_INO);
+ brelse(bh);
+
+ /* Load inline directory into rb-tree via ext4_htree_fill_tree */
+ dir_info = kzalloc_obj(*dir_info);
+ KUNIT_ASSERT_NOT_NULL(test, dir_info);
+ dir_file.f_inode = dir;
+ dir_file.private_data = dir_info;
+ KUNIT_EXPECT_EQ(test,
+ ext4_htree_fill_tree(&dir_file, 0, 0, &next_hash), 3);
+ KUNIT_EXPECT_EQ(test, next_hash, ~0U);
+ ext4_htree_free_dir_info(dir_info);
+
+ /* Delete the inline entry -> directory becomes empty again */
+ bh = ext4_find_entry_test(dir, &child_dentry.d_name, &de, &inlined);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(bh));
+ KUNIT_ASSERT_EQ(test, ext4_delete_entry_test(NULL, dir, de, bh), 0);
+ brelse(bh);
+ KUNIT_EXPECT_TRUE(test, ext4_empty_dir(dir));
+}
+
+/*
+ * Test 12: Directory link count helpers (ext4_inc_count_test,
+ * ext4_dec_count_test) and VFS lookup / parent resolution (ext4_lookup,
+ * ext4_get_parent).
+ */
+static void test_namei_nlink_and_lookup_parent(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *root_dir = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct dentry parent_dentry, lookup_dentry, *res_dentry;
+ struct buffer_head *bh;
+ struct ext4_dir_entry_2 *de = NULL;
+ char long_name[EXT4_NAME_LEN + 2];
+
+ /* Test ext4_inc_count_test and ext4_dec_count_test on indexed dir */
+ ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
+ set_nlink(dir, 2);
+ ext4_inc_count_test(dir);
+ KUNIT_EXPECT_EQ(test, dir->i_nlink, 3U);
+ ext4_dec_count_test(dir);
+ KUNIT_EXPECT_EQ(test, dir->i_nlink, 2U);
+
+ /* Overflow past EXT4_LINK_MAX wraps nlink to 1 and keeps it at 1 */
+ set_nlink(dir, EXT4_LINK_MAX);
+ ext4_inc_count_test(dir);
+ KUNIT_EXPECT_EQ(test, dir->i_nlink, 1U);
+ ext4_inc_count_test(dir);
+ KUNIT_EXPECT_EQ(test, dir->i_nlink, 1U);
+ ext4_dec_count_test(dir);
+ KUNIT_EXPECT_EQ(test, dir->i_nlink, 1U);
+ ext4_clear_inode_flag(dir, EXT4_INODE_INDEX);
+
+ /* Initialize directory for lookup and get_parent tests */
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir), 0);
+ init_mock_dentry(&parent_dentry, NULL, "dir", dir, &ctx->sb);
+
+ /* 1) Name longer than EXT4_NAME_LEN -> -ENAMETOOLONG */
+ memset(long_name, 'a', sizeof(long_name) - 1);
+ long_name[sizeof(long_name) - 1] = '\0';
+ init_mock_dentry(&lookup_dentry, &parent_dentry, long_name, NULL,
+ &ctx->sb);
+ res_dentry = ext4_dir_inode_operations.lookup(dir, &lookup_dentry, 0);
+ KUNIT_EXPECT_PTR_EQ(test, res_dentry, ERR_PTR(-ENAMETOOLONG));
+
+ /* 2) Negative lookup returns NULL without calling ext4_iget */
+ ctx->last_iget_ino = 0;
+ init_mock_dentry(&lookup_dentry, &parent_dentry, "nonexistent", NULL,
+ &ctx->sb);
+ res_dentry = ext4_dir_inode_operations.lookup(dir, &lookup_dentry, 0);
+ /*
+ * ext4_lookup() hashes negative dentries via d_splice_alias(); unhash
+ * it and wait for RCU readers before reusing or freeing stack memory.
+ */
+ d_drop(&lookup_dentry);
+ synchronize_rcu();
+ KUNIT_EXPECT_NULL(test, res_dentry);
+ KUNIT_EXPECT_EQ(test, ctx->last_iget_ino, 0UL);
+
+ /* 3) Positive lookup invokes ext4_iget with target inode number */
+ init_mock_dentry(&lookup_dentry, &parent_dentry, "target", file1,
+ &ctx->sb);
+ KUNIT_ASSERT_EQ(test,
+ ext4_add_entry_test(NULL, &lookup_dentry, file1), 0);
+ ctx->fail_iget = -ESTALE;
+ /* -ESTALE from ext4_iget is translated to -EFSCORRUPTED */
+ res_dentry = ext4_dir_inode_operations.lookup(dir, &lookup_dentry, 0);
+ KUNIT_EXPECT_PTR_EQ(test, res_dentry, ERR_PTR(-EFSCORRUPTED));
+ KUNIT_EXPECT_EQ(test, ctx->last_iget_ino, TEST_FILE1_INO);
+
+ /* 4) Self-linked directory entry (de->inode == dir->i_ino) -> -EFSCORRUPTED */
+ bh = ext4_find_entry_test(dir, &lookup_dentry.d_name, &de, NULL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(bh));
+ de->inode = cpu_to_le32(dir->i_ino);
+ brelse(bh);
+ res_dentry = ext4_dir_inode_operations.lookup(dir, &lookup_dentry, 0);
+ KUNIT_EXPECT_PTR_EQ(test, res_dentry, ERR_PTR(-EFSCORRUPTED));
+
+ /* 5) Invalid inode number (e.g. reserved ino 5) -> -EFSCORRUPTED */
+ de->inode = cpu_to_le32(5);
+ res_dentry = ext4_dir_inode_operations.lookup(dir, &lookup_dentry, 0);
+ KUNIT_EXPECT_PTR_EQ(test, res_dentry, ERR_PTR(-EFSCORRUPTED));
+
+ /* 6) ext4_get_parent looks up ".." and calls ext4_iget(TEST_ROOT_INO) */
+ ctx->fail_iget = -EUCLEAN;
+ ctx->last_iget_ino = 0;
+ res_dentry = ext4_get_parent(&parent_dentry);
+ KUNIT_EXPECT_PTR_EQ(test, res_dentry, ERR_PTR(-EUCLEAN));
+ KUNIT_EXPECT_EQ(test, ctx->last_iget_ino, (unsigned long)TEST_ROOT_INO);
+
+ /* Corrupt '..' inode to invalid ino 5 -> ext4_get_parent returns -EFSCORRUPTED */
+ bh = map_and_get_dir_bh(ctx, dir, 0);
+ de = ext4_next_entry((struct ext4_dir_entry_2 *)bh->b_data,
+ TEST_BLK_SIZE);
+ de->inode = cpu_to_le32(5);
+ res_dentry = ext4_get_parent(&parent_dentry);
+ KUNIT_EXPECT_PTR_EQ(test, res_dentry, ERR_PTR(-EFSCORRUPTED));
+}
+
+/*
+ * Test 13: Hard link, unlink, and rmdir operations (__ext4_link,
+ * ext4_dir_inode_operations.link, __ext4_unlink,
+ * ext4_dir_inode_operations.unlink, ext4_dir_inode_operations.rmdir).
+ */
+static void test_namei_link_unlink_and_rmdir(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *root_dir = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *subdir = get_mock_inode(ctx, TEST_SUBDIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct dentry dir_dentry, f1_dentry, f1_link_dentry, sub_dentry;
+ int err;
+
+ ctx->es.s_feature_ro_compat |=
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir), 0);
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, dir, subdir), 0);
+ ext4_inc_count_test(dir);
+
+ init_mock_dentry(&dir_dentry, NULL, "dir", dir, &ctx->sb);
+ init_mock_dentry(&f1_dentry, &dir_dentry, "orig", file1, &ctx->sb);
+ init_mock_dentry(&f1_link_dentry, &dir_dentry, "hardlink", file1,
+ &ctx->sb);
+ init_mock_dentry(&sub_dentry, &dir_dentry, "subdir", subdir, &ctx->sb);
+
+ /* Link a 0-nlink tmpfile -> nlink becomes 1 and ext4_orphan_del runs */
+ set_nlink(file1, 0);
+ inode_state_set_raw(file1, I_DIRTY | I_LINKABLE);
+ ctx->orphan_del_calls = 0;
+ err = __ext4_link(dir, file1, &f1_dentry.d_name, &f1_dentry);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, file1->i_nlink, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->orphan_del_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->last_orphan_del_ino, TEST_FILE1_INO);
+
+ /* Create a second hard link via __ext4_link */
+ err = __ext4_link(dir, file1, &f1_link_dentry.d_name, &f1_link_dentry);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, file1->i_nlink, 2U);
+
+ /* Cross-project link with EXT4_INODE_PROJINHERIT returns -EXDEV */
+ ext4_set_inode_flag(dir, EXT4_INODE_PROJINHERIT);
+ EXT4_I(file1)->i_projid = make_kprojid(&init_user_ns, 42);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dir_inode_operations.link(&f1_dentry, dir,
+ &f1_link_dentry), -EXDEV);
+ EXT4_I(file1)->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+
+ /* Max link count check returns -EMLINK */
+ set_nlink(file1, EXT4_LINK_MAX);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dir_inode_operations.link(&f1_dentry, dir,
+ &f1_link_dentry), -EMLINK);
+ set_nlink(file1, 2);
+
+ /* Unlink first link -> nlink drops to 1, not orphaned yet */
+ ctx->orphan_add_calls = 0;
+ err = ext4_dir_inode_operations.unlink(dir, &f1_dentry);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, file1->i_nlink, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->orphan_add_calls, 0U);
+
+ /* Unlink last link -> nlink drops to 0 and inode is added to orphans */
+ err = ext4_dir_inode_operations.unlink(dir, &f1_link_dentry);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, file1->i_nlink, 0U);
+ KUNIT_EXPECT_EQ(test, ctx->orphan_add_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->last_orphan_add_ino, TEST_FILE1_INO);
+
+ /* Unlinking missing entry returns -ENOENT */
+ KUNIT_EXPECT_EQ(test,
+ ext4_dir_inode_operations.unlink(dir, &f1_link_dentry),
+ -ENOENT);
+
+ /*
+ * Test __ext4_unlink when directory entry points to a different inode:
+ * normally rejected with -ENOENT, but permitted during EXT4_FC_REPLAY.
+ */
+ {
+ struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+
+ set_nlink(file1, 1);
+ set_nlink(file2, 1);
+ KUNIT_ASSERT_EQ(test,
+ ext4_add_entry_test(NULL, &f1_dentry, file1), 0);
+ KUNIT_EXPECT_EQ(test,
+ __ext4_unlink(dir, &f1_dentry.d_name, file2,
+ &f1_dentry), -ENOENT);
+ ctx->sbi.s_mount_state |= EXT4_FC_REPLAY;
+ KUNIT_EXPECT_EQ(test,
+ __ext4_unlink(dir, &f1_dentry.d_name, file2,
+ &f1_dentry), 0);
+ ctx->sbi.s_mount_state &= ~EXT4_FC_REPLAY;
+ }
+
+ /* Add subdir to dir and test rmdir */
+ KUNIT_ASSERT_EQ(test,
+ ext4_add_entry_test(NULL, &sub_dentry, subdir), 0);
+
+ /* Non-empty subdir cannot be removed (-ENOTEMPTY) */
+ init_mock_dentry(&f1_dentry, &sub_dentry, "inside", file1, &ctx->sb);
+ set_nlink(file1, 1);
+ KUNIT_ASSERT_EQ(test,
+ ext4_add_entry_test(NULL, &f1_dentry, file1), 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dir_inode_operations.rmdir(dir, &sub_dentry), -ENOTEMPTY);
+
+ /* Unlink file inside subdir, then rmdir subdir succeeds */
+ KUNIT_ASSERT_EQ(test,
+ ext4_dir_inode_operations.unlink(subdir, &f1_dentry), 0);
+ KUNIT_ASSERT_EQ(test,
+ ext4_dir_inode_operations.rmdir(dir, &sub_dentry), 0);
+ KUNIT_EXPECT_EQ(test, subdir->i_nlink, 0U);
+ KUNIT_EXPECT_EQ(test, subdir->i_size, 0LL);
+ KUNIT_EXPECT_EQ(test, dir->i_nlink, 2U);
+ KUNIT_EXPECT_EQ(test, ctx->last_orphan_add_ino, TEST_SUBDIR_INO);
+}
+
+/*
+ * Test 14: Rename, overwrite rename, cross-directory subdirectory rename,
+ * RENAME_EXCHANGE (ext4_cross_rename), and slow symlink block initialization
+ * (ext4_init_symlink_block_test).
+ */
+static void test_namei_rename_and_exchange(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *root_dir = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir1 = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *dir2 = get_mock_inode(ctx, TEST_DIR2_INO);
+ struct inode *subdir = get_mock_inode(ctx, TEST_SUBDIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+ struct inode *symlink_inode = get_mock_inode(ctx, TEST_FILE3_INO);
+ struct dentry d1_dentry, d2_dentry;
+ struct dentry old_dentry, new_dentry, sub_dentry;
+ struct buffer_head *bh;
+ struct ext4_dir_entry_2 *de = NULL;
+ const char *sym_target = "/very/long/symlink/target/path/for/ext4/test";
+ struct fscrypt_str disk_link = {
+ .name = (unsigned char *)sym_target,
+ .len = strlen(sym_target) + 1,
+ };
+ int err;
+
+ ctx->es.s_feature_ro_compat |=
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir1), 0);
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir2), 0);
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, dir1, subdir), 0);
+ set_nlink(dir1, 3); /* '.', '..', and subdir */
+ set_nlink(dir2, 2);
+
+ init_mock_dentry(&d1_dentry, NULL, "dir1", dir1, &ctx->sb);
+ init_mock_dentry(&d2_dentry, NULL, "dir2", dir2, &ctx->sb);
+
+ /* Invalid rename flags are rejected with -EINVAL */
+ init_mock_dentry(&old_dentry, &d1_dentry, "f1", file1, &ctx->sb);
+ init_mock_dentry(&new_dentry, &d1_dentry, "f1_new", NULL, &ctx->sb);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dir_inode_operations.rename(&nop_mnt_idmap, dir1,
+ &old_dentry, dir1, &new_dentry, ~0U), -EINVAL);
+
+ /* Add f1 and f2 in dir1, and subdir in dir1 */
+ KUNIT_ASSERT_EQ(test,
+ ext4_add_entry_test(NULL, &old_dentry, file1), 0);
+ init_mock_dentry(&sub_dentry, &d1_dentry, "sub", subdir, &ctx->sb);
+ KUNIT_ASSERT_EQ(test,
+ ext4_add_entry_test(NULL, &sub_dentry, subdir), 0);
+
+ /* 1) Same-directory rename: f1 -> f1_renamed */
+ err = ext4_dir_inode_operations.rename(&nop_mnt_idmap, dir1,
+ &old_dentry, dir1, &new_dentry,
+ 0);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_NULL(test,
+ ext4_find_entry_test(dir1, &old_dentry.d_name, &de, NULL));
+ bh = ext4_find_entry_test(dir1, &new_dentry.d_name, &de, NULL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(bh));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), TEST_FILE1_INO);
+ brelse(bh);
+
+ /* 2) Overwrite rename: f1_renamed overwrites existing f2 in dir1 */
+ init_mock_dentry(&old_dentry, &d1_dentry, "f1_new", file1, &ctx->sb);
+ init_mock_dentry(&new_dentry, &d1_dentry, "f2", file2, &ctx->sb);
+ KUNIT_ASSERT_EQ(test,
+ ext4_add_entry_test(NULL, &new_dentry, file2), 0);
+ ctx->orphan_add_calls = 0;
+ err = ext4_dir_inode_operations.rename(&nop_mnt_idmap, dir1,
+ &old_dentry, dir1, &new_dentry,
+ 0);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, file2->i_nlink, 0U);
+ KUNIT_EXPECT_EQ(test, ctx->orphan_add_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->last_orphan_add_ino, TEST_FILE2_INO);
+
+ /* 3) Cross-directory subdir move: dir1/sub -> dir2/sub_moved */
+ init_mock_dentry(&new_dentry, &d2_dentry, "sub_moved", NULL, &ctx->sb);
+ err = ext4_dir_inode_operations.rename(&nop_mnt_idmap, dir1,
+ &sub_dentry, dir2, &new_dentry,
+ 0);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, dir1->i_nlink, 2U);
+ KUNIT_EXPECT_EQ(test, dir2->i_nlink, 3U);
+
+ /* Verify subdir's '..' now points to dir2 */
+ bh = map_and_get_dir_bh(ctx, subdir, 0);
+ de = ext4_next_entry((struct ext4_dir_entry_2 *)bh->b_data,
+ TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), TEST_DIR2_INO);
+
+ /*
+ * 4) RENAME_EXCHANGE (ext4_cross_rename): exchange regular file
+ * dir1/f2 with directory dir2/sub_moved across parents!
+ */
+ init_mock_dentry(&old_dentry, &d1_dentry, "f2", file1, &ctx->sb);
+ init_mock_dentry(&new_dentry, &d2_dentry, "sub_moved", subdir,
+ &ctx->sb);
+
+ /* Cross-project RENAME_EXCHANGE and normal rename return -EXDEV */
+ ext4_set_inode_flag(dir2, EXT4_INODE_PROJINHERIT);
+ EXT4_I(dir2)->i_projid = make_kprojid(&init_user_ns, 77);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dir_inode_operations.rename(&nop_mnt_idmap, dir1,
+ &old_dentry, dir2, &new_dentry, RENAME_EXCHANGE),
+ -EXDEV);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dir_inode_operations.rename(&nop_mnt_idmap, dir1,
+ &old_dentry, dir2, &new_dentry, 0),
+ -EXDEV);
+ ext4_clear_inode_flag(dir2, EXT4_INODE_PROJINHERIT);
+ EXT4_I(dir2)->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+
+ err = ext4_dir_inode_operations.rename(&nop_mnt_idmap, dir1,
+ &old_dentry, dir2, &new_dentry,
+ RENAME_EXCHANGE);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, dir1->i_nlink, 3U);
+ KUNIT_EXPECT_EQ(test, dir2->i_nlink, 2U);
+ de = ext4_next_entry((struct ext4_dir_entry_2 *)bh->b_data,
+ TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), TEST_DIR_INO);
+
+ /*
+ * Overwriting a target inode whose i_nlink == 0 returns -EFSCORRUPTED
+ * (in dir2, "sub_moved" now points to file1 after the exchange).
+ */
+ init_mock_dentry(&old_dentry, &d1_dentry, "f2", subdir, &ctx->sb);
+ init_mock_dentry(&new_dentry, &d2_dentry, "sub_moved", file1, &ctx->sb);
+ set_nlink(file1, 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_dir_inode_operations.rename(&nop_mnt_idmap, dir2,
+ &new_dentry, dir2, &new_dentry, 0),
+ -EFSCORRUPTED);
+ set_nlink(file1, 1);
+
+ /* 5) Test ext4_init_symlink_block_test */
+ err = ext4_init_symlink_block_test(NULL, symlink_inode, &disk_link);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, symlink_inode->i_size,
+ (loff_t)strlen(sym_target));
+ bh = map_and_get_dir_bh(ctx, symlink_inode, 0);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ KUNIT_EXPECT_STREQ(test, bh->b_data, sym_target);
+}
+
+#if IS_ENABLED(CONFIG_UNICODE)
+/*
+ * Test 15: Case-insensitive (EXT4_INODE_CASEFOLD) filename setup, directory
+ * lookup, and strict UTF-8 validation.
+ */
+static void test_namei_casefold_lookup_and_strict(struct kunit *test)
+{
+ struct namei_test_ctx *ctx = test->priv;
+ struct inode *root_dir = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct unicode_map *um;
+ struct dentry parent_dentry, add_dentry;
+ struct qstr upper_q = QSTR_INIT(" readme.md ", 11);
+ struct qstr bad_utf8 = QSTR_INIT("\xff\xfe", 2);
+ struct buffer_head *bh;
+ struct ext4_dir_entry_2 *de = NULL;
+
+ um = utf8_load(UNICODE_AGE(12, 1, 0));
+ if (IS_ERR(um))
+ kunit_skip(test, "utf8data module not available");
+
+ ctx->sb.s_encoding = um;
+ ctx->es.s_feature_incompat |=
+ cpu_to_le32(EXT4_FEATURE_INCOMPAT_CASEFOLD);
+ ext4_set_inode_flag(dir, EXT4_INODE_CASEFOLD);
+
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir), 0);
+ init_mock_dentry(&parent_dentry, NULL, "cfdir", dir, &ctx->sb);
+ init_mock_dentry(&add_dentry, &parent_dentry, " ReadMe.MD ", file1,
+ &ctx->sb);
+
+ KUNIT_ASSERT_EQ(test,
+ ext4_add_entry_test(NULL, &add_dentry, file1), 0);
+
+ /* Case-insensitive lookup with different case finds the entry */
+ bh = ext4_find_entry_test(dir, &upper_q, &de, NULL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(bh));
+ KUNIT_ASSERT_NOT_NULL(test, de);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), TEST_FILE1_INO);
+ brelse(bh);
+
+ /* Enable SB_STRICT_ENCODING -> invalid UTF-8 sequence is rejected */
+ ctx->sb.s_encoding_flags |= SB_STRICT_ENCODING;
+ add_dentry.__d_name = bad_utf8;
+ KUNIT_EXPECT_EQ(test,
+ ext4_add_entry_test(NULL, &add_dentry, file1),
+ -EINVAL);
+}
+#endif /* CONFIG_UNICODE */
+
+static struct kunit_case namei_test_cases[] = {
+ KUNIT_CASE(test_namei_dirent_tail_and_csum),
+ KUNIT_CASE(test_namei_dx_helpers_and_csum),
+ KUNIT_CASE_PARAM(test_namei_read_dirblock, read_dirblock_gen_params),
+ KUNIT_CASE(test_namei_append_and_init_dir),
+ KUNIT_CASE(test_namei_search_insert_delete_dentry),
+ KUNIT_CASE(test_namei_empty_dir),
+ KUNIT_CASE(test_namei_dx_map_pack_and_move),
+ KUNIT_CASE_PARAM(test_namei_check_dx_root_and_probe,
+ dx_probe_gen_params),
+ KUNIT_CASE(test_namei_add_find_and_htree_split),
+ KUNIT_CASE(test_namei_dx_index_level_growth_and_fallback),
+ KUNIT_CASE(test_namei_inline_dir_lifecycle),
+ KUNIT_CASE(test_namei_nlink_and_lookup_parent),
+ KUNIT_CASE(test_namei_link_unlink_and_rmdir),
+ KUNIT_CASE(test_namei_rename_and_exchange),
+#if IS_ENABLED(CONFIG_UNICODE)
+ KUNIT_CASE(test_namei_casefold_lookup_and_strict),
+#endif
+ {}
+};
+
+static struct kunit_suite namei_test_suite = {
+ .name = "ext4_namei_test",
+ .init = namei_test_init,
+ .exit = namei_test_exit,
+ .test_cases = namei_test_cases,
+};
+
+kunit_test_suites(&namei_test_suite);
+
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index a6386c1d237f..63d01a9711a2 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -41,6 +41,7 @@
#include "xattr.h"
#include "acl.h"
+#include "namei.h"
#include <trace/events/ext4.h>
/*
@@ -104,26 +105,12 @@ static struct buffer_head *ext4_append(handle_t *handle,
static int ext4_dx_csum_verify(struct inode *inode,
struct ext4_dir_entry *dirent);
-/*
- * Hints to ext4_read_dirblock regarding whether we expect a directory
- * block being read to be an index block, or a block containing
- * directory entries (and if the latter, whether it was found via a
- * logical block in an htree index block). This is used to control
- * what sort of sanity checkinig ext4_read_dirblock() will do on the
- * directory block read from the storage device. EITHER will means
- * the caller doesn't know what kind of directory block will be read,
- * so no specific verification will be done.
- */
-typedef enum {
- EITHER, INDEX, DIRENT, DIRENT_HTREE
-} dirblock_type_t;
-
#define ext4_read_dirblock(inode, block, type) \
__ext4_read_dirblock((inode), (block), (type), __func__, __LINE__)
static struct buffer_head *__ext4_read_dirblock(struct inode *inode,
ext4_lblk_t block,
- dirblock_type_t type,
+ enum dirblock_type type,
const char *func,
unsigned int line)
{
@@ -218,79 +205,6 @@ static struct buffer_head *__ext4_read_dirblock(struct inode *inode,
#define dxtrace(command)
#endif
-struct fake_dirent
-{
- __le32 inode;
- __le16 rec_len;
- u8 name_len;
- u8 file_type;
-};
-
-struct dx_countlimit
-{
- __le16 limit;
- __le16 count;
-};
-
-struct dx_entry
-{
- __le32 hash;
- __le32 block;
-};
-
-/*
- * dx_root_info is laid out so that if it should somehow get overlaid by a
- * dirent the two low bits of the hash version will be zero. Therefore, the
- * hash version mod 4 should never be 0. Sincerely, the paranoia department.
- */
-
-struct dx_root
-{
- struct fake_dirent dot;
- char dot_name[4];
- struct fake_dirent dotdot;
- char dotdot_name[4];
- struct dx_root_info
- {
- __le32 reserved_zero;
- u8 hash_version;
- u8 info_length; /* 8 */
- u8 indirect_levels;
- u8 unused_flags;
- }
- info;
- struct dx_entry entries[];
-};
-
-struct dx_node
-{
- struct fake_dirent fake;
- struct dx_entry entries[];
-};
-
-
-struct dx_frame
-{
- struct buffer_head *bh;
- struct dx_entry *entries;
- struct dx_entry *at;
-};
-
-struct dx_map_entry
-{
- u32 hash;
- u16 offs;
- u16 size;
-};
-
-/*
- * This goes at the end of each htree block.
- */
-struct dx_tail {
- u32 dt_reserved;
- __le32 dt_checksum; /* crc32c(uuid+inum+dirblock) */
-};
-
static struct buffer_head * ext4_dx_find_entry(struct inode *dir,
struct ext4_filename *fname,
struct ext4_dir_entry_2 **res_dir);
@@ -308,6 +222,7 @@ void ext4_initialize_dirent_tail(struct buffer_head *bh,
sizeof(struct ext4_dir_entry_tail), blocksize);
t->det_reserved_ft = EXT4_FT_DIR_CSUM;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_initialize_dirent_tail);
/* Walk through a dirent block to find a checksum "dirent" at the tail */
static struct ext4_dir_entry_tail *get_dirent_tail(struct inode *inode,
@@ -382,6 +297,7 @@ int ext4_dirblock_csum_verify(struct inode *inode, struct buffer_head *bh)
return 1;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dirblock_csum_verify);
static void ext4_dirblock_csum_set(struct inode *inode,
struct buffer_head *bh)
@@ -408,6 +324,7 @@ int ext4_handle_dirty_dirblock(handle_t *handle,
ext4_dirblock_csum_set(inode, bh);
return ext4_handle_dirty_metadata(handle, inode, bh);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_handle_dirty_dirblock);
static struct dx_countlimit *get_dx_countlimit(struct inode *inode,
struct ext4_dir_entry *dirent,
@@ -519,82 +436,6 @@ static inline int ext4_handle_dirty_dx_node(handle_t *handle,
return ext4_handle_dirty_metadata(handle, inode, bh);
}
-/*
- * p is at least 6 bytes before the end of page
- */
-static inline struct ext4_dir_entry_2 *
-ext4_next_entry(struct ext4_dir_entry_2 *p, unsigned long blocksize)
-{
- return (struct ext4_dir_entry_2 *)((char *)p +
- ext4_rec_len_from_disk(p->rec_len, blocksize));
-}
-
-/*
- * Future: use high four bits of block for coalesce-on-delete flags
- * Mask them off for now.
- */
-
-static inline ext4_lblk_t dx_get_block(struct dx_entry *entry)
-{
- return le32_to_cpu(entry->block) & 0x0fffffff;
-}
-
-static inline void dx_set_block(struct dx_entry *entry, ext4_lblk_t value)
-{
- entry->block = cpu_to_le32(value);
-}
-
-static inline unsigned dx_get_hash(struct dx_entry *entry)
-{
- return le32_to_cpu(entry->hash);
-}
-
-static inline void dx_set_hash(struct dx_entry *entry, unsigned value)
-{
- entry->hash = cpu_to_le32(value);
-}
-
-static inline unsigned dx_get_count(struct dx_entry *entries)
-{
- return le16_to_cpu(((struct dx_countlimit *) entries)->count);
-}
-
-static inline unsigned dx_get_limit(struct dx_entry *entries)
-{
- return le16_to_cpu(((struct dx_countlimit *) entries)->limit);
-}
-
-static inline void dx_set_count(struct dx_entry *entries, unsigned value)
-{
- ((struct dx_countlimit *) entries)->count = cpu_to_le16(value);
-}
-
-static inline void dx_set_limit(struct dx_entry *entries, unsigned value)
-{
- ((struct dx_countlimit *) entries)->limit = cpu_to_le16(value);
-}
-
-static inline unsigned dx_root_limit(struct inode *dir, unsigned infosize)
-{
- unsigned int entry_space = dir->i_sb->s_blocksize -
- ext4_dir_rec_len(1, NULL) -
- ext4_dir_rec_len(2, NULL) - infosize;
-
- if (ext4_has_feature_metadata_csum(dir->i_sb))
- entry_space -= sizeof(struct dx_tail);
- return entry_space / sizeof(struct dx_entry);
-}
-
-static inline unsigned dx_node_limit(struct inode *dir)
-{
- unsigned int entry_space = dir->i_sb->s_blocksize -
- ext4_dir_rec_len(0, dir);
-
- if (ext4_has_feature_metadata_csum(dir->i_sb))
- entry_space -= sizeof(struct dx_tail);
- return entry_space / sizeof(struct dx_entry);
-}
-
/*
* Debug
*/
@@ -1258,6 +1099,7 @@ int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,
dx_release(frames);
return (err);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_htree_fill_tree);
static inline int search_dirblock(struct buffer_head *bh,
struct inode *dir,
@@ -1395,6 +1237,7 @@ int ext4_fname_setup_ci_filename(struct inode *dir, const struct qstr *iname,
else
return ext4fs_dirhash(dir, iname->name, iname->len, hinfo);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fname_setup_ci_filename);
#endif
/*
@@ -1488,6 +1331,7 @@ int ext4_search_dir(struct buffer_head *bh, char *search_buf, int buf_size,
}
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_search_dir);
static int is_dx_internal_node(struct inode *dir, ext4_lblk_t block,
struct ext4_dir_entry *de)
@@ -1837,6 +1681,7 @@ struct dentry *ext4_get_parent(struct dentry *child)
return d_obtain_alias(ext4_iget(child->d_sb, ino, EXT4_IGET_NORMAL));
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_parent);
/*
* Move count entries from end of map between two memory locations.
@@ -2066,6 +1911,7 @@ int ext4_find_dest_de(struct inode *dir, struct buffer_head *bh,
*dest_de = de;
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_dest_de);
void ext4_insert_dentry(struct inode *dir,
struct inode *inode,
@@ -2098,6 +1944,7 @@ void ext4_insert_dentry(struct inode *dir,
cpu_to_le32(hinfo->minor_hash);
}
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_insert_dentry);
/*
* Add a new entry into a directory (leaf) block. If de is non-NULL,
@@ -2705,6 +2552,7 @@ int ext4_generic_delete_entry(struct inode *dir,
}
return -ENOENT;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_generic_delete_entry);
static int ext4_delete_entry(handle_t *handle,
struct inode *dir,
@@ -2967,6 +2815,7 @@ int ext4_init_dirblock(handle_t *handle, struct inode *inode,
set_buffer_verified(bh);
return ext4_handle_dirty_dirblock(handle, inode, bh);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_dirblock);
int ext4_init_new_dir(handle_t *handle, struct inode *dir,
struct inode *inode)
@@ -2993,6 +2842,7 @@ int ext4_init_new_dir(handle_t *handle, struct inode *dir,
brelse(dir_block);
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_new_dir);
static struct dentry *ext4_mkdir(struct mnt_idmap *idmap, struct inode *dir,
struct dentry *dentry, umode_t mode)
@@ -3134,6 +2984,7 @@ bool ext4_empty_dir(struct inode *inode)
brelse(bh);
return true;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_empty_dir);
static int ext4_rmdir(struct inode *dir, struct dentry *dentry)
{
@@ -3297,6 +3148,7 @@ int __ext4_unlink(struct inode *dir, const struct qstr *d_name,
brelse(bh);
return retval;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_unlink);
static int ext4_unlink(struct inode *dir, struct dentry *dentry)
{
@@ -3490,6 +3342,7 @@ int __ext4_link(struct inode *dir, struct inode *inode,
goto retry;
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_link);
static int ext4_link(struct dentry *old_dentry,
struct inode *dir, struct dentry *dentry)
@@ -4241,6 +4094,7 @@ const struct inode_operations ext4_dir_inode_operations = {
.fileattr_get = ext4_fileattr_get,
.fileattr_set = ext4_fileattr_set,
};
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dir_inode_operations);
const struct inode_operations ext4_special_inode_operations = {
.setattr = ext4_setattr,
@@ -4249,3 +4103,159 @@ const struct inode_operations ext4_special_inode_operations = {
.get_inode_acl = ext4_get_acl,
.set_acl = ext4_set_acl,
};
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_special_inode_operations);
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+struct buffer_head *ext4_append_test(handle_t *handle, struct inode *inode,
+ ext4_lblk_t *block)
+{
+ return ext4_append(handle, inode, block);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_append_test);
+
+struct buffer_head *ext4_read_dirblock_test(struct inode *inode,
+ ext4_lblk_t block,
+ enum dirblock_type type)
+{
+ return ext4_read_dirblock(inode, block, type);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_dirblock_test);
+
+void ext4_dx_csum_set_test(struct inode *inode, struct ext4_dir_entry *dirent)
+{
+ ext4_dx_csum_set(inode, dirent);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dx_csum_set_test);
+
+int ext4_dx_csum_verify_test(struct inode *inode,
+ struct ext4_dir_entry *dirent)
+{
+ return ext4_dx_csum_verify(inode, dirent);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dx_csum_verify_test);
+
+struct dx_frame *dx_probe_test(struct ext4_filename *fname, struct inode *dir,
+ struct dx_hash_info *hinfo,
+ struct dx_frame *frame_in)
+{
+ return dx_probe(fname, dir, hinfo, frame_in);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(dx_probe_test);
+
+void dx_release_test(struct dx_frame *frames)
+{
+ dx_release(frames);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(dx_release_test);
+
+int ext4_htree_next_block_test(struct inode *dir, __u32 hash,
+ struct dx_frame *frame,
+ struct dx_frame *frames, __u32 *start_hash)
+{
+ return ext4_htree_next_block(dir, hash, frame, frames, start_hash);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_htree_next_block_test);
+
+int dx_make_map_test(struct inode *dir, struct buffer_head *bh,
+ struct dx_hash_info *hinfo,
+ struct dx_map_entry *map_tail)
+{
+ return dx_make_map(dir, bh, hinfo, map_tail);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(dx_make_map_test);
+
+void dx_sort_map_test(struct dx_map_entry *map, unsigned int count)
+{
+ dx_sort_map(map, count);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(dx_sort_map_test);
+
+void dx_insert_block_test(struct dx_frame *frame, u32 hash, ext4_lblk_t block)
+{
+ dx_insert_block(frame, hash, block);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(dx_insert_block_test);
+
+struct ext4_dir_entry_2 *dx_move_dirents_test(struct inode *dir, char *from,
+ char *to,
+ struct dx_map_entry *map,
+ int count,
+ unsigned int blocksize)
+{
+ return dx_move_dirents(dir, from, to, map, count, blocksize);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(dx_move_dirents_test);
+
+struct ext4_dir_entry_2 *dx_pack_dirents_test(struct inode *dir, char *base,
+ unsigned int blocksize)
+{
+ return dx_pack_dirents(dir, base, blocksize);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(dx_pack_dirents_test);
+
+struct ext4_dir_entry_2 *do_split_test(handle_t *handle, struct inode *dir,
+ struct buffer_head **bh,
+ struct dx_frame *frame,
+ struct dx_hash_info *hinfo)
+{
+ return do_split(handle, dir, bh, frame, hinfo);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(do_split_test);
+
+bool ext4_check_dx_root_test(struct inode *dir, struct dx_root *root)
+{
+ return ext4_check_dx_root(dir, root);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_check_dx_root_test);
+
+struct buffer_head *ext4_find_entry_test(struct inode *dir,
+ const struct qstr *d_name,
+ struct ext4_dir_entry_2 **res_dir,
+ int *inlined)
+{
+ return ext4_find_entry(dir, d_name, res_dir, inlined);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_entry_test);
+
+struct buffer_head *ext4_lookup_entry_test(struct inode *dir,
+ struct dentry *dentry,
+ struct ext4_dir_entry_2 **res_dir)
+{
+ return ext4_lookup_entry(dir, dentry, res_dir);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_lookup_entry_test);
+
+int ext4_add_entry_test(handle_t *handle, struct dentry *dentry,
+ struct inode *inode)
+{
+ return ext4_add_entry(handle, dentry, inode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_add_entry_test);
+
+int ext4_delete_entry_test(handle_t *handle, struct inode *dir,
+ struct ext4_dir_entry_2 *de_del,
+ struct buffer_head *bh)
+{
+ return ext4_delete_entry(handle, dir, de_del, bh);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_delete_entry_test);
+
+void ext4_inc_count_test(struct inode *inode)
+{
+ ext4_inc_count(inode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inc_count_test);
+
+void ext4_dec_count_test(struct inode *inode)
+{
+ ext4_dec_count(inode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dec_count_test);
+
+int ext4_init_symlink_block_test(handle_t *handle, struct inode *inode,
+ struct fscrypt_str *disk_link)
+{
+ return ext4_init_symlink_block(handle, inode, disk_link);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_symlink_block_test);
+#endif
diff --git a/fs/ext4/namei.h b/fs/ext4/namei.h
new file mode 100644
index 000000000000..2539ed588966
--- /dev/null
+++ b/fs/ext4/namei.h
@@ -0,0 +1,213 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Internal definitions and helpers for fs/ext4/namei.c
+ */
+
+#ifndef _EXT4_NAMEI_H
+#define _EXT4_NAMEI_H
+
+#include "ext4.h"
+
+/*
+ * Hints to ext4_read_dirblock regarding whether we expect a directory
+ * block being read to be an index block, or a block containing
+ * directory entries (and if the latter, whether it was found via a
+ * logical block in an htree index block). This is used to control
+ * what sort of sanity checkinig ext4_read_dirblock() will do on the
+ * directory block read from the storage device. EITHER will means
+ * the caller doesn't know what kind of directory block will be read,
+ * so no specific verification will be done.
+ */
+enum dirblock_type {
+ EITHER, INDEX, DIRENT, DIRENT_HTREE
+};
+
+struct fake_dirent {
+ __le32 inode;
+ __le16 rec_len;
+ u8 name_len;
+ u8 file_type;
+};
+
+struct dx_countlimit {
+ __le16 limit;
+ __le16 count;
+};
+
+struct dx_entry {
+ __le32 hash;
+ __le32 block;
+};
+
+/*
+ * dx_root_info is laid out so that if it should somehow get overlaid by a
+ * dirent the two low bits of the hash version will be zero. Therefore, the
+ * hash version mod 4 should never be 0. Sincerely, the paranoia department.
+ */
+struct dx_root {
+ struct fake_dirent dot;
+ char dot_name[4];
+ struct fake_dirent dotdot;
+ char dotdot_name[4];
+ struct dx_root_info {
+ __le32 reserved_zero;
+ u8 hash_version;
+ u8 info_length; /* 8 */
+ u8 indirect_levels;
+ u8 unused_flags;
+ } info;
+ struct dx_entry entries[];
+};
+
+struct dx_node {
+ struct fake_dirent fake;
+ struct dx_entry entries[];
+};
+
+struct dx_frame {
+ struct buffer_head *bh;
+ struct dx_entry *entries;
+ struct dx_entry *at;
+};
+
+struct dx_map_entry {
+ u32 hash;
+ u16 offs;
+ u16 size;
+};
+
+/*
+ * This goes at the end of each htree block.
+ */
+struct dx_tail {
+ u32 dt_reserved;
+ __le32 dt_checksum; /* crc32c(uuid+inum+dirblock) */
+};
+
+/*
+ * p is at least 6 bytes before the end of page
+ */
+static inline struct ext4_dir_entry_2 *
+ext4_next_entry(struct ext4_dir_entry_2 *p, unsigned long blocksize)
+{
+ return (struct ext4_dir_entry_2 *)((char *)p +
+ ext4_rec_len_from_disk(p->rec_len, blocksize));
+}
+
+/*
+ * Future: use high four bits of block for coalesce-on-delete flags
+ * Mask them off for now.
+ */
+static inline ext4_lblk_t dx_get_block(struct dx_entry *entry)
+{
+ return le32_to_cpu(entry->block) & 0x0fffffff;
+}
+
+static inline void dx_set_block(struct dx_entry *entry, ext4_lblk_t value)
+{
+ entry->block = cpu_to_le32(value);
+}
+
+static inline unsigned int dx_get_hash(struct dx_entry *entry)
+{
+ return le32_to_cpu(entry->hash);
+}
+
+static inline void dx_set_hash(struct dx_entry *entry, unsigned int value)
+{
+ entry->hash = cpu_to_le32(value);
+}
+
+static inline unsigned int dx_get_count(struct dx_entry *entries)
+{
+ return le16_to_cpu(((struct dx_countlimit *)entries)->count);
+}
+
+static inline unsigned int dx_get_limit(struct dx_entry *entries)
+{
+ return le16_to_cpu(((struct dx_countlimit *)entries)->limit);
+}
+
+static inline void dx_set_count(struct dx_entry *entries, unsigned int value)
+{
+ ((struct dx_countlimit *)entries)->count = cpu_to_le16(value);
+}
+
+static inline void dx_set_limit(struct dx_entry *entries, unsigned int value)
+{
+ ((struct dx_countlimit *)entries)->limit = cpu_to_le16(value);
+}
+
+static inline unsigned int dx_root_limit(struct inode *dir, unsigned int infosize)
+{
+ unsigned int entry_space = dir->i_sb->s_blocksize -
+ ext4_dir_rec_len(1, NULL) -
+ ext4_dir_rec_len(2, NULL) - infosize;
+
+ if (ext4_has_feature_metadata_csum(dir->i_sb))
+ entry_space -= sizeof(struct dx_tail);
+ return entry_space / sizeof(struct dx_entry);
+}
+
+static inline unsigned int dx_node_limit(struct inode *dir)
+{
+ unsigned int entry_space = dir->i_sb->s_blocksize -
+ ext4_dir_rec_len(0, dir);
+
+ if (ext4_has_feature_metadata_csum(dir->i_sb))
+ entry_space -= sizeof(struct dx_tail);
+ return entry_space / sizeof(struct dx_entry);
+}
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+struct buffer_head *ext4_append_test(handle_t *handle, struct inode *inode,
+ ext4_lblk_t *block);
+struct buffer_head *ext4_read_dirblock_test(struct inode *inode,
+ ext4_lblk_t block,
+ enum dirblock_type type);
+void ext4_dx_csum_set_test(struct inode *inode, struct ext4_dir_entry *dirent);
+int ext4_dx_csum_verify_test(struct inode *inode,
+ struct ext4_dir_entry *dirent);
+struct dx_frame *dx_probe_test(struct ext4_filename *fname, struct inode *dir,
+ struct dx_hash_info *hinfo,
+ struct dx_frame *frame_in);
+void dx_release_test(struct dx_frame *frames);
+int ext4_htree_next_block_test(struct inode *dir, __u32 hash,
+ struct dx_frame *frame,
+ struct dx_frame *frames, __u32 *start_hash);
+int dx_make_map_test(struct inode *dir, struct buffer_head *bh,
+ struct dx_hash_info *hinfo,
+ struct dx_map_entry *map_tail);
+void dx_sort_map_test(struct dx_map_entry *map, unsigned int count);
+void dx_insert_block_test(struct dx_frame *frame, u32 hash, ext4_lblk_t block);
+struct ext4_dir_entry_2 *dx_move_dirents_test(struct inode *dir, char *from,
+ char *to,
+ struct dx_map_entry *map,
+ int count,
+ unsigned int blocksize);
+struct ext4_dir_entry_2 *dx_pack_dirents_test(struct inode *dir, char *base,
+ unsigned int blocksize);
+struct ext4_dir_entry_2 *do_split_test(handle_t *handle, struct inode *dir,
+ struct buffer_head **bh,
+ struct dx_frame *frame,
+ struct dx_hash_info *hinfo);
+bool ext4_check_dx_root_test(struct inode *dir, struct dx_root *root);
+struct buffer_head *ext4_find_entry_test(struct inode *dir,
+ const struct qstr *d_name,
+ struct ext4_dir_entry_2 **res_dir,
+ int *inlined);
+struct buffer_head *ext4_lookup_entry_test(struct inode *dir,
+ struct dentry *dentry,
+ struct ext4_dir_entry_2 **res_dir);
+int ext4_add_entry_test(handle_t *handle, struct dentry *dentry,
+ struct inode *inode);
+int ext4_delete_entry_test(handle_t *handle, struct inode *dir,
+ struct ext4_dir_entry_2 *de_del,
+ struct buffer_head *bh);
+void ext4_inc_count_test(struct inode *inode);
+void ext4_dec_count_test(struct inode *inode);
+int ext4_init_symlink_block_test(handle_t *handle, struct inode *inode,
+ struct fscrypt_str *disk_link);
+#endif /* CONFIG_EXT4_KUNIT_TESTS */
+
+#endif /* _EXT4_NAMEI_H */
diff --git a/fs/ext4/orphan.c b/fs/ext4/orphan.c
index b4675aa7ea96..03538d498811 100644
--- a/fs/ext4/orphan.c
+++ b/fs/ext4/orphan.c
@@ -5,6 +5,7 @@
#include <linux/quotaops.h>
#include <linux/buffer_head.h>
#include <linux/string_choices.h>
+#include <kunit/static_stub.h>
#include "ext4.h"
#include "ext4_jbd2.h"
@@ -107,6 +108,8 @@ int ext4_orphan_add(handle_t *handle, struct inode *inode)
int err = 0, rc;
bool dirty = false;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_orphan_add, handle, inode);
+
if (!sbi->s_journal || is_bad_inode(inode))
return 0;
@@ -187,6 +190,7 @@ int ext4_orphan_add(handle_t *handle, struct inode *inode)
ext4_std_error(sb, err);
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_orphan_add);
static int ext4_orphan_file_del(handle_t *handle, struct inode *inode)
{
@@ -232,6 +236,8 @@ int ext4_orphan_del(handle_t *handle, struct inode *inode)
struct ext4_iloc iloc;
int err = 0;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_orphan_del, handle, inode);
+
if (!sbi->s_journal && !(sbi->s_mount_state & EXT4_ORPHAN_FS))
return 0;
@@ -308,6 +314,7 @@ int ext4_orphan_del(handle_t *handle, struct inode *inode)
brelse(iloc.bh);
goto out_err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_orphan_del);
#ifdef CONFIG_QUOTA
static int ext4_quota_on_mount(struct super_block *sb, int type)
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 06/27] ext4: expand KUnit tests for fs/ext4/mballoc.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (4 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 05/27] ext4: add KUnit tests for fs/ext4/namei.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (20 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Expand the ext4_mballoc_test KUnit test suite in fs/ext4/mballoc-test.c
from 7 subtests (21 test cases) to 32 subtests (96 parameterized test
cases across 1K, 4K, and 64K block sizes) to comprehensively exercise
the multi-block allocator in fs/ext4/mballoc.c:
- Low-level bitmap helpers (mb_set_bit, mb_clear_bit, mb_test_bit,
mb_find_next_zero_bit, mb_find_next_bit, mb_clear_bits, mb_set_bits,
and mb_test_and_clear_bits across clean, partially clear, and fully
clear ranges).
- Buddy bitmap lookup and extent discovery (mb_find_buddy,
mb_find_order_for_block, mb_find_extent for free, used, and merged
power-of-two ranges, and mb_regenerate_buddy).
- Corrupted block bitmap detection in ext4_mb_generate_buddy (setting
EXT4_GROUP_INFO_BBITMAP_CORRUPT when bitmap free count mismatches
grp->bb_free) and double-free / EXT4_STATE_FC_REPLAY handling in
mb_free_blocks.
- Group fragment size order calculation (mb_avg_fragment_size_order),
per-order XArray insertion/migration/removal
(mb_update_avg_fragment_size and mb_set_largest_free_order), and
largest free order tracking.
- Group eligibility filtering (ext4_mb_good_group_nolock and
ext4_mb_good_group) across CR_POWER2_ALIGNED, CR_GOAL_LEN_FAST,
CR_BEST_AVAIL_LEN, CR_GOAL_LEN_SLOW, and CR_ANY_FREE, including
BBITMAP_CORRUPT, 0 free blocks, uninitialized bitmaps, and flex_bg
metadata allocation rules.
- Goal-based allocation (ext4_mb_find_by_goal) covering power-of-two
buddies, non-power-of-two extents, EXT4_MB_HINT_MERGE tail merging,
stripe-aligned requests, and occupied goals.
- Buddy and bitmap group scanning (ext4_mb_simple_scan_group,
ext4_mb_scan_aligned, ext4_mb_complex_scan_group,
ext4_mb_measure_extent, and ext4_mb_try_best_found) including
s_mb_max_to_scan limits and EXT4_MB_HINT_FIRST.
- Regular allocator orchestration (ext4_mb_regular_allocator) for
EXT4_MB_HINT_GOAL_ONLY, stream allocation (EXT4_MB_STREAM_ALLOC) with
s_mb_last_groups updates, and MB_OPTIMIZE_SCAN with CR_BEST_AVAIL_LEN
goal length trimming.
- Block bitmap prefetching (ext4_mb_prefetch and ext4_mb_prefetch_fini)
and allocation context initialization and request normalization
(ext4_mb_initialize_context and ext4_mb_normalize_request across
metadata/no-normalize, small/large/huge files, stripe alignment, and
existing preallocation overlap avoidance).
- Inode and locality group preallocation lifecycles (ext4_mb_pa_alloc,
ext4_mb_pa_put_free, ext4_mb_new_preallocation,
ext4_mb_use_preallocated, ext4_mb_generate_from_pa,
ext4_mb_discard_group_preallocations, ext4_discard_preallocations,
ext4_mb_discard_preallocations, and locality group list trimming past
PREALLOC_TB_SIZE).
- Top-level allocation and deallocation entry points
(ext4_mb_new_blocks including PA satisfaction and ENOSPC retry after
discarding group PAs, ext4_mb_mark_context, ext4_mb_mark_bb,
ext4_free_blocks with EXT4_FREE_BLOCKS_NO_QUOT_UPDATE /
VALIDATED / METADATA and cross-group extents, mb_free_metadata
merging and splitting with ext4_process_freed_data,
ext4_group_add_blocks, ext4_trim_fs with a static stub for
ext4_issue_discard, and ext4_mballoc_query_range).
Export the tested functions with EXPORT_SYMBOL_FOR_EXT4_TEST and add a
KUNIT_STATIC_STUB_REDIRECT hook to ext4_issue_discard.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/mballoc-test.c | 1855 +++++++++++++++++++++++++++++++++++++++-
fs/ext4/mballoc.c | 201 ++++-
fs/ext4/mballoc.h | 49 ++
3 files changed, 2089 insertions(+), 16 deletions(-)
diff --git a/fs/ext4/mballoc-test.c b/fs/ext4/mballoc-test.c
index d31780075c21..402f4efdf25a 100644
--- a/fs/ext4/mballoc-test.c
+++ b/fs/ext4/mballoc-test.c
@@ -79,6 +79,7 @@ static int mbt_mb_init(struct super_block *sb)
sb->s_bdev = kzalloc_obj(*sb->s_bdev);
if (sb->s_bdev == NULL)
return -ENOMEM;
+ bdev_set_flag(sb->s_bdev, BD_READ_ONLY);
sb->s_bdev->bd_queue = kzalloc_obj(struct request_queue);
if (sb->s_bdev->bd_queue == NULL) {
@@ -162,10 +163,15 @@ static struct super_block *mbt_ext4_alloc_super_block(void)
goto out_deactivate;
bgl_lock_init(sbi->s_blockgroup_lock);
+ spin_lock_init(&sbi->s_error_lock);
+ ratelimit_state_init(&sbi->s_err_ratelimit_state, 5 * HZ, 10);
+ ratelimit_state_init(&sbi->s_warning_ratelimit_state, 5 * HZ, 10);
+ ratelimit_state_init(&sbi->s_msg_ratelimit_state, 5 * HZ, 10);
sbi->s_es = &fsb->es;
sbi->s_sb = sb;
sb->s_fs_info = sbi;
+ set_opt(sb, ERRORS_CONT);
up_write(&sb->s_umount);
return sb;
@@ -341,15 +347,72 @@ ext4_mb_mark_context_stub(handle_t *handle, struct super_block *sb, bool state,
{
struct mbt_grp_ctx *grp_ctx = MBT_GRP_CTX(sb, group);
struct buffer_head *bitmap_bh = &grp_ctx->bitmap_bh;
+ struct ext4_group_desc *gdp = &grp_ctx->desc;
+ ext4_grpblk_t i, already = 0, changed = len;
+ __u32 free;
- if (state)
+ for (i = 0; i < len; i++) {
+ if (mb_test_bit_test(blkoff + i, bitmap_bh->b_data) == state)
+ already++;
+ }
+ if (flags & 0x0001 /* EXT4_MB_BITMAP_MARKED_CHECK */)
+ changed = len - already;
+
+ free = ext4_free_group_clusters(sb, gdp);
+ if (state) {
mb_set_bits(bitmap_bh->b_data, blkoff, len);
- else
+ if (free >= changed)
+ ext4_free_group_clusters_set(sb, gdp, free - changed);
+ } else {
mb_clear_bits_test(bitmap_bh->b_data, blkoff, len);
+ ext4_free_group_clusters_set(sb, gdp, free + changed);
+ }
+
+ if (ret_changed)
+ *ret_changed = changed;
return 0;
}
+static int
+ext4_issue_discard_stub(struct super_block *sb, ext4_group_t block_group,
+ ext4_grpblk_t cluster, int count)
+{
+ return 0;
+}
+
+static struct inode *mbt_alloc_test_inode(struct kunit *test,
+ struct super_block *sb,
+ umode_t mode)
+{
+ struct ext4_inode_info *ei;
+ struct inode *inode;
+
+ ei = kunit_kzalloc(test, sizeof(*ei), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ei);
+
+ INIT_LIST_HEAD(&ei->i_orphan);
+ init_rwsem(&ei->xattr_sem);
+ init_rwsem(&ei->i_data_sem);
+ ei->i_prealloc_node = RB_ROOT;
+ atomic_set(&ei->i_prealloc_active, 0);
+ rwlock_init(&ei->i_prealloc_lock);
+ spin_lock_init(&ei->i_block_reservation_lock);
+ spin_lock_init(&ei->i_raw_lock);
+
+ inode = &ei->vfs_inode;
+ inode_init_once(inode);
+ ext4_fc_init_inode(inode);
+ inode->i_sb = sb;
+ inode->i_ino = 12;
+ inode->i_mode = mode;
+ inode->i_flags |= S_NOQUOTA;
+ atomic_set(&inode->i_writecount, 1);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+
+ return inode;
+}
+
#define TEST_GOAL_GROUP 1
static int mbt_kunit_init(struct kunit *test)
{
@@ -382,6 +445,9 @@ static int mbt_kunit_init(struct kunit *test)
kunit_activate_static_stub(test,
ext4_mb_mark_context,
ext4_mb_mark_context_stub);
+ kunit_activate_static_stub(test,
+ ext4_issue_discard,
+ ext4_issue_discard_stub);
/* stub function will be called in mbt_mb_init->ext4_mb_init */
if (mbt_mb_init(sb) != 0) {
@@ -402,7 +468,10 @@ static void mbt_kunit_exit(struct kunit *test)
if (!sb)
return;
+ if (sb->s_blocksize <= PAGE_SIZE)
+ ext4_mb_discard_preallocations_test(sb, 0);
mbt_mb_release(sb);
+ rcu_barrier();
mbt_ctx_release(sb);
mbt_ext4_free_super_block(sb);
}
@@ -417,11 +486,7 @@ static void test_new_blocks_simple(struct kunit *test)
ext4_fsblk_t found;
struct ext4_sb_info *sbi = EXT4_SB(sb);
- inode = kunit_kzalloc(test, sizeof(*inode), GFP_KERNEL);
- if (!inode)
- return;
-
- inode->i_sb = sb;
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
ar.inode = inode;
/* get block at goal */
@@ -527,10 +592,7 @@ test_free_blocks_simple_range(struct kunit *test, ext4_group_t goal_group,
struct inode *inode;
ext4_fsblk_t block;
- inode = kunit_kzalloc(test, sizeof(*inode), GFP_KERNEL);
- if (!inode)
- return;
- inode->i_sb = sb;
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
if (len == 0)
return;
@@ -601,10 +663,7 @@ static void test_mark_diskspace_used(struct kunit *test)
mbt_generate_test_ranges(sb, ranges, TEST_RANGE_COUNT);
- inode = kunit_kzalloc(test, sizeof(*inode), GFP_KERNEL);
- if (!inode)
- return;
- inode->i_sb = sb;
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
ac.ac_status = AC_STATUS_FOUND;
ac.ac_sb = sb;
@@ -949,6 +1008,1747 @@ static void test_mb_mark_used_cost(struct kunit *test)
ext4_mb_unload_buddy_test(&e4b);
}
+static void test_mb_bit_ops(struct kunit *test)
+{
+ u8 *buf;
+ void *unaligned;
+ int bit, i;
+
+ buf = kunit_kzalloc(test, 128, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+
+ /*
+ * Use an unaligned pointer (offset by 3 bytes) to exercise
+ * mb_correct_addr_and_bit() on 64-bit/32-bit architectures.
+ */
+ unaligned = buf + 3;
+
+ mb_set_bit_test(0, unaligned);
+ mb_set_bit_test(7, unaligned);
+ mb_set_bit_test(64, unaligned);
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(0, unaligned), 0);
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(1, unaligned), 0);
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(7, unaligned), 0);
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(64, unaligned), 0);
+
+ mb_clear_bit_test(7, unaligned);
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(7, unaligned), 0);
+
+ bit = mb_find_next_bit_test(unaligned, 128, 1);
+ KUNIT_EXPECT_EQ(test, bit, 64);
+
+ /* Clamping when no set bit exists before max */
+ bit = mb_find_next_bit_test(unaligned, 60, 1);
+ KUNIT_EXPECT_EQ(test, bit, 60);
+
+ /* Zero-length mb_set_bits / mb_clear_bits should be a no-op */
+ memset(buf, 0, 128);
+ mb_set_bits(unaligned, 10, 0);
+ KUNIT_EXPECT_EQ(test, mb_find_next_bit_test(unaligned, 256, 0), 256);
+
+ /* Multi-word unaligned range spanning head bits, 32-bit words, and tail */
+ mb_set_bits(unaligned, 5, 75);
+ KUNIT_EXPECT_EQ(test, mb_find_next_bit_test(unaligned, 256, 0), 5);
+ KUNIT_EXPECT_EQ(test, mb_find_next_zero_bit_test(unaligned, 256, 5), 80);
+ /* Clamping when no zero bit exists before max */
+ KUNIT_EXPECT_EQ(test, mb_find_next_zero_bit_test(unaligned, 50, 5), 50);
+
+ /* Clear a sub-range across 32-bit boundaries */
+ mb_clear_bits_test(unaligned, 13, 50);
+ for (i = 0; i < 100; i++) {
+ int expected = (i >= 5 && i < 13) || (i >= 63 && i < 80);
+
+ KUNIT_EXPECT_EQ(test, !!mb_test_bit_test(i, unaligned), expected);
+ }
+}
+
+static void test_mb_test_and_clear_bits(struct kunit *test)
+{
+ u8 *buf;
+ void *bm;
+ int ret, i;
+
+ buf = kunit_kzalloc(test, 128, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
+ bm = buf + 3;
+
+ /* Case 1: All bits in range [5, 85) are set -> returns -1 and clears all */
+ memset(buf, 0xff, 128);
+ ret = mb_test_and_clear_bits_test(bm, 5, 80);
+ KUNIT_EXPECT_EQ(test, ret, -1);
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(4, bm), 0);
+ for (i = 5; i < 85; i++)
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(i, bm), 0);
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(85, bm), 0);
+
+ /* Case 2: Zero bit in the unaligned head (bit 7) */
+ memset(buf, 0xff, 128);
+ mb_clear_bit_test(7, bm);
+ ret = mb_test_and_clear_bits_test(bm, 5, 80);
+ KUNIT_EXPECT_EQ(test, ret, 7);
+ for (i = 5; i < 85; i++)
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(i, bm), 0);
+
+ /* Case 3: Zero bit inside the 32-bit word fast-path region (bit 45) */
+ memset(buf, 0xff, 128);
+ mb_clear_bit_test(45, bm);
+ ret = mb_test_and_clear_bits_test(bm, 5, 80);
+ KUNIT_EXPECT_EQ(test, ret, 45);
+ for (i = 5; i < 85; i++)
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(i, bm), 0);
+}
+
+static void test_mb_find_buddy_and_order(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_buddy e4b;
+ struct ext4_free_extent ex;
+ void *bb;
+ int max = 0, ret;
+ int ord0, ord1, ord2, ord3;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /* Order 0 returns bitmap and bit count */
+ bb = mb_find_buddy_test(&e4b, 0, &max);
+ KUNIT_EXPECT_PTR_EQ(test, bb, e4b.bd_bitmap);
+ KUNIT_EXPECT_EQ(test, max, (int)(sb->s_blocksize << 3));
+
+ /* Valid higher order returns buddy offset */
+ bb = mb_find_buddy_test(&e4b, 1, &max);
+ KUNIT_EXPECT_PTR_EQ(test, bb, e4b.bd_buddy + sbi->s_mb_offsets[1]);
+ KUNIT_EXPECT_EQ(test, max, sbi->s_mb_maxs[1]);
+
+ /* Out-of-bounds order (> bd_blkbits + 1) returns NULL and max = 0 */
+ bb = mb_find_buddy_test(&e4b, MB_NUM_ORDERS(sb), &max);
+ KUNIT_EXPECT_NULL(test, bb);
+ KUNIT_EXPECT_EQ(test, max, 0);
+
+ /*
+ * Mark cluster 0 used in group 1: this splits the group so that:
+ * - cluster 1 is free at order 0
+ * - clusters [2, 4) are free at order 1
+ * - clusters [4, 8) are free at order 2
+ * - clusters [8, 16) are free at order 3
+ */
+ ex.fe_group = TEST_GOAL_GROUP;
+ ex.fe_start = 0;
+ ex.fe_len = 1;
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ mb_mark_used_test(&e4b, &ex);
+ ord0 = mb_find_order_for_block_test(&e4b, 1);
+ ord1 = mb_find_order_for_block_test(&e4b, 2);
+ ord2 = mb_find_order_for_block_test(&e4b, 4);
+ ord3 = mb_find_order_for_block_test(&e4b, 8);
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+
+ KUNIT_EXPECT_EQ(test, ord0, 0);
+ KUNIT_EXPECT_EQ(test, ord1, 1);
+ KUNIT_EXPECT_EQ(test, ord2, 2);
+ KUNIT_EXPECT_EQ(test, ord3, 3);
+
+ ext4_mb_unload_buddy_test(&e4b);
+}
+
+static void test_mb_find_extent(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_buddy e4b;
+ struct ext4_free_extent ex, found_used, found_small, found_coalesced, found_unaligned;
+ int ret, len_used, len_small, len_coalesced, len_unaligned;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /*
+ * Leave only [10, 26) (16 clusters) free:
+ * [10, 12) order 1 (2) + [12, 16) order 2 (4) +
+ * [16, 24) order 3 (8) + [24, 26) order 1 (2).
+ */
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ ex.fe_group = TEST_GOAL_GROUP;
+ ex.fe_start = 0;
+ ex.fe_len = 10;
+ mb_mark_used_test(&e4b, &ex);
+
+ ex.fe_start = 26;
+ ex.fe_len = EXT4_CLUSTERS_PER_GROUP(sb) - 26;
+ mb_mark_used_test(&e4b, &ex);
+
+ len_used = mb_find_extent_test(&e4b, 5, 8, &found_used);
+ len_small = mb_find_extent_test(&e4b, 10, 1, &found_small);
+ len_coalesced = mb_find_extent_test(&e4b, 10, 20, &found_coalesced);
+ len_unaligned = mb_find_extent_test(&e4b, 11, 20, &found_unaligned);
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+
+ /* Used block returns 0 */
+ KUNIT_EXPECT_EQ(test, len_used, 0);
+ KUNIT_EXPECT_EQ(test, found_used.fe_len, 0);
+
+ /* Needed = 1 at block 10 returns the order-1 buddy [10, 12) */
+ KUNIT_EXPECT_EQ(test, len_small, 2);
+ KUNIT_EXPECT_EQ(test, found_small.fe_start, 10);
+ KUNIT_EXPECT_EQ(test, found_small.fe_len, 2);
+
+ /* Needed = 20 at block 10 coalesces across orders 1, 2, 3, 1 -> 16 clusters */
+ KUNIT_EXPECT_EQ(test, len_coalesced, 16);
+ KUNIT_EXPECT_EQ(test, found_coalesced.fe_start, 10);
+ KUNIT_EXPECT_EQ(test, found_coalesced.fe_len, 16);
+
+ /* Unaligned start at block 11 coalesces [11, 26) -> 15 clusters */
+ KUNIT_EXPECT_EQ(test, len_unaligned, 15);
+ KUNIT_EXPECT_EQ(test, found_unaligned.fe_start, 11);
+ KUNIT_EXPECT_EQ(test, found_unaligned.fe_len, 15);
+
+ ext4_mb_unload_buddy_test(&e4b);
+}
+
+static void test_mb_generate_buddy_corrupt(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_group_info *grp;
+ void *bitmap, *buddy;
+ ext4_grpblk_t max = EXT4_CLUSTERS_PER_GROUP(sb);
+ bool is_corrupt;
+ ext4_grpblk_t corrected_free;
+ int i;
+
+ bitmap = kunit_kzalloc(test, sb->s_blocksize, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bitmap);
+ buddy = kunit_kzalloc(test, sb->s_blocksize, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buddy);
+
+ grp = ext4_get_group_info(sb, TEST_GOAL_GROUP);
+ KUNIT_ASSERT_NOT_NULL(test, grp);
+
+ /* Mark 10 clusters used in bitmap, so actual free is max - 10 */
+ mb_set_bits(bitmap, max, sb->s_blocksize * 8 - max);
+ mb_set_bits(bitmap, 0, 10);
+
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ for (i = 0; i < MB_NUM_ORDERS(sb); i++)
+ grp->bb_counters[i] = 0;
+ /* Intentionally set wrong bb_free = max (mismatch with max - 10) */
+ grp->bb_free = max;
+ memset(buddy, 0xff, sb->s_blocksize);
+ ext4_mb_generate_buddy_test(sb, buddy, bitmap, TEST_GOAL_GROUP, grp);
+ is_corrupt = EXT4_MB_GRP_BBITMAP_CORRUPT(grp);
+ corrected_free = grp->bb_free;
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+
+ KUNIT_EXPECT_TRUE(test, is_corrupt);
+ KUNIT_EXPECT_EQ(test, corrected_free, max - 10);
+}
+
+static void test_mb_free_blocks_double_free_and_fc_replay(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_buddy e4b;
+ struct ext4_free_extent ex;
+ bool corrupt_normal, corrupt_fc;
+ ext4_grpblk_t free_after_fc;
+ int ret;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ /* Part 1: Normal mode double-free marks group bitmap corrupt */
+ ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ /* Clusters [10, 14) are already free; freeing them again is a double free */
+ mb_free_blocks_test(NULL, &e4b, 10, 4);
+ corrupt_normal = EXT4_MB_GRP_BBITMAP_CORRUPT(e4b.bd_info);
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+ ext4_mb_unload_buddy_test(&e4b);
+
+ KUNIT_EXPECT_TRUE(test, corrupt_normal);
+
+ /*
+ * Part 2: In EXT4_FC_REPLAY mode, freeing an already-freed range
+ * triggers mb_regenerate_buddy() instead of marking the group corrupt.
+ */
+ ret = ext4_mb_load_buddy_test(sb, 2, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ext4_lock_group(sb, 2);
+ ex.fe_group = 2;
+ ex.fe_start = 10;
+ ex.fe_len = 10;
+ mb_mark_used_test(&e4b, &ex);
+ /* Free sub-range [12, 16) first so [12, 16) is already free */
+ mb_free_blocks_test(NULL, &e4b, 12, 4);
+
+ /* Re-freeing [12, 16) in EXT4_FC_REPLAY mode regenerates buddy cleanly */
+ sbi->s_mount_state |= EXT4_FC_REPLAY;
+ mb_free_blocks_test(NULL, &e4b, 12, 4);
+ sbi->s_mount_state &= ~EXT4_FC_REPLAY;
+
+ corrupt_fc = EXT4_MB_GRP_BBITMAP_CORRUPT(e4b.bd_info);
+ free_after_fc = e4b.bd_info->bb_free;
+ ext4_unlock_group(sb, 2);
+ ext4_mb_unload_buddy_test(&e4b);
+
+ KUNIT_EXPECT_FALSE(test, corrupt_fc);
+ KUNIT_EXPECT_EQ(test, free_after_fc, EXT4_CLUSTERS_PER_GROUP(sb) - 6);
+}
+
+static void test_mb_xarray_orders_and_frag_size(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_group_info *grp;
+ int ord_1, ord_4, ord_7, ord_8;
+ int old_lfo, new_lfo, new_afso;
+ void *xa_lfo_entry, *xa_afso_entry, *xa_old_entry;
+ int i;
+
+ set_opt2(sb, MB_OPTIMIZE_SCAN);
+
+ /* mb_avg_fragment_size_order computes clamp(fls(len) - 2, 0, MB_NUM_ORDERS - 1) */
+ ord_1 = mb_avg_fragment_size_order_test(sb, 1);
+ ord_4 = mb_avg_fragment_size_order_test(sb, 4);
+ ord_7 = mb_avg_fragment_size_order_test(sb, 7);
+ ord_8 = mb_avg_fragment_size_order_test(sb, 8);
+ KUNIT_EXPECT_EQ(test, ord_1, 0);
+ KUNIT_EXPECT_EQ(test, ord_4, 1);
+ KUNIT_EXPECT_EQ(test, ord_7, 1);
+ KUNIT_EXPECT_EQ(test, ord_8, 2);
+
+ grp = ext4_get_group_info(sb, TEST_GOAL_GROUP);
+ KUNIT_ASSERT_NOT_NULL(test, grp);
+
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ old_lfo = grp->bb_largest_free_order;
+ for (i = 0; i < MB_NUM_ORDERS(sb); i++)
+ grp->bb_counters[i] = 0;
+ grp->bb_counters[3] = 2;
+ grp->bb_free = 16;
+ grp->bb_fragments = 2;
+
+ mb_set_largest_free_order_test(sb, grp);
+ mb_update_avg_fragment_size_test(sb, grp);
+
+ new_lfo = grp->bb_largest_free_order;
+ new_afso = grp->bb_avg_fragment_size_order;
+ xa_lfo_entry = xa_load(&sbi->s_mb_largest_free_orders[3], TEST_GOAL_GROUP);
+ xa_afso_entry = xa_load(&sbi->s_mb_avg_fragment_size[2], TEST_GOAL_GROUP);
+
+ /* Now mark group completely full and verify XArray entries are erased */
+ grp->bb_counters[3] = 0;
+ grp->bb_free = 0;
+ grp->bb_fragments = 0;
+ mb_set_largest_free_order_test(sb, grp);
+ mb_update_avg_fragment_size_test(sb, grp);
+ xa_old_entry = xa_load(&sbi->s_mb_largest_free_orders[3], TEST_GOAL_GROUP);
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+
+ KUNIT_EXPECT_EQ(test, old_lfo, -1);
+ KUNIT_EXPECT_EQ(test, new_lfo, 3);
+ KUNIT_EXPECT_EQ(test, new_afso, 2);
+ KUNIT_EXPECT_PTR_EQ(test, xa_lfo_entry, grp);
+ KUNIT_EXPECT_PTR_EQ(test, xa_afso_entry, grp);
+ KUNIT_EXPECT_NULL(test, xa_old_entry);
+}
+
+static void test_mb_good_group(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_buddy e4b;
+ struct ext4_allocation_context ac = {};
+ struct ext4_group_info *grp1, *grp2;
+ int ret;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ /* Initialize group 1 buddy cache so bb_fragments and orders are populated */
+ ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ext4_mb_unload_buddy_test(&e4b);
+
+ grp1 = ext4_get_group_info(sb, TEST_GOAL_GROUP);
+ KUNIT_ASSERT_NOT_NULL(test, grp1);
+
+ ac.ac_sb = sb;
+ ac.ac_2order = 3;
+ ac.ac_g_ex.fe_len = 8;
+ ac.ac_flags = EXT4_MB_HINT_DATA;
+
+ /* Group 1 has all clusters free in 1 fragment -> eligible at all CRs */
+ KUNIT_EXPECT_TRUE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_POWER2_ALIGNED));
+ KUNIT_EXPECT_TRUE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_GOAL_LEN_FAST));
+ KUNIT_EXPECT_TRUE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_BEST_AVAIL_LEN));
+ KUNIT_EXPECT_TRUE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_GOAL_LEN_SLOW));
+ KUNIT_EXPECT_TRUE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_ANY_FREE));
+
+ /*
+ * Simulate high fragmentation: free = 16, fragments = 8 (avg = 2 < 8),
+ * largest_free_order = 1 (< ac_2order = 3).
+ * CR_POWER2_ALIGNED, CR_GOAL_LEN_FAST, CR_BEST_AVAIL_LEN must reject,
+ * while CR_GOAL_LEN_SLOW (free 16 >= 8) and CR_ANY_FREE accept.
+ */
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ grp1->bb_free = 16;
+ grp1->bb_fragments = 8;
+ grp1->bb_largest_free_order = 1;
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+
+ KUNIT_EXPECT_FALSE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_POWER2_ALIGNED));
+ KUNIT_EXPECT_FALSE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_GOAL_LEN_FAST));
+ KUNIT_EXPECT_FALSE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_BEST_AVAIL_LEN));
+ KUNIT_EXPECT_TRUE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_GOAL_LEN_SLOW));
+ KUNIT_EXPECT_TRUE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_ANY_FREE));
+
+ /* When free = 4 < fe_len = 8, CR_GOAL_LEN_SLOW rejects, CR_ANY_FREE accepts */
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ grp1->bb_free = 4;
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+
+ KUNIT_EXPECT_FALSE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_GOAL_LEN_SLOW));
+ KUNIT_EXPECT_TRUE(test, ext4_mb_good_group_test(&ac, TEST_GOAL_GROUP,
+ CR_ANY_FREE));
+
+ /* Flex_bg first group avoidance at CR_POWER2_ALIGNED for data */
+ ret = ext4_mb_load_buddy_test(sb, 0, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ext4_mb_unload_buddy_test(&e4b);
+ sbi->s_log_groups_per_flex = 2; /* flex_size = 4 >= EXT4_FLEX_SIZE_DIR_ALLOC_SCHEME */
+ KUNIT_EXPECT_FALSE(test, ext4_mb_good_group_test(&ac, 0, CR_POWER2_ALIGNED));
+ sbi->s_log_groups_per_flex = 0;
+
+ /*
+ * Test ext4_mb_good_group_nolock on uninitialized group 2:
+ * At inexpensive CR_POWER2_ALIGNED, it skips initializing group 2 and returns 0.
+ * At expensive CR_GOAL_LEN_SLOW (with STRICT_CHECK), it initializes group 2
+ * and returns 1.
+ */
+ grp2 = ext4_get_group_info(sb, 2);
+ KUNIT_ASSERT_NOT_NULL(test, grp2);
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_NEED_INIT(grp2));
+
+ ac.ac_criteria = CR_POWER2_ALIGNED;
+ ret = ext4_mb_good_group_nolock_test(&ac, 2, CR_POWER2_ALIGNED);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_NEED_INIT(grp2));
+
+ ac.ac_flags |= EXT4_MB_STRICT_CHECK;
+ ac.ac_criteria = CR_GOAL_LEN_SLOW;
+ ret = ext4_mb_good_group_nolock_test(&ac, 2, CR_GOAL_LEN_SLOW);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_FALSE(test, EXT4_MB_GRP_NEED_INIT(grp2));
+}
+
+static void test_mb_find_by_goal(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_buddy e4b;
+ struct ext4_allocation_context ac = {};
+ struct inode *inode;
+ int ret;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
+
+ /* Mark [24, 32) used in group 1 so [20, 24) has only 4 free clusters */
+ mbt_ctx_mark_used(sb, TEST_GOAL_GROUP, 24, 8);
+ ext4_get_group_info(sb, TEST_GOAL_GROUP)->bb_free -= 8;
+
+ /* 1. Exact goal hit at [10, 14) */
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_flags = EXT4_MB_HINT_TRY_GOAL;
+ ac.ac_g_ex.fe_group = TEST_GOAL_GROUP;
+ ac.ac_g_ex.fe_start = 10;
+ ac.ac_g_ex.fe_len = 4;
+ ac.ac_o_ex = ac.ac_g_ex;
+
+ ret = ext4_mb_find_by_goal_test(&ac, &e4b);
+ ext4_mb_release_context_test(&ac);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_FOUND);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_group, TEST_GOAL_GROUP);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_start, 10);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_len, 4);
+
+ /* 2. Partial goal at [20, 28) without HINT_MERGE fails to match */
+ memset(&ac, 0, sizeof(ac));
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_flags = EXT4_MB_HINT_TRY_GOAL;
+ ac.ac_g_ex.fe_group = TEST_GOAL_GROUP;
+ ac.ac_g_ex.fe_start = 20;
+ ac.ac_g_ex.fe_len = 8;
+ ac.ac_o_ex = ac.ac_g_ex;
+
+ ret = ext4_mb_find_by_goal_test(&ac, &e4b);
+ ext4_mb_release_context_test(&ac);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_CONTINUE);
+
+ /* 3. Partial goal at [20, 28) WITH EXT4_MB_HINT_MERGE allocates [20, 24) */
+ memset(&ac, 0, sizeof(ac));
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_flags = EXT4_MB_HINT_TRY_GOAL | EXT4_MB_HINT_MERGE;
+ ac.ac_g_ex.fe_group = TEST_GOAL_GROUP;
+ ac.ac_g_ex.fe_start = 20;
+ ac.ac_g_ex.fe_len = 8;
+ ac.ac_o_ex = ac.ac_g_ex;
+
+ ret = ext4_mb_find_by_goal_test(&ac, &e4b);
+ ext4_mb_release_context_test(&ac);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_FOUND);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_start, 20);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_len, 4);
+
+ /*
+ * 4. Stripe-aligned check: s_stripe = 8 clusters; unaligned start 36
+ * rejected, aligned 40 accepted.
+ */
+ sbi->s_stripe = EXT4_C2B(sbi, 8);
+ memset(&ac, 0, sizeof(ac));
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_flags = EXT4_MB_HINT_TRY_GOAL;
+ ac.ac_g_ex.fe_group = TEST_GOAL_GROUP;
+ ac.ac_g_ex.fe_start = 36;
+ ac.ac_g_ex.fe_len = 8;
+ ac.ac_o_ex = ac.ac_g_ex;
+
+ ret = ext4_mb_find_by_goal_test(&ac, &e4b);
+ ext4_mb_release_context_test(&ac);
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_CONTINUE);
+
+ ac.ac_g_ex.fe_start = 40;
+ ac.ac_o_ex = ac.ac_g_ex;
+ ret = ext4_mb_find_by_goal_test(&ac, &e4b);
+ ext4_mb_release_context_test(&ac);
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_FOUND);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_start, 40);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_len, 8);
+ sbi->s_stripe = 0;
+}
+
+static void test_mb_simple_and_aligned_scan(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_buddy e4b;
+ struct ext4_allocation_context ac = {};
+ struct inode *inode;
+ int ret;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
+
+ /*
+ * Mark cluster 0 used in group 1 so:
+ * - order 3 buddy [8, 16) is the first order-3 buddy
+ * - first 16-cluster stripe [0, 16) is not completely free, so
+ * stripe-aligned scan must skip to cluster 16.
+ */
+ mbt_ctx_mark_used(sb, TEST_GOAL_GROUP, 0, 1);
+
+ ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /* 1. ext4_mb_simple_scan_group with ac_2order = 3 (8 clusters) */
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_criteria = CR_POWER2_ALIGNED;
+ ac.ac_2order = 3;
+ ac.ac_g_ex.fe_len = 8;
+ ac.ac_o_ex.fe_len = 8;
+
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ ext4_mb_simple_scan_group_test(&ac, &e4b);
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+ ext4_mb_release_context_test(&ac);
+
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_FOUND);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_group, TEST_GOAL_GROUP);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_start, 8);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_len, 8);
+
+ /* 2. ext4_mb_scan_aligned with stripe = 16 clusters */
+ sbi->s_stripe = EXT4_C2B(sbi, 16);
+ memset(&ac, 0, sizeof(ac));
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_criteria = CR_GOAL_LEN_FAST;
+ ac.ac_g_ex.fe_len = 16;
+ ac.ac_o_ex.fe_len = 16;
+
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ ext4_mb_scan_aligned_test(&ac, &e4b);
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+ ext4_mb_release_context_test(&ac);
+
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_FOUND);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_group, TEST_GOAL_GROUP);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_start, 16);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_len, 16);
+
+ sbi->s_stripe = 0;
+ ext4_mb_unload_buddy_test(&e4b);
+}
+
+static void test_mb_complex_scan_and_measure(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_buddy e4b;
+ struct ext4_allocation_context ac = {};
+ struct inode *inode;
+ ext4_grpblk_t max = EXT4_CLUSTERS_PER_GROUP(sb);
+ int ret;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
+
+ /*
+ * Create 3 power-of-2 aligned free extents in TEST_GOAL_GROUP:
+ * - [0, 4) len 4 (order 2 buddy)
+ * - [16, 32) len 16 (order 4 buddy)
+ * - [40, 48) len 8 (order 3 buddy)
+ * Mark everything else used (total free = 28 clusters).
+ */
+ mbt_ctx_mark_used(sb, TEST_GOAL_GROUP, 4, 12);
+ mbt_ctx_mark_used(sb, TEST_GOAL_GROUP, 32, 8);
+ mbt_ctx_mark_used(sb, TEST_GOAL_GROUP, 48, max - 48);
+ ext4_get_group_info(sb, TEST_GOAL_GROUP)->bb_free = 28;
+
+ ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /*
+ * Case 1: Goal len = 5 at CR_GOAL_LEN_SLOW.
+ * Scans [0, 4) (len 4 < 5), [16, 32) (order 4 buddy -> len 16 > 5),
+ * and [40, 48) (order 3 buddy -> len 8 > 5, smaller than 16 so preferred!),
+ * then allocates 5 clusters from [40, 48) at finish_group.
+ */
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_criteria = CR_GOAL_LEN_SLOW;
+ ac.ac_g_ex.fe_len = 5;
+ ac.ac_o_ex.fe_len = 5;
+
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ ext4_mb_complex_scan_group_test(&ac, &e4b);
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+ ext4_mb_release_context_test(&ac);
+
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_FOUND);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_start, 40);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_len, 5);
+
+ /*
+ * Case 2: Goal len = 32 (larger than any free extent) with s_mb_max_to_scan = 1.
+ * Scanning [0, 4) and [16, 32) increments ac_found to 2 > s_mb_max_to_scan,
+ * triggering AC_STATUS_BREAK with best smaller extent [16, 32) (len 16)
+ * recorded in ac_b_ex. Calling ext4_mb_try_best_found() then claims [16, 32).
+ */
+ sbi->s_mb_max_to_scan = 1;
+ memset(&ac, 0, sizeof(ac));
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_criteria = CR_ANY_FREE;
+ ac.ac_g_ex.fe_len = 32;
+ ac.ac_o_ex.fe_len = 32;
+
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ ext4_mb_complex_scan_group_test(&ac, &e4b);
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_BREAK);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_start, 16);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_len, 16);
+
+ ext4_mb_try_best_found_test(&ac, &e4b);
+ ext4_mb_release_context_test(&ac);
+
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_FOUND);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_start, 16);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_len, 16);
+
+ /*
+ * Case 3: EXT4_MB_HINT_FIRST immediately takes the first free extent [0, 4).
+ */
+ sbi->s_mb_max_to_scan = 200;
+ memset(&ac, 0, sizeof(ac));
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_criteria = CR_ANY_FREE;
+ ac.ac_flags = EXT4_MB_HINT_FIRST;
+ ac.ac_g_ex.fe_len = 4;
+ ac.ac_o_ex.fe_len = 4;
+
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ ext4_mb_complex_scan_group_test(&ac, &e4b);
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+ ext4_mb_release_context_test(&ac);
+
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_FOUND);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_start, 0);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_len, 4);
+
+ ext4_mb_unload_buddy_test(&e4b);
+}
+
+static void test_mb_regular_allocator(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_allocation_context ac = {};
+ struct inode *inode;
+ int ret, hash;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
+
+ /*
+ * 1. EXT4_MB_HINT_GOAL_ONLY when goal block (group 0, cluster 0) is already
+ * marked used: must stop immediately without scanning other blocks.
+ */
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_flags = EXT4_MB_HINT_GOAL_ONLY;
+ ac.ac_g_ex.fe_group = 0;
+ ac.ac_g_ex.fe_start = 0;
+ ac.ac_g_ex.fe_len = 1;
+ ac.ac_o_ex = ac.ac_g_ex;
+
+ ret = ext4_mb_regular_allocator_test(&ac);
+ ext4_mb_release_context_test(&ac);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_CONTINUE);
+
+ /*
+ * 2. Stream allocation (EXT4_MB_STREAM_ALLOC) with group 0 full:
+ * allocates in group 1 and updates s_mb_last_groups[hash] to 1.
+ */
+ mbt_ctx_mark_used(sb, 0, 0, EXT4_CLUSTERS_PER_GROUP(sb));
+ ext4_free_group_clusters_set(sb, &MBT_GRP_CTX(sb, 0)->desc, 0);
+ ext4_get_group_info(sb, 0)->bb_free = 0;
+
+ memset(&ac, 0, sizeof(ac));
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_flags = EXT4_MB_STREAM_ALLOC | EXT4_MB_HINT_DATA;
+ ac.ac_g_ex.fe_len = 8;
+ ac.ac_orig_goal_len = 8;
+ ac.ac_o_ex.fe_len = 8;
+
+ ret = ext4_mb_regular_allocator_test(&ac);
+ ext4_mb_release_context_test(&ac);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_FOUND);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_group, 1);
+
+ hash = (unsigned int)inode->i_ino % sbi->s_mb_nr_global_goals;
+ KUNIT_EXPECT_EQ(test, READ_ONCE(sbi->s_mb_last_groups[hash]), 1);
+
+ /*
+ * 3. MB_OPTIMIZE_SCAN with CR_BEST_AVAIL_LEN goal length trimming:
+ * Make group 2 the only group with free blocks, fragmented into two
+ * 8-cluster chunks [0, 8) and [16, 24) so largest_free_order = 3
+ * and avg_fragment_size = 8.
+ * Note: group 2 may have already been prefetched during step 2, so
+ * mark its blocks used via mb_mark_used_test on the loaded buddy.
+ */
+ set_opt2(sb, MB_OPTIMIZE_SCAN);
+ mbt_ctx_mark_used(sb, 1, 0, EXT4_CLUSTERS_PER_GROUP(sb));
+ ext4_free_group_clusters_set(sb, &MBT_GRP_CTX(sb, 1)->desc, 0);
+ ext4_get_group_info(sb, 1)->bb_free = 0;
+ ext4_get_group_info(sb, 1)->bb_fragments = 0;
+ mb_set_largest_free_order_test(sb, ext4_get_group_info(sb, 1));
+ mb_update_avg_fragment_size_test(sb, ext4_get_group_info(sb, 1));
+
+ mbt_ctx_mark_used(sb, 3, 0, EXT4_CLUSTERS_PER_GROUP(sb));
+ ext4_free_group_clusters_set(sb, &MBT_GRP_CTX(sb, 3)->desc, 0);
+ ext4_get_group_info(sb, 3)->bb_free = 0;
+ ext4_get_group_info(sb, 3)->bb_fragments = 0;
+ mb_set_largest_free_order_test(sb, ext4_get_group_info(sb, 3));
+ mb_update_avg_fragment_size_test(sb, ext4_get_group_info(sb, 3));
+
+ {
+ struct ext4_buddy e4b;
+ struct ext4_free_extent ex;
+
+ ret = ext4_mb_load_buddy_test(sb, 2, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ext4_lock_group(sb, 2);
+ ex.fe_group = 2;
+ ex.fe_start = 8;
+ ex.fe_len = 8;
+ mb_mark_used_test(&e4b, &ex);
+ ex.fe_start = 24;
+ ex.fe_len = EXT4_CLUSTERS_PER_GROUP(sb) - 24;
+ mb_mark_used_test(&e4b, &ex);
+ ext4_unlock_group(sb, 2);
+ ext4_mb_unload_buddy_test(&e4b);
+ }
+
+ memset(&ac, 0, sizeof(ac));
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_status = AC_STATUS_CONTINUE;
+ ac.ac_flags = EXT4_MB_HINT_DATA;
+ ac.ac_g_ex.fe_group = 0;
+ ac.ac_g_ex.fe_len = 16;
+ ac.ac_orig_goal_len = 16;
+ ac.ac_o_ex.fe_len = 8;
+
+ ret = ext4_mb_regular_allocator_test(&ac);
+ ext4_mb_release_context_test(&ac);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ac.ac_status, AC_STATUS_FOUND);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_group, 2);
+ KUNIT_EXPECT_EQ(test, ac.ac_b_ex.fe_len, 8);
+}
+
+static void test_mb_prefetch(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_group_info *grp0, *grp1, *grp2;
+ ext4_group_t next_grp;
+ int ios = 0;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ grp0 = ext4_get_group_info(sb, 0);
+ grp1 = ext4_get_group_info(sb, 1);
+ grp2 = ext4_get_group_info(sb, 2);
+ KUNIT_ASSERT_NOT_NULL(test, grp0);
+ KUNIT_ASSERT_NOT_NULL(test, grp1);
+ KUNIT_ASSERT_NOT_NULL(test, grp2);
+
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_NEED_INIT(grp0));
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_NEED_INIT(grp1));
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_NEED_INIT(grp2));
+
+ next_grp = ext4_mb_prefetch(sb, 0, 2, &ios);
+ KUNIT_EXPECT_EQ(test, next_grp, 2);
+ /* First prefetch of uninitialized buffers increments ios count */
+ KUNIT_EXPECT_EQ(test, ios, 2);
+
+ /* Repeated prefetch on the same groups does not re-read bitmaps */
+ ios = 0;
+ ext4_mb_prefetch(sb, 0, 2, &ios);
+ KUNIT_EXPECT_EQ(test, ios, 0);
+
+ /* ext4_mb_prefetch_fini initializes the prefetched groups [0, 2) */
+ ext4_mb_prefetch_fini(sb, next_grp, 2);
+ KUNIT_EXPECT_FALSE(test, EXT4_MB_GRP_NEED_INIT(grp0));
+ KUNIT_EXPECT_FALSE(test, EXT4_MB_GRP_NEED_INIT(grp1));
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_NEED_INIT(grp2));
+}
+
+static void test_mb_initialize_context(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_allocation_context ac = {};
+ struct ext4_allocation_request ar = {};
+ struct inode *inode;
+
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
+
+ /* 1. Small open file (<= s_mb_stream_request) uses locality group alloc */
+ i_size_write(inode, sb->s_blocksize);
+ ar.inode = inode;
+ ar.logical = 0;
+ ar.len = 1;
+ ar.goal = ext4_group_first_block_no(sb, TEST_GOAL_GROUP);
+ ar.flags = EXT4_MB_HINT_DATA;
+
+ ext4_mb_initialize_context_test(&ac, &ar);
+ ext4_mb_release_context_test(&ac);
+ KUNIT_EXPECT_NE(test, ac.ac_flags & EXT4_MB_HINT_GROUP_ALLOC, 0);
+ KUNIT_EXPECT_NOT_NULL(test, ac.ac_lg);
+ KUNIT_EXPECT_EQ(test, ac.ac_g_ex.fe_group, TEST_GOAL_GROUP);
+
+ /* 2. Large open file (> s_mb_stream_request) uses stream alloc */
+ memset(&ac, 0, sizeof(ac));
+ i_size_write(inode, (loff_t)(sbi->s_mb_stream_request + 64) << sb->s_blocksize_bits);
+ ext4_mb_initialize_context_test(&ac, &ar);
+ ext4_mb_release_context_test(&ac);
+ KUNIT_EXPECT_NE(test, ac.ac_flags & EXT4_MB_STREAM_ALLOC, 0);
+ KUNIT_EXPECT_NULL(test, ac.ac_lg);
+
+ /*
+ * 3. Closed file (i_writecount == 0) with size == isize and
+ * size > s_mb_stream_request sets EXT4_MB_HINT_NOPREALLOC.
+ */
+ memset(&ac, 0, sizeof(ac));
+ atomic_set(&inode->i_writecount, 0);
+ ar.logical = EXT4_LBLK_CMASK(sbi, sbi->s_mb_stream_request + EXT4_C2B(sbi, 2));
+ ar.len = 1;
+ i_size_write(inode, (loff_t)(ar.logical + EXT4_C2B(sbi, 1)) << sb->s_blocksize_bits);
+ /* Also test out-of-range ar.goal fallback to group 0 */
+ ar.goal = ext4_blocks_count(sbi->s_es) + 100;
+ ext4_mb_initialize_context_test(&ac, &ar);
+ ext4_mb_release_context_test(&ac);
+ KUNIT_EXPECT_NE(test, ac.ac_flags & EXT4_MB_HINT_NOPREALLOC, 0);
+ KUNIT_EXPECT_EQ(test, ac.ac_o_ex.fe_group, 0);
+ KUNIT_EXPECT_EQ(test, ac.ac_o_ex.fe_start, 0);
+}
+
+static void test_mb_normalize_request(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_allocation_context ac = {};
+ struct ext4_allocation_request ar = {};
+ struct inode *inode;
+ struct ext4_inode_info *ei;
+ struct ext4_prealloc_space left_pa = {}, right_pa = {};
+ int bsbits = sb->s_blocksize_bits;
+
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
+ ei = EXT4_I(inode);
+
+ /* 1. Metadata request (no EXT4_MB_HINT_DATA) is not normalized */
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_o_ex.fe_logical = 0;
+ ac.ac_o_ex.fe_len = 1;
+ ac.ac_g_ex = ac.ac_o_ex;
+ ext4_mb_normalize_request_test(&ac, &ar);
+ KUNIT_EXPECT_EQ(test, ac.ac_g_ex.fe_len, 1);
+
+ /* 2. Locality group request normalizes fe_len to s_mb_group_prealloc */
+ memset(&ac, 0, sizeof(ac));
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_flags = EXT4_MB_HINT_DATA | EXT4_MB_HINT_GROUP_ALLOC;
+ ac.ac_lg = raw_cpu_ptr(sbi->s_locality_groups);
+ ac.ac_o_ex.fe_len = 1;
+ ac.ac_g_ex = ac.ac_o_ex;
+ ext4_mb_normalize_request_test(&ac, &ar);
+ KUNIT_EXPECT_EQ(test, ac.ac_g_ex.fe_len, sbi->s_mb_group_prealloc);
+
+ /* 3. Small file (<= 16KB) normalizes up to max(SZ_16K, cluster_size) */
+ if ((SZ_16K >> bsbits) >= sbi->s_cluster_ratio) {
+ memset(&ac, 0, sizeof(ac));
+ i_size_write(inode, 4096);
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_flags = EXT4_MB_HINT_DATA;
+ ac.ac_o_ex.fe_logical = 0;
+ ac.ac_o_ex.fe_len = 1;
+ ac.ac_g_ex = ac.ac_o_ex;
+ ext4_mb_normalize_request_test(&ac, &ar);
+ KUNIT_EXPECT_EQ(test, ac.ac_g_ex.fe_len,
+ EXT4_NUM_B2C(sbi, SZ_16K >> bsbits));
+ }
+
+ /* 4. 200KB file normalizes up to next power of two (256KB) */
+ if ((SZ_256K >> bsbits) >= sbi->s_cluster_ratio) {
+ memset(&ac, 0, sizeof(ac));
+ i_size_write(inode, 200 * 1024);
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_flags = EXT4_MB_HINT_DATA;
+ ac.ac_o_ex.fe_logical = 0;
+ ac.ac_o_ex.fe_len = 1;
+ ac.ac_g_ex = ac.ac_o_ex;
+ ext4_mb_normalize_request_test(&ac, &ar);
+ KUNIT_EXPECT_EQ(test, ac.ac_g_ex.fe_len,
+ EXT4_NUM_B2C(sbi, SZ_256K >> bsbits));
+ }
+
+ /*
+ * 5. Trimming against left/right allocated neighbors (ar.pleft / ar.pright)
+ * and against existing inode preallocations (ext4_mb_pa_adjust_overlap).
+ */
+ if (((loff_t)EXT4_C2B(sbi, 16) << bsbits) <= SZ_1M) {
+ memset(&ac, 0, sizeof(ac));
+ memset(&ar, 0, sizeof(ar));
+ i_size_write(inode, (loff_t)EXT4_C2B(sbi, 16) << bsbits);
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_flags = EXT4_MB_HINT_DATA;
+ ac.ac_o_ex.fe_logical = EXT4_C2B(sbi, 4);
+ ac.ac_o_ex.fe_len = 1;
+ ac.ac_g_ex = ac.ac_o_ex;
+
+ /* Left neighbor ends at cluster 2 (logical block EXT4_C2B(sbi, 2) - 1) */
+ ar.lleft = EXT4_C2B(sbi, 2) - 1;
+ ar.pleft = ext4_group_first_block_no(sb, TEST_GOAL_GROUP) + ar.lleft;
+ /* Right neighbor starts at cluster 10 */
+ ar.lright = EXT4_C2B(sbi, 10);
+ ar.pright = ext4_group_first_block_no(sb, TEST_GOAL_GROUP) + ar.lright;
+
+ /*
+ * Also insert a left PA covering clusters [2, 3) and a right PA covering
+ * clusters [8, 10) into ei->i_prealloc_node so ext4_mb_pa_adjust_overlap
+ * trims the range further to clusters [3, 8) (5 clusters).
+ */
+ spin_lock_init(&left_pa.pa_lock);
+ left_pa.pa_lstart = EXT4_C2B(sbi, 2);
+ left_pa.pa_len = 1;
+ left_pa.pa_free = 1;
+ left_pa.pa_deleted = 0;
+
+ spin_lock_init(&right_pa.pa_lock);
+ right_pa.pa_lstart = EXT4_C2B(sbi, 8);
+ right_pa.pa_len = 2;
+ right_pa.pa_free = 2;
+ right_pa.pa_deleted = 0;
+
+ rb_link_node(&left_pa.pa_node.inode_node, NULL,
+ &ei->i_prealloc_node.rb_node);
+ rb_insert_color(&left_pa.pa_node.inode_node, &ei->i_prealloc_node);
+ rb_link_node(&right_pa.pa_node.inode_node,
+ &left_pa.pa_node.inode_node,
+ &left_pa.pa_node.inode_node.rb_right);
+ rb_insert_color(&right_pa.pa_node.inode_node, &ei->i_prealloc_node);
+
+ ext4_mb_normalize_request_test(&ac, &ar);
+ ei->i_prealloc_node = RB_ROOT;
+
+ KUNIT_EXPECT_EQ(test, ac.ac_g_ex.fe_logical, EXT4_C2B(sbi, 3));
+ KUNIT_EXPECT_EQ(test, ac.ac_g_ex.fe_len, 5);
+ }
+}
+
+static void test_mb_inode_pa_lifecycle(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct inode *inode;
+ struct ext4_inode_info *ei;
+ struct ext4_allocation_request ar = {};
+ struct ext4_group_info *grp;
+ ext4_group_t pa_grp;
+ ext4_grpblk_t pa_off, free_before;
+ ext4_fsblk_t blk1, blk2;
+ void *pa_bitmap;
+ int err = 0;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
+ ei = EXT4_I(inode);
+ /* Large file size (> s_mb_stream_request) so inode PA is used */
+ i_size_write(inode, (loff_t)(sbi->s_mb_stream_request + 64) << sb->s_blocksize_bits);
+
+ ar.inode = inode;
+ ar.logical = 0;
+ ar.len = 1;
+ ar.goal = ext4_group_first_block_no(sb, TEST_GOAL_GROUP);
+ ar.flags = EXT4_MB_HINT_DATA;
+
+ blk1 = ext4_mb_new_blocks(NULL, &ar, &err);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_NE(test, blk1, 0ULL);
+ KUNIT_EXPECT_EQ(test, ar.len, 1);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ei->i_prealloc_active), 1);
+ KUNIT_EXPECT_FALSE(test, RB_EMPTY_ROOT(&ei->i_prealloc_node));
+
+ ext4_get_group_no_and_offset(sb, blk1, &pa_grp, &pa_off);
+ grp = ext4_get_group_info(sb, pa_grp);
+ free_before = ext4_free_group_clusters(sb, &MBT_GRP_CTX(sb, pa_grp)->desc) + 1;
+
+ /* Verify ext4_mb_generate_from_pa marks the PA's clusters in a bitmap */
+ pa_bitmap = kunit_kzalloc(test, sb->s_blocksize, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pa_bitmap);
+ ext4_lock_group(sb, pa_grp);
+ ext4_mb_generate_from_pa_test(sb, pa_bitmap, pa_grp);
+ ext4_unlock_group(sb, pa_grp);
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(pa_off, pa_bitmap), 0);
+
+ /* Second sequential allocation is served directly from the inode PA */
+ ar.logical = EXT4_C2B(sbi, 1);
+ ar.len = 1;
+ ar.goal = blk1 + EXT4_C2B(sbi, 1);
+ blk2 = ext4_mb_new_blocks(NULL, &ar, &err);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, blk2, blk1 + EXT4_C2B(sbi, 1));
+
+ /* Discard inode preallocations and verify cleanup */
+ ext4_discard_preallocations(inode);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ei->i_prealloc_active), 0);
+ KUNIT_EXPECT_TRUE(test, RB_EMPTY_ROOT(&ei->i_prealloc_node));
+ /* Only the 2 allocated clusters should remain used in the group */
+ KUNIT_EXPECT_EQ(test, grp->bb_free, free_before - 2);
+}
+
+static void test_mb_group_pa_lifecycle(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct inode *inode1, *inode2;
+ struct ext4_allocation_request ar = {};
+ struct ext4_group_info *grp;
+ ext4_fsblk_t blk1, blk2;
+ int err = 0, discarded;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ inode1 = mbt_alloc_test_inode(test, sb, S_IFREG);
+ inode2 = mbt_alloc_test_inode(test, sb, S_IFREG);
+ inode2->i_ino = 13;
+
+ /* Small files (<= s_mb_stream_request) use locality group PA (MB_GROUP_PA) */
+ i_size_write(inode1, sb->s_blocksize);
+ i_size_write(inode2, sb->s_blocksize);
+
+ ar.inode = inode1;
+ ar.logical = 0;
+ ar.len = 1;
+ ar.goal = ext4_group_first_block_no(sb, TEST_GOAL_GROUP);
+ ar.flags = EXT4_MB_HINT_DATA;
+
+ blk1 = ext4_mb_new_blocks(NULL, &ar, &err);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, blk1, ext4_group_first_block_no(sb, TEST_GOAL_GROUP));
+
+ /* Second small file allocation reuses the locality group PA */
+ ar.inode = inode2;
+ ar.logical = 0;
+ ar.len = 1;
+ ar.goal = ext4_group_first_block_no(sb, TEST_GOAL_GROUP);
+ blk2 = ext4_mb_new_blocks(NULL, &ar, &err);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, blk2, blk1 + EXT4_C2B(sbi, 1));
+
+ /* Discard all group preallocations and verify free count is restored */
+ discarded = ext4_mb_discard_preallocations_test(sb, 0);
+ rcu_barrier();
+ KUNIT_EXPECT_EQ(test, discarded, (int)sbi->s_mb_group_prealloc - 2);
+
+ grp = ext4_get_group_info(sb, TEST_GOAL_GROUP);
+ KUNIT_EXPECT_EQ(test, grp->bb_free, EXT4_CLUSTERS_PER_GROUP(sb) - 2);
+}
+
+static void test_mb_lg_list_trim(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_locality_group *lg;
+ struct inode *inode;
+ struct ext4_buddy e4b;
+ struct ext4_prealloc_space *pa;
+ int i, ret, count = 0;
+ int order = fls(15) - 1; /* pa_free = 15 -> order 3 */
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
+ sbi->s_mb_group_prealloc = 16;
+
+ ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /*
+ * Create 9 locality-group PAs in bucket order 3 (each of length 16 with
+ * 1 cluster used -> pa_free = 15). When the 9th PA is added via
+ * ext4_mb_release_context -> ext4_mb_add_n_trim, lg_prealloc_count > 8
+ * triggers ext4_mb_discard_lg_preallocations() to trim the list to 5!
+ */
+ for (i = 0; i < 9; i++) {
+ struct ext4_allocation_context ac = {};
+ struct ext4_free_extent ex = {
+ .fe_group = TEST_GOAL_GROUP,
+ .fe_start = i * 32,
+ .fe_len = 16,
+ };
+
+ ac.ac_sb = sb;
+ ac.ac_inode = inode;
+ ac.ac_flags = EXT4_MB_HINT_DATA | EXT4_MB_HINT_GROUP_ALLOC;
+ ac.ac_lg = raw_cpu_ptr(sbi->s_locality_groups);
+ mutex_lock(&ac.ac_lg->lg_mutex);
+
+ ret = ext4_mb_pa_alloc_test(&ac);
+ if (ret) {
+ mutex_unlock(&ac.ac_lg->lg_mutex);
+ ext4_mb_unload_buddy_test(&e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ }
+
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ mb_mark_used_test(&e4b, &ex);
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+
+ ac.ac_status = AC_STATUS_FOUND;
+ ac.ac_o_ex.fe_len = 1;
+ ac.ac_b_ex = ex;
+ ac.ac_f_ex = ex;
+ ext4_mb_new_preallocation_test(&ac);
+ /* Release context updates pa_free to 15 and runs ext4_mb_add_n_trim */
+ ext4_mb_release_context_test(&ac);
+ }
+
+ ext4_mb_unload_buddy_test(&e4b);
+
+ lg = raw_cpu_ptr(sbi->s_locality_groups);
+ spin_lock(&lg->lg_prealloc_lock);
+ list_for_each_entry(pa, &lg->lg_prealloc_list[order], pa_node.lg_list)
+ count++;
+ spin_unlock(&lg->lg_prealloc_lock);
+
+ KUNIT_EXPECT_EQ(test, count, 5);
+}
+
+static void test_mb_new_blocks_and_enospc_retry(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct inode *inode1, *inode2;
+ struct ext4_allocation_request ar = {};
+ ext4_grpblk_t max = EXT4_CLUSTERS_PER_GROUP(sb);
+ ext4_fsblk_t blk;
+ int err = 0;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ inode1 = mbt_alloc_test_inode(test, sb, S_IFREG);
+ inode2 = mbt_alloc_test_inode(test, sb, S_IFREG);
+ inode2->i_ino = 14;
+ i_size_write(inode1, sb->s_blocksize);
+
+ /*
+ * Leave only 16 free clusters in group 1 and mark groups 0, 2, 3 full.
+ * Set s_mb_group_prealloc = 16 so allocating 1 cluster for inode1
+ * preallocates the remaining 15 clusters into a group PA (leaving
+ * 0 free clusters in buddy for all groups).
+ */
+ mbt_ctx_mark_used(sb, 0, 0, max);
+ ext4_free_group_clusters_set(sb, &MBT_GRP_CTX(sb, 0)->desc, 0);
+ ext4_get_group_info(sb, 0)->bb_free = 0;
+
+ mbt_ctx_mark_used(sb, 1, 16, max - 16);
+ ext4_free_group_clusters_set(sb, &MBT_GRP_CTX(sb, 1)->desc, 16);
+ ext4_get_group_info(sb, 1)->bb_free = 16;
+
+ mbt_ctx_mark_used(sb, 2, 0, max);
+ ext4_free_group_clusters_set(sb, &MBT_GRP_CTX(sb, 2)->desc, 0);
+ ext4_get_group_info(sb, 2)->bb_free = 0;
+
+ mbt_ctx_mark_used(sb, 3, 0, max);
+ ext4_free_group_clusters_set(sb, &MBT_GRP_CTX(sb, 3)->desc, 0);
+ ext4_get_group_info(sb, 3)->bb_free = 0;
+
+ percpu_counter_set(&sbi->s_freeclusters_counter, 16);
+ sbi->s_mb_group_prealloc = 16;
+
+ ar.inode = inode1;
+ ar.logical = 0;
+ ar.len = 1;
+ ar.goal = ext4_group_first_block_no(sb, 1);
+ ar.flags = EXT4_MB_HINT_DATA;
+
+ blk = ext4_mb_new_blocks(NULL, &ar, &err);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ext4_get_group_info(sb, 1)->bb_free, 0);
+
+ /*
+ * Now request 4 metadata clusters (no EXT4_MB_HINT_DATA so it cannot
+ * use the PA directly). First allocator pass finds 0 free clusters,
+ * then ext4_mb_discard_preallocations_should_retry() discards the PA
+ * (recovering 15 clusters) and retries, succeeding!
+ */
+ memset(&ar, 0, sizeof(ar));
+ ar.inode = inode2;
+ ar.len = 4;
+ ar.goal = ext4_group_first_block_no(sb, 1);
+ ar.flags = 0;
+
+ blk = ext4_mb_new_blocks(NULL, &ar, &err);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_NE(test, blk, 0ULL);
+ KUNIT_EXPECT_EQ(test, ar.len, 4);
+
+ /*
+ * The 4-cluster power-of-2 allocation claimed [4, 8), leaving two
+ * fragments [1, 4) (3 clusters) and [8, 16) (8 clusters).
+ * Requesting 11 clusters allocates the largest fragment (8 clusters),
+ * then requesting 3 clusters allocates [1, 4), and the next request
+ * returns -ENOSPC.
+ */
+ ar.len = 11;
+ blk = ext4_mb_new_blocks(NULL, &ar, &err);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ar.len, 8);
+
+ ar.len = 3;
+ blk = ext4_mb_new_blocks(NULL, &ar, &err);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ar.len, 3);
+
+ ar.len = 1;
+ blk = ext4_mb_new_blocks(NULL, &ar, &err);
+ KUNIT_EXPECT_EQ(test, err, -ENOSPC);
+ KUNIT_EXPECT_EQ(test, blk, 0ULL);
+}
+
+static int
+ext4_handle_dirty_metadata_Updated_stub(const char *where, unsigned int line,
+ handle_t *handle, struct inode *inode,
+ struct buffer_head *bh)
+{
+ return 0;
+}
+
+static void test_mb_mark_context_and_mark_bb(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_group_desc *gdp1, *gdp2;
+ ext4_grpblk_t max = EXT4_CLUSTERS_PER_GROUP(sb);
+ ext4_grpblk_t changed = 0;
+ ext4_fsblk_t boundary_blk;
+ __u32 free_before;
+ int ret;
+
+ /* Test the real ext4_mb_mark_context implementation */
+ kunit_deactivate_static_stub(test, ext4_mb_mark_context);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ ext4_handle_dirty_metadata_Updated_stub);
+
+ gdp1 = ext4_get_group_desc(sb, 1, NULL);
+ gdp2 = ext4_get_group_desc(sb, 2, NULL);
+ KUNIT_ASSERT_NOT_NULL(test, gdp1);
+ KUNIT_ASSERT_NOT_NULL(test, gdp2);
+ free_before = ext4_free_group_clusters(sb, gdp1);
+
+ /* 1. Mark [10, 20) used with EXT4_MB_BITMAP_MARKED_CHECK (0x1) */
+ ret = ext4_mb_mark_context(NULL, sb, true, 1, 10, 10, 0x0001, &changed);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, changed, 10);
+ KUNIT_EXPECT_EQ(test, ext4_free_group_clusters(sb, gdp1), free_before - 10);
+
+ /* 2. Mark overlapping [15, 25) used -> only 5 new clusters changed */
+ ret = ext4_mb_mark_context(NULL, sb, true, 1, 15, 10, 0x0001, &changed);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, changed, 5);
+ KUNIT_EXPECT_EQ(test, ext4_free_group_clusters(sb, gdp1), free_before - 15);
+
+ /* 3. Clear [10, 25) -> 15 clusters freed */
+ ret = ext4_mb_mark_context(NULL, sb, false, 1, 10, 15, 0x0001, &changed);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, changed, 15);
+ KUNIT_EXPECT_EQ(test, ext4_free_group_clusters(sb, gdp1), free_before);
+
+ /*
+ * 4. Test ext4_mb_mark_bb across the boundary between group 1 and group 2:
+ * 2 clusters at the end of group 1 + 3 clusters at the start of group 2.
+ */
+ kunit_activate_static_stub(test, ext4_mb_mark_context,
+ ext4_mb_mark_context_stub);
+ boundary_blk = ext4_group_first_block_no(sb, 2) - EXT4_C2B(sbi, 2);
+ ext4_mb_mark_bb(sb, boundary_blk, EXT4_C2B(sbi, 5), true);
+
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(max - 2, mbt_ctx_bitmap(sb, 1)), 0);
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(max - 1, mbt_ctx_bitmap(sb, 1)), 0);
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(0, mbt_ctx_bitmap(sb, 2)), 0);
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(2, mbt_ctx_bitmap(sb, 2)), 0);
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(3, mbt_ctx_bitmap(sb, 2)), 0);
+
+ ext4_mb_mark_bb(sb, boundary_blk, EXT4_C2B(sbi, 5), false);
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(max - 2, mbt_ctx_bitmap(sb, 1)), 0);
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(0, mbt_ctx_bitmap(sb, 2)), 0);
+}
+
+static void test_ext4_free_blocks_flags_and_cross_group(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct inode *inode;
+ struct ext4_buddy e4b;
+ ext4_grpblk_t max = EXT4_CLUSTERS_PER_GROUP(sb);
+ ext4_fsblk_t grp1_first = ext4_group_first_block_no(sb, 1);
+ ext4_fsblk_t grp2_first = ext4_group_first_block_no(sb, 2);
+ s64 free_before;
+ int ret;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
+
+ /* Mark clusters [10, 20) and [max - 2, max) in group 1, and [0, 3) in group 2 used */
+ mbt_ctx_mark_used(sb, 1, 10, 10);
+ mbt_ctx_mark_used(sb, 1, max - 2, 2);
+ mbt_ctx_mark_used(sb, 2, 0, 3);
+ ext4_get_group_info(sb, 1)->bb_free -= 12;
+ ext4_get_group_info(sb, 2)->bb_free -= 3;
+
+ ret = ext4_mb_load_buddy_test(sb, 1, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ext4_mb_unload_buddy_test(&e4b);
+ ret = ext4_mb_load_buddy_test(sb, 2, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ext4_mb_unload_buddy_test(&e4b);
+
+ /*
+ * 1. EXT4_FREE_BLOCKS_NOFREE_FIRST_CLUSTER | EXT4_FREE_BLOCKS_NOFREE_LAST_CLUSTER:
+ * Freeing from cluster 10 + 2 blocks through cluster 12 + 3 blocks
+ * skips partial clusters 10 and 12 and frees only full cluster 11!
+ */
+ ext4_free_blocks(NULL, inode, NULL,
+ grp1_first + EXT4_C2B(sbi, 10) + 2,
+ EXT4_C2B(sbi, 2) - 2 + 3,
+ EXT4_FREE_BLOCKS_NOFREE_FIRST_CLUSTER |
+ EXT4_FREE_BLOCKS_NOFREE_LAST_CLUSTER |
+ EXT4_FREE_BLOCKS_NO_QUOT_UPDATE);
+
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(10, mbt_ctx_bitmap(sb, 1)), 0);
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(11, mbt_ctx_bitmap(sb, 1)), 0);
+ KUNIT_EXPECT_NE(test, mb_test_bit_test(12, mbt_ctx_bitmap(sb, 1)), 0);
+
+ /*
+ * 2. EXT4_FREE_BLOCKS_RERESERVE_CLUSTER: frees cluster 12 in bitmap & buddy
+ * without incrementing s_freeclusters_counter.
+ */
+ free_before = percpu_counter_sum(&sbi->s_freeclusters_counter);
+ ext4_free_blocks(NULL, inode, NULL,
+ grp1_first + EXT4_C2B(sbi, 12),
+ EXT4_C2B(sbi, 1),
+ EXT4_FREE_BLOCKS_RERESERVE_CLUSTER |
+ EXT4_FREE_BLOCKS_NO_QUOT_UPDATE);
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(12, mbt_ctx_bitmap(sb, 1)), 0);
+ KUNIT_EXPECT_EQ(test, percpu_counter_sum(&sbi->s_freeclusters_counter),
+ free_before);
+
+ /*
+ * 3. Cross-group boundary free: 2 clusters at end of group 1 + 3 clusters
+ * at start of group 2.
+ */
+ ext4_free_blocks(NULL, inode, NULL,
+ grp2_first - EXT4_C2B(sbi, 2),
+ EXT4_C2B(sbi, 5),
+ EXT4_FREE_BLOCKS_NO_QUOT_UPDATE);
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(max - 2, mbt_ctx_bitmap(sb, 1)), 0);
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(max - 1, mbt_ctx_bitmap(sb, 1)), 0);
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(0, mbt_ctx_bitmap(sb, 2)), 0);
+ KUNIT_EXPECT_EQ(test, mb_test_bit_test(2, mbt_ctx_bitmap(sb, 2)), 0);
+}
+
+static void test_mb_free_metadata_and_process_freed_data(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct inode *inode;
+ struct ext4_buddy e4b;
+ struct ext4_group_info *grp;
+ struct ext4_free_data *entry;
+ transaction_t trans = { .t_tid = 42 };
+ handle_t handle = { .h_transaction = &trans };
+ ext4_fsblk_t grp1_first = ext4_group_first_block_no(sb, TEST_GOAL_GROUP);
+ ext4_grpblk_t merged_start = 0, merged_count = 0;
+ int node_count = 0, ret;
+ struct rb_node *n;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
+
+ /* Mark clusters [20, 28) used in group 1 and load buddy */
+ mbt_ctx_mark_used(sb, TEST_GOAL_GROUP, 20, 8);
+ grp = ext4_get_group_info(sb, TEST_GOAL_GROUP);
+ grp->bb_free -= 8;
+
+ ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ext4_mb_unload_buddy_test(&e4b);
+
+ /*
+ * Free [20, 22), then [25, 28), then the middle [22, 25) as metadata
+ * with a valid journal handle (tid = 42).
+ * This exercises rbtree insertion and both prev and next extent merging!
+ */
+ ext4_free_blocks(&handle, inode, NULL,
+ grp1_first + EXT4_C2B(sbi, 20), EXT4_C2B(sbi, 2),
+ EXT4_FREE_BLOCKS_METADATA | EXT4_FREE_BLOCKS_NO_QUOT_UPDATE);
+ ext4_free_blocks(&handle, inode, NULL,
+ grp1_first + EXT4_C2B(sbi, 25), EXT4_C2B(sbi, 3),
+ EXT4_FREE_BLOCKS_METADATA | EXT4_FREE_BLOCKS_NO_QUOT_UPDATE);
+ ext4_free_blocks(&handle, inode, NULL,
+ grp1_first + EXT4_C2B(sbi, 22), EXT4_C2B(sbi, 3),
+ EXT4_FREE_BLOCKS_METADATA | EXT4_FREE_BLOCKS_NO_QUOT_UPDATE);
+
+ KUNIT_EXPECT_EQ(test, atomic_read(&sbi->s_mb_free_pending), 8);
+ /* Blocks are not yet freed in buddy until transaction commit */
+ KUNIT_EXPECT_EQ(test, grp->bb_free, EXT4_CLUSTERS_PER_GROUP(sb) - 8);
+
+ ext4_lock_group(sb, TEST_GOAL_GROUP);
+ for (n = rb_first(&grp->bb_free_root); n; n = rb_next(n)) {
+ entry = rb_entry(n, struct ext4_free_data, efd_node);
+ merged_start = entry->efd_start_cluster;
+ merged_count = entry->efd_count;
+ node_count++;
+ }
+ ext4_unlock_group(sb, TEST_GOAL_GROUP);
+
+ /* All 3 freed extents for tid 42 must have merged into a single node [20, 28) */
+ KUNIT_EXPECT_EQ(test, node_count, 1);
+ KUNIT_EXPECT_EQ(test, merged_start, 20);
+ KUNIT_EXPECT_EQ(test, merged_count, 8);
+
+ /* Commit tid 42 -> ext4_process_freed_data releases [20, 28) into buddy */
+ ext4_process_freed_data(sb, 42);
+ KUNIT_EXPECT_EQ(test, atomic_read(&sbi->s_mb_free_pending), 0);
+ KUNIT_EXPECT_TRUE(test, RB_EMPTY_ROOT(&grp->bb_free_root));
+ KUNIT_EXPECT_EQ(test, grp->bb_free, EXT4_CLUSTERS_PER_GROUP(sb));
+}
+
+static void test_ext4_group_add_blocks(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct ext4_buddy e4b;
+ struct ext4_group_info *grp;
+ ext4_fsblk_t grp1_first = ext4_group_first_block_no(sb, TEST_GOAL_GROUP);
+ s64 free_before;
+ int ret;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ /* Mark [50, 60) used in group 1 and initialize buddy */
+ mbt_ctx_mark_used(sb, TEST_GOAL_GROUP, 50, 10);
+ grp = ext4_get_group_info(sb, TEST_GOAL_GROUP);
+ grp->bb_free -= 10;
+
+ ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ext4_mb_unload_buddy_test(&e4b);
+
+ free_before = percpu_counter_sum(&sbi->s_freeclusters_counter);
+ ret = ext4_group_add_blocks(NULL, sb,
+ grp1_first + EXT4_C2B(sbi, 50),
+ EXT4_C2B(sbi, 10));
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp->bb_free, EXT4_CLUSTERS_PER_GROUP(sb));
+ KUNIT_EXPECT_EQ(test, percpu_counter_sum(&sbi->s_freeclusters_counter),
+ free_before + 10);
+
+ /* Cross-group boundary addition must fail with -EINVAL */
+ ret = ext4_group_add_blocks(NULL, sb,
+ grp1_first + EXT4_C2B(sbi, EXT4_CLUSTERS_PER_GROUP(sb) - 2),
+ EXT4_C2B(sbi, 5));
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ /* System zone block (block 0) must fail with -EINVAL */
+ ret = ext4_group_add_blocks(NULL, sb, 0, EXT4_C2B(sbi, 1));
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+}
+
+static int mbt_discard_calls;
+static int mbt_discard_clusters;
+
+static int
+ext4_issue_discard_count_stub(struct super_block *sb, ext4_group_t block_group,
+ ext4_grpblk_t cluster, int count)
+{
+ mbt_discard_calls++;
+ mbt_discard_clusters += count;
+ return 0;
+}
+
+static void test_ext4_trim_fs(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct ext4_sb_info *sbi = EXT4_SB(sb);
+ struct fstrim_range range = {};
+ struct ext4_group_info *grp1;
+ struct inode *inode;
+ ext4_grpblk_t max = EXT4_CLUSTERS_PER_GROUP(sb);
+ int ret;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ inode = mbt_alloc_test_inode(test, sb, S_IFREG);
+ kunit_activate_static_stub(test, ext4_issue_discard,
+ ext4_issue_discard_count_stub);
+
+ /*
+ * Mark groups 0, 2, 3 completely used.
+ * In group 1, leave only [10, 14) (4 clusters) and [30, 50) (20 clusters) free.
+ */
+ mbt_ctx_mark_used(sb, 0, 0, max);
+ ext4_get_group_info(sb, 0)->bb_free = 0;
+ mbt_ctx_mark_used(sb, 2, 0, max);
+ ext4_get_group_info(sb, 2)->bb_free = 0;
+ mbt_ctx_mark_used(sb, 3, 0, max);
+ ext4_get_group_info(sb, 3)->bb_free = 0;
+
+ mbt_ctx_mark_used(sb, 1, 0, 10);
+ mbt_ctx_mark_used(sb, 1, 14, 16);
+ mbt_ctx_mark_used(sb, 1, 50, max - 50);
+ grp1 = ext4_get_group_info(sb, 1);
+ grp1->bb_free = 24;
+
+ /* Invalid range checks */
+ range.start = 0;
+ range.len = sb->s_blocksize - 1;
+ range.minlen = 0;
+ KUNIT_EXPECT_EQ(test, ext4_trim_fs(sb, &range), -EINVAL);
+
+ /* Trim with minlen = 10 clusters: skips [10, 14) and trims only [30, 50) (20 clusters) */
+ mbt_discard_calls = 0;
+ mbt_discard_clusters = 0;
+ range.start = 0;
+ range.len = (u64)ext4_blocks_count(sbi->s_es) << sb->s_blocksize_bits;
+ range.minlen = (u64)EXT4_C2B(sbi, 10) << sb->s_blocksize_bits;
+
+ ret = ext4_trim_fs(sb, &range);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, mbt_discard_calls, 1);
+ KUNIT_EXPECT_EQ(test, mbt_discard_clusters, 20);
+ KUNIT_EXPECT_EQ(test, range.len,
+ (u64)EXT4_C2B(sbi, 20) << sb->s_blocksize_bits);
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_WAS_TRIMMED(grp1));
+
+ /* Second trim with same minlen is skipped because WAS_TRIMMED is set */
+ range.len = (u64)ext4_blocks_count(sbi->s_es) << sb->s_blocksize_bits;
+ ret = ext4_trim_fs(sb, &range);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, mbt_discard_calls, 1);
+ KUNIT_EXPECT_EQ(test, range.len, 0ULL);
+
+ /* Freeing a block clears WAS_TRIMMED, allowing subsequent trim */
+ ext4_free_blocks(NULL, inode, NULL,
+ ext4_group_first_block_no(sb, 1) + EXT4_C2B(sbi, 50),
+ EXT4_C2B(sbi, 1), EXT4_FREE_BLOCKS_NO_QUOT_UPDATE);
+ KUNIT_EXPECT_FALSE(test, EXT4_MB_GRP_WAS_TRIMMED(grp1));
+}
+
+struct mbt_query_ctx {
+ int meta_calls;
+ ext4_grpblk_t meta_start;
+ ext4_grpblk_t meta_len;
+ int free_calls;
+ ext4_grpblk_t free_starts[4];
+ ext4_grpblk_t free_lens[4];
+ int stop_after;
+};
+
+static int mbt_query_meta_cb(struct super_block *sb, ext4_group_t agno,
+ ext4_grpblk_t start, ext4_grpblk_t len, void *priv)
+{
+ struct mbt_query_ctx *q = priv;
+
+ q->meta_calls++;
+ q->meta_start = start;
+ q->meta_len = len;
+ return 0;
+}
+
+static int mbt_query_free_cb(struct super_block *sb, ext4_group_t agno,
+ ext4_grpblk_t start, ext4_grpblk_t len, void *priv)
+{
+ struct mbt_query_ctx *q = priv;
+
+ if (q->free_calls < 4) {
+ q->free_starts[q->free_calls] = start;
+ q->free_lens[q->free_calls] = len;
+ }
+ q->free_calls++;
+ if (q->stop_after && q->free_calls >= q->stop_after)
+ return -ECANCELED;
+ return 0;
+}
+
+static void test_ext4_mballoc_query_range(struct kunit *test)
+{
+ struct super_block *sb = (struct super_block *)test->priv;
+ struct mbt_query_ctx q = {};
+ ext4_grpblk_t max = EXT4_CLUSTERS_PER_GROUP(sb);
+ int ret;
+
+ if (sb->s_blocksize > PAGE_SIZE)
+ kunit_skip(test, "blocksize exceeds pagesize");
+
+ /*
+ * In group 1:
+ * [0, 10) used (before bb_first_free = 10 -> reported to meta_formatter)
+ * [10, 15) free (len 5)
+ * [15, 25) used
+ * [25, 32) free (len 7)
+ * [32, max) used
+ */
+ mbt_ctx_mark_used(sb, TEST_GOAL_GROUP, 0, 10);
+ mbt_ctx_mark_used(sb, TEST_GOAL_GROUP, 15, 10);
+ mbt_ctx_mark_used(sb, TEST_GOAL_GROUP, 32, max - 32);
+ ext4_get_group_info(sb, TEST_GOAL_GROUP)->bb_free = 12;
+
+ ret = ext4_mballoc_query_range(sb, TEST_GOAL_GROUP, 0, 100,
+ mbt_query_meta_cb, mbt_query_free_cb, &q);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, q.meta_calls, 1);
+ KUNIT_EXPECT_EQ(test, q.meta_start, 0);
+ KUNIT_EXPECT_EQ(test, q.meta_len, 10);
+ KUNIT_EXPECT_EQ(test, q.free_calls, 2);
+ KUNIT_EXPECT_EQ(test, q.free_starts[0], 10);
+ KUNIT_EXPECT_EQ(test, q.free_lens[0], 5);
+ KUNIT_EXPECT_EQ(test, q.free_starts[1], 25);
+ KUNIT_EXPECT_EQ(test, q.free_lens[1], 7);
+
+ /* Early abort from formatter callback propagates error code */
+ memset(&q, 0, sizeof(q));
+ q.stop_after = 1;
+ ret = ext4_mballoc_query_range(sb, TEST_GOAL_GROUP, 0, 100,
+ NULL, mbt_query_free_cb, &q);
+ KUNIT_EXPECT_EQ(test, ret, -ECANCELED);
+ KUNIT_EXPECT_EQ(test, q.free_calls, 1);
+}
+
static const struct mbt_ext4_block_layout mbt_test_layouts[] = {
{
.blocksize_bits = 10,
@@ -991,6 +2791,31 @@ static struct kunit_case mbt_test_cases[] = {
KUNIT_CASE_PARAM(test_mb_mark_used, mbt_layouts_gen_params),
KUNIT_CASE_PARAM(test_mb_free_blocks, mbt_layouts_gen_params),
KUNIT_CASE_PARAM(test_mark_diskspace_used, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_bit_ops, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_test_and_clear_bits, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_find_buddy_and_order, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_find_extent, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_generate_buddy_corrupt, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_free_blocks_double_free_and_fc_replay, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_xarray_orders_and_frag_size, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_good_group, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_find_by_goal, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_simple_and_aligned_scan, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_complex_scan_and_measure, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_regular_allocator, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_prefetch, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_initialize_context, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_normalize_request, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_inode_pa_lifecycle, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_group_pa_lifecycle, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_lg_list_trim, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_new_blocks_and_enospc_retry, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_mark_context_and_mark_bb, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_ext4_free_blocks_flags_and_cross_group, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_mb_free_metadata_and_process_freed_data, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_ext4_group_add_blocks, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_ext4_trim_fs, mbt_layouts_gen_params),
+ KUNIT_CASE_PARAM(test_ext4_mballoc_query_range, mbt_layouts_gen_params),
KUNIT_CASE_PARAM_ATTR(test_mb_mark_used_cost, mbt_layouts_gen_params,
{ .speed = KUNIT_SPEED_SLOW }),
{}
diff --git a/fs/ext4/mballoc.c b/fs/ext4/mballoc.c
index 0f73fb826dbf..46ba07e017a3 100644
--- a/fs/ext4/mballoc.c
+++ b/fs/ext4/mballoc.c
@@ -3957,11 +3957,14 @@ void ext4_mb_release(struct super_block *sb)
kfree(sbi->s_mb_last_groups);
}
-static inline int ext4_issue_discard(struct super_block *sb,
+int ext4_issue_discard(struct super_block *sb,
ext4_group_t block_group, ext4_grpblk_t cluster, int count)
{
ext4_fsblk_t discard_block;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_issue_discard, sb, block_group,
+ cluster, count);
+
discard_block = (EXT4_C2B(EXT4_SB(sb), cluster) +
ext4_group_first_block_no(sb, block_group));
count = EXT4_C2B(EXT4_SB(sb), count);
@@ -7275,12 +7278,199 @@ void ext4_free_blocks_simple_test(struct inode *inode, ext4_fsblk_t block,
}
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_blocks_simple_test);
+void mb_set_bit_test(int bit, void *addr)
+{
+ mb_set_bit(bit, addr);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(mb_set_bit_test);
+
+int mb_test_and_clear_bits_test(void *bm, int cur, int len)
+{
+ return mb_test_and_clear_bits(bm, cur, len);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(mb_test_and_clear_bits_test);
+
+void *mb_find_buddy_test(struct ext4_buddy *e4b, int order, int *max)
+{
+ return mb_find_buddy(e4b, order, max);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(mb_find_buddy_test);
+
+int mb_find_order_for_block_test(struct ext4_buddy *e4b, int block)
+{
+ return mb_find_order_for_block(e4b, block);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(mb_find_order_for_block_test);
+
+int mb_find_extent_test(struct ext4_buddy *e4b, int block, int needed,
+ struct ext4_free_extent *ex)
+{
+ return mb_find_extent(e4b, block, needed, ex);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(mb_find_extent_test);
+
+void mb_regenerate_buddy_test(struct ext4_buddy *e4b)
+{
+ mb_regenerate_buddy(e4b);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(mb_regenerate_buddy_test);
+
+int mb_avg_fragment_size_order_test(struct super_block *sb, ext4_grpblk_t len)
+{
+ return mb_avg_fragment_size_order(sb, len);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(mb_avg_fragment_size_order_test);
+
+void mb_update_avg_fragment_size_test(struct super_block *sb,
+ struct ext4_group_info *grp)
+{
+ mb_update_avg_fragment_size(sb, grp);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(mb_update_avg_fragment_size_test);
+
+void mb_set_largest_free_order_test(struct super_block *sb,
+ struct ext4_group_info *grp)
+{
+ mb_set_largest_free_order(sb, grp);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(mb_set_largest_free_order_test);
+
+bool ext4_mb_good_group_test(struct ext4_allocation_context *ac,
+ ext4_group_t group, enum criteria cr)
+{
+ return ext4_mb_good_group(ac, group, cr);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_good_group_test);
+
+int ext4_mb_good_group_nolock_test(struct ext4_allocation_context *ac,
+ ext4_group_t group, enum criteria cr)
+{
+ return ext4_mb_good_group_nolock(ac, group, cr);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_good_group_nolock_test);
+
+int ext4_mb_find_by_goal_test(struct ext4_allocation_context *ac,
+ struct ext4_buddy *e4b)
+{
+ return ext4_mb_find_by_goal(ac, e4b);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_find_by_goal_test);
+
+void ext4_mb_simple_scan_group_test(struct ext4_allocation_context *ac,
+ struct ext4_buddy *e4b)
+{
+ ext4_mb_simple_scan_group(ac, e4b);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_simple_scan_group_test);
+
+void ext4_mb_scan_aligned_test(struct ext4_allocation_context *ac,
+ struct ext4_buddy *e4b)
+{
+ ext4_mb_scan_aligned(ac, e4b);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_scan_aligned_test);
+
+void ext4_mb_complex_scan_group_test(struct ext4_allocation_context *ac,
+ struct ext4_buddy *e4b)
+{
+ ext4_mb_complex_scan_group(ac, e4b);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_complex_scan_group_test);
+
+void ext4_mb_measure_extent_test(struct ext4_allocation_context *ac,
+ struct ext4_free_extent *ex,
+ struct ext4_buddy *e4b)
+{
+ ext4_mb_measure_extent(ac, ex, e4b);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_measure_extent_test);
+
+void ext4_mb_try_best_found_test(struct ext4_allocation_context *ac,
+ struct ext4_buddy *e4b)
+{
+ ext4_mb_try_best_found(ac, e4b);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_try_best_found_test);
+
+int ext4_mb_regular_allocator_test(struct ext4_allocation_context *ac)
+{
+ return ext4_mb_regular_allocator(ac);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_regular_allocator_test);
+
+void ext4_mb_initialize_context_test(struct ext4_allocation_context *ac,
+ struct ext4_allocation_request *ar)
+{
+ ext4_mb_initialize_context(ac, ar);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_initialize_context_test);
+
+void ext4_mb_normalize_request_test(struct ext4_allocation_context *ac,
+ struct ext4_allocation_request *ar)
+{
+ ext4_mb_normalize_request(ac, ar);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_normalize_request_test);
+
+bool ext4_mb_use_preallocated_test(struct ext4_allocation_context *ac)
+{
+ return ext4_mb_use_preallocated(ac);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_use_preallocated_test);
+
+void ext4_mb_new_preallocation_test(struct ext4_allocation_context *ac)
+{
+ ext4_mb_new_preallocation(ac);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_new_preallocation_test);
+
+void ext4_mb_release_context_test(struct ext4_allocation_context *ac)
+{
+ ext4_mb_release_context(ac);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_release_context_test);
+
+int ext4_mb_pa_alloc_test(struct ext4_allocation_context *ac)
+{
+ return ext4_mb_pa_alloc(ac);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_pa_alloc_test);
+
+void ext4_mb_pa_put_free_test(struct ext4_allocation_context *ac)
+{
+ ext4_mb_pa_put_free(ac);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_pa_put_free_test);
+
+void ext4_mb_generate_from_pa_test(struct super_block *sb, void *bitmap,
+ ext4_group_t group)
+{
+ ext4_mb_generate_from_pa(sb, bitmap, group);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_generate_from_pa_test);
+
+int ext4_mb_discard_group_preallocations_test(struct super_block *sb,
+ ext4_group_t group, int *busy)
+{
+ return ext4_mb_discard_group_preallocations(sb, group, busy);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_discard_group_preallocations_test);
+
+int ext4_mb_discard_preallocations_test(struct super_block *sb, int needed)
+{
+ return ext4_mb_discard_preallocations(sb, needed);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_discard_preallocations_test);
+
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_wait_block_bitmap);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_init);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_group_desc);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_count_free_clusters);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_group_info);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_group_clusters);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_group_clusters_set);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_group_no_and_offset);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_group_number);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_release);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_block_bitmap_nowait);
EXPORT_SYMBOL_FOR_EXT4_TEST(mb_set_bits);
@@ -7288,4 +7478,13 @@ EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_init_inode);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_mark_context);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_new_blocks);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_blocks);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_prefetch);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_prefetch_fini);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_discard_preallocations);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_mark_bb);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_process_freed_data);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_group_add_blocks);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_trim_fs);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mballoc_query_range);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_issue_discard);
#endif
diff --git a/fs/ext4/mballoc.h b/fs/ext4/mballoc.h
index 39333ce72cbd..17751ed6c9f5 100644
--- a/fs/ext4/mballoc.h
+++ b/fs/ext4/mballoc.h
@@ -275,6 +275,8 @@ extern int ext4_mb_mark_context(handle_t *handle,
ext4_group_t group, ext4_grpblk_t blkoff,
ext4_grpblk_t len, int flags,
ext4_grpblk_t *ret_changed);
+extern int ext4_issue_discard(struct super_block *sb,
+ ext4_group_t block_group, ext4_grpblk_t cluster, int count);
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
extern void mb_clear_bits_test(void *bm, int cur, int len);
extern ext4_fsblk_t
@@ -283,7 +285,54 @@ ext4_mb_new_blocks_simple_test(struct ext4_allocation_request *ar,
extern int mb_find_next_zero_bit_test(void *addr, int max, int start);
extern int mb_find_next_bit_test(void *addr, int max, int start);
extern void mb_clear_bit_test(int bit, void *addr);
+extern void mb_set_bit_test(int bit, void *addr);
extern int mb_test_bit_test(int bit, void *addr);
+extern int mb_test_and_clear_bits_test(void *bm, int cur, int len);
+extern void *mb_find_buddy_test(struct ext4_buddy *e4b, int order, int *max);
+extern int mb_find_order_for_block_test(struct ext4_buddy *e4b, int block);
+extern int mb_find_extent_test(struct ext4_buddy *e4b, int block, int needed,
+ struct ext4_free_extent *ex);
+extern void mb_regenerate_buddy_test(struct ext4_buddy *e4b);
+extern int mb_avg_fragment_size_order_test(struct super_block *sb,
+ ext4_grpblk_t len);
+extern void mb_update_avg_fragment_size_test(struct super_block *sb,
+ struct ext4_group_info *grp);
+extern void mb_set_largest_free_order_test(struct super_block *sb,
+ struct ext4_group_info *grp);
+extern bool ext4_mb_good_group_test(struct ext4_allocation_context *ac,
+ ext4_group_t group, enum criteria cr);
+extern int ext4_mb_good_group_nolock_test(struct ext4_allocation_context *ac,
+ ext4_group_t group, enum criteria cr);
+extern int ext4_mb_find_by_goal_test(struct ext4_allocation_context *ac,
+ struct ext4_buddy *e4b);
+extern void ext4_mb_simple_scan_group_test(struct ext4_allocation_context *ac,
+ struct ext4_buddy *e4b);
+extern void ext4_mb_scan_aligned_test(struct ext4_allocation_context *ac,
+ struct ext4_buddy *e4b);
+extern void ext4_mb_complex_scan_group_test(struct ext4_allocation_context *ac,
+ struct ext4_buddy *e4b);
+extern void ext4_mb_measure_extent_test(struct ext4_allocation_context *ac,
+ struct ext4_free_extent *ex,
+ struct ext4_buddy *e4b);
+extern void ext4_mb_try_best_found_test(struct ext4_allocation_context *ac,
+ struct ext4_buddy *e4b);
+extern int ext4_mb_regular_allocator_test(struct ext4_allocation_context *ac);
+extern void ext4_mb_initialize_context_test(struct ext4_allocation_context *ac,
+ struct ext4_allocation_request *ar);
+extern void ext4_mb_normalize_request_test(struct ext4_allocation_context *ac,
+ struct ext4_allocation_request *ar);
+extern bool ext4_mb_use_preallocated_test(struct ext4_allocation_context *ac);
+extern void ext4_mb_new_preallocation_test(struct ext4_allocation_context *ac);
+extern void ext4_mb_release_context_test(struct ext4_allocation_context *ac);
+extern int ext4_mb_pa_alloc_test(struct ext4_allocation_context *ac);
+extern void ext4_mb_pa_put_free_test(struct ext4_allocation_context *ac);
+extern void ext4_mb_generate_from_pa_test(struct super_block *sb, void *bitmap,
+ ext4_group_t group);
+extern int ext4_mb_discard_group_preallocations_test(struct super_block *sb,
+ ext4_group_t group,
+ int *busy);
+extern int ext4_mb_discard_preallocations_test(struct super_block *sb,
+ int needed);
extern int
ext4_mb_mark_diskspace_used_test(struct ext4_allocation_context *ac,
handle_t *handle);
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 07/27] ext4: add KUnit tests for fs/ext4/inline.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (5 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 06/27] ext4: expand KUnit tests for fs/ext4/mballoc.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (19 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add the ext4_inline_test KUnit test suite in fs/ext4/inline-test.c to
comprehensively exercise inline data operations for regular files,
symlinks, and directories in fs/ext4/inline.c without requiring a
mounted block device:
- Inline capacity and discovery (ext4_get_max_inline_size and
ext4_find_inline_data_nolock) across fresh inodes, inodes with zero
extra_isize or 128-byte inode size, inodes with coexisting user
xattrs in ibody, 0-byte and non-empty system.data xattrs, and
corrupted xattr entries (e_name_len overflow and non-zero
e_value_inum).
- Inline file creation, expansion, overwrite, and destruction
(ext4_try_to_write_inline_data, ext4_write_inline_data_end, and
ext4_destroy_inline_data) spanning i_block (< 60 bytes) and the
ibody system.data xattr (>= 60 bytes), short writes with failed-write
truncation, non-uptodate folios, emergency shutdown
(EXT4_FLAGS_SHUTDOWN), concurrent inline flag clearing, and corrupted
inline xattr state detection.
- Inline page reading, symlinks, and iomap mapping
(ext4_readpage_inline on index 0 and index > 0, i_inline_size >
PAGE_SIZE corruption detection, ext4_read_inline_link for short and
long symlinks, and ext4_inline_data_iomap).
- Inline file truncation (ext4_inline_data_truncate) across size
extensions, shrinking within system.data xattr, shrinking into
i_block with tail zeroing, truncating to zero, and clearing the
extent status tree when EXT4_STATE_MAY_INLINE_DATA is unset.
- Inline-to-extent conversion for regular files
(ext4_convert_inline_data, ext4_convert_inline_data_to_extent, and
ext4_da_convert_inline_data_to_extent via
ext4_generic_write_inline_data), including DIOREAD_NOLOCK, DELALLOC,
and journaled data modes, plus error rollback via
ext4_restore_inline_data when block allocation, buffer lookup, or
metadata dirtying fails.
- Inline directory operations (ext4_update_final_de,
ext4_try_create_inline_dir, ext4_get_first_inline_block,
ext4_try_add_inline_entry across i_block and system.data xattr,
duplicate -EEXIST handling, ext4_find_inline_entry,
ext4_read_inline_dir with cookie/iversion revalidation,
ext4_inlinedir_to_tree, ext4_delete_inline_entry, empty_inline_dir,
conversion of full inline directories to extent-mapped directory
blocks with rollback on allocation failure, and corrupted dirent
detection).
Export the tested functions with EXPORT_SYMBOL_FOR_EXT4_TEST and add
KUNIT_STATIC_STUB_REDIRECT hooks for ext4_inline_getblk,
ext4_da_get_block_prep, and ext4_inode_journal_mode.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 2 +-
fs/ext4/ext4.h | 2 +
fs/ext4/ext4_jbd2.c | 3 +
fs/ext4/inline-test.c | 1825 +++++++++++++++++++++++++++++++++++++++++
fs/ext4/inline.c | 30 +-
fs/ext4/inode.c | 4 +
6 files changed, 1864 insertions(+), 2 deletions(-)
create mode 100644 fs/ext4/inline-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 8fe7b512ebb4..5a90bf5b28e3 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -17,7 +17,7 @@ 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 xattr-test.o \
- namei-test.o
+ namei-test.o inline-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 b57176b61d80..bcb4eb10d363 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3765,6 +3765,8 @@ extern const struct file_operations ext4_file_operations;
extern loff_t ext4_llseek(struct file *file, loff_t offset, int origin);
/* inline.c */
+extern struct buffer_head *ext4_inline_getblk(struct super_block *sb,
+ ext4_fsblk_t block);
extern int ext4_get_max_inline_size(struct inode *inode);
extern int ext4_find_inline_data_nolock(struct inode *inode);
extern int ext4_destroy_inline_data(handle_t *handle, struct inode *inode);
diff --git a/fs/ext4/ext4_jbd2.c b/fs/ext4/ext4_jbd2.c
index d6ffb6f08606..caba7ac68841 100644
--- a/fs/ext4/ext4_jbd2.c
+++ b/fs/ext4/ext4_jbd2.c
@@ -10,6 +10,8 @@
int ext4_inode_journal_mode(struct inode *inode)
{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_inode_journal_mode, inode);
+
if (EXT4_JOURNAL(inode) == NULL)
return EXT4_INODE_WRITEBACK_DATA_MODE; /* writeback */
/* We do not support data journalling with delayed allocation */
@@ -437,5 +439,6 @@ int __ext4_handle_dirty_metadata(const char *where, unsigned int line,
}
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_journal_mode);
EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_handle_dirty_metadata);
#endif
diff --git a/fs/ext4/inline-test.c b/fs/ext4/inline-test.c
new file mode 100644
index 000000000000..a75f46475c4a
--- /dev/null
+++ b/fs/ext4/inline-test.c
@@ -0,0 +1,1825 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for ext4 inline data operations (fs/ext4/inline.c)
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/backing-dev.h>
+#include <linux/buffer_head.h>
+#include <linux/fs.h>
+#include <linux/iomap.h>
+#include <linux/iversion.h>
+#include <linux/limits.h>
+#include <linux/mm.h>
+#include <linux/pagemap.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "xattr.h"
+#include "truncate.h"
+#include "namei.h"
+
+#define TEST_INODE_SIZE 256
+#define TEST_EXTRA_ISIZE 32
+#define TEST_BLOCK_SIZE 1024
+#define TEST_BLOCK_BITS 10
+#define EXT4_MIN_INLINE_DATA_SIZE ((sizeof(__le32) * EXT4_N_BLOCKS))
+#define EXT4_INLINE_DOTDOT_SIZE 4
+/*
+ * For a 256-byte inode with 32-byte i_extra_isize:
+ * EXT4_GOOD_OLD_INODE_SIZE (128) + 32 = 160 bytes fixed fields
+ * Remaining ibody space = 96 bytes
+ * 4-byte xattr magic + 20-byte system.data entry + 4-byte gap = 28 bytes
+ * Available xattr value space = 68 bytes
+ * Plus 60 bytes in i_block (EXT4_MIN_INLINE_DATA_SIZE) = 128 bytes max inline size.
+ */
+#define TEST_MAX_INLINE_SIZE 128
+
+#define TEST_INO 12
+#define TEST_PARENT_INO 2
+#define TEST_PBLK 100
+
+struct inline_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 ext4_inode_info ei;
+ struct buffer_head iloc_bh;
+ u8 raw_inode_buf[TEST_BLOCK_SIZE];
+ struct buffer_head data_bh;
+ u8 data_block_buf[PAGE_SIZE];
+ int get_inode_loc_err;
+ int map_blocks_ret;
+ ext4_fsblk_t map_pblk;
+ unsigned int map_flags;
+ bool getblk_fail;
+ int dirty_metadata_err;
+ int dirty_metadata_fail_on_call;
+ int dirty_metadata_calls;
+ int da_get_block_prep_err;
+ int journal_mode;
+};
+
+static struct inline_test_ctx *t_ctx;
+
+static const struct super_operations inline_test_sops = {};
+
+static bool test_release_folio(struct folio *folio, gfp_t wait)
+{
+ return try_to_free_buffers(folio);
+}
+
+static const struct address_space_operations test_inline_aops = {
+ .dirty_folio = noop_dirty_folio,
+ .invalidate_folio = block_invalidate_folio,
+ .release_folio = test_release_folio,
+};
+
+/* --- Static Stubs --- */
+
+static int stub_get_inode_loc(struct inode *inode, struct ext4_iloc *iloc)
+{
+ if (t_ctx->get_inode_loc_err)
+ return t_ctx->get_inode_loc_err;
+
+ memset(iloc, 0, sizeof(*iloc));
+ atomic_inc(&t_ctx->iloc_bh.b_count);
+ iloc->bh = &t_ctx->iloc_bh;
+ iloc->offset = 0;
+ iloc->block_group = 0;
+ return 0;
+}
+
+static int stub_handle_dirty_metadata(const char *where, unsigned int line,
+ handle_t *handle, struct inode *inode,
+ struct buffer_head *bh)
+{
+ t_ctx->dirty_metadata_calls++;
+ if (t_ctx->dirty_metadata_fail_on_call > 0 &&
+ t_ctx->dirty_metadata_calls == t_ctx->dirty_metadata_fail_on_call)
+ return t_ctx->dirty_metadata_err;
+ if (t_ctx->dirty_metadata_fail_on_call == 0 &&
+ t_ctx->dirty_metadata_err)
+ return t_ctx->dirty_metadata_err;
+ set_buffer_dirty(bh);
+ return 0;
+}
+
+static int stub_mark_iloc_dirty(handle_t *handle, struct inode *inode,
+ struct ext4_iloc *iloc)
+{
+ brelse(iloc->bh);
+ return 0;
+}
+
+static int stub_mark_inode_dirty(handle_t *handle, struct inode *inode,
+ const char *func, unsigned int line)
+{
+ return 0;
+}
+
+static int stub_map_blocks(handle_t *handle, struct inode *inode,
+ struct ext4_map_blocks *map, int flags)
+{
+ if (t_ctx->map_blocks_ret < 0)
+ return t_ctx->map_blocks_ret;
+
+ map->m_pblk = t_ctx->map_pblk;
+ map->m_flags = t_ctx->map_flags;
+ if (t_ctx->map_blocks_ret > 0)
+ map->m_len = t_ctx->map_blocks_ret;
+ return t_ctx->map_blocks_ret;
+}
+
+static struct buffer_head *stub_inline_getblk(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ if (t_ctx->getblk_fail)
+ return NULL;
+
+ atomic_inc(&t_ctx->data_bh.b_count);
+ t_ctx->data_bh.b_blocknr = block;
+ return &t_ctx->data_bh;
+}
+
+static int stub_da_get_block_prep(struct inode *inode, sector_t iblock,
+ struct buffer_head *bh, int create)
+{
+ if (t_ctx->da_get_block_prep_err)
+ return t_ctx->da_get_block_prep_err;
+
+ set_buffer_new(bh);
+ set_buffer_delay(bh);
+ bh->b_blocknr = 0;
+ bh->b_size = inode->i_sb->s_blocksize;
+ return 0;
+}
+
+static int stub_inode_journal_mode(struct inode *inode)
+{
+ return t_ctx->journal_mode;
+}
+
+/* --- Helpers --- */
+
+static void *raw_inline_i_block(struct inline_test_ctx *ctx)
+{
+ struct ext4_inode *raw_inode = (struct ext4_inode *)ctx->raw_inode_buf;
+
+ return (void *)raw_inode->i_block;
+}
+
+static void *raw_inline_xattr_pos(struct inline_test_ctx *ctx)
+{
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_inode *raw_inode = (struct ext4_inode *)ctx->raw_inode_buf;
+ struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
+ struct ext4_xattr_entry *entry;
+
+ entry = (struct ext4_xattr_entry *)((void *)raw_inode +
+ ctx->ei.i_inline_off);
+ return (void *)IFIRST(header) + le16_to_cpu(entry->e_value_offs);
+}
+
+static void reset_inode_state(struct inline_test_ctx *ctx, umode_t mode)
+{
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_inode *raw_inode;
+
+ if (!list_empty(&ctx->ei.i_orphan))
+ list_del_init(&ctx->ei.i_orphan);
+
+ truncate_inode_pages(&inode->i_data, 0);
+
+ memset(ctx->raw_inode_buf, 0, sizeof(ctx->raw_inode_buf));
+ memset(ctx->data_block_buf, 0, sizeof(ctx->data_block_buf));
+ memset(ctx->ei.i_data, 0, sizeof(ctx->ei.i_data));
+
+ raw_inode = (struct ext4_inode *)ctx->raw_inode_buf;
+ raw_inode->i_extra_isize = cpu_to_le16(TEST_EXTRA_ISIZE);
+
+ ctx->ei.i_extra_isize = TEST_EXTRA_ISIZE;
+ ctx->ei.i_inline_off = 0;
+ ctx->ei.i_inline_size = 0;
+ ctx->ei.i_flags = 0;
+ ext4_clear_state_flags(&ctx->ei);
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ctx->ei.i_disksize = 0;
+
+ inode->i_mode = mode;
+ inode->i_size = 0;
+ inode->i_blocks = 0;
+ inode_set_iversion(inode, 1);
+ inode_state_set_raw(inode, I_DIRTY);
+}
+
+static int create_inline_file(struct inode *inode, const void *data, size_t len)
+{
+ struct folio *folio = NULL;
+ void *kaddr;
+ int ret;
+
+ inode_lock(inode);
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, len,
+ &folio);
+ if (ret != 1) {
+ inode_unlock(inode);
+ return ret < 0 ? ret : -ENOSPC;
+ }
+
+ if (data && len) {
+ kaddr = kmap_local_folio(folio, 0);
+ memcpy(kaddr, data, len);
+ kunmap_local(kaddr);
+ }
+
+ ret = ext4_write_inline_data_end(inode, 0, len, len, folio);
+ inode_unlock(inode);
+ if (ret < 0)
+ return ret;
+
+ i_size_write(inode, len);
+ EXT4_I(inode)->i_disksize = len;
+ return 0;
+}
+
+static int inline_test_init(struct kunit *test)
+{
+ struct inline_test_ctx *ctx;
+ struct inode *inode;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+ t_ctx = ctx;
+ test->priv = ctx;
+
+ /* Superblock and ext4_sb_info setup */
+ ctx->sb.s_blocksize = TEST_BLOCK_SIZE;
+ ctx->sb.s_blocksize_bits = TEST_BLOCK_BITS;
+ ctx->sb.s_maxbytes = MAX_LFS_FILESIZE;
+ ctx->sb.s_time_gran = 1;
+ ctx->sb.s_bdi = &noop_backing_dev_info;
+ ctx->sb.s_fs_info = &ctx->sbi;
+ ctx->sb.s_op = &inline_test_sops;
+ snprintf(ctx->sb.s_id, sizeof(ctx->sb.s_id), "ext4-inline-test");
+ init_rwsem(&ctx->sb.s_umount);
+
+ ctx->bdev.bd_mapping = &ctx->bd_mapping;
+ bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+ ctx->sb.s_bdev = &ctx->bdev;
+
+ ctx->es.s_feature_incompat =
+ cpu_to_le32(EXT4_FEATURE_INCOMPAT_INLINE_DATA |
+ EXT4_FEATURE_INCOMPAT_EXTENTS |
+ EXT4_FEATURE_INCOMPAT_FILETYPE);
+ ctx->es.s_first_data_block = cpu_to_le32(1);
+ ctx->es.s_inodes_count = cpu_to_le32(1024);
+ ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ ctx->es.s_inode_size = cpu_to_le16(TEST_INODE_SIZE);
+
+ 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);
+ ctx->sbi.s_sbh = &ctx->sb_bh;
+
+ ctx->sbi.s_sb = &ctx->sb;
+ ctx->sbi.s_es = &ctx->es;
+ ctx->sbi.s_inode_size = TEST_INODE_SIZE;
+ ctx->sbi.s_want_extra_isize = TEST_EXTRA_ISIZE;
+ ctx->sbi.s_cluster_bits = 0;
+ ctx->sbi.s_cluster_ratio = 1;
+ INIT_LIST_HEAD(&ctx->sbi.s_orphan);
+ mutex_init(&ctx->sbi.s_orphan_lock);
+ set_opt(&ctx->sb, ERRORS_CONT);
+ spin_lock_init(&ctx->sbi.s_bdev_wb_lock);
+ spin_lock_init(&ctx->sbi.s_error_lock);
+ spin_lock_init(&ctx->sbi.s_md_lock);
+ ratelimit_state_init(&ctx->sbi.s_err_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+ ratelimit_state_init(&ctx->sbi.s_warning_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+ ratelimit_state_init(&ctx->sbi.s_msg_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+
+ /* Mock buffer_head for raw inode (iloc.bh) */
+ ctx->iloc_bh.b_data = (char *)ctx->raw_inode_buf;
+ ctx->iloc_bh.b_size = TEST_BLOCK_SIZE;
+ ctx->iloc_bh.b_blocknr = 10;
+ ctx->iloc_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->iloc_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->iloc_bh);
+ set_buffer_mapped(&ctx->iloc_bh);
+
+ /* Mock buffer_head for allocated data block (data_bh) */
+ ctx->data_bh.b_data = (char *)ctx->data_block_buf;
+ ctx->data_bh.b_size = TEST_BLOCK_SIZE;
+ ctx->data_bh.b_blocknr = TEST_PBLK;
+ ctx->data_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->data_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->data_bh);
+ set_buffer_mapped(&ctx->data_bh);
+
+ /* Default stub behavior */
+ ctx->map_blocks_ret = 1;
+ ctx->map_pblk = TEST_PBLK;
+ ctx->map_flags = EXT4_MAP_MAPPED;
+ ctx->journal_mode = EXT4_INODE_WRITEBACK_DATA_MODE;
+
+ /* Initialize mock ext4_inode_info and embedded vfs_inode */
+ inode = &ctx->ei.vfs_inode;
+ inode->i_sb = &ctx->sb;
+ inode->i_ino = TEST_INO;
+ inode->i_blkbits = TEST_BLOCK_BITS;
+ 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);
+ inode_state_set_raw(inode, I_DIRTY);
+
+ address_space_init_once(&inode->i_data);
+ inode->i_data.host = inode;
+ inode->i_data.a_ops = &test_inline_aops;
+ mapping_set_gfp_mask(&inode->i_data, GFP_HIGHUSER_MOVABLE);
+ init_rwsem(&inode->i_data.invalidate_lock);
+ inode->i_mapping = &inode->i_data;
+
+ init_rwsem(&ctx->ei.xattr_sem);
+ init_rwsem(&ctx->ei.i_data_sem);
+ spin_lock_init(&ctx->ei.i_raw_lock);
+ spin_lock_init(&ctx->ei.i_block_reservation_lock);
+ INIT_LIST_HEAD(&ctx->ei.i_orphan);
+ ctx->ei.i_prealloc_node = RB_ROOT;
+ rwlock_init(&ctx->ei.i_prealloc_lock);
+ ext4_es_init_tree(&ctx->ei.i_es_tree);
+ rwlock_init(&ctx->ei.i_es_lock);
+ INIT_LIST_HEAD(&ctx->ei.i_es_list);
+ ctx->ei.i_es_all_nr = 0;
+ ctx->ei.i_es_shk_nr = 0;
+
+ reset_inode_state(ctx, S_IFREG | 0644);
+
+ /* Activate static stubs */
+ kunit_activate_static_stub(test, ext4_get_inode_loc,
+ stub_get_inode_loc);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ stub_handle_dirty_metadata);
+ kunit_activate_static_stub(test, ext4_mark_iloc_dirty,
+ stub_mark_iloc_dirty);
+ kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+ stub_mark_inode_dirty);
+ kunit_activate_static_stub(test, ext4_map_blocks,
+ stub_map_blocks);
+ kunit_activate_static_stub(test, ext4_inline_getblk,
+ stub_inline_getblk);
+ kunit_activate_static_stub(test, ext4_da_get_block_prep,
+ stub_da_get_block_prep);
+ kunit_activate_static_stub(test, ext4_inode_journal_mode,
+ stub_inode_journal_mode);
+
+ return 0;
+}
+
+static void inline_test_exit(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+
+ if (current->journal_info)
+ ext4_journal_stop(ext4_journal_current_handle());
+
+ if (ctx) {
+ if (!list_empty(&ctx->ei.i_orphan))
+ list_del_init(&ctx->ei.i_orphan);
+ truncate_inode_pages(&ctx->ei.vfs_inode.i_data, 0);
+ }
+ t_ctx = NULL;
+}
+
+/* =========================================================================
+ * 1. Capacity and Lookup Tests (ext4_get_max_inline_size /
+ * ext4_find_inline_data_nolock)
+ * =========================================================================
+ */
+
+static void test_max_inline_size_variants(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ int ret;
+
+ /* 1. Fresh 256-byte inode with 32-byte extra_isize -> 128 bytes */
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode),
+ TEST_MAX_INLINE_SIZE);
+
+ /* 2. Inode with i_extra_isize == 0 -> 0 */
+ ctx->ei.i_extra_isize = 0;
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode), 0);
+ ctx->ei.i_extra_isize = TEST_EXTRA_ISIZE;
+
+ /* 3. Old 128-byte inode size (no xattr space) -> 0 */
+ ctx->sbi.s_inode_size = EXT4_GOOD_OLD_INODE_SIZE;
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode), 0);
+ ctx->sbi.s_inode_size = TEST_INODE_SIZE;
+
+ /* 4. ext4_get_inode_loc error -> 0 */
+ ctx->get_inode_loc_err = -EIO;
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode), 0);
+ ctx->get_inode_loc_err = 0;
+
+ /* 5. With a user xattr in ibody reducing free space */
+ {
+ struct ext4_xattr_ibody_find is = {
+ .s = { .not_found = -ENODATA, },
+ };
+ struct ext4_xattr_info i = {
+ .name_index = EXT4_XATTR_INDEX_USER,
+ .name = "foo",
+ .value = "12345678",
+ .value_len = 8,
+ };
+
+ ret = ext4_get_inode_loc(inode, &is.iloc);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_xattr_ibody_find(inode, &i, &is);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_xattr_ibody_set(NULL, inode, &i, &is);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ brelse(is.iloc.bh);
+ }
+ /*
+ * "foo" entry takes EXT4_XATTR_LEN(3) = 20 bytes + EXT4_XATTR_SIZE(8) = 8
+ * Total 28 bytes consumed in ibody -> max inline size drops from 128 to 100.
+ */
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode),
+ TEST_MAX_INLINE_SIZE - 28);
+
+ /* 6. When inline data already exists in system.data, its space counts as usable */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ {
+ char buf[80];
+
+ memset(buf, 'A', sizeof(buf));
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ }
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode),
+ TEST_MAX_INLINE_SIZE);
+
+ /*
+ * 7. When ibody free space is smaller than EXT4_XATTR_LEN(strlen("data"))
+ * (20 bytes), get_max_inline_xattr_value_size returns 0 -> max is 0.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ {
+ struct ext4_xattr_ibody_find is = {
+ .s = { .not_found = -ENODATA, },
+ };
+ char val[60];
+ struct ext4_xattr_info i = {
+ .name_index = EXT4_XATTR_INDEX_USER,
+ .name = "bigattr",
+ .value = val,
+ .value_len = sizeof(val),
+ };
+
+ memset(val, 'x', sizeof(val));
+ ret = ext4_get_inode_loc(inode, &is.iloc);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_xattr_ibody_find(inode, &i, &is);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_xattr_ibody_set(NULL, inode, &i, &is);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ brelse(is.iloc.bh);
+ }
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode), 0);
+
+ /*
+ * 8. Corrupted xattr entry in ibody (e_name_len overflows end of inode)
+ * -> get_max_inline_xattr_value_size returns 0 -> total max is 0.
+ */
+ {
+ struct ext4_inode *raw_inode =
+ (struct ext4_inode *)ctx->raw_inode_buf;
+ struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
+ struct ext4_xattr_entry *entry = IFIRST(header);
+
+ entry->e_name_len = 250;
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode), 0);
+ }
+}
+
+static void test_find_inline_data_nolock(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_iloc iloc = { 0 };
+ char buf[90];
+ int ret;
+
+ /* 1. i_extra_isize == 0 -> returns 0 immediately without setting inline state */
+ ctx->ei.i_extra_isize = 0;
+ ret = ext4_get_inode_loc(inode, &iloc);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ brelse(iloc.bh);
+ ctx->ei.i_extra_isize = TEST_EXTRA_ISIZE;
+
+ /* 2. ext4_get_inode_loc error -> propagates error */
+ ctx->get_inode_loc_err = -EIO;
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ ctx->get_inode_loc_err = 0;
+
+ /* 3. Inode without EXT4_STATE_XATTR -> returns 0, inline_size == 0 */
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 0);
+
+ /* 4. Inode with EXT4_STATE_XATTR set, but no system.data xattr -> 0 */
+ ext4_set_inode_state(inode, EXT4_STATE_XATTR);
+ {
+ struct ext4_inode *raw_inode =
+ (struct ext4_inode *)ctx->raw_inode_buf;
+ struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
+
+ header->h_magic = cpu_to_le32(EXT4_XATTR_MAGIC);
+ }
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 0);
+
+ /* 5. Inode with 30-byte inline file (0-byte system.data xattr) */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ memset(buf, 'B', sizeof(buf));
+ ret = create_inline_file(inode, buf, 30);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ctx->ei.i_inline_off = 0;
+ ctx->ei.i_inline_size = 0;
+ ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_NE(test, ctx->ei.i_inline_off, 0);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+
+ /* 6. Inode with 90-byte inline file (30-byte system.data xattr) */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, 90);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ctx->ei.i_inline_off = 0;
+ ctx->ei.i_inline_size = 0;
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 90);
+ KUNIT_EXPECT_GT(test, ctx->ei.i_inline_off,
+ EXT4_GOOD_OLD_INODE_SIZE + TEST_EXTRA_ISIZE);
+
+ /* 7. Corrupted system.data entry with non-zero e_value_inum -> -EFSCORRUPTED */
+ {
+ struct ext4_inode *raw_inode =
+ (struct ext4_inode *)ctx->raw_inode_buf;
+ struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
+ struct ext4_xattr_entry *entry = IFIRST(header);
+
+ entry->e_value_inum = cpu_to_le32(99);
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+ }
+}
+
+/* =========================================================================
+ * 2. Inline File Write & Destroy Lifecycle Tests
+ * =========================================================================
+ */
+
+static void test_write_and_destroy_inline_data(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_iloc iloc = { 0 };
+ struct folio *folio = NULL;
+ handle_t *handle;
+ char pattern[100];
+ char verify_buf[100];
+ void *kaddr;
+ int i, ret;
+
+ for (i = 0; i < sizeof(pattern); i++)
+ pattern[i] = (char)('A' + (i % 26));
+
+ /*
+ * Step 1: Write 20 bytes into a brand-new inode (fits in i_block).
+ */
+ inode_lock(inode);
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 20,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, folio);
+ KUNIT_EXPECT_TRUE(test, folio_test_locked(folio));
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size,
+ (u16)EXT4_MIN_INLINE_DATA_SIZE);
+
+ kaddr = kmap_local_folio(folio, 0);
+ memcpy(kaddr, pattern, 20);
+ kunmap_local(kaddr);
+
+ ret = ext4_write_inline_data_end(inode, 0, 20, 20, folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 20);
+ KUNIT_EXPECT_NULL(test, current->journal_info);
+ i_size_write(inode, 20);
+ ctx->ei.i_disksize = 20;
+
+ /* Verify i_block contents */
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_i_block(ctx), pattern, 20);
+
+ /*
+ * Step 2: Expand from 20 bytes to 90 bytes (crosses 60-byte boundary
+ * into ibody system.data xattr via ext4_update_inline_data).
+ */
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 20, 70,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, folio);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 90);
+
+ /* Verify existing first 20 bytes were loaded into the folio by prepare_inline_data */
+ kaddr = kmap_local_folio(folio, 0);
+ KUNIT_EXPECT_MEMEQ(test, kaddr, pattern, 20);
+ memcpy((char *)kaddr + 20, pattern + 20, 70);
+ kunmap_local(kaddr);
+
+ ret = ext4_write_inline_data_end(inode, 20, 70, 70, folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 70);
+ i_size_write(inode, 90);
+ ctx->ei.i_disksize = 90;
+
+ /*
+ * Step 3: Overwrite 15 bytes purely within the xattr region
+ * (pos = 65, len = 15 -> pos >= EXT4_MIN_INLINE_DATA_SIZE).
+ */
+ memset(pattern + 65, 'Z', 15);
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 65, 15,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ kaddr = kmap_local_folio(folio, 0);
+ memcpy((char *)kaddr + 65, pattern + 65, 15);
+ kunmap_local(kaddr);
+ ret = ext4_write_inline_data_end(inode, 65, 15, 15, folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 15);
+
+ /* Verify both i_block (first 60B) and ibody xattr (remaining 30B) */
+ memcpy(verify_buf, raw_inline_i_block(ctx), EXT4_MIN_INLINE_DATA_SIZE);
+ memcpy(verify_buf + EXT4_MIN_INLINE_DATA_SIZE,
+ raw_inline_xattr_pos(ctx), 30);
+ KUNIT_EXPECT_MEMEQ(test, verify_buf, pattern, 90);
+
+ /*
+ * Step 4: Destroy inline data on a regular file -> clears inline state
+ * and initializes extent tree (EXT4_INODE_EXTENTS).
+ */
+ handle = ext4_journal_start(inode, EXT4_HT_INODE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_get_inode_loc(inode, &iloc);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_destroy_inline_data(handle, inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_off, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 0);
+ KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ brelse(iloc.bh);
+
+ /* Calling destroy again when inline_off == 0 is a no-op returning 0 */
+ ret = ext4_destroy_inline_data(handle, inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ ext4_journal_stop(handle);
+
+ /*
+ * Step 5: Initial creation directly > 60 bytes (75 bytes) on a non-extent
+ * mode inode (e.g. S_IFIFO) -> destroy does NOT set EXT4_INODE_EXTENTS.
+ */
+ reset_inode_state(ctx, S_IFIFO | 0644);
+ ret = create_inline_file(inode, pattern, 75);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 75);
+
+ handle = ext4_journal_start(inode, EXT4_HT_INODE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_destroy_inline_data(handle, inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ ext4_journal_stop(handle);
+}
+
+static void test_write_inline_short_copy_and_edge_cases(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct folio *folio = NULL;
+ char buf[80];
+ void *kaddr;
+ int ret;
+
+ memset(buf, 'M', sizeof(buf));
+ ret = create_inline_file(inode, buf, 20);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /*
+ * 1. Short write (copied < len) when expanding from 20B to 80B:
+ * prepare_inline_data expands xattr to 80B, but only 10 bytes are
+ * copied (pos=20, len=60, copied=10 -> pos+copied=30 > i_size=20).
+ * ext4_write_inline_data_end adds to orphan, updates i_disksize to 30,
+ * and calls ext4_truncate_failed_write -> shrinks inline data back to
+ * 60B (i_block only).
+ */
+ inode_lock(inode);
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 20, 60,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 80);
+
+ kaddr = kmap_local_folio(folio, 0);
+ memcpy((char *)kaddr + 20, "0123456789", 10);
+ kunmap_local(kaddr);
+
+ ret = ext4_write_inline_data_end(inode, 20, 60, 10, folio);
+ KUNIT_EXPECT_EQ(test, ret, 10);
+ KUNIT_EXPECT_EQ(test, i_size_read(inode), 30);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size,
+ (u16)EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->ei.i_orphan));
+ inode_unlock(inode);
+
+ /*
+ * 2. Short write (copied < len) when folio is NOT uptodate ->
+ * copied is forced to 0.
+ */
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 30, 10,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ folio_clear_uptodate(folio);
+ ret = ext4_write_inline_data_end(inode, 30, 10, 5, folio);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, i_size_read(inode), 30);
+ inode_unlock(inode);
+
+ /*
+ * 3. Emergency shutdown flag (EXT4_FLAGS_SHUTDOWN):
+ * 3a. Set before ext4_try_to_write_inline_data -> ext4_journal_start
+ * fails with -EIO.
+ * 3b. Set after ext4_try_to_write_inline_data -> ext4_write_inline_data
+ * returns early without modifying inline data.
+ */
+ set_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 10,
+ &folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ clear_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 10,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ kaddr = kmap_local_folio(folio, 0);
+ memcpy(kaddr, "XXXXXXXXXX", 10);
+ kunmap_local(kaddr);
+ set_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+ ret = ext4_write_inline_data_end(inode, 0, 10, 10, folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 10);
+ /* i_block should still have 'M's because shutdown skipped ext4_write_inline_data */
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_i_block(ctx), buf, 10);
+ clear_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+
+ /*
+ * 4. Race condition where EXT4_INODE_INLINE_DATA is cleared before
+ * ext4_write_inline_data_end runs -> skips inline copy and returns 0.
+ */
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 10,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ret = ext4_write_inline_data_end(inode, 0, 10, 10, folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+
+ /*
+ * 5. Corruption detection in ext4_create_inline_data when system.data
+ * already exists on an inode with i_inline_off == 0 (e.g. corrupted
+ * e_value_inum so ext4_find_inline_data_nolock leaves i_inline_off == 0).
+ */
+ {
+ struct ext4_inode *raw_inode =
+ (struct ext4_inode *)ctx->raw_inode_buf;
+ struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
+ struct ext4_xattr_entry *entry = IFIRST(header);
+
+ entry->e_value_inum = cpu_to_le32(1);
+ ctx->ei.i_inline_off = 0;
+ ctx->ei.i_inline_size = 0;
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0,
+ 10, &folio);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+ }
+
+ /*
+ * 6. Corruption detection in ext4_update_inline_data when inline state
+ * claims inline data exists, but system.data xattr was wiped.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ctx->ei.i_inline_off = 164;
+ ctx->ei.i_inline_size = EXT4_MIN_INLINE_DATA_SIZE;
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 80,
+ &folio);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+}
+
+/* =========================================================================
+ * 3. Reading Inline Data, Symlinks, and iomap Tests
+ * =========================================================================
+ */
+
+static void test_readpage_inline_link_and_iomap(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct folio *folio;
+ struct iomap iomap = { 0 };
+ char content[85];
+ const char *short_link = "/usr/bin/python3";
+ char long_link[85];
+ char *target;
+ void *kaddr;
+ int i, ret;
+
+ for (i = 0; i < sizeof(content); i++)
+ content[i] = (char)('0' + (i % 10));
+
+ /* 1. ext4_readpage_inline and ext4_inline_data_iomap on non-inline inode -> -EAGAIN */
+ folio = __filemap_get_folio(&inode->i_data, 0,
+ FGP_LOCK | FGP_ACCESSED | FGP_CREAT,
+ GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ ret = ext4_readpage_inline(inode, folio);
+ KUNIT_EXPECT_EQ(test, ret, -EAGAIN);
+ folio_unlock(folio);
+ folio_put(folio);
+
+ ret = ext4_inline_data_iomap(inode, &iomap);
+ KUNIT_EXPECT_EQ(test, ret, -EAGAIN);
+
+ /* 2. Create 85-byte inline file and test ext4_readpage_inline on index 0 */
+ truncate_inode_pages(&inode->i_data, 0);
+ ret = create_inline_file(inode, content, sizeof(content));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ truncate_inode_pages(&inode->i_data, 0);
+
+ folio = __filemap_get_folio(&inode->i_data, 0,
+ FGP_LOCK | FGP_ACCESSED | FGP_CREAT,
+ GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ ret = ext4_readpage_inline(inode, folio);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_TRUE(test, folio_test_uptodate(folio));
+ KUNIT_EXPECT_FALSE(test, folio_test_locked(folio));
+
+ kaddr = kmap_local_folio(folio, 0);
+ KUNIT_EXPECT_MEMEQ(test, kaddr, content, sizeof(content));
+ /* Verify zero-padding past inline_size */
+ KUNIT_EXPECT_EQ(test, ((char *)kaddr)[sizeof(content)], 0);
+ KUNIT_EXPECT_EQ(test, ((char *)kaddr)[PAGE_SIZE - 1], 0);
+ kunmap_local(kaddr);
+ folio_put(folio);
+
+ /* 3. ext4_readpage_inline on folio->index > 0 zeros the folio and marks uptodate */
+ folio = __filemap_get_folio(&inode->i_data, 1,
+ FGP_LOCK | FGP_ACCESSED | FGP_CREAT,
+ GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ kaddr = kmap_local_folio(folio, 0);
+ memset(kaddr, 0xff, PAGE_SIZE);
+ kunmap_local(kaddr);
+
+ ret = ext4_readpage_inline(inode, folio);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_TRUE(test, folio_test_uptodate(folio));
+ kaddr = kmap_local_folio(folio, 0);
+ KUNIT_EXPECT_EQ(test, ((unsigned char *)kaddr)[0], 0);
+ kunmap_local(kaddr);
+ folio_put(folio);
+
+ /* 4. ext4_readpage_inline when ext4_get_inode_loc fails -> propagates error */
+ truncate_inode_pages(&inode->i_data, 0);
+ ctx->get_inode_loc_err = -EIO;
+ folio = __filemap_get_folio(&inode->i_data, 0,
+ FGP_LOCK | FGP_ACCESSED | FGP_CREAT,
+ GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ ret = ext4_readpage_inline(inode, folio);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ folio_put(folio);
+ ctx->get_inode_loc_err = 0;
+
+ /*
+ * 5. ext4_readpage_inline when i_inline_size > PAGE_SIZE -> -EFSCORRUPTED.
+ * i_inline_size is a u16, so it can only exceed PAGE_SIZE when
+ * PAGE_SIZE < U16_MAX (e.g. 4KB, 8KB, 16KB, or 32KB base pages).
+ */
+ if (PAGE_SIZE < U16_MAX) {
+ truncate_inode_pages(&inode->i_data, 0);
+ ctx->ei.i_inline_size = U16_MAX;
+ i_size_write(inode, U16_MAX);
+ folio = __filemap_get_folio(&inode->i_data, 0,
+ FGP_LOCK | FGP_ACCESSED | FGP_CREAT,
+ GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ ret = ext4_readpage_inline(inode, folio);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+ folio_put(folio);
+ ctx->ei.i_inline_size = sizeof(content);
+ i_size_write(inode, sizeof(content));
+ }
+
+ /* 6. ext4_inline_data_iomap populates IOMAP_INLINE with inline address/len */
+ memset(&iomap, 0, sizeof(iomap));
+ ret = ext4_inline_data_iomap(inode, &iomap);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_INLINE);
+ KUNIT_EXPECT_EQ(test, iomap.offset, 0ULL);
+ KUNIT_EXPECT_EQ(test, iomap.length, (u64)sizeof(content));
+ KUNIT_EXPECT_EQ(test, iomap.addr,
+ ((u64)ctx->iloc_bh.b_blocknr << TEST_BLOCK_BITS) +
+ offsetof(struct ext4_inode, i_block));
+
+ /* 7. ext4_inline_data_iomap error when ext4_get_inode_loc fails */
+ ctx->get_inode_loc_err = -EIO;
+ ret = ext4_inline_data_iomap(inode, &iomap);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ ctx->get_inode_loc_err = 0;
+
+ /* 8. ext4_read_inline_link: short symlink (in i_block) and long symlink (in xattr) */
+ reset_inode_state(ctx, S_IFLNK | 0777);
+ ret = create_inline_file(inode, short_link, strlen(short_link));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ target = ext4_read_inline_link(inode);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(target));
+ KUNIT_EXPECT_STREQ(test, target, short_link);
+ kfree(target);
+
+ reset_inode_state(ctx, S_IFLNK | 0777);
+ memset(long_link, 'a', sizeof(long_link) - 1);
+ long_link[sizeof(long_link) - 1] = '\0';
+ ret = create_inline_file(inode, long_link, strlen(long_link));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ target = ext4_read_inline_link(inode);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(target));
+ KUNIT_EXPECT_STREQ(test, target, long_link);
+ kfree(target);
+
+ /* ext4_read_inline_link error when ext4_get_inode_loc fails */
+ ctx->get_inode_loc_err = -EIO;
+ target = ext4_read_inline_link(inode);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(target));
+ KUNIT_EXPECT_EQ(test, PTR_ERR(target), -EIO);
+ ctx->get_inode_loc_err = 0;
+}
+
+/* =========================================================================
+ * 4. Inline Truncation Tests (ext4_inline_data_truncate)
+ * =========================================================================
+ */
+
+static void test_inline_data_truncate(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ int has_inline = 1;
+ char buf[100];
+ int i, ret;
+
+ for (i = 0; i < sizeof(buf); i++)
+ buf[i] = (char)('a' + (i % 26));
+
+ /* 1. Non-inline inode -> sets *has_inline = 0 and returns */
+ inode_lock(inode);
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, has_inline, 0);
+
+ /* 2. Create a 100-byte inline file (60B in i_block + 40B in xattr) */
+ ret = create_inline_file(inode, buf, 100);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 100);
+
+ /* 3. Truncate extending i_size (100 -> 120): no-op on inline size */
+ inode_lock(inode);
+ i_size_write(inode, 120);
+ has_inline = 1;
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, has_inline, 1);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 100);
+
+ /* 4. Shrink within xattr space (100 -> 75 bytes) */
+ inode_lock(inode);
+ i_size_write(inode, 75);
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, has_inline, 1);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 75);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 75);
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_xattr_pos(ctx),
+ buf + EXT4_MIN_INLINE_DATA_SIZE, 15);
+
+ /* 5. Shrink from xattr space into i_block (75 -> 25 bytes): zeros i_block[25..59] */
+ inode_lock(inode);
+ i_size_write(inode, 25);
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size,
+ (u16)EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 25);
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_i_block(ctx), buf, 25);
+ for (i = 25; i < EXT4_MIN_INLINE_DATA_SIZE; i++)
+ KUNIT_EXPECT_EQ(test, ((char *)raw_inline_i_block(ctx))[i], 0);
+
+ /* 6. Shrink to 0 bytes: zeros all of i_block */
+ inode_lock(inode);
+ i_size_write(inode, 0);
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 0);
+ for (i = 0; i < EXT4_MIN_INLINE_DATA_SIZE; i++)
+ KUNIT_EXPECT_EQ(test, ((char *)raw_inline_i_block(ctx))[i], 0);
+
+ /*
+ * 7. When !EXT4_STATE_MAY_INLINE_DATA (being converted),
+ * ext4_inline_data_truncate clears the extent status tree.
+ */
+ ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ inode_lock(inode);
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->ei.i_orphan));
+}
+
+/* =========================================================================
+ * 5. Inline-to-Extent Conversion Tests for Regular Files
+ * =========================================================================
+ */
+
+static void test_convert_inline_data_and_rollback(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ char buf[80];
+ int i, ret;
+
+ for (i = 0; i < sizeof(buf); i++)
+ buf[i] = (char)('A' + (i % 26));
+
+ /* 1. Non-inline inode -> clears MAY_INLINE_DATA and returns 0 immediately */
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA));
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+
+ /*
+ * 2. Inline inode without EXT4_STATE_MAY_INLINE_DATA -> calls
+ * filemap_flush() and finishes converting inline data if still inline.
+ */
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_MEMEQ(test, ctx->data_block_buf, buf, sizeof(buf));
+
+ /*
+ * 3. Error rollback when ext4_map_blocks fails (-ENOSPC):
+ * ext4_restore_inline_data restores the original 80 bytes of inline data.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->map_blocks_ret = -ENOSPC;
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, sizeof(buf));
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_i_block(ctx), buf,
+ EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_xattr_pos(ctx),
+ buf + EXT4_MIN_INLINE_DATA_SIZE,
+ sizeof(buf) - EXT4_MIN_INLINE_DATA_SIZE);
+ ctx->map_blocks_ret = 1;
+
+ /*
+ * 4. Error rollback when ext4_map_blocks returns unmapped block -> -EIO
+ */
+ ctx->map_flags = 0;
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+ ctx->map_flags = EXT4_MAP_MAPPED;
+
+ /*
+ * 5. Error rollback when ext4_inline_getblk returns NULL -> -ENOMEM
+ */
+ ctx->getblk_fail = true;
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, -ENOMEM);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+ ctx->getblk_fail = false;
+
+ /*
+ * 6. Error rollback when ext4_handle_dirty_metadata fails on data_bh
+ */
+ ctx->dirty_metadata_err = -EIO;
+ ctx->dirty_metadata_fail_on_call = 1;
+ ctx->dirty_metadata_calls = 0;
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+ ctx->dirty_metadata_err = 0;
+ ctx->dirty_metadata_fail_on_call = 0;
+
+ /*
+ * 7. Successful conversion via ext4_convert_inline_data():
+ * copies 80 bytes into data_bh, destroys inline data, initializes
+ * extent tree.
+ */
+ memset(ctx->data_block_buf, 0, sizeof(ctx->data_block_buf));
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_MEMEQ(test, ctx->data_block_buf, buf, sizeof(buf));
+}
+
+static void test_write_triggers_inline_to_extent_conversion(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct folio *folio = NULL;
+ char buf[50];
+ int ret;
+
+ memset(buf, 'K', sizeof(buf));
+
+ /*
+ * 1. Standard write (ext4_try_to_write_inline_data) exceeding
+ * TEST_MAX_INLINE_SIZE (pos=0, len=200) converts inline data via
+ * ext4_convert_inline_data_to_extent -> returns 0 so caller proceeds
+ * with normal block write.
+ */
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ inode_lock(inode);
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+
+ /*
+ * 2. Conversion with DIOREAD_NOLOCK + DELALLOC enabled and journal_data
+ * mode (exercises ext4_should_dioread_nolock and ext4_should_journal_data
+ * branches in ext4_convert_inline_data_to_extent).
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ set_opt(&ctx->sb, DIOREAD_NOLOCK);
+ set_opt(&ctx->sb, DELALLOC);
+ ctx->journal_mode = EXT4_INODE_JOURNAL_DATA_MODE;
+
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+
+ clear_opt(&ctx->sb, DIOREAD_NOLOCK);
+ clear_opt(&ctx->sb, DELALLOC);
+ ctx->journal_mode = EXT4_INODE_WRITEBACK_DATA_MODE;
+
+ /*
+ * 3. Error paths in ext4_convert_inline_data_to_extent when
+ * ext4_get_block fails during ext4_block_write_begin (-EIO and -ENOSPC):
+ * cleans up orphan and propagates error.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ctx->map_blocks_ret = -EIO;
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->ei.i_orphan));
+ ctx->map_blocks_ret = 1;
+
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ctx->map_blocks_ret = -ENOSPC;
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->ei.i_orphan));
+ ctx->map_blocks_ret = 1;
+
+ /*
+ * 4. Delayed allocation conversion (da = true in
+ * ext4_generic_write_inline_data):
+ * 4a. Fresh inode without inline data yet -> clears MAY_INLINE_DATA
+ * and returns 0.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ folio = NULL;
+ ret = ext4_generic_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio, true);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA));
+
+ /*
+ * 4b. Existing inline data -> ext4_da_convert_inline_data_to_extent
+ * loads inline bytes into folio, marks buffer delayed, and clears
+ * EXT4_STATE_MAY_INLINE_DATA while keeping inline data until
+ * writepages.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ truncate_inode_pages(&inode->i_data, 0);
+
+ folio = NULL;
+ ret = ext4_generic_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio, true);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA));
+
+ /*
+ * 4c. Error in ext4_da_get_block_prep during DA conversion ->
+ * propagates error and unlocks folio.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ truncate_inode_pages(&inode->i_data, 0);
+
+ ctx->da_get_block_prep_err = -ENOSPC;
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_generic_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio, true);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+ ctx->da_get_block_prep_err = 0;
+}
+
+/* =========================================================================
+ * 6. Inline Directory Operations Tests
+ * =========================================================================
+ */
+
+struct test_dir_ctx {
+ struct dir_context ctx;
+ int count;
+ char names[16][32];
+ u64 inos[16];
+ int max_emit;
+ struct inode *mutate_inode_on_second;
+};
+
+static bool test_filldir_actor(struct dir_context *dctx, const char *name,
+ int namelen, loff_t offset, u64 ino,
+ unsigned int d_type)
+{
+ struct test_dir_ctx *tctx =
+ container_of(dctx, struct test_dir_ctx, ctx);
+
+ if (tctx->max_emit > 0 && tctx->count >= tctx->max_emit)
+ return false;
+
+ if (tctx->count < 16) {
+ int copy_len = min(namelen, 31);
+
+ memcpy(tctx->names[tctx->count], name, copy_len);
+ tctx->names[tctx->count][copy_len] = '\0';
+ tctx->inos[tctx->count] = ino;
+ }
+ tctx->count++;
+
+ if (tctx->mutate_inode_on_second && tctx->count == 2) {
+ inode_inc_iversion(tctx->mutate_inode_on_second);
+ tctx->mutate_inode_on_second = NULL;
+ }
+ return true;
+}
+
+struct inline_dir_test_env {
+ struct inode parent;
+ struct inode child;
+ struct qstr qname;
+ struct ext4_filename fname;
+ struct dir_private_info info;
+ struct file filp;
+ struct test_dir_ctx dctx;
+ struct ext4_dir_entry_2 bogus_de;
+};
+
+static struct inline_dir_test_env *
+alloc_dir_test_env(struct kunit *test, struct inline_test_ctx *ctx)
+{
+ struct inline_dir_test_env *env;
+
+ env = kunit_kzalloc(test, sizeof(*env), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, env);
+ env->parent.i_sb = &ctx->sb;
+ env->parent.i_ino = TEST_PARENT_INO;
+ env->parent.i_mode = S_IFDIR | 0755;
+ return env;
+}
+
+static void init_mock_fname_and_child(struct inline_dir_test_env *env,
+ struct super_block *sb,
+ const char *name, ino_t ino,
+ umode_t mode)
+{
+ memset(&env->fname, 0, sizeof(env->fname));
+ memset(&env->qname, 0, sizeof(env->qname));
+ memset(&env->child, 0, sizeof(env->child));
+
+ env->child.i_sb = sb;
+ env->child.i_ino = ino;
+ env->child.i_mode = mode;
+
+ env->qname.name = (const unsigned char *)name;
+ env->qname.len = strlen(name);
+ env->fname.usr_fname = &env->qname;
+ env->fname.disk_name.name = (unsigned char *)name;
+ env->fname.disk_name.len = strlen(name);
+}
+
+static void test_update_final_de(struct kunit *test)
+{
+ u8 buf[64];
+ struct ext4_dir_entry_2 *de1, *de2;
+
+ memset(buf, 0, sizeof(buf));
+
+ /* 1. old_size == 0 -> sets de1->rec_len to new_size (32) */
+ de1 = (struct ext4_dir_entry_2 *)buf;
+ ext4_update_final_de(buf, 0, 32);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(de1->rec_len), 32);
+
+ /* 2. Two entries in 32 bytes (12 + 20), expand to 56 bytes -> last entry becomes 44 */
+ de1->rec_len = cpu_to_le16(12);
+ de2 = (struct ext4_dir_entry_2 *)(buf + 12);
+ de2->rec_len = cpu_to_le16(20);
+ ext4_update_final_de(buf, 32, 56);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(de1->rec_len), 12);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(de2->rec_len), 44);
+}
+
+static void test_inline_dir_lifecycle_and_operations(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *dir = &ctx->ei.vfs_inode;
+ struct inline_dir_test_env *env;
+ struct ext4_dir_entry_2 *de = NULL;
+ struct buffer_head *bh = NULL;
+ handle_t *handle;
+ int has_inline_data = 1;
+ int ret;
+
+ env = alloc_dir_test_env(test, ctx);
+ reset_inode_state(ctx, S_IFDIR | 0755);
+
+ /*
+ * 1. Create inline directory via ext4_try_create_inline_dir.
+ */
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_create_inline_dir(handle, &env->parent, dir);
+ ext4_journal_stop(handle);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(dir));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size,
+ (u16)EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_EQ(test, i_size_read(dir),
+ (loff_t)EXT4_MIN_INLINE_DATA_SIZE);
+
+ /* Newly created inline dir is empty */
+ KUNIT_EXPECT_TRUE(test, empty_inline_dir(dir, &has_inline_data));
+ KUNIT_EXPECT_EQ(test, has_inline_data, 1);
+
+ /*
+ * 2. Verify ext4_get_first_inline_block returns the inline buffer
+ * starting at parent_de (whose inode field holds the parent ino).
+ */
+ bh = ext4_get_first_inline_block(dir, &de, &ret);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_PARENT_INO);
+ brelse(bh);
+
+ /*
+ * 3. Add first entry "file1" (fits in i_block alongside parent ino).
+ * Return value 1 means added inline without block conversion.
+ */
+ init_mock_fname_and_child(env, &ctx->sb, "file1", 101, S_IFREG | 0644);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size,
+ (u16)EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
+
+ /*
+ * 4. Adding duplicate "file1" returns -EEXIST.
+ */
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, -EEXIST);
+
+ /*
+ * 5. Add more entries ("file2_longer_name", "file3_longer_name",
+ * "file4_in_xattr") to overflow i_block (56B) and trigger expansion
+ * into ibody system.data xattr via ext4_update_inline_dir (132B).
+ */
+ init_mock_fname_and_child(env, &ctx->sb, "file2_longer_name", 102,
+ S_IFREG | 0644);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+
+ init_mock_fname_and_child(env, &ctx->sb, "file3_longer_name", 103,
+ S_IFDIR | 0755);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, TEST_MAX_INLINE_SIZE);
+
+ init_mock_fname_and_child(env, &ctx->sb, "file4_in_xattr", 104,
+ S_IFREG | 0644);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+
+ /*
+ * 6. Lookup entries in both i_block ("file1") and xattr ("file4_in_xattr"),
+ * and verify missing entry returns NULL.
+ */
+ init_mock_fname_and_child(env, &ctx->sb, "file1", 0, 0);
+ bh = ext4_find_inline_entry(dir, &env->fname, &de, &has_inline_data);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, has_inline_data, 1);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), 101U);
+ brelse(bh);
+
+ init_mock_fname_and_child(env, &ctx->sb, "file4_in_xattr", 0, 0);
+ bh = ext4_find_inline_entry(dir, &env->fname, &de, &has_inline_data);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), 104U);
+ brelse(bh);
+
+ init_mock_fname_and_child(env, &ctx->sb, "nonexistent", 0, 0);
+ bh = ext4_find_inline_entry(dir, &env->fname, &de, &has_inline_data);
+ KUNIT_EXPECT_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, has_inline_data, 1);
+
+ /*
+ * 7. Test ext4_read_inline_dir (full iteration, partial iteration,
+ * and iversion change re-validation).
+ */
+ env->filp.f_inode = dir;
+ env->filp.private_data = &env->info;
+ env->dctx.ctx.actor = test_filldir_actor;
+ env->dctx.ctx.pos = 0;
+
+ ret = ext4_read_inline_dir(&env->filp, &env->dctx.ctx,
+ &has_inline_data);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, has_inline_data, 1);
+ /*
+ * Expect ".", "..", "file1", "file2_longer_name",
+ * "file3_longer_name", "file4_in_xattr"
+ */
+ KUNIT_EXPECT_EQ(test, env->dctx.count, 6);
+ KUNIT_EXPECT_STREQ(test, env->dctx.names[0], ".");
+ KUNIT_EXPECT_STREQ(test, env->dctx.names[1], "..");
+ KUNIT_EXPECT_STREQ(test, env->dctx.names[2], "file1");
+ KUNIT_EXPECT_STREQ(test, env->dctx.names[5], "file4_in_xattr");
+ /*
+ * extra_size = (dotdot_size - EXT4_INLINE_DOTDOT_SIZE) +
+ * TEST_MAX_INLINE_SIZE = (24 - 4) + 128 = 148
+ */
+ KUNIT_EXPECT_EQ(test, env->dctx.ctx.pos,
+ (loff_t)(TEST_MAX_INLINE_SIZE + 20));
+
+ /* Calling again at EOF (pos == inline_size) emits nothing */
+ env->dctx.count = 0;
+ ret = ext4_read_inline_dir(&env->filp, &env->dctx.ctx,
+ &has_inline_data);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, env->dctx.count, 0);
+
+ /* Mutating inode iversion during iteration triggers offset revalidation */
+ memset(&env->dctx, 0, sizeof(env->dctx));
+ env->dctx.ctx.actor = test_filldir_actor;
+ env->dctx.ctx.pos = 0;
+ env->dctx.mutate_inode_on_second = dir;
+ ret = ext4_read_inline_dir(&env->filp, &env->dctx.ctx,
+ &has_inline_data);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, env->dctx.count, 6);
+
+ /*
+ * 8. Test ext4_inlinedir_to_tree (htree rbtree population).
+ */
+ {
+ struct dir_private_info *info;
+ struct dx_hash_info hinfo = {
+ .hash_version = DX_HASH_HALF_MD4,
+ };
+
+ memset(&env->filp, 0, sizeof(env->filp));
+ env->filp.f_inode = dir;
+ info = kzalloc_obj(*info, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, info);
+ env->filp.private_data = info;
+ ret = ext4_inlinedir_to_tree(&env->filp, dir, 0, &hinfo,
+ 0, 0, &has_inline_data);
+ KUNIT_EXPECT_GT(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, has_inline_data, 1);
+
+ /* With start_hash = 0xffffffffU, entries with smaller hash are skipped */
+ ext4_htree_free_dir_info(info);
+ info = kzalloc_obj(*info, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, info);
+ env->filp.private_data = info;
+ ret = ext4_inlinedir_to_tree(&env->filp, dir, 0, &hinfo,
+ 0xffffffffU, 0xffffffffU,
+ &has_inline_data);
+ KUNIT_EXPECT_GE(test, ret, 0);
+ ext4_htree_free_dir_info(info);
+ }
+
+ /*
+ * 9. Delete entries in both i_block and xattr regions and verify
+ * empty_inline_dir behavior when only xattr has an entry vs completely empty.
+ */
+ {
+ static const char * const del_names[] = {
+ "file1",
+ "file2_longer_name",
+ "file3_longer_name",
+ };
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(del_names); i++) {
+ init_mock_fname_and_child(env, &ctx->sb, del_names[i],
+ 0, 0);
+ bh = ext4_find_inline_entry(dir, &env->fname, &de,
+ &has_inline_data);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_delete_inline_entry(handle, dir, de, bh,
+ &has_inline_data);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ brelse(bh);
+ }
+ }
+
+ /* i_block now has no live entries, but "file4_in_xattr" is still in xattr */
+ KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
+
+ /* Delete "file4_in_xattr" -> directory becomes completely empty */
+ init_mock_fname_and_child(env, &ctx->sb, "file4_in_xattr", 0, 0);
+ bh = ext4_find_inline_entry(dir, &env->fname, &de, &has_inline_data);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+
+ /* Deleting an address outside inline buffers returns -ENOENT */
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_delete_inline_entry(handle, dir, &env->bogus_de, bh,
+ &has_inline_data);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, -ENOENT);
+
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_delete_inline_entry(handle, dir, de, bh, &has_inline_data);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ brelse(bh);
+
+ KUNIT_EXPECT_TRUE(test, empty_inline_dir(dir, &has_inline_data));
+}
+
+static void test_inline_dir_conversion_and_corruptions(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *dir = &ctx->ei.vfs_inode;
+ struct inline_dir_test_env *env;
+ struct ext4_dir_entry_2 *de;
+ handle_t *handle;
+ int has_inline_data = 1;
+ int i, ret;
+
+ env = alloc_dir_test_env(test, ctx);
+ reset_inode_state(ctx, S_IFDIR | 0755);
+
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_create_inline_dir(handle, &env->parent, dir);
+ ext4_journal_stop(handle);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /* Fill the inline directory up to its 128-byte limit */
+ for (i = 0; i < 4; i++) {
+ char name[24];
+
+ snprintf(name, sizeof(name), "entry_long_name_%d", i);
+ init_mock_fname_and_child(env, &ctx->sb, name, 200 + i,
+ S_IFREG | 0644);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir,
+ &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ }
+
+ /*
+ * 1. When full, adding another entry triggers ext4_convert_inline_data_nolock.
+ * Test rollback first when ext4_map_blocks fails (-ENOSPC).
+ */
+ init_mock_fname_and_child(env, &ctx->sb, "overflow_entry_name", 250,
+ S_IFREG | 0644);
+ ctx->map_blocks_ret = -ENOSPC;
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(dir));
+ ctx->map_blocks_ret = 1;
+
+ /*
+ * 2. Successful conversion of full inline directory to a normal
+ * 1024-byte directory block -> ext4_try_add_inline_entry returns 0
+ * and data_block_buf starts with "." and ".." followed by copied entries.
+ */
+ memset(ctx->data_block_buf, 0, sizeof(ctx->data_block_buf));
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(dir));
+ KUNIT_EXPECT_EQ(test, i_size_read(dir), (loff_t)TEST_BLOCK_SIZE);
+
+ /* Verify "." and ".." headers in converted block */
+ de = (struct ext4_dir_entry_2 *)ctx->data_block_buf;
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_INO);
+ KUNIT_EXPECT_EQ(test, de->name_len, 1);
+ KUNIT_EXPECT_EQ(test, de->name[0], '.');
+ de = ext4_next_entry(de, TEST_BLOCK_SIZE);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_PARENT_INO);
+ KUNIT_EXPECT_EQ(test, de->name_len, 2);
+ KUNIT_EXPECT_MEMEQ(test, de->name, "..", 2);
+
+ /*
+ * 3. Non-inline directory behavior on inline dir functions:
+ * sets *has_inline_data = 0 and returns early.
+ */
+ has_inline_data = 1;
+ KUNIT_EXPECT_TRUE(test, empty_inline_dir(dir, &has_inline_data));
+ KUNIT_EXPECT_EQ(test, has_inline_data, 0);
+
+ /*
+ * 4. Corrupted inline directory checks:
+ * 4a. Parent ino ("..") == 0 -> empty_inline_dir returns false.
+ * 4b. Zero rec_len dirent -> empty_inline_dir returns false,
+ * ext4_inlinedir_to_tree returns -EFSCORRUPTED, and
+ * ext4_convert_inline_data_nolock aborts via ext4_check_all_de.
+ */
+ reset_inode_state(ctx, S_IFDIR | 0755);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_create_inline_dir(handle, &env->parent, dir);
+ ext4_journal_stop(handle);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /* Corrupt parent ino to 0 */
+ *(__le32 *)raw_inline_i_block(ctx) = cpu_to_le32(0);
+ KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
+ *(__le32 *)raw_inline_i_block(ctx) = cpu_to_le32(TEST_PARENT_INO);
+
+ /* Corrupt first dirent rec_len to 0 */
+ de = (struct ext4_dir_entry_2 *)((char *)raw_inline_i_block(ctx) +
+ EXT4_INLINE_DOTDOT_SIZE);
+ de->rec_len = cpu_to_le16(0);
+ KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
+
+ /* ext4_convert_inline_data on corrupted inline dir fails in ext4_check_all_de */
+ ret = ext4_convert_inline_data(dir);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(dir));
+}
+
+static struct kunit_case ext4_inline_test_cases[] = {
+ KUNIT_CASE(test_max_inline_size_variants),
+ KUNIT_CASE(test_find_inline_data_nolock),
+ KUNIT_CASE(test_write_and_destroy_inline_data),
+ KUNIT_CASE(test_write_inline_short_copy_and_edge_cases),
+ KUNIT_CASE(test_readpage_inline_link_and_iomap),
+ KUNIT_CASE(test_inline_data_truncate),
+ KUNIT_CASE(test_convert_inline_data_and_rollback),
+ KUNIT_CASE(test_write_triggers_inline_to_extent_conversion),
+ KUNIT_CASE(test_update_final_de),
+ KUNIT_CASE(test_inline_dir_lifecycle_and_operations),
+ KUNIT_CASE(test_inline_dir_conversion_and_corruptions),
+ {}
+};
+
+static struct kunit_suite ext4_inline_test_suite = {
+ .name = "ext4_inline_test",
+ .init = inline_test_init,
+ .exit = inline_test_exit,
+ .test_cases = ext4_inline_test_cases,
+};
+
+kunit_test_suites(&ext4_inline_test_suite);
+
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 0e662a9f7574..97cc2c61ce25 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -9,6 +9,7 @@
#include <linux/namei.h>
#include <linux/iversion.h>
#include <linux/sched/mm.h>
+#include <kunit/static_stub.h>
#include "ext4_jbd2.h"
#include "ext4.h"
@@ -1096,6 +1097,13 @@ static void ext4_restore_inline_data(handle_t *handle, struct inode *inode,
ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
}
+struct buffer_head *ext4_inline_getblk(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_inline_getblk, sb, block);
+ return sb_getblk(sb, block);
+}
+
static int ext4_convert_inline_data_nolock(handle_t *handle,
struct inode *inode,
struct ext4_iloc *iloc)
@@ -1144,7 +1152,7 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
goto out_restore;
}
- data_bh = sb_getblk(inode->i_sb, map.m_pblk);
+ data_bh = ext4_inline_getblk(inode->i_sb, map.m_pblk);
if (!data_bh) {
error = -ENOMEM;
goto out_restore;
@@ -2018,5 +2026,25 @@ int ext4_convert_inline_data(struct inode *inode)
}
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inline_getblk);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_max_inline_size);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_inline_data_nolock);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_readpage_inline);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_generic_write_inline_data);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_try_to_write_inline_data);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_write_inline_data_end);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_update_final_de);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_try_add_inline_entry);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inlinedir_to_tree);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_inline_dir);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_inline_link);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_first_inline_block);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_try_create_inline_dir);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_inline_entry);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_delete_inline_entry);
+EXPORT_SYMBOL_FOR_EXT4_TEST(empty_inline_dir);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_destroy_inline_data);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inline_data_iomap);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inline_data_truncate);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_convert_inline_data);
#endif
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index c8a06c455b3f..105c731b95b1 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -2030,6 +2030,9 @@ int ext4_da_get_block_prep(struct inode *inode, sector_t iblock,
sector_t invalid_block = ~((sector_t) 0xffff);
int ret = 0;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_da_get_block_prep, inode, iblock, bh,
+ create);
+
BUG_ON(create == 0);
BUG_ON(bh->b_size != inode->i_sb->s_blocksize);
@@ -6999,6 +7002,7 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
}
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_get_block_prep);
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);
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 08/27] ext4: zero s_fc_replay_state in ext4_fc_replay_cleanup()
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (6 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 07/27] ext4: add KUnit tests for fs/ext4/inline.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (18 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
ext4_fc_replay_cleanup() frees sbi->s_fc_replay_state.fc_regions and
sbi->s_fc_replay_state.fc_modified_inodes, but leaves the dangling
pointers and non-zero size/usage counters in sbi->s_fc_replay_state.
This makes ext4_fc_replay_cleanup() non-idempotent: calling it more than
once triggers a slab double-free of fc_regions and fc_modified_inodes.
In particular, the ext4_fast_commit_test KUnit suite calls
ext4_fc_replay_cleanup() explicitly at the end of replay test cases and
again from its fc_cleanup_all() teardown action, causing SLUB
double-free errors in test_fc_replay_inode_and_dentries() and
test_fc_replay_ranges_and_end_to_end(). Leaving stale pointers and a
non-zero fc_regions_valid count in s_fc_replay_state after replay
completes also risks a use-after-free if ext4_fc_replay_check_excluded()
is ever invoked after cleanup, or a double-free if mount error paths are
updated to call ext4_fc_replay_cleanup().
Zero sbi->s_fc_replay_state in ext4_fc_replay_cleanup() after freeing
both buffers so the replay state is fully reset and repeated calls are
safe no-ops.
Fixes: 8016e29f4362 ("ext4: fast commit recovery path")
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/fast_commit.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c
index 0cac890cf370..15bd040c2329 100644
--- a/fs/ext4/fast_commit.c
+++ b/fs/ext4/fast_commit.c
@@ -2398,6 +2398,7 @@ void ext4_fc_replay_cleanup(struct super_block *sb)
sbi->s_mount_state &= ~EXT4_FC_REPLAY;
kfree(sbi->s_fc_replay_state.fc_regions);
kfree(sbi->s_fc_replay_state.fc_modified_inodes);
+ memset(&sbi->s_fc_replay_state, 0, sizeof(sbi->s_fc_replay_state));
}
static bool ext4_fc_value_len_isvalid(struct ext4_sb_info *sbi,
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 09/27] ext4: add KUnit tests for fs/ext4/fast_commit.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (7 preceding siblings ...)
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:06 ` 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
` (17 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add the ext4_fast_commit_test KUnit test suite in
fs/ext4/fast_commit-test.c to comprehensively exercise fast commit
tracking, snapshotting, TLV serialization, commit execution, and journal
replay in fs/ext4/fast_commit.c without requiring a mounted block
device:
- Fast commit initialization and eligibility helpers (ext4_fc_init,
ext4_fc_init_inode, ext4_fc_disabled, ext4_fc_eligible,
ext4_fc_set_snap_err, and ext4_fc_snap_stats_update_max).
- Ineligibility tracking (ext4_fc_mark_ineligible) with active handles,
NULL handles with or without a running transaction, monotonic
s_fc_ineligible_tid updates, and per-reason statistics counters.
- Inode and logical range tracking (ext4_fc_track_inode and
ext4_fc_track_range), covering directory skips, data-journalling and
inline-data ineligibility, special inodes (< EXT4_FIRST_INO), same-TID
bounding-interval range merging, cross-TID resets,
EXT4_STATE_FC_COMMITTING to EXT4_STATE_FC_REQUEUE transitions, and
FC_Q_MAIN vs. FC_Q_STAGING queue routing.
- Dentry update tracking (ext4_fc_track_create, ext4_fc_track_link, and
ext4_fc_track_unlink) and inode deletion cleanup (ext4_fc_del),
including root dentry parent handling, EXT4_FC_TAG_CREAT removal, and
concurrent wait-bit wakeups across EXT4_STATE_FC_FLUSHING_DATA and
EXT4_STATE_FC_COMMITTING.
- Fast commit buffer space reservation and TLV serialization
(ext4_fc_reserve_space, ext4_fc_add_tlv, and ext4_fc_write_tail),
including oversize rejection, block boundary EXT4_FC_TAG_PAD insertion
and buffer submission, and EXT4_FC_TAG_TAIL padding and CRC32C
calculation.
- Inode and extent-status range snapshotting (ext4_fc_snapshot_inode,
ext4_fc_count_snapshot_inodes, ext4_fc_alloc_snapshot_inodes,
ext4_fc_snapshot_inodes, ext4_fc_write_inode, and
ext4_fc_write_inode_data), including ext4_get_inode_loc_noio fallback,
ES miss/delayed/unsupported status errors, inode and range capacity
caps (-E2BIG), extent splitting past EXT_INIT_MAX_LEN (32768), and
create-only dentry inode snapshotting.
- Fast and full commit cleanup (ext4_fc_cleanup), EWMA commit time stats
(ext4_fc_update_stats), and procfs stats output (ext4_fc_info_show).
- Top-level commit execution and fallbacks (ext4_fc_commit) across
already-committed TIDs, disabled fast commits, jbd2_fc_begin_commit
failures, ineligible filesystems, data flush -EIO errors, snapshot
-EAGAIN fallbacks, jbd2_fc_wait_bufs -EIO errors, and consecutive fast
commits with external journal device cache flushes.
- Recovery region exclusion and scan validation (ext4_fc_record_regions,
ext4_fc_replay_check_excluded, ext4_fc_record_modified_inode,
ext4_fc_value_len_isvalid, tag2str, and PASS_SCAN validation of
missing HEAD, unexpected offset, unsupported features, wrong TID,
bad tail CRC, and partial multi-commit logs).
- Recovery replay of EXT4_FC_TAG_INODE (extent and inline-data inodes),
EXT4_FC_TAG_CREAT (regular files and subdirectories), EXT4_FC_TAG_LINK
(-EEXIST idempotency), and EXT4_FC_TAG_UNLINK (missing entries and
rename-overwrite inode mismatches).
- Recovery replay of EXT4_FC_TAG_ADD_RANGE and EXT4_FC_TAG_DEL_RANGE,
error handling, and an end-to-end ext4_fc_commit() -> PASS_SCAN ->
PASS_REPLAY round-trip.
Export the tested fast commit entry points with
EXPORT_SYMBOL_FOR_EXT4_TEST and add KUNIT_STATIC_STUB_REDIRECT hooks for
ext4_fc_get_buf, ext4_fc_submit_bh, ext4_fc_issue_flush,
ext4_get_inode_loc_noio, ext4_get_fc_inode_loc, ext4_mark_inode_used,
ext4_mb_mark_bb, and ext4_es_lookup_extent.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 3 +-
fs/ext4/ext4.h | 35 +
fs/ext4/extents_status.c | 3 +
fs/ext4/fast_commit-test.c | 2656 ++++++++++++++++++++++++++++++++++++
fs/ext4/fast_commit.c | 180 ++-
fs/ext4/ialloc.c | 3 +
fs/ext4/inode.c | 6 +
fs/ext4/mballoc.c | 3 +-
8 files changed, 2882 insertions(+), 7 deletions(-)
create mode 100644 fs/ext4/fast_commit-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 5a90bf5b28e3..7bd91839c3ce 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -17,7 +17,8 @@ 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 xattr-test.o \
- namei-test.o inline-test.o
+ namei-test.o inline-test.o \
+ fast_commit-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 bcb4eb10d363..afa2db3b6d57 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3070,6 +3070,9 @@ extern int ext4_init_inode_table(struct super_block *sb,
void ext4_end_bitmap_read(struct bio *bio);
/* fast_commit.c */
+int ext4_fc_get_buf(journal_t *journal, struct buffer_head **bh);
+void ext4_fc_submit_bh(struct super_block *sb, bool is_tail);
+void ext4_fc_issue_flush(struct block_device *bdev);
int ext4_fc_info_show(struct seq_file *seq, void *v);
void ext4_fc_init(struct super_block *sb, journal_t *journal);
void ext4_fc_init_inode(struct inode *inode);
@@ -3096,6 +3099,38 @@ void ext4_fc_destroy_dentry_cache(void);
int ext4_fc_record_regions(struct super_block *sb, int ino,
ext4_lblk_t lblk, ext4_fsblk_t pblk,
int len, int replay);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+void ext4_fc_set_snap_err_test(int *snap_err, int err);
+bool ext4_fc_disabled_test(struct super_block *sb);
+bool ext4_fc_eligible_test(struct super_block *sb);
+void ext4_fc_snap_stats_update_max_test(atomic64_t *stat, u64 value);
+void ext4_fc_wake_inode_state_test(struct inode *inode, int bit);
+u8 *ext4_fc_reserve_space_test(struct super_block *sb, int len, u32 *crc);
+int ext4_fc_write_tail_test(struct super_block *sb, u32 crc);
+bool ext4_fc_add_tlv_test(struct super_block *sb, u16 tag, u16 len, u8 *val,
+ u32 *crc);
+int ext4_fc_write_inode_test(struct inode *inode, u32 *crc);
+int ext4_fc_write_inode_data_test(struct inode *inode, u32 *crc);
+void ext4_fc_free_inode_snap_test(struct inode *inode);
+int ext4_fc_snapshot_inode_test(struct inode *inode,
+ unsigned int nr_ranges_total,
+ unsigned int *nr_rangesp, int *snap_err);
+unsigned int ext4_fc_count_snapshot_inodes_test(struct super_block *sb);
+int ext4_fc_alloc_snapshot_inodes_test(struct super_block *sb,
+ struct inode ***inodesp,
+ unsigned int *nr_inodesp);
+int ext4_fc_snapshot_inodes_test(journal_t *journal, struct inode **inodes,
+ unsigned int inodes_size,
+ unsigned int *nr_inodesp,
+ unsigned int *nr_rangesp, int *snap_err);
+void ext4_fc_update_stats_test(struct super_block *sb, int status,
+ u64 commit_time, int nblks, tid_t commit_tid);
+void ext4_fc_cleanup_test(journal_t *journal, int full, tid_t tid);
+int ext4_fc_record_modified_inode_test(struct super_block *sb, int ino);
+bool ext4_fc_value_len_isvalid_test(struct ext4_sb_info *sbi, int tag, int len);
+int ext4_fc_replay_test(journal_t *journal, struct buffer_head *bh,
+ enum passtype pass, int off, tid_t expected_tid);
+#endif
/* mballoc.c */
extern const struct seq_operations ext4_mb_seq_groups_ops;
diff --git a/fs/ext4/extents_status.c b/fs/ext4/extents_status.c
index 64bac0c4c1fa..075b9efb6dd4 100644
--- a/fs/ext4/extents_status.c
+++ b/fs/ext4/extents_status.c
@@ -1099,6 +1099,9 @@ int ext4_es_lookup_extent(struct inode *inode, ext4_lblk_t lblk,
struct rb_node *node;
int found = 0;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_es_lookup_extent, inode, lblk,
+ next_lblk, es, pseq);
+
if (EXT4_SB(inode->i_sb)->s_mount_state & EXT4_FC_REPLAY)
return 0;
diff --git a/fs/ext4/fast_commit-test.c b/fs/ext4/fast_commit-test.c
new file mode 100644
index 000000000000..57745e51bea6
--- /dev/null
+++ b/fs/ext4/fast_commit-test.c
@@ -0,0 +1,2656 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for ext4 fast commit operations (fs/ext4/fast_commit.c)
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/backing-dev.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/iversion.h>
+#include <linux/jbd2.h>
+#include <linux/kthread.h>
+#include <linux/mm.h>
+#include <linux/pagemap.h>
+#include <linux/quotaops.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "ext4_extents.h"
+#include "extents_status.h"
+#include "fast_commit.h"
+#include "namei.h"
+
+#define TEST_BLK_SIZE 1024
+#define TEST_BLK_BITS 10
+#define TEST_INODE_SIZE 256
+#define TEST_EXTRA_ISIZE 32
+#define TEST_FIRST_PBLK 100
+#define TEST_MAX_BLOCKS 32
+#define TEST_MAX_LBLOCKS 16
+#define TEST_MAX_INODES 8
+#define TEST_MAX_FC_BUFS 16
+#define TEST_MAX_BB_MARKS 32
+
+#define TEST_ROOT_INO EXT4_ROOT_INO
+#define TEST_DIR_INO 12
+#define TEST_FILE1_INO 13
+#define TEST_FILE2_INO 14
+#define TEST_SUBDIR_INO 15
+#define TEST_CSUM_SEED 0x12345678U
+
+struct fc_mock_block {
+ bool in_use;
+ struct buffer_head bh;
+ char data[TEST_BLK_SIZE] __aligned(8);
+};
+
+struct fc_mock_inode_slot {
+ bool allocated;
+ struct ext4_inode_info ei;
+ struct jbd2_inode jinode;
+ struct buffer_head iloc_bh;
+ char raw_buf[TEST_BLK_SIZE] __aligned(8);
+ ext4_fsblk_t lblk_to_pblk[TEST_MAX_LBLOCKS];
+};
+
+struct fc_bb_mark {
+ ext4_fsblk_t block;
+ int len;
+ bool state;
+};
+
+struct fc_test_ctx {
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ struct ext4_super_block es;
+ struct block_device bdev;
+ struct block_device ext_jdev;
+ struct address_space bd_mapping;
+ struct buffer_head sb_bh;
+
+ journal_t journal;
+ transaction_t running_txn;
+ handle_t handle;
+ struct buffer_head *fc_wbuf[TEST_MAX_FC_BUFS];
+ struct buffer_head fc_bhs[TEST_MAX_FC_BUFS];
+ char fc_buf_data[TEST_MAX_FC_BUFS][TEST_BLK_SIZE] __aligned(8);
+
+ struct fc_mock_inode_slot inodes[TEST_MAX_INODES];
+ struct fc_mock_block blocks[TEST_MAX_BLOCKS];
+
+ /* Shrinker callbacks saved during init */
+ unsigned long (*shrinker_count)(struct shrinker *shrink,
+ struct shrink_control *sc);
+ unsigned long (*shrinker_scan)(struct shrinker *shrink,
+ struct shrink_control *sc);
+
+ /* Fault injection and call tracking */
+ int journal_mode;
+ int fail_get_inode_loc;
+ int fail_get_inode_loc_noio;
+ int fail_get_fc_inode_loc;
+ int fail_dirty_metadata;
+ int fail_dirty_metadata_on_call;
+ int dirty_metadata_calls;
+ bool fail_sync_dirty_buffer;
+ int fail_mark_inode_used;
+ int fail_iget;
+ int fail_iget_on_call;
+ unsigned long fail_iget_ino;
+ int iget_calls;
+ int fail_map_blocks;
+ int force_map_blocks_ret;
+ ext4_fsblk_t force_map_pblk;
+ unsigned int force_map_flags;
+ int fail_find_extent;
+ int fail_insert_extent;
+ int fail_fc_get_buf;
+ int fail_fc_get_buf_on_call;
+ int fc_get_buf_calls;
+ bool fail_fc_wait_bufs;
+ int submit_inode_data_err;
+
+ unsigned int fc_submit_bh_calls;
+ unsigned int fc_submit_tail_calls;
+ unsigned int fc_flush_calls;
+ struct block_device *last_flushed_bdev;
+ unsigned int mark_inode_used_calls;
+ int last_marked_ino;
+
+ struct fc_bb_mark bb_marks[TEST_MAX_BB_MARKS];
+ unsigned int nr_bb_marks;
+
+ /* Custom ES lookup stub override */
+ bool use_custom_es_lookup;
+ struct extent_status custom_es;
+ int custom_es_ret;
+};
+
+static const struct super_operations fc_test_sops = {};
+
+#ifdef CONFIG_QUOTA
+static qsize_t *fc_test_get_reserved_space(struct inode *inode)
+{
+ return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations fc_test_qops = {
+ .get_reserved_space = fc_test_get_reserved_space,
+};
+#endif
+
+static unsigned long fc_dummy_shrinker_count(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return 0;
+}
+
+static unsigned long fc_dummy_shrinker_scan(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return SHRINK_STOP;
+}
+
+static int fc_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static int fc_set_anon(struct super_block *sb, struct fs_context *fc)
+{
+ return set_anon_super_fc(sb, fc);
+}
+
+static struct file_system_type fc_fs_type = {
+ .name = "fast_commit_test",
+ .init_fs_context = fc_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+/* --- Mock Inode, Block, and Dentry Helpers --- */
+
+static struct fc_mock_inode_slot *find_slot_by_ino(struct fc_test_ctx *ctx,
+ unsigned long ino)
+{
+ int i;
+
+ for (i = 0; i < TEST_MAX_INODES; i++) {
+ if (ctx->inodes[i].allocated &&
+ ctx->inodes[i].ei.vfs_inode.i_ino == ino)
+ return &ctx->inodes[i];
+ }
+ return NULL;
+}
+
+static struct fc_mock_inode_slot *find_slot(struct fc_test_ctx *ctx,
+ struct inode *inode)
+{
+ return find_slot_by_ino(ctx, inode->i_ino);
+}
+
+static struct inode *get_mock_inode(struct fc_test_ctx *ctx, unsigned long ino)
+{
+ struct fc_mock_inode_slot *slot = find_slot_by_ino(ctx, ino);
+
+ return slot ? &slot->ei.vfs_inode : NULL;
+}
+
+static struct fc_mock_block *get_mock_block(struct fc_test_ctx *ctx,
+ ext4_fsblk_t pblk)
+{
+ unsigned long idx;
+
+ if (pblk < TEST_FIRST_PBLK)
+ return NULL;
+ idx = pblk - TEST_FIRST_PBLK;
+ if (idx >= TEST_MAX_BLOCKS)
+ return NULL;
+ return &ctx->blocks[idx];
+}
+
+static ext4_fsblk_t alloc_mock_pblk(struct fc_test_ctx *ctx)
+{
+ int i;
+
+ for (i = 0; i < TEST_MAX_BLOCKS; i++) {
+ if (!ctx->blocks[i].in_use) {
+ ctx->blocks[i].in_use = true;
+ memset(ctx->blocks[i].data, 0, TEST_BLK_SIZE);
+ set_buffer_uptodate(&ctx->blocks[i].bh);
+ clear_buffer_verified(&ctx->blocks[i].bh);
+ return TEST_FIRST_PBLK + i;
+ }
+ }
+ return 0;
+}
+
+static void init_mock_inode_slot(struct fc_test_ctx *ctx,
+ struct fc_mock_inode_slot *slot,
+ unsigned long ino, umode_t mode)
+{
+ 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 = mode;
+ inode->i_blkbits = TEST_BLK_BITS;
+ inode->i_flags |= S_NOQUOTA;
+ inode->i_mapping = &inode->i_data;
+ set_nlink(inode, S_ISDIR(mode) ? 2 : 1);
+ atomic_set(&inode->i_count, 1);
+
+ spin_lock_init(&inode->i_lock);
+ init_rwsem(&inode->i_rwsem);
+ init_rwsem(&inode->i_data.invalidate_lock);
+ 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);
+ inode_state_set_raw(inode, I_DIRTY);
+
+ init_rwsem(&slot->ei.xattr_sem);
+ init_rwsem(&slot->ei.i_data_sem);
+ INIT_LIST_HEAD(&slot->ei.i_orphan);
+ spin_lock_init(&slot->ei.i_fc_lock);
+ spin_lock_init(&slot->ei.i_raw_lock);
+ spin_lock_init(&slot->ei.i_block_reservation_lock);
+ ext4_es_init_tree(&slot->ei.i_es_tree);
+ ext4_init_pending_tree(&slot->ei.i_pending_tree);
+ rwlock_init(&slot->ei.i_es_lock);
+ INIT_LIST_HEAD(&slot->ei.i_es_list);
+ ext4_fc_init_inode(inode);
+
+ slot->ei.i_extra_isize = TEST_EXTRA_ISIZE;
+ slot->ei.i_csum_seed = TEST_CSUM_SEED ^ (u32)ino;
+ slot->ei.i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+
+ jbd2_journal_init_jbd_inode(&slot->jinode, inode);
+ slot->ei.jinode = &slot->jinode;
+
+ slot->iloc_bh.b_data = slot->raw_buf;
+ slot->iloc_bh.b_size = TEST_BLK_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_mode = cpu_to_le16(mode);
+ raw_inode->i_links_count = cpu_to_le16(inode->i_nlink);
+ raw_inode->i_extra_isize = cpu_to_le16(TEST_EXTRA_ISIZE);
+ if (S_ISREG(mode)) {
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ext4_ext_tree_init(NULL, inode);
+ raw_inode->i_flags = cpu_to_le32(EXT4_EXTENTS_FL);
+ memcpy(raw_inode->i_block, slot->ei.i_data,
+ sizeof(raw_inode->i_block));
+ }
+}
+
+static void init_mock_dentry(struct dentry *dentry, struct dentry *parent,
+ const char *name, struct inode *inode,
+ struct super_block *sb)
+{
+ memset(dentry, 0, sizeof(*dentry));
+ spin_lock_init(&dentry->d_lock);
+ seqcount_spinlock_init(&dentry->d_seq, &dentry->d_lock);
+ INIT_HLIST_NODE(&dentry->d_sib);
+ INIT_HLIST_HEAD(&dentry->d_children);
+ INIT_HLIST_BL_NODE(&dentry->d_hash);
+ dentry->d_parent = parent ? parent : dentry;
+ dentry->d_sb = sb;
+ strscpy((char *)dentry->d_shortname.string, name,
+ sizeof(dentry->d_shortname.string));
+ dentry->__d_name.name = dentry->d_shortname.string;
+ dentry->__d_name.len = strlen(name);
+ if (inode) {
+ dentry->d_inode = inode;
+ dentry->d_flags = S_ISDIR(inode->i_mode) ?
+ DCACHE_DIRECTORY_TYPE : DCACHE_REGULAR_TYPE;
+ } else {
+ dentry->d_flags = DCACHE_MISS_TYPE;
+ }
+}
+
+static void sync_inode_to_raw(struct fc_mock_inode_slot *slot)
+{
+ struct inode *inode = &slot->ei.vfs_inode;
+ struct ext4_inode *raw = (struct ext4_inode *)slot->raw_buf;
+
+ raw->i_mode = cpu_to_le16(inode->i_mode);
+ raw->i_links_count = cpu_to_le16(inode->i_nlink);
+ raw->i_size_lo = cpu_to_le32(lower_32_bits(inode->i_size));
+ raw->i_size_high = cpu_to_le32(upper_32_bits(inode->i_size));
+ raw->i_flags = cpu_to_le32(slot->ei.i_flags);
+ raw->i_generation = cpu_to_le32(inode->i_generation);
+ raw->i_extra_isize = cpu_to_le16(slot->ei.i_extra_isize);
+ memcpy(raw->i_block, slot->ei.i_data, sizeof(raw->i_block));
+}
+
+/* --- KUnit Static Stubs --- */
+
+static int stub_inode_journal_mode(struct inode *inode)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+
+ return ctx->journal_mode;
+}
+
+static void stub_da_update_reserve_space(struct inode *inode, int used,
+ int quota_claim)
+{
+}
+
+static void stub_da_release_space(struct inode *inode, int to_free)
+{
+}
+
+static int stub_get_inode_loc(struct inode *inode, struct ext4_iloc *iloc)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ struct fc_mock_inode_slot *slot;
+
+ if (ctx->fail_get_inode_loc)
+ return ctx->fail_get_inode_loc;
+
+ slot = find_slot(ctx, inode);
+ if (!slot)
+ return -ENOENT;
+
+ get_bh(&slot->iloc_bh);
+ iloc->bh = &slot->iloc_bh;
+ iloc->offset = 0;
+ iloc->block_group = 0;
+ return 0;
+}
+
+static int stub_get_inode_loc_noio(struct inode *inode, struct ext4_iloc *iloc)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ struct fc_mock_inode_slot *slot;
+
+ if (ctx->fail_get_inode_loc_noio)
+ return ctx->fail_get_inode_loc_noio;
+
+ slot = find_slot(ctx, inode);
+ if (!slot)
+ return -EAGAIN;
+
+ get_bh(&slot->iloc_bh);
+ iloc->bh = &slot->iloc_bh;
+ iloc->offset = 0;
+ iloc->block_group = 0;
+ return 0;
+}
+
+static int stub_get_fc_inode_loc(struct super_block *sb, unsigned long ino,
+ struct ext4_iloc *iloc)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ struct fc_mock_inode_slot *slot;
+
+ if (ctx->fail_get_fc_inode_loc)
+ return ctx->fail_get_fc_inode_loc;
+
+ slot = find_slot_by_ino(ctx, ino);
+ if (!slot)
+ return -ENOENT;
+
+ get_bh(&slot->iloc_bh);
+ iloc->bh = &slot->iloc_bh;
+ iloc->offset = 0;
+ iloc->block_group = 0;
+ return 0;
+}
+
+static int stub_handle_dirty_metadata(const char *where, unsigned int line,
+ handle_t *handle, struct inode *inode,
+ struct buffer_head *bh)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+
+ ctx->dirty_metadata_calls++;
+ if (ctx->fail_dirty_metadata_on_call > 0 &&
+ ctx->dirty_metadata_calls == ctx->fail_dirty_metadata_on_call)
+ return ctx->fail_dirty_metadata;
+ if (ctx->fail_dirty_metadata_on_call == 0 && ctx->fail_dirty_metadata)
+ return ctx->fail_dirty_metadata;
+
+ if (ctx->fail_sync_dirty_buffer) {
+ /*
+ * __sync_dirty_buffer() checks test_clear_buffer_dirty(bh)
+ * and returns -EIO without submitting a bio if !buffer_mapped.
+ */
+ set_buffer_dirty(bh);
+ clear_buffer_mapped(bh);
+ } else {
+ clear_buffer_dirty(bh);
+ set_buffer_mapped(bh);
+ }
+ return 0;
+}
+
+static int stub_mark_iloc_dirty(handle_t *handle, struct inode *inode,
+ struct ext4_iloc *iloc)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ struct fc_mock_inode_slot *slot = find_slot(ctx, inode);
+
+ if (slot)
+ sync_inode_to_raw(slot);
+ brelse(iloc->bh);
+ return 0;
+}
+
+static int stub_mark_inode_dirty(handle_t *handle, struct inode *inode,
+ const char *func, unsigned int line)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ struct fc_mock_inode_slot *slot = find_slot(ctx, inode);
+
+ if (slot)
+ sync_inode_to_raw(slot);
+ return 0;
+}
+
+static int stub_ext_dirty(const char *where, unsigned int line,
+ handle_t *handle, struct inode *inode,
+ struct ext4_ext_path *path)
+{
+ return 0;
+}
+
+static void stub_free_blocks(handle_t *handle, struct inode *inode,
+ struct buffer_head *bh, ext4_fsblk_t block,
+ unsigned long count, int flags)
+{
+}
+
+static int stub_orphan_add(handle_t *handle, struct inode *inode)
+{
+ return 0;
+}
+
+static int stub_orphan_del(handle_t *handle, struct inode *inode)
+{
+ return 0;
+}
+
+static int stub_mark_inode_used(struct super_block *sb, int ino)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+
+ ctx->mark_inode_used_calls++;
+ ctx->last_marked_ino = ino;
+ if (ctx->fail_mark_inode_used)
+ return ctx->fail_mark_inode_used;
+ return 0;
+}
+
+static void stub_mb_mark_bb(struct super_block *sb, ext4_fsblk_t block,
+ int len, bool state)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+
+ if (ctx->nr_bb_marks < TEST_MAX_BB_MARKS) {
+ ctx->bb_marks[ctx->nr_bb_marks].block = block;
+ ctx->bb_marks[ctx->nr_bb_marks].len = len;
+ ctx->bb_marks[ctx->nr_bb_marks].state = state;
+ }
+ ctx->nr_bb_marks++;
+}
+
+static struct inode *stub_ext4_iget(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 fc_test_ctx *ctx = test->priv;
+ struct fc_mock_inode_slot *slot;
+ struct inode *inode;
+ struct ext4_inode *raw;
+
+ ctx->iget_calls++;
+ if (ctx->fail_iget_on_call > 0 &&
+ ctx->iget_calls == ctx->fail_iget_on_call)
+ return ERR_PTR(ctx->fail_iget ? ctx->fail_iget : -ENOENT);
+ if (ctx->fail_iget_ino && ino == ctx->fail_iget_ino)
+ return ERR_PTR(ctx->fail_iget ? ctx->fail_iget : -ENOENT);
+ if (ctx->fail_iget && !ctx->fail_iget_on_call && !ctx->fail_iget_ino)
+ return ERR_PTR(ctx->fail_iget);
+
+ slot = find_slot_by_ino(ctx, ino);
+ if (!slot)
+ return ERR_PTR(-ENOENT);
+
+ inode = &slot->ei.vfs_inode;
+ raw = (struct ext4_inode *)slot->raw_buf;
+ if (raw->i_mode) {
+ inode->i_mode = le16_to_cpu(raw->i_mode);
+ set_nlink(inode, le16_to_cpu(raw->i_links_count));
+ inode->i_generation = le32_to_cpu(raw->i_generation);
+ i_size_write(inode, ext4_isize(sb, raw));
+ slot->ei.i_flags = le32_to_cpu(raw->i_flags);
+ if (slot->ei.i_flags & EXT4_INLINE_DATA_FL) {
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ memcpy(slot->ei.i_data, raw->i_block,
+ sizeof(raw->i_block));
+ } else if (slot->ei.i_flags & EXT4_EXTENTS_FL) {
+ struct ext4_extent_header *eh =
+ (struct ext4_extent_header *)raw->i_block;
+ struct ext4_extent_header *ieh =
+ ext_inode_hdr(&slot->ei.vfs_inode);
+
+ if (eh->eh_magic == EXT4_EXT_MAGIC &&
+ ieh->eh_entries == 0)
+ memcpy(slot->ei.i_data, raw->i_block,
+ sizeof(raw->i_block));
+ }
+ }
+
+ atomic_inc(&inode->i_count);
+ return inode;
+}
+
+static int stub_map_blocks(handle_t *handle, struct inode *inode,
+ struct ext4_map_blocks *map, int flags)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ struct fc_mock_inode_slot *slot;
+ ext4_fsblk_t pblk;
+
+ if (ctx->fail_map_blocks)
+ return ctx->fail_map_blocks;
+ if (ctx->force_map_blocks_ret) {
+ map->m_pblk = ctx->force_map_pblk;
+ map->m_flags = ctx->force_map_flags;
+ if (ctx->force_map_blocks_ret > 0)
+ map->m_len = ctx->force_map_blocks_ret;
+ return ctx->force_map_blocks_ret;
+ }
+
+ if (S_ISREG(inode->i_mode) &&
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS)) {
+ kunit_deactivate_static_stub(test, ext4_map_blocks);
+ int ret = ext4_ext_map_blocks(handle, inode, map, flags);
+
+ kunit_activate_static_stub(test, ext4_map_blocks,
+ stub_map_blocks);
+ return ret;
+ }
+
+ slot = find_slot(ctx, inode);
+ if (!slot || map->m_lblk >= TEST_MAX_LBLOCKS)
+ return -EFSCORRUPTED;
+
+ pblk = slot->lblk_to_pblk[map->m_lblk];
+ if (pblk) {
+ map->m_pblk = pblk;
+ map->m_len = 1;
+ map->m_flags = EXT4_MAP_MAPPED;
+ return 1;
+ }
+
+ if (!(flags & EXT4_GET_BLOCKS_CREATE)) {
+ map->m_pblk = 0;
+ map->m_flags = 0;
+ return 0;
+ }
+
+ pblk = alloc_mock_pblk(ctx);
+ if (!pblk)
+ return -ENOSPC;
+
+ slot->lblk_to_pblk[map->m_lblk] = pblk;
+ map->m_pblk = pblk;
+ map->m_len = 1;
+ map->m_flags = EXT4_MAP_MAPPED | EXT4_MAP_NEW;
+ return 1;
+}
+
+static struct buffer_head *stub_ext4_getblk(handle_t *handle,
+ struct inode *inode,
+ ext4_lblk_t block, int map_flags)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ struct fc_mock_inode_slot *slot;
+ struct fc_mock_block *blk;
+ ext4_fsblk_t pblk;
+
+ slot = find_slot(ctx, inode);
+ if (!slot || block >= TEST_MAX_LBLOCKS)
+ return ERR_PTR(-EFSCORRUPTED);
+
+ pblk = slot->lblk_to_pblk[block];
+ if (!pblk) {
+ if (!(map_flags & EXT4_GET_BLOCKS_CREATE))
+ return NULL;
+ pblk = alloc_mock_pblk(ctx);
+ if (!pblk)
+ return ERR_PTR(-ENOSPC);
+ slot->lblk_to_pblk[block] = pblk;
+ }
+
+ blk = get_mock_block(ctx, pblk);
+ if (!blk)
+ return ERR_PTR(-EFSCORRUPTED);
+
+ get_bh(&blk->bh);
+ return &blk->bh;
+}
+
+static struct ext4_ext_path *stub_find_extent(struct inode *inode,
+ ext4_lblk_t block,
+ struct ext4_ext_path *path,
+ int flags)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ struct ext4_ext_path *ret;
+
+ if (ctx->fail_find_extent)
+ return ERR_PTR(ctx->fail_find_extent);
+
+ kunit_deactivate_static_stub(test, ext4_find_extent);
+ ret = ext4_find_extent(inode, block, path, flags);
+ kunit_activate_static_stub(test, ext4_find_extent, stub_find_extent);
+ return ret;
+}
+
+static struct ext4_ext_path *stub_insert_extent(handle_t *handle,
+ struct inode *inode,
+ struct ext4_ext_path *path,
+ struct ext4_extent *newext,
+ int gb_flags)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ struct ext4_ext_path *ret;
+
+ if (ctx->fail_insert_extent) {
+ ext4_free_ext_path(path);
+ return ERR_PTR(ctx->fail_insert_extent);
+ }
+
+ kunit_deactivate_static_stub(test, ext4_ext_insert_extent);
+ ret = ext4_ext_insert_extent(handle, inode, path, newext, gb_flags);
+ kunit_activate_static_stub(test, ext4_ext_insert_extent,
+ stub_insert_extent);
+ return ret;
+}
+
+static int stub_es_lookup_extent(struct inode *inode, ext4_lblk_t lblk,
+ ext4_lblk_t *next_lblk,
+ struct extent_status *es, u64 *pseq)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ int ret;
+
+ if (ctx->use_custom_es_lookup) {
+ *es = ctx->custom_es;
+ return ctx->custom_es_ret;
+ }
+
+ kunit_deactivate_static_stub(test, ext4_es_lookup_extent);
+ ret = ext4_es_lookup_extent(inode, lblk, next_lblk, es, pseq);
+ kunit_activate_static_stub(test, ext4_es_lookup_extent,
+ stub_es_lookup_extent);
+ return ret;
+}
+
+static int stub_fc_get_buf(journal_t *journal, struct buffer_head **bh_out)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ struct buffer_head *bh;
+ int fc_off;
+
+ *bh_out = NULL;
+ ctx->fc_get_buf_calls++;
+ if (ctx->fail_fc_get_buf_on_call > 0 &&
+ ctx->fc_get_buf_calls == ctx->fail_fc_get_buf_on_call)
+ return ctx->fail_fc_get_buf ? ctx->fail_fc_get_buf : -ENOSPC;
+ if (ctx->fail_fc_get_buf && !ctx->fail_fc_get_buf_on_call)
+ return ctx->fail_fc_get_buf;
+
+ if (journal->j_fc_off + journal->j_fc_first >= journal->j_fc_last)
+ return -EINVAL;
+
+ fc_off = journal->j_fc_off;
+ if (fc_off >= TEST_MAX_FC_BUFS)
+ return -ENOSPC;
+
+ journal->j_fc_off++;
+ bh = &ctx->fc_bhs[fc_off];
+ memset(bh->b_data, 0, journal->j_blocksize);
+ set_buffer_uptodate(bh);
+ get_bh(bh);
+ journal->j_fc_wbuf[fc_off] = bh;
+ *bh_out = bh;
+ return 0;
+}
+
+static void stub_fc_submit_bh(struct super_block *sb, bool is_tail)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+ struct buffer_head *bh = EXT4_SB(sb)->s_fc_bh;
+
+ ctx->fc_submit_bh_calls++;
+ if (is_tail)
+ ctx->fc_submit_tail_calls++;
+ set_buffer_dirty(bh);
+ if (ctx->fail_fc_wait_bufs)
+ clear_buffer_uptodate(bh);
+ else
+ set_buffer_uptodate(bh);
+ EXT4_SB(sb)->s_fc_bh = NULL;
+}
+
+static void stub_fc_issue_flush(struct block_device *bdev)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct fc_test_ctx *ctx = test->priv;
+
+ ctx->fc_flush_calls++;
+ ctx->last_flushed_bdev = bdev;
+}
+
+static int mock_submit_inode_data_buffers(struct jbd2_inode *jinode)
+{
+ struct inode *inode = jinode->i_vfs_inode;
+ struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+ struct fc_test_ctx *ctx = container_of(sbi->s_es, struct fc_test_ctx, es);
+
+ return ctx->submit_inode_data_err;
+}
+
+/*
+ * Wrapper for journal->j_fc_cleanup_callback used during ext4_fc_commit() tests.
+ * Calls the real ext4_fc_cleanup() and, on fallback commits (tid != 0), marks
+ * the mock transaction as committed so jbd2_complete_transaction() returns
+ * immediately without waiting for a kjournald2 thread.
+ */
+static void test_j_fc_cleanup_cb(journal_t *journal, int full, tid_t tid)
+{
+ ext4_fc_cleanup_test(journal, full, tid);
+ if (!full && tid != 0) {
+ write_lock(&journal->j_state_lock);
+ journal->j_commit_request = tid;
+ journal->j_commit_sequence = tid;
+ journal->j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+ write_unlock(&journal->j_state_lock);
+ }
+}
+
+/* --- Suite Setup & Teardown --- */
+
+static void fc_cleanup_all(void *priv)
+{
+ struct fc_test_ctx *ctx = priv;
+ int i;
+
+ ctx->sbi->s_mount_state &= ~EXT4_FC_REPLAY;
+ ext4_fc_cleanup_test(&ctx->journal, 1, 999999);
+ ext4_fc_replay_cleanup(ctx->sb);
+
+ for (i = 0; i < TEST_MAX_INODES; i++) {
+ if (!ctx->inodes[i].allocated)
+ continue;
+ down_write(&ctx->inodes[i].ei.i_data_sem);
+ ext4_es_remove_extent(&ctx->inodes[i].ei.vfs_inode, 0,
+ EXT_MAX_BLOCKS);
+ up_write(&ctx->inodes[i].ei.i_data_sem);
+ ext4_fc_free_inode_snap_test(&ctx->inodes[i].ei.vfs_inode);
+ }
+
+ ext4_es_unregister_shrinker(ctx->sbi);
+ deactivate_super(ctx->sb);
+}
+
+static int fast_commit_test_init(struct kunit *test)
+{
+ struct fc_test_ctx *ctx;
+ struct ext4_sb_info *sbi;
+ struct super_block *sb;
+ struct fs_context *fc;
+ int i, err;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+ sbi = kunit_kzalloc(test, sizeof(*sbi), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, sbi);
+
+ fc = fs_context_for_mount(&fc_fs_type, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fc));
+ sb = sget_fc(fc, NULL, fc_set_anon);
+ put_fs_context(fc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(sb));
+
+ ctx->sb = sb;
+ ctx->sbi = sbi;
+ test->priv = ctx;
+
+ /* Superblock and ext4_sb_info setup */
+ 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_bdi = &noop_backing_dev_info;
+ sb->s_fs_info = sbi;
+ sb->s_op = &fc_test_sops;
+#ifdef CONFIG_QUOTA
+ sb->dq_op = &fc_test_qops;
+#endif
+ strscpy(sb->s_id, "ext4-fc-test", sizeof(sb->s_id));
+
+ ctx->bdev.bd_mapping = &ctx->bd_mapping;
+ sb->s_bdev = &ctx->bdev;
+
+ 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(EXT4_GOOD_OLD_FIRST_INO);
+ ctx->es.s_inode_size = cpu_to_le16(TEST_INODE_SIZE);
+ ctx->es.s_inodes_count = cpu_to_le32(1024);
+ ctx->es.s_first_data_block = cpu_to_le32(1);
+ ctx->es.s_feature_compat =
+ cpu_to_le32(EXT4_FEATURE_COMPAT_DIR_INDEX |
+ EXT4_FEATURE_COMPAT_FAST_COMMIT);
+ ctx->es.s_feature_incompat =
+ cpu_to_le32(EXT4_FEATURE_INCOMPAT_FILETYPE |
+ EXT4_FEATURE_INCOMPAT_EXTENTS |
+ EXT4_FEATURE_INCOMPAT_INLINE_DATA);
+ ctx->es.s_feature_ro_compat =
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+
+ 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_sb = sb;
+ sbi->s_es = &ctx->es;
+ sbi->s_sbh = &ctx->sb_bh;
+ sbi->s_first_ino = EXT4_GOOD_OLD_FIRST_INO;
+ sbi->s_inode_size = TEST_INODE_SIZE;
+ sbi->s_want_extra_isize = TEST_EXTRA_ISIZE;
+ sbi->s_desc_size = EXT4_MIN_DESC_SIZE;
+ sbi->s_blocks_per_group = 32768;
+ sbi->s_inodes_per_group = 1024;
+ sbi->s_inodes_per_block = TEST_BLK_SIZE / TEST_INODE_SIZE;
+ sbi->s_csum_seed = TEST_CSUM_SEED;
+ sbi->s_cluster_bits = 0;
+ sbi->s_cluster_ratio = 1;
+ set_opt(sb, ERRORS_CONT);
+ set_opt2(sb, JOURNAL_FAST_COMMIT);
+
+ 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]);
+ atomic_set(&sbi->s_fc_subtid, 0);
+ INIT_LIST_HEAD(&sbi->s_orphan);
+ mutex_init(&sbi->s_orphan_lock);
+ spin_lock_init(&sbi->s_bdev_wb_lock);
+ spin_lock_init(&sbi->s_error_lock);
+ spin_lock_init(&sbi->s_md_lock);
+ 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);
+ up_write(&sb->s_umount);
+
+ err = ext4_es_register_shrinker(sbi);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ ctx->shrinker_count = sbi->s_es_shrinker->count_objects;
+ ctx->shrinker_scan = sbi->s_es_shrinker->scan_objects;
+ WRITE_ONCE(sbi->s_es_shrinker->count_objects, fc_dummy_shrinker_count);
+ WRITE_ONCE(sbi->s_es_shrinker->scan_objects, fc_dummy_shrinker_scan);
+
+ /* Mock journal, running transaction, and handle setup */
+ rwlock_init(&ctx->journal.j_state_lock);
+ spin_lock_init(&ctx->journal.j_list_lock);
+ mutex_init(&ctx->journal.j_barrier);
+ init_waitqueue_head(&ctx->journal.j_wait_transaction_locked);
+ init_waitqueue_head(&ctx->journal.j_wait_done_commit);
+ init_waitqueue_head(&ctx->journal.j_wait_commit);
+ init_waitqueue_head(&ctx->journal.j_wait_updates);
+ init_waitqueue_head(&ctx->journal.j_wait_reserved);
+ init_waitqueue_head(&ctx->journal.j_fc_wait);
+ atomic_set(&ctx->journal.j_reserved_credits, 0);
+ ctx->journal.j_private = sb;
+ ctx->journal.j_blocksize = TEST_BLK_SIZE;
+ ctx->journal.j_dev = &ctx->bdev;
+ ctx->journal.j_fs_dev = &ctx->bdev;
+ ctx->journal.j_task = current;
+ ctx->journal.j_fc_first = 100;
+ ctx->journal.j_fc_last = 100 + TEST_MAX_FC_BUFS;
+ ctx->journal.j_fc_off = 0;
+ ctx->journal.j_fc_wbuf = ctx->fc_wbuf;
+ ctx->journal.j_stats.ts_tid = 1;
+ ctx->journal.j_commit_sequence = 9;
+ ctx->journal.j_commit_request = 9;
+ ctx->journal.j_submit_inode_data_buffers =
+ mock_submit_inode_data_buffers;
+
+ ctx->running_txn.t_tid = 10;
+ ctx->running_txn.t_state = T_RUNNING;
+ atomic_set(&ctx->running_txn.t_updates, 0);
+ ctx->journal.j_running_transaction = &ctx->running_txn;
+ ctx->handle.h_transaction = &ctx->running_txn;
+
+ sbi->s_journal = &ctx->journal;
+ ext4_fc_init(sb, &ctx->journal);
+ ctx->journal.j_fc_cleanup_callback = test_j_fc_cleanup_cb;
+
+ for (i = 0; i < TEST_MAX_FC_BUFS; i++) {
+ struct buffer_head *bh = &ctx->fc_bhs[i];
+
+ bh->b_data = ctx->fc_buf_data[i];
+ bh->b_size = TEST_BLK_SIZE;
+ bh->b_blocknr = 500 + i;
+ bh->b_bdev = &ctx->bdev;
+ atomic_set(&bh->b_count, 1);
+ set_buffer_uptodate(bh);
+ set_buffer_mapped(bh);
+ }
+
+ for (i = 0; i < TEST_MAX_BLOCKS; i++) {
+ struct buffer_head *bh = &ctx->blocks[i].bh;
+
+ bh->b_data = ctx->blocks[i].data;
+ bh->b_size = TEST_BLK_SIZE;
+ bh->b_blocknr = TEST_FIRST_PBLK + i;
+ bh->b_bdev = &ctx->bdev;
+ atomic_set(&bh->b_count, 1);
+ set_buffer_uptodate(bh);
+ set_buffer_mapped(bh);
+ }
+
+ ctx->journal_mode = EXT4_INODE_ORDERED_DATA_MODE;
+
+ kunit_activate_static_stub(test, ext4_inode_journal_mode,
+ stub_inode_journal_mode);
+ kunit_activate_static_stub(test, ext4_da_update_reserve_space,
+ stub_da_update_reserve_space);
+ kunit_activate_static_stub(test, ext4_da_release_space,
+ stub_da_release_space);
+ kunit_activate_static_stub(test, ext4_get_inode_loc,
+ stub_get_inode_loc);
+ kunit_activate_static_stub(test, ext4_get_inode_loc_noio,
+ stub_get_inode_loc_noio);
+ kunit_activate_static_stub(test, ext4_get_fc_inode_loc,
+ stub_get_fc_inode_loc);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ stub_handle_dirty_metadata);
+ kunit_activate_static_stub(test, ext4_mark_iloc_dirty,
+ stub_mark_iloc_dirty);
+ kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+ stub_mark_inode_dirty);
+ kunit_activate_static_stub(test, __ext4_ext_dirty,
+ stub_ext_dirty);
+ kunit_activate_static_stub(test, ext4_free_blocks,
+ stub_free_blocks);
+ kunit_activate_static_stub(test, ext4_orphan_add,
+ stub_orphan_add);
+ kunit_activate_static_stub(test, ext4_orphan_del,
+ stub_orphan_del);
+ kunit_activate_static_stub(test, ext4_mark_inode_used,
+ stub_mark_inode_used);
+ kunit_activate_static_stub(test, ext4_mb_mark_bb,
+ stub_mb_mark_bb);
+ kunit_activate_static_stub(test, __ext4_iget,
+ stub_ext4_iget);
+ kunit_activate_static_stub(test, ext4_map_blocks,
+ stub_map_blocks);
+ kunit_activate_static_stub(test, ext4_getblk,
+ stub_ext4_getblk);
+ kunit_activate_static_stub(test, ext4_find_extent,
+ stub_find_extent);
+ kunit_activate_static_stub(test, ext4_ext_insert_extent,
+ stub_insert_extent);
+ kunit_activate_static_stub(test, ext4_es_lookup_extent,
+ stub_es_lookup_extent);
+ kunit_activate_static_stub(test, ext4_fc_get_buf,
+ stub_fc_get_buf);
+ kunit_activate_static_stub(test, ext4_fc_submit_bh,
+ stub_fc_submit_bh);
+ kunit_activate_static_stub(test, ext4_fc_issue_flush,
+ stub_fc_issue_flush);
+
+ init_mock_inode_slot(ctx, &ctx->inodes[0], TEST_ROOT_INO,
+ S_IFDIR | 0755);
+ init_mock_inode_slot(ctx, &ctx->inodes[1], TEST_DIR_INO,
+ S_IFDIR | 0755);
+ init_mock_inode_slot(ctx, &ctx->inodes[2], TEST_FILE1_INO,
+ S_IFREG | 0644);
+ init_mock_inode_slot(ctx, &ctx->inodes[3], TEST_FILE2_INO,
+ S_IFREG | 0644);
+ init_mock_inode_slot(ctx, &ctx->inodes[4], TEST_SUBDIR_INO,
+ S_IFDIR | 0755);
+
+ err = kunit_add_action_or_reset(test, fc_cleanup_all, ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ return 0;
+}
+
+/* =========================================================================
+ * 1. Initialization, Eligibility, and Snapshot Stat Helpers
+ * =========================================================================
+ */
+
+static void test_fc_init_and_eligibility(struct kunit *test)
+{
+ struct fc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *inode = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ journal_t tmp_journal = {};
+ atomic64_t max_stat = ATOMIC64_INIT(0);
+ int snap_err = EXT4_FC_SNAP_ERR_NONE;
+
+ /*
+ * 1) ext4_fc_init sets j_fc_replay_callback unconditionally, but sets
+ * j_fc_cleanup_callback only when JOURNAL_FAST_COMMIT is enabled.
+ */
+ clear_opt2(sb, JOURNAL_FAST_COMMIT);
+ ext4_fc_init(sb, &tmp_journal);
+ KUNIT_EXPECT_NOT_NULL(test, tmp_journal.j_fc_replay_callback);
+ KUNIT_EXPECT_NULL(test, tmp_journal.j_fc_cleanup_callback);
+
+ set_opt2(sb, JOURNAL_FAST_COMMIT);
+ ext4_fc_init(sb, &tmp_journal);
+ KUNIT_EXPECT_NOT_NULL(test, tmp_journal.j_fc_replay_callback);
+ KUNIT_EXPECT_NOT_NULL(test, tmp_journal.j_fc_cleanup_callback);
+
+ /* 2) ext4_fc_init_inode clears state and resets ranges */
+ ei->i_fc_lblk_start = 42;
+ ei->i_fc_lblk_len = 10;
+ ext4_set_inode_state(inode, EXT4_STATE_FC_COMMITTING);
+ ext4_set_inode_state(inode, EXT4_STATE_FC_REQUEUE);
+ ext4_fc_init_inode(inode);
+ KUNIT_EXPECT_EQ(test, ei->i_fc_lblk_start, 0U);
+ KUNIT_EXPECT_EQ(test, ei->i_fc_lblk_len, 0U);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(inode, EXT4_STATE_FC_COMMITTING));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(inode, EXT4_STATE_FC_REQUEUE));
+ KUNIT_EXPECT_TRUE(test, list_empty(&ei->i_fc_list));
+ KUNIT_EXPECT_TRUE(test, list_empty(&ei->i_fc_dilist));
+ KUNIT_EXPECT_NULL(test, ei->i_fc_snap);
+
+ /* 3) ext4_fc_disabled and ext4_fc_eligible transitions */
+ KUNIT_EXPECT_FALSE(test, ext4_fc_disabled_test(sb));
+ KUNIT_EXPECT_TRUE(test, ext4_fc_eligible_test(sb));
+
+ ctx->sbi->s_mount_state |= EXT4_FC_REPLAY;
+ KUNIT_EXPECT_TRUE(test, ext4_fc_disabled_test(sb));
+ KUNIT_EXPECT_FALSE(test, ext4_fc_eligible_test(sb));
+ ctx->sbi->s_mount_state &= ~EXT4_FC_REPLAY;
+
+ ext4_set_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+ KUNIT_EXPECT_FALSE(test, ext4_fc_disabled_test(sb));
+ KUNIT_EXPECT_FALSE(test, ext4_fc_eligible_test(sb));
+ ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+
+ clear_opt2(sb, JOURNAL_FAST_COMMIT);
+ KUNIT_EXPECT_TRUE(test, ext4_fc_disabled_test(sb));
+ KUNIT_EXPECT_FALSE(test, ext4_fc_eligible_test(sb));
+ set_opt2(sb, JOURNAL_FAST_COMMIT);
+
+ /* 4) ext4_fc_set_snap_err preserves first error */
+ ext4_fc_set_snap_err_test(&snap_err, EXT4_FC_SNAP_ERR_ES_MISS);
+ KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_ES_MISS);
+ ext4_fc_set_snap_err_test(&snap_err, EXT4_FC_SNAP_ERR_NOMEM);
+ KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_ES_MISS);
+
+ /* 5) ext4_fc_snap_stats_update_max monotonically updates max */
+ ext4_fc_snap_stats_update_max_test(&max_stat, 100);
+ KUNIT_EXPECT_EQ(test, atomic64_read(&max_stat), 100LL);
+ ext4_fc_snap_stats_update_max_test(&max_stat, 50);
+ KUNIT_EXPECT_EQ(test, atomic64_read(&max_stat), 100LL);
+ ext4_fc_snap_stats_update_max_test(&max_stat, 250);
+ KUNIT_EXPECT_EQ(test, atomic64_read(&max_stat), 250LL);
+}
+
+/* =========================================================================
+ * 2. Ineligibility Marking (ext4_fc_mark_ineligible)
+ * =========================================================================
+ */
+
+static void test_fc_mark_ineligible(struct kunit *test)
+{
+ struct fc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = ctx->sbi;
+
+ /* 1) No-op when fast commit is disabled */
+ clear_opt2(sb, JOURNAL_FAST_COMMIT);
+ ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_XATTR, &ctx->handle);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+ KUNIT_EXPECT_EQ(test,
+ sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_XATTR],
+ 0U);
+ set_opt2(sb, JOURNAL_FAST_COMMIT);
+
+ /* 2) Valid handle records t_tid and sets EXT4_MF_FC_INELIGIBLE */
+ ctx->running_txn.t_tid = 10;
+ ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_XATTR, &ctx->handle);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 10U);
+ KUNIT_EXPECT_EQ(test,
+ sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_XATTR],
+ 1U);
+
+ /* 3) Higher TID advances s_fc_ineligible_tid; lower TID does not */
+ ctx->running_txn.t_tid = 15;
+ ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_RESIZE, &ctx->handle);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 15U);
+ KUNIT_EXPECT_EQ(test,
+ sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_RESIZE],
+ 1U);
+
+ ctx->running_txn.t_tid = 12;
+ ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_RESIZE, &ctx->handle);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 15U);
+ KUNIT_EXPECT_EQ(test,
+ sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_RESIZE],
+ 2U);
+
+ /* 4) NULL handle uses journal->j_running_transaction when present */
+ ctx->running_txn.t_tid = 20;
+ ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_NOMEM, NULL);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 20U);
+ KUNIT_EXPECT_EQ(test,
+ sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_NOMEM],
+ 1U);
+
+ /* 5) NULL handle with no running transaction sets flag without tid update */
+ ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+ sbi->s_fc_ineligible_tid = 0;
+ ctx->journal.j_running_transaction = NULL;
+ ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_RENAME_DIR, NULL);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 0U);
+ KUNIT_EXPECT_EQ(test,
+ sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_RENAME_DIR],
+ 1U);
+
+ ctx->journal.j_running_transaction = &ctx->running_txn;
+ ctx->running_txn.t_tid = 10;
+ ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+}
+
+/* =========================================================================
+ * 3. Inode and Logical Range Tracking
+ * =========================================================================
+ */
+
+static void test_fc_track_inode_and_range(struct kunit *test)
+{
+ struct fc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = ctx->sbi;
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+ struct ext4_inode_info *ei1 = EXT4_I(file1);
+ struct ext4_inode_info *ei2 = EXT4_I(file2);
+
+ /* 1) Directory inodes are ignored by track_inode and track_range */
+ ext4_fc_track_inode(&ctx->handle, dir);
+ ext4_fc_track_range(&ctx->handle, dir, 0, 5);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(dir)->i_fc_list));
+
+ /* 2) Data journalling inode marks fs ineligible */
+ ctx->journal_mode = EXT4_INODE_JOURNAL_DATA_MODE;
+ ext4_fc_track_inode(&ctx->handle, file1);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+ KUNIT_EXPECT_EQ(test,
+ sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_INODE_JOURNAL_DATA],
+ 1U);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ei1->i_fc_list));
+ ctx->journal_mode = EXT4_INODE_ORDERED_DATA_MODE;
+ ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+ sbi->s_fc_ineligible_tid = 0;
+
+ /* 3) Inline data inode marks fs ineligible on track_range */
+ ext4_set_inode_flag(file1, EXT4_INODE_INLINE_DATA);
+ ext4_set_inode_state(file1, EXT4_STATE_MAY_INLINE_DATA);
+ ei1->i_inline_off = 164;
+ ext4_fc_track_range(&ctx->handle, file1, 0, 1);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+ KUNIT_EXPECT_EQ(test,
+ sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_XATTR],
+ 1U);
+ ext4_clear_inode_flag(file1, EXT4_INODE_INLINE_DATA);
+ ext4_clear_inode_state(file1, EXT4_STATE_MAY_INLINE_DATA);
+ ei1->i_inline_off = 0;
+ ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+ sbi->s_fc_ineligible_tid = 0;
+
+ /* 4) Special inode (< EXT4_FIRST_INO) does not record range */
+ file1->i_ino = EXT4_ROOT_INO;
+ ext4_fc_track_range(&ctx->handle, file1, 5, 10);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 0U);
+ list_del_init(&ei1->i_fc_list);
+ file1->i_ino = TEST_FILE1_INO;
+ ei1->i_sync_tid = 0;
+
+ /*
+ * 5) Range tracking within the same TID merges disjoint and overlapping
+ * ranges into their bounding interval, and subsequent track_inode
+ * in the same TID preserves the range.
+ */
+ ctx->running_txn.t_tid = 10;
+ ext4_fc_track_inode(&ctx->handle, file1);
+ KUNIT_EXPECT_EQ(test, ei1->i_sync_tid, 10U);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 0U);
+ KUNIT_EXPECT_FALSE(test, list_empty(&ei1->i_fc_list));
+
+ /* Initial range [10, 20] -> start=10, len=11 */
+ ext4_fc_track_range(&ctx->handle, file1, 10, 20);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 10U);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 11U);
+
+ /* Expand right [25, 30] -> [10, 30], start=10, len=21 */
+ ext4_fc_track_range(&ctx->handle, file1, 25, 30);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 10U);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 21U);
+
+ /* Expand left [4, 8] -> [4, 30], start=4, len=27 */
+ ext4_fc_track_range(&ctx->handle, file1, 4, 8);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 4U);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 27U);
+
+ /* Contained sub-range [12, 18] -> unchanged [4, 30] */
+ ext4_fc_track_range(&ctx->handle, file1, 12, 18);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 4U);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 27U);
+
+ /* Calling track_inode again in same TID keeps tracked range intact */
+ ext4_fc_track_inode(&ctx->handle, file1);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 4U);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 27U);
+
+ /*
+ * 6) Tracking while EXT4_STATE_FC_COMMITTING is set marks the inode
+ * with EXT4_STATE_FC_REQUEUE, and tracking in a new TID resets the
+ * previous range.
+ */
+ ext4_set_inode_state(file1, EXT4_STATE_FC_COMMITTING);
+ ctx->running_txn.t_tid = 11;
+ ext4_fc_track_range(&ctx->handle, file1, 50, 55);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_state(file1, EXT4_STATE_FC_REQUEUE));
+ KUNIT_EXPECT_EQ(test, ei1->i_sync_tid, 11U);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 50U);
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 6U);
+ ext4_clear_inode_state(file1, EXT4_STATE_FC_COMMITTING);
+ ext4_clear_inode_state(file1, EXT4_STATE_FC_REQUEUE);
+
+ /*
+ * 7) Tracking a new inode while JBD2_FAST_COMMIT_ONGOING is set
+ * enqueues it on FC_Q_STAGING rather than FC_Q_MAIN.
+ */
+ ctx->journal.j_flags |= JBD2_FAST_COMMIT_ONGOING;
+ ext4_fc_track_inode(&ctx->handle, file2);
+ KUNIT_EXPECT_FALSE(test, list_empty(&sbi->s_fc_q[FC_Q_STAGING]));
+ KUNIT_EXPECT_PTR_EQ(test,
+ list_first_entry(&sbi->s_fc_q[FC_Q_STAGING],
+ struct ext4_inode_info, i_fc_list),
+ ei2);
+ ctx->journal.j_flags &= ~JBD2_FAST_COMMIT_ONGOING;
+}
+
+/* =========================================================================
+ * 4. Dentry Update Tracking and Inode Deletion (ext4_fc_del)
+ * =========================================================================
+ */
+
+struct fc_del_wait_work {
+ struct work_struct work;
+ struct inode *inode;
+};
+
+static void fc_del_unblock_worker(struct work_struct *work)
+{
+ struct fc_del_wait_work *w =
+ container_of(work, struct fc_del_wait_work, work);
+ struct inode *inode = w->inode;
+ int alloc_ctx;
+
+ /*
+ * Simulate the commit thread finishing data flush and transitioning
+ * FLUSHING_DATA -> COMMITTING -> done while ext4_fc_del() is waiting.
+ */
+ alloc_ctx = ext4_fc_lock(inode->i_sb);
+ ext4_clear_inode_state(inode, EXT4_STATE_FC_FLUSHING_DATA);
+ ext4_set_inode_state(inode, EXT4_STATE_FC_COMMITTING);
+ ext4_fc_wake_inode_state_test(inode, EXT4_STATE_FC_FLUSHING_DATA);
+ ext4_fc_unlock(inode->i_sb, alloc_ctx);
+
+ alloc_ctx = ext4_fc_lock(inode->i_sb);
+ ext4_clear_inode_state(inode, EXT4_STATE_FC_COMMITTING);
+ ext4_fc_wake_inode_state_test(inode, EXT4_STATE_FC_COMMITTING);
+ ext4_fc_unlock(inode->i_sb, alloc_ctx);
+}
+
+static void test_fc_track_dentry_and_del(struct kunit *test)
+{
+ struct fc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = ctx->sbi;
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+ struct ext4_inode_info *ei1 = EXT4_I(file1);
+ struct dentry d_parent, d_f1, d_f1_link, d_f2;
+ struct ext4_fc_dentry_update *fcd;
+ struct fc_del_wait_work wait_work;
+
+ init_mock_dentry(&d_parent, NULL, "dir", dir, sb);
+ init_mock_dentry(&d_f1, &d_parent, "file1", file1, sb);
+ init_mock_dentry(&d_f1_link, &d_parent, "file1_ln", file1, sb);
+ init_mock_dentry(&d_f2, &d_parent, "file2", file2, sb);
+
+ /* 1) Encrypted parent directory marks fs ineligible */
+ dir->i_flags |= S_ENCRYPTED;
+ ext4_fc_track_create(&ctx->handle, &d_f1);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+ KUNIT_EXPECT_EQ(test,
+ sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_ENCRYPTED_FILENAME],
+ 1U);
+ KUNIT_EXPECT_TRUE(test, list_empty(&sbi->s_fc_dentry_q[FC_Q_MAIN]));
+ dir->i_flags &= ~S_ENCRYPTED;
+ ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+ sbi->s_fc_ineligible_tid = 0;
+
+ /* 2) Track create, link, and unlink onto MAIN and STAGING queues */
+ ext4_fc_track_create(&ctx->handle, &d_f1);
+ KUNIT_EXPECT_FALSE(test, list_empty(&sbi->s_fc_dentry_q[FC_Q_MAIN]));
+ KUNIT_EXPECT_FALSE(test, list_empty(&ei1->i_fc_dilist));
+
+ fcd = list_first_entry(&sbi->s_fc_dentry_q[FC_Q_MAIN],
+ struct ext4_fc_dentry_update, fcd_list);
+ KUNIT_EXPECT_EQ(test, fcd->fcd_op, EXT4_FC_TAG_CREAT);
+ KUNIT_EXPECT_EQ(test, fcd->fcd_parent, (int)TEST_DIR_INO);
+ KUNIT_EXPECT_EQ(test, fcd->fcd_ino, (int)TEST_FILE1_INO);
+ KUNIT_EXPECT_STREQ(test, (const char *)fcd->fcd_name.name.name, "file1");
+
+ ext4_fc_track_link(&ctx->handle, file1, &d_f1_link);
+ ext4_fc_track_unlink(&ctx->handle, &d_f1_link);
+
+ ctx->journal.j_flags |= JBD2_FULL_COMMIT_ONGOING;
+ ext4_fc_track_create(&ctx->handle, &d_f2);
+ KUNIT_EXPECT_FALSE(test, list_empty(&sbi->s_fc_dentry_q[FC_Q_STAGING]));
+ ctx->journal.j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+
+ /*
+ * 3) ext4_fc_del removes file2's CREAT entry from s_fc_dentry_q and
+ * i_fc_dilist even when file2 is not on s_fc_q.
+ */
+ ext4_fc_del(file2);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(file2)->i_fc_dilist));
+ KUNIT_EXPECT_TRUE(test, list_empty(&sbi->s_fc_dentry_q[FC_Q_STAGING]));
+
+ /*
+ * 4) ext4_fc_del waits for EXT4_STATE_FC_FLUSHING_DATA and
+ * EXT4_STATE_FC_COMMITTING to clear before deleting file1.
+ */
+ ext4_fc_track_inode(&ctx->handle, file1);
+ ext4_set_inode_state(file1, EXT4_STATE_FC_FLUSHING_DATA);
+ wait_work.inode = file1;
+ INIT_WORK_ONSTACK(&wait_work.work, fc_del_unblock_worker);
+ schedule_work(&wait_work.work);
+
+ ext4_fc_del(file1);
+ flush_work(&wait_work.work);
+ destroy_work_on_stack(&wait_work.work);
+
+ KUNIT_EXPECT_TRUE(test, list_empty(&ei1->i_fc_list));
+ KUNIT_EXPECT_TRUE(test, list_empty(&ei1->i_fc_dilist));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(file1, EXT4_STATE_FC_FLUSHING_DATA));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(file1, EXT4_STATE_FC_COMMITTING));
+}
+
+/* =========================================================================
+ * 5. Space Reservation, Padding, and TLV Writing
+ * =========================================================================
+ */
+
+static void test_fc_reserve_space_and_tlv_writing(struct kunit *test)
+{
+ struct fc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = ctx->sbi;
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct ext4_fc_tl tl;
+ struct ext4_fc_tail tail;
+ u8 payload[600];
+ u8 *dst;
+ u32 crc = 0;
+
+ memset(payload, 0xAB, sizeof(payload));
+
+ /* 1) Request exceeding bsize - EXT4_FC_TAG_BASE_LEN fails immediately */
+ dst = ext4_fc_reserve_space_test(sb,
+ TEST_BLK_SIZE - EXT4_FC_TAG_BASE_LEN + 1,
+ &crc);
+ KUNIT_EXPECT_NULL(test, dst);
+ KUNIT_EXPECT_EQ(test, ctx->fc_get_buf_calls, 0);
+
+ /* 2) ext4_fc_get_buf failure on initial block returns NULL */
+ ctx->fail_fc_get_buf = -ENOSPC;
+ dst = ext4_fc_reserve_space_test(sb, 32, &crc);
+ KUNIT_EXPECT_NULL(test, dst);
+ ctx->fail_fc_get_buf = 0;
+
+ /*
+ * 3) Add a 600-byte TLV into block 0 (takes 604 bytes), then add
+ * another 600-byte TLV. The second TLV does not fit in the
+ * remaining 420 bytes of block 0, so ext4_fc_reserve_space writes
+ * an EXT4_FC_TAG_PAD TLV of length 416 (420 - 4), submits block 0,
+ * and allocates block 1.
+ */
+ KUNIT_EXPECT_TRUE(test,
+ ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD,
+ sizeof(payload), payload, &crc));
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes,
+ (unsigned int)(EXT4_FC_TAG_BASE_LEN + sizeof(payload)));
+ KUNIT_EXPECT_EQ(test, ctx->fc_submit_bh_calls, 0U);
+
+ KUNIT_EXPECT_TRUE(test,
+ ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD,
+ sizeof(payload), payload, &crc));
+ KUNIT_EXPECT_EQ(test, ctx->fc_submit_bh_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->fc_submit_tail_calls, 0U);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes,
+ (unsigned int)(TEST_BLK_SIZE + EXT4_FC_TAG_BASE_LEN +
+ sizeof(payload)));
+
+ /* Verify PAD TLV was written at offset 604 of block 0 */
+ memcpy(&tl, ctx->fc_buf_data[0] + EXT4_FC_TAG_BASE_LEN + sizeof(payload),
+ EXT4_FC_TAG_BASE_LEN);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(tl.fc_tag), (u16)EXT4_FC_TAG_PAD);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(tl.fc_len),
+ (u16)(TEST_BLK_SIZE - 2 * EXT4_FC_TAG_BASE_LEN -
+ sizeof(payload)));
+
+ /* 4) Write tail tag into block 1 and verify rounding + tail submission */
+ KUNIT_EXPECT_EQ(test, ext4_fc_write_tail_test(sb, crc), 0);
+ KUNIT_EXPECT_EQ(test, ctx->fc_submit_bh_calls, 2U);
+ KUNIT_EXPECT_EQ(test, ctx->fc_submit_tail_calls, 1U);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes, (unsigned int)(2 * TEST_BLK_SIZE));
+
+ memcpy(&tl, ctx->fc_buf_data[1] + EXT4_FC_TAG_BASE_LEN + sizeof(payload),
+ EXT4_FC_TAG_BASE_LEN);
+ memcpy(&tail,
+ ctx->fc_buf_data[1] + 2 * EXT4_FC_TAG_BASE_LEN + sizeof(payload),
+ sizeof(tail));
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(tl.fc_tag), (u16)EXT4_FC_TAG_TAIL);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(tail.fc_tid), 10U);
+
+ /* 5) Missing inode snapshot increments snap_fail_no_snap */
+ KUNIT_EXPECT_EQ(test, ext4_fc_write_inode_test(file1, &crc),
+ -ECANCELED);
+ KUNIT_EXPECT_EQ(test, ext4_fc_write_inode_data_test(file1, &crc),
+ -ECANCELED);
+ KUNIT_EXPECT_EQ(test,
+ atomic64_read(&sbi->s_fc_snap_stats.snap_fail_no_snap),
+ 2LL);
+}
+
+/* =========================================================================
+ * 6. Inode and Extent Status Range Snapshotting
+ * =========================================================================
+ */
+
+static void test_fc_snapshot_inode_and_ranges(struct kunit *test)
+{
+ struct fc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = ctx->sbi;
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct ext4_inode_info *ei1 = EXT4_I(file1);
+ struct dentry d_parent, d_f2;
+ struct inode **inodes = NULL;
+ unsigned int nr_inodes = 0, nr_ranges = 0;
+ int snap_err = EXT4_FC_SNAP_ERR_NONE;
+ u32 crc = 0;
+
+ /* 1) ext4_get_inode_loc_noio failure records EXT4_FC_SNAP_ERR_INODE_LOC */
+ ctx->fail_get_inode_loc_noio = -EAGAIN;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_snapshot_inode_test(file1, 0, &nr_ranges,
+ &snap_err),
+ -EAGAIN);
+ KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_INODE_LOC);
+ KUNIT_EXPECT_EQ(test,
+ atomic64_read(&sbi->s_fc_snap_stats.snap_fail_inode_loc),
+ 1LL);
+ ctx->fail_get_inode_loc_noio = 0;
+
+ /* 2) ES miss on tracked range records EXT4_FC_SNAP_ERR_ES_MISS */
+ snap_err = EXT4_FC_SNAP_ERR_NONE;
+ ext4_fc_track_range(&ctx->handle, file1, 0, 3);
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_snapshot_inode_test(file1, 0, &nr_ranges,
+ &snap_err),
+ -EAGAIN);
+ KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_ES_MISS);
+ KUNIT_EXPECT_EQ(test,
+ atomic64_read(&sbi->s_fc_snap_stats.snap_fail_es_miss),
+ 1LL);
+
+ /* 3) Delayed extent in ES tree records EXT4_FC_SNAP_ERR_ES_DELAYED */
+ snap_err = EXT4_FC_SNAP_ERR_NONE;
+ down_write(&ei1->i_data_sem);
+ ext4_es_insert_delayed_extent(file1, 0, 4, false, false);
+ up_write(&ei1->i_data_sem);
+ ext4_fc_track_range(&ctx->handle, file1, 0, 3);
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_snapshot_inode_test(file1, 0, &nr_ranges,
+ &snap_err),
+ -EAGAIN);
+ KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_ES_DELAYED);
+ KUNIT_EXPECT_EQ(test,
+ atomic64_read(&sbi->s_fc_snap_stats.snap_fail_es_delayed),
+ 1LL);
+ down_write(&ei1->i_data_sem);
+ ext4_es_remove_extent(file1, 0, EXT_MAX_BLOCKS);
+ up_write(&ei1->i_data_sem);
+
+ /* 4) Unsupported ES status records EXT4_FC_SNAP_ERR_ES_OTHER */
+ snap_err = EXT4_FC_SNAP_ERR_NONE;
+ ctx->use_custom_es_lookup = true;
+ ctx->custom_es_ret = 1;
+ ctx->custom_es.es_lblk = 0;
+ ctx->custom_es.es_len = 4;
+ ctx->custom_es.es_pblk = 0;
+ ext4_es_set_referenced(&ctx->custom_es);
+ ext4_fc_track_range(&ctx->handle, file1, 0, 3);
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_snapshot_inode_test(file1, 0, &nr_ranges,
+ &snap_err),
+ -EAGAIN);
+ KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_ES_OTHER);
+ KUNIT_EXPECT_EQ(test,
+ atomic64_read(&sbi->s_fc_snap_stats.snap_fail_es_other),
+ 1LL);
+ ctx->use_custom_es_lookup = false;
+
+ /* 5) Exceeding EXT4_FC_SNAPSHOT_MAX_RANGES returns -E2BIG */
+ snap_err = EXT4_FC_SNAP_ERR_NONE;
+ down_write(&ei1->i_data_sem);
+ ext4_es_insert_extent(file1, 0, 4, 200, EXTENT_STATUS_WRITTEN, false);
+ up_write(&ei1->i_data_sem);
+ ext4_fc_track_range(&ctx->handle, file1, 0, 3);
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_snapshot_inode_test(file1, 2048, &nr_ranges,
+ &snap_err),
+ -E2BIG);
+ KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_RANGES_CAP);
+ KUNIT_EXPECT_EQ(test,
+ atomic64_read(&sbi->s_fc_snap_stats.snap_fail_ranges_cap),
+ 1LL);
+
+ /*
+ * 6) Snapshot mixed written, hole, unwritten, and large (>32768)
+ * extents, verifying automatic splitting at EXT_INIT_MAX_LEN (32768)
+ * and EXT_UNWRITTEN_MAX_LEN (32767).
+ */
+ down_write(&ei1->i_data_sem);
+ ext4_es_remove_extent(file1, 0, EXT_MAX_BLOCKS);
+ /* [0, 3]: written at pblk 200 */
+ ext4_es_insert_extent(file1, 0, 4, 200, EXTENT_STATUS_WRITTEN, false);
+ /* [4, 5]: hole */
+ ext4_es_insert_extent(file1, 4, 2, 0, EXTENT_STATUS_HOLE, false);
+ /* [6, 40005]: 40000 unwritten blocks at pblk 1000 (splits into 32767 + 7233) */
+ ext4_es_insert_extent(file1, 6, 40000, 1000, EXTENT_STATUS_UNWRITTEN,
+ false);
+ up_write(&ei1->i_data_sem);
+
+ snap_err = EXT4_FC_SNAP_ERR_NONE;
+ ext4_fc_track_range(&ctx->handle, file1, 1, 40005);
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_snapshot_inode_test(file1, 0, &nr_ranges,
+ &snap_err),
+ 0);
+ /* 1 written [1..3] + 1 hole [4..5] + 2 split unwritten [6..32772, 32773..40005] = 4 */
+ KUNIT_EXPECT_EQ(test, nr_ranges, 4U);
+ KUNIT_EXPECT_NOT_NULL(test, ei1->i_fc_snap);
+
+ /* Serialize the snapshotted inode and ranges into fast commit buffer */
+ sbi->s_fc_bytes = 0;
+ ctx->journal.j_fc_off = 0;
+ KUNIT_EXPECT_EQ(test, ext4_fc_write_inode_data_test(file1, &crc), 0);
+ KUNIT_EXPECT_EQ(test, ext4_fc_write_inode_test(file1, &crc), 0);
+
+ /*
+ * 7) Count and snapshot both s_fc_q[MAIN] inodes and create-only
+ * inodes from s_fc_dentry_q[MAIN].
+ */
+ init_mock_dentry(&d_parent, NULL, "dir", dir, sb);
+ init_mock_dentry(&d_f2, &d_parent, "file2", file2, sb);
+ ext4_fc_track_create(&ctx->handle, &d_f2);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(file2)->i_fc_list));
+ KUNIT_EXPECT_EQ(test, ext4_fc_count_snapshot_inodes_test(sb), 2U);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_alloc_snapshot_inodes_test(sb, &inodes,
+ &nr_inodes),
+ 0);
+ KUNIT_EXPECT_EQ(test, nr_inodes, 2U);
+ KUNIT_ASSERT_NOT_NULL(test, inodes);
+
+ /* Capacity smaller than queued inode count returns -E2BIG */
+ snap_err = EXT4_FC_SNAP_ERR_NONE;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_snapshot_inodes_test(&ctx->journal, inodes, 1,
+ &nr_inodes, &nr_ranges,
+ &snap_err),
+ -E2BIG);
+ KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_INODES_CAP);
+
+ snap_err = EXT4_FC_SNAP_ERR_NONE;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_snapshot_inodes_test(&ctx->journal, inodes, 2,
+ &nr_inodes, &nr_ranges,
+ &snap_err),
+ 0);
+ KUNIT_EXPECT_EQ(test, nr_inodes, 2U);
+ /* Create-only inode (file2) is marked EXT4_STATE_FC_COMMITTING */
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_state(file2, EXT4_STATE_FC_COMMITTING));
+ KUNIT_EXPECT_NOT_NULL(test, EXT4_I(file2)->i_fc_snap);
+ kvfree(inodes);
+}
+
+/* =========================================================================
+ * 7. Fast Commit Cleanup, Stats, and Procfs Output (ext4_fc_info_show)
+ * =========================================================================
+ */
+
+static void test_fc_cleanup_and_stats(struct kunit *test)
+{
+ struct fc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = ctx->sbi;
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+ struct ext4_inode_info *ei1 = EXT4_I(file1);
+ struct ext4_inode_info *ei2 = EXT4_I(file2);
+ struct seq_file seq = {};
+ char *buf;
+
+ /*
+ * 1) Fast commit cleanup (full = 0):
+ * - file1 (no FC_REQUEUE) is removed from FC_Q_MAIN and reset.
+ * - file2 (FC_REQUEUE set) is requeued and preserves its range.
+ */
+ ctx->running_txn.t_tid = 10;
+ ext4_fc_track_range(&ctx->handle, file1, 2, 5);
+ ext4_fc_track_range(&ctx->handle, file2, 8, 12);
+ ext4_set_inode_state(file1, EXT4_STATE_FC_COMMITTING);
+ ext4_set_inode_state(file2, EXT4_STATE_FC_COMMITTING);
+ ext4_set_inode_state(file2, EXT4_STATE_FC_REQUEUE);
+ sbi->s_fc_bytes = 512;
+
+ ext4_fc_cleanup_test(&ctx->journal, 0, 10);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ei1->i_fc_list));
+ KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 0U);
+ KUNIT_EXPECT_FALSE(test, list_empty(&ei2->i_fc_list));
+ KUNIT_EXPECT_EQ(test, ei2->i_fc_lblk_start, 8U);
+ KUNIT_EXPECT_EQ(test, ei2->i_fc_lblk_len, 5U);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(file2, EXT4_STATE_FC_REQUEUE));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(file2, EXT4_STATE_FC_COMMITTING));
+ /* Fast commit cleanup does not reset s_fc_bytes */
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes, 512U);
+
+ /*
+ * 2) Full commit cleanup (full = 1):
+ * - Inodes with i_sync_tid > tid are requeued; inodes with
+ * i_sync_tid <= tid are removed and reset.
+ * - s_fc_ineligible_tid is cleared only when tid >= s_fc_ineligible_tid.
+ * - s_fc_bytes is reset to 0.
+ */
+ ctx->running_txn.t_tid = 15;
+ ext4_fc_track_range(&ctx->handle, file1, 1, 3);
+ ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_XATTR, &ctx->handle);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 15U);
+
+ /* Full commit of TID 10: file2 (tid 10) removed, file1 (tid 15) kept */
+ ext4_fc_cleanup_test(&ctx->journal, 1, 10);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ei2->i_fc_list));
+ KUNIT_EXPECT_FALSE(test, list_empty(&ei1->i_fc_list));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes, 0U);
+
+ /* Full commit of TID 15: file1 removed and ineligibility cleared */
+ ext4_fc_cleanup_test(&ctx->journal, 1, 15);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ei1->i_fc_list));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 0U);
+
+ /*
+ * 3) ext4_fc_update_stats and ext4_fc_info_show formatting
+ */
+ memset(&sbi->s_fc_stats, 0, sizeof(sbi->s_fc_stats));
+ ext4_fc_update_stats_test(sb, EXT4_FC_STATUS_OK, 40000, 2, 10);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_num_commits, 1UL);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_numblks, 2UL);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.s_fc_avg_commit_time, 40000ULL);
+
+ /* EWMA update: (80000 + 40000 * 3) / 4 = 50000 */
+ ext4_fc_update_stats_test(sb, EXT4_FC_STATUS_OK, 80000, 3, 10);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_num_commits, 2UL);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_numblks, 5UL);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.s_fc_avg_commit_time, 50000ULL);
+
+ ext4_fc_update_stats_test(sb, EXT4_FC_STATUS_INELIGIBLE, 0, 0, 10);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_ineligible_commits, 1UL);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 0UL);
+
+ ext4_fc_update_stats_test(sb, EXT4_FC_STATUS_FAILED, 0, 0, 10);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_ineligible_commits, 2UL);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 1UL);
+
+ ext4_fc_update_stats_test(sb, EXT4_FC_STATUS_SKIPPED, 0, 0, 10);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_skipped_commits, 1UL);
+
+ buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+ seq.buf = buf;
+ seq.size = PAGE_SIZE;
+ seq.private = sb;
+
+ /* Non-start token produces no output */
+ KUNIT_EXPECT_EQ(test, ext4_fc_info_show(&seq, NULL), 0);
+ KUNIT_EXPECT_EQ(test, seq.count, 0UL);
+
+ /* SEQ_START_TOKEN outputs full stats table */
+ KUNIT_EXPECT_EQ(test, ext4_fc_info_show(&seq, SEQ_START_TOKEN), 0);
+ KUNIT_EXPECT_GT(test, seq.count, 0UL);
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "fc stats:\n2 commits\n2 ineligible\n5 numblks\n50us avg_commit_time"));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "Ineligible reasons:"));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "Snapshot stats:"));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "Snapshot failures:"));
+}
+
+/* =========================================================================
+ * 8. Top-Level Commit Paths and Fallbacks (ext4_fc_commit)
+ * =========================================================================
+ */
+
+static void test_fc_commit_paths(struct kunit *test)
+{
+ struct fc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = ctx->sbi;
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct ext4_inode_info *ei1 = EXT4_I(file1);
+ struct ext4_fc_tl tl;
+
+ /* 1) Already-committed TID is skipped (-EALREADY) */
+ ctx->journal.j_commit_sequence = 10;
+ KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_skipped_commits, 1UL);
+
+ /* 1b) JOURNAL_FAST_COMMIT disabled delegates to jbd2_complete_transaction */
+ clear_opt2(sb, JOURNAL_FAST_COMMIT);
+ KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_skipped_commits, 1UL);
+ set_opt2(sb, JOURNAL_FAST_COMMIT);
+
+ /* 1c) jbd2_fc_begin_commit failure (ts_tid == 0 -> -EINVAL) records FAILED */
+ ctx->journal.j_commit_sequence = 9;
+ ctx->journal.j_commit_request = 10;
+ ctx->journal.j_stats.ts_tid = 0;
+ ctx->journal.j_running_transaction = NULL;
+ KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 1UL);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_ineligible_commits, 1UL);
+ ctx->journal.j_stats.ts_tid = 1;
+ ctx->journal.j_running_transaction = &ctx->running_txn;
+ sbi->s_fc_stats.fc_failed_commits = 0;
+ sbi->s_fc_stats.fc_ineligible_commits = 0;
+
+ /* 2) Ineligible filesystem falls back to full commit */
+ ctx->journal.j_commit_sequence = 9;
+ ctx->journal.j_commit_request = 9;
+ ctx->running_txn.t_tid = 10;
+ ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_XATTR, &ctx->handle);
+ KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_ineligible_commits, 1UL);
+ ctx->journal.j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+ ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+ sbi->s_fc_ineligible_tid = 0;
+
+ /* 3) Data flush failure (-EIO) clears FLUSHING_DATA and falls back */
+ ctx->journal.j_commit_sequence = 9;
+ ctx->journal.j_commit_request = 9;
+ ctx->running_txn.t_tid = 10;
+ ext4_fc_track_inode(&ctx->handle, file1);
+ WRITE_ONCE(ei1->jinode->i_flags, JI_WRITE_DATA);
+ ctx->submit_inode_data_err = -EIO;
+ KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(file1, EXT4_STATE_FC_FLUSHING_DATA));
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 1UL);
+ ctx->journal.j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+ WRITE_ONCE(ei1->jinode->i_flags, 0);
+ ctx->submit_inode_data_err = 0;
+
+ /* 4) Snapshot ES miss (-EAGAIN) records INELIGIBLE status and falls back */
+ ctx->journal.j_commit_sequence = 9;
+ ctx->journal.j_commit_request = 9;
+ ctx->running_txn.t_tid = 10;
+ ext4_fc_track_range(&ctx->handle, file1, 0, 1);
+ KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_ineligible_commits, 3UL);
+ /* Failed commits count does not increment on -EAGAIN */
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 1UL);
+ ctx->journal.j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+
+ /* 5) jbd2_fc_wait_bufs I/O failure (-EIO) records FAILED status */
+ ctx->journal.j_commit_sequence = 9;
+ ctx->journal.j_commit_request = 9;
+ ctx->running_txn.t_tid = 10;
+ ctx->journal.j_fc_off = 0;
+ sbi->s_fc_bytes = 0;
+ ext4_fc_track_inode(&ctx->handle, file1);
+ ctx->fail_fc_wait_bufs = true;
+ KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 2UL);
+ ctx->journal.j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+ ctx->fail_fc_wait_bufs = false;
+
+ /*
+ * 6) Two consecutive successful fast commits in the same TID, with an
+ * external journal device (j_fs_dev != j_dev) triggering cache flush.
+ */
+ ctx->journal.j_commit_sequence = 9;
+ ctx->journal.j_commit_request = 9;
+ ctx->running_txn.t_tid = 10;
+ ctx->journal.j_fc_off = 0;
+ sbi->s_fc_bytes = 0;
+ atomic_set(&sbi->s_fc_subtid, 0);
+ ctx->journal.j_fs_dev = &ctx->ext_jdev;
+ ctx->fc_flush_calls = 0;
+
+ down_write(&ei1->i_data_sem);
+ ext4_es_insert_extent(file1, 0, 2, 200, EXTENT_STATUS_WRITTEN, false);
+ up_write(&ei1->i_data_sem);
+ ext4_fc_track_range(&ctx->handle, file1, 0, 1);
+
+ KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+ KUNIT_EXPECT_EQ(test, atomic_read(&sbi->s_fc_subtid), 1);
+ KUNIT_EXPECT_EQ(test, ctx->fc_flush_calls, 1U);
+ KUNIT_EXPECT_PTR_EQ(test, ctx->last_flushed_bdev, &ctx->ext_jdev);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes, (unsigned int)TEST_BLK_SIZE);
+
+ /* Block 0 starts with EXT4_FC_TAG_HEAD */
+ memcpy(&tl, ctx->fc_buf_data[0], EXT4_FC_TAG_BASE_LEN);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(tl.fc_tag), (u16)EXT4_FC_TAG_HEAD);
+
+ /* Second fast commit in the same TID (block 1 starts with INODE, not HEAD) */
+ ext4_fc_track_inode(&ctx->handle, file1);
+ KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+ KUNIT_EXPECT_EQ(test, atomic_read(&sbi->s_fc_subtid), 2);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes, (unsigned int)(2 * TEST_BLK_SIZE));
+ memcpy(&tl, ctx->fc_buf_data[1], EXT4_FC_TAG_BASE_LEN);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(tl.fc_tag), (u16)EXT4_FC_TAG_INODE);
+
+ ctx->journal.j_fs_dev = &ctx->bdev;
+}
+
+/* =========================================================================
+ * 9. Replay Regions, Modified Inode Tracking, and Scan Validation
+ * =========================================================================
+ */
+
+static void test_fc_replay_regions_and_scan(struct kunit *test)
+{
+ struct fc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = ctx->sbi;
+ struct ext4_fc_replay_state *state = &sbi->s_fc_replay_state;
+ struct ext4_fc_tl tl;
+ struct ext4_fc_head head;
+ int i;
+
+ /*
+ * 1) ext4_fc_record_regions and ext4_fc_replay_check_excluded across
+ * dynamic krealloc (> EXT4_FC_REPLAY_REALLOC_INCREMENT = 4)
+ */
+ for (i = 0; i < 6; i++) {
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_record_regions(sb, 12 + i, i * 10,
+ 100 + i * 10, 5, 1),
+ 0);
+ }
+ KUNIT_EXPECT_EQ(test, state->fc_regions_used, 6);
+ KUNIT_EXPECT_EQ(test, state->fc_regions_valid, 6);
+ KUNIT_EXPECT_GE(test, state->fc_regions_size, 6);
+
+ KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 100));
+ KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 104));
+ KUNIT_EXPECT_FALSE(test, ext4_fc_replay_check_excluded(sb, 105));
+ KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 152));
+
+ /* Unvalidated scan region (replay = 0) is not excluded until validated */
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_record_regions(sb, 99, 0, 500, 10, 0),
+ 0);
+ KUNIT_EXPECT_EQ(test, state->fc_regions_used, 7);
+ KUNIT_EXPECT_EQ(test, state->fc_regions_valid, 6);
+ KUNIT_EXPECT_FALSE(test, ext4_fc_replay_check_excluded(sb, 502));
+
+ /* Next replay=1 addition resyncs fc_regions_used to fc_regions_valid */
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_record_regions(sb, 100, 0, 600, 4, 1),
+ 0);
+ KUNIT_EXPECT_EQ(test, state->fc_regions_used, 7);
+ KUNIT_EXPECT_EQ(test, state->fc_regions_valid, 7);
+ KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 601));
+
+ /* 2) ext4_fc_record_modified_inode deduplication and krealloc growth */
+ for (i = 0; i < 6; i++) {
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_record_modified_inode_test(sb, 20 + i),
+ 0);
+ /* Duplicate ino is ignored */
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_record_modified_inode_test(sb, 20 + i),
+ 0);
+ }
+ KUNIT_EXPECT_EQ(test, state->fc_modified_inodes_used, 6);
+
+ ext4_fc_replay_cleanup(sb);
+ memset(state, 0, sizeof(*state));
+
+ /* 3) ext4_fc_value_len_isvalid_test and tag2str */
+ KUNIT_EXPECT_TRUE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_ADD_RANGE,
+ sizeof(struct ext4_fc_add_range)));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_ADD_RANGE, 1));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_DEL_RANGE,
+ sizeof(struct ext4_fc_del_range)));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_CREAT,
+ sizeof(struct ext4_fc_dentry_info) + 1));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_CREAT,
+ sizeof(struct ext4_fc_dentry_info)));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_LINK,
+ sizeof(struct ext4_fc_dentry_info) +
+ EXT4_NAME_LEN + 1));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_INODE,
+ sizeof(struct ext4_fc_inode) +
+ EXT4_GOOD_OLD_INODE_SIZE));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_INODE,
+ sizeof(struct ext4_fc_inode) +
+ TEST_INODE_SIZE + 1));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_PAD, 0));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_TAIL,
+ sizeof(struct ext4_fc_tail)));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_TAIL,
+ sizeof(struct ext4_fc_tail) - 1));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_HEAD,
+ sizeof(struct ext4_fc_head)));
+ KUNIT_EXPECT_FALSE(test,
+ ext4_fc_value_len_isvalid_test(sbi, 0x9999, 8));
+ KUNIT_EXPECT_STREQ(test, tag2str(EXT4_FC_TAG_HEAD), "HEAD");
+ KUNIT_EXPECT_STREQ(test, tag2str(0x9999), "ERROR");
+
+ /* 4) Scan error cases */
+ /* Block 0 not starting with HEAD returns 0 (early stop) */
+ memset(ctx->fc_buf_data[0], 0, TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_SCAN, 0, 10),
+ 0);
+
+ /* Unexpected block offset returns -EFSCORRUPTED */
+ state->fc_replay_expected_off = 2;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_SCAN, 1, 10),
+ -EFSCORRUPTED);
+
+ /* HEAD with unsupported features returns -EOPNOTSUPP */
+ memset(state, 0, sizeof(*state));
+ memset(ctx->fc_buf_data[0], 0, TEST_BLK_SIZE);
+ tl.fc_tag = cpu_to_le16(EXT4_FC_TAG_HEAD);
+ tl.fc_len = cpu_to_le16(sizeof(head));
+ head.fc_features = cpu_to_le32(0x1);
+ head.fc_tid = cpu_to_le32(10);
+ memcpy(ctx->fc_buf_data[0], &tl, EXT4_FC_TAG_BASE_LEN);
+ memcpy(ctx->fc_buf_data[0] + EXT4_FC_TAG_BASE_LEN, &head, sizeof(head));
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_SCAN, 0, 10),
+ -EOPNOTSUPP);
+
+ /* HEAD with wrong TID returns JBD2_FC_REPLAY_STOP */
+ memset(state, 0, sizeof(*state));
+ head.fc_features = 0;
+ head.fc_tid = cpu_to_le32(99);
+ memcpy(ctx->fc_buf_data[0] + EXT4_FC_TAG_BASE_LEN, &head, sizeof(head));
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_SCAN, 0, 10),
+ JBD2_FC_REPLAY_STOP);
+
+ /* Corrupted TAIL CRC on first commit returns -EFSBADCRC */
+ memset(state, 0, sizeof(*state));
+ sbi->s_fc_bytes = 0;
+ ctx->journal.j_fc_off = 0;
+ ctx->running_txn.t_tid = 10;
+ head.fc_tid = cpu_to_le32(10);
+ {
+ u32 crc = 0;
+
+ ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD, sizeof(head),
+ (u8 *)&head, &crc);
+ ext4_fc_write_tail_test(sb, crc);
+ }
+ /* Corrupt last byte of tail.fc_crc */
+ ctx->fc_buf_data[0][2 * EXT4_FC_TAG_BASE_LEN + sizeof(head) +
+ offsetof(struct ext4_fc_tail, fc_crc)] ^= 0xFF;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_SCAN, 0, 10),
+ -EFSBADCRC);
+
+ /*
+ * 5) Valid first commit in block 0 followed by a corrupted second
+ * commit in block 1 stops cleanly with JBD2_FC_REPLAY_STOP and
+ * keeps fc_replay_num_tags from block 0.
+ */
+ memset(state, 0, sizeof(*state));
+ sbi->s_fc_bytes = 0;
+ ctx->journal.j_fc_off = 0;
+ {
+ u32 crc = 0;
+
+ ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD, sizeof(head),
+ (u8 *)&head, &crc);
+ ext4_fc_write_tail_test(sb, crc);
+ }
+ /* Block 1 has an invalid TLV (tag=0) */
+ memset(ctx->fc_buf_data[1], 0, TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_SCAN, 0, 10),
+ JBD2_FC_REPLAY_CONTINUE);
+ KUNIT_EXPECT_EQ(test, state->fc_replay_num_tags, 2);
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[1],
+ PASS_SCAN, 1, 10),
+ JBD2_FC_REPLAY_STOP);
+ KUNIT_EXPECT_EQ(test, state->fc_replay_num_tags, 2);
+ ext4_fc_replay_cleanup(sb);
+ memset(state, 0, sizeof(*state));
+}
+
+/* =========================================================================
+ * 10. Replay of Inode, Create, Link, and Unlink Tags
+ * =========================================================================
+ */
+
+static void test_fc_replay_inode_and_dentries(struct kunit *test)
+{
+ struct fc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = ctx->sbi;
+ struct inode *root = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct inode *subdir = get_mock_inode(ctx, TEST_SUBDIR_INO);
+ struct fc_mock_inode_slot *f1_slot = find_slot(ctx, file1);
+ struct dentry d_dir, d_f1, d_f1_link, d_subdir;
+ struct ext4_dir_entry_2 *de = NULL;
+ struct buffer_head *bh;
+ struct qstr q_f1 = QSTR_INIT("file1", 5);
+ struct qstr q_f1_ln = QSTR_INIT("file1_ln", 8);
+ struct qstr q_subdir = QSTR_INIT("subdir", 6);
+ struct ext4_fc_head head = {
+ .fc_features = 0,
+ .fc_tid = cpu_to_le32(10),
+ };
+ u32 crc = 0;
+ int snap_err = EXT4_FC_SNAP_ERR_NONE;
+
+ /* Initialize parent directory on-disk block */
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root, dir), 0);
+
+ init_mock_dentry(&d_dir, NULL, "dir", dir, sb);
+ init_mock_dentry(&d_f1, &d_dir, "file1", file1, sb);
+ init_mock_dentry(&d_f1_link, &d_dir, "file1_ln", file1, sb);
+ init_mock_dentry(&d_subdir, &d_dir, "subdir", subdir, sb);
+
+ /* Prepare file1 and subdir snapshots */
+ file1->i_generation = 0x4242;
+ i_size_write(file1, 2048);
+ sync_inode_to_raw(f1_slot);
+ sync_inode_to_raw(find_slot(ctx, subdir));
+ KUNIT_ASSERT_EQ(test,
+ ext4_fc_snapshot_inode_test(file1, 0, NULL, &snap_err),
+ 0);
+ KUNIT_ASSERT_EQ(test,
+ ext4_fc_snapshot_inode_test(subdir, 0, NULL, &snap_err),
+ 0);
+
+ /*
+ * Build a fast commit block containing:
+ * HEAD -> INODE(file1) -> CREAT(file1) -> LINK(file1_ln) ->
+ * INODE(subdir) -> CREAT(subdir) -> UNLINK(file1) -> TAIL
+ */
+ sbi->s_fc_bytes = 0;
+ ctx->journal.j_fc_off = 0;
+ ctx->running_txn.t_tid = 10;
+
+ ext4_fc_track_create(&ctx->handle, &d_f1);
+ ext4_fc_track_link(&ctx->handle, file1, &d_f1_link);
+ ext4_fc_track_create(&ctx->handle, &d_subdir);
+ ext4_fc_track_unlink(&ctx->handle, &d_f1);
+
+ KUNIT_ASSERT_TRUE(test,
+ ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD,
+ sizeof(head), (u8 *)&head, &crc));
+ /* Commit all queued dentry updates into block 0 */
+ {
+ struct ext4_fc_dentry_update *fcd;
+
+ list_for_each_entry(fcd, &sbi->s_fc_dentry_q[FC_Q_MAIN], fcd_list) {
+ struct ext4_fc_dentry_info fcd_info = {
+ .fc_parent_ino = cpu_to_le32(fcd->fcd_parent),
+ .fc_ino = cpu_to_le32(fcd->fcd_ino),
+ };
+ u8 d_buf[64];
+ int dlen = fcd->fcd_name.name.len;
+
+ if (fcd->fcd_op == EXT4_FC_TAG_CREAT) {
+ struct inode *c_ino =
+ get_mock_inode(ctx, fcd->fcd_ino);
+ KUNIT_ASSERT_EQ(test,
+ ext4_fc_write_inode_test(c_ino, &crc),
+ 0);
+ }
+ memcpy(d_buf, &fcd_info, sizeof(fcd_info));
+ memcpy(d_buf + sizeof(fcd_info),
+ fcd->fcd_name.name.name, dlen);
+ KUNIT_ASSERT_TRUE(test,
+ ext4_fc_add_tlv_test(sb, fcd->fcd_op,
+ sizeof(fcd_info) + dlen,
+ d_buf, &crc));
+ }
+ }
+ KUNIT_ASSERT_EQ(test, ext4_fc_write_tail_test(sb, crc), 0);
+
+ /* Clear file1 generation in raw_buf to verify replay restores it */
+ ((struct ext4_inode *)f1_slot->raw_buf)->i_generation = 0;
+ file1->i_generation = 0;
+
+ /* Scan and replay block 0 */
+ memset(&sbi->s_fc_replay_state, 0, sizeof(sbi->s_fc_replay_state));
+ KUNIT_ASSERT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_SCAN, 0, 10),
+ JBD2_FC_REPLAY_CONTINUE);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_replay_state.fc_replay_num_tags, 8);
+
+ /* Error injection: ext4_get_fc_inode_loc failure aborts INODE replay */
+ ctx->fail_get_fc_inode_loc = -EIO;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 0, 10),
+ -EIO);
+ ctx->fail_get_fc_inode_loc = 0;
+
+ /* Error injection: sync_dirty_buffer failure aborts INODE replay */
+ sbi->s_fc_replay_state.fc_replay_num_tags = 8;
+ ctx->fail_sync_dirty_buffer = true;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 0, 10),
+ -EIO);
+ ctx->fail_sync_dirty_buffer = false;
+
+ /* Successful full replay of block 0 */
+ sbi->s_fc_replay_state.fc_replay_num_tags = 8;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 0, 10),
+ JBD2_FC_REPLAY_CONTINUE);
+ KUNIT_EXPECT_EQ(test, sbi->s_fc_replay_state.fc_replay_num_tags, 0);
+
+ /* Calling PASS_REPLAY once fc_replay_num_tags == 0 runs bitmaps & stops */
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 1, 10),
+ 0);
+
+ /* Verify replayed state: file1 generation restored, file1 unlinked, file1_ln & subdir present */
+ KUNIT_EXPECT_EQ(test, file1->i_generation, 0x4242U);
+
+ bh = ext4_find_entry_test(dir, &q_f1, &de, NULL);
+ KUNIT_EXPECT_NULL(test, bh);
+
+ bh = ext4_find_entry_test(dir, &q_f1_ln, &de, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_FILE1_INO);
+ brelse(bh);
+
+ bh = ext4_find_entry_test(dir, &q_subdir, &de, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_SUBDIR_INO);
+ brelse(bh);
+
+ /*
+ * Exercise additional replay branches:
+ * - LINK on an already-existing entry ("file1_ln") ignores -EEXIST
+ * - UNLINK on an already-removed entry ("file1") returns 0
+ * - UNLINK when dentry points to a different inode (simulating
+ * rename-overwrite) leaves the dentry intact
+ * - INODE replay of an inline-data inode copies raw_fc_inode->i_block
+ */
+ {
+ struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+ struct fc_mock_inode_slot *f2_slot = find_slot(ctx, file2);
+ struct dentry d_f1_overwrite;
+ struct ext4_inode *raw_f2 = (struct ext4_inode *)f2_slot->raw_buf;
+ struct ext4_fc_dentry_info fcd_info;
+ u8 d_buf[64];
+
+ /* Prepare inline-data file2 snapshot */
+ ext4_clear_inode_flag(file2, EXT4_INODE_EXTENTS);
+ ext4_set_inode_flag(file2, EXT4_INODE_INLINE_DATA);
+ sync_inode_to_raw(f2_slot);
+ memcpy(raw_f2->i_block, "inline-test-payload", 20);
+ KUNIT_ASSERT_EQ(test,
+ ext4_fc_snapshot_inode_test(file2, 0, NULL,
+ &snap_err),
+ 0);
+ memset(raw_f2->i_block, 0, sizeof(raw_f2->i_block));
+
+ /* Build block: HEAD -> LINK(file1_ln) -> UNLINK(file1) -> INODE(file2) -> TAIL */
+ sbi->s_fc_bytes = 0;
+ ctx->journal.j_fc_off = 0;
+ crc = 0;
+ KUNIT_ASSERT_TRUE(test,
+ ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD,
+ sizeof(head),
+ (u8 *)&head, &crc));
+
+ /* LINK("file1_ln") when "file1_ln" already exists */
+ fcd_info.fc_parent_ino = cpu_to_le32(TEST_DIR_INO);
+ fcd_info.fc_ino = cpu_to_le32(TEST_FILE1_INO);
+ memcpy(d_buf, &fcd_info, sizeof(fcd_info));
+ memcpy(d_buf + sizeof(fcd_info), "file1_ln", 8);
+ KUNIT_ASSERT_TRUE(test,
+ ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_LINK,
+ sizeof(fcd_info) + 8,
+ d_buf, &crc));
+
+ /* UNLINK("file1", ino=TEST_FILE1_INO) */
+ memcpy(d_buf, &fcd_info, sizeof(fcd_info));
+ memcpy(d_buf + sizeof(fcd_info), "file1", 5);
+ KUNIT_ASSERT_TRUE(test,
+ ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_UNLINK,
+ sizeof(fcd_info) + 5,
+ d_buf, &crc));
+
+ /* INODE(file2) with inline data */
+ KUNIT_ASSERT_EQ(test, ext4_fc_write_inode_test(file2, &crc), 0);
+ KUNIT_ASSERT_EQ(test, ext4_fc_write_tail_test(sb, crc), 0);
+
+ /* 1) Replay when "file1" does not exist (UNLINK no-op) */
+ sbi->s_fc_replay_state.fc_replay_num_tags = 5;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 0, 10),
+ JBD2_FC_REPLAY_CONTINUE);
+ KUNIT_EXPECT_EQ(test,
+ memcmp(raw_f2->i_block, "inline-test-payload", 20),
+ 0);
+
+ /* 2) Add "file1" pointing to file2, then replay UNLINK(file1, ino=file1) */
+ init_mock_dentry(&d_f1_overwrite, &d_dir, "file1", file2, sb);
+ KUNIT_ASSERT_EQ(test,
+ __ext4_link(dir, file2, &d_f1_overwrite.d_name,
+ &d_f1_overwrite),
+ 0);
+
+ sbi->s_fc_replay_state.fc_replay_num_tags = 5;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 0, 10),
+ JBD2_FC_REPLAY_CONTINUE);
+
+ /* Verify "file1" still points to file2 after UNLINK(file1, ino=file1) */
+ bh = ext4_find_entry_test(dir, &q_f1, &de, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode),
+ (u32)TEST_FILE2_INO);
+ brelse(bh);
+ }
+
+ ext4_fc_replay_cleanup(sb);
+}
+
+/* =========================================================================
+ * 11. Replay of Extent Ranges and End-to-End Commit-to-Replay Round-Trip
+ * =========================================================================
+ */
+
+static void test_fc_replay_ranges_and_end_to_end(struct kunit *test)
+{
+ struct fc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = ctx->sbi;
+ struct inode *root = get_mock_inode(ctx, TEST_ROOT_INO);
+ struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+ struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+ struct ext4_inode_info *ei1 = EXT4_I(file1);
+ struct fc_mock_inode_slot *f1_slot = find_slot(ctx, file1);
+ struct dentry d_dir, d_f1;
+ struct ext4_map_blocks map;
+ struct ext4_dir_entry_2 *de = NULL;
+ struct buffer_head *bh;
+ struct qstr q_f1 = QSTR_INIT("e2e_file", 8);
+
+ KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root, dir), 0);
+ init_mock_dentry(&d_dir, NULL, "dir", dir, sb);
+ init_mock_dentry(&d_f1, &d_dir, "e2e_file", file1, sb);
+
+ /*
+ * Set up file1 modifications for a real ext4_fc_commit():
+ * - [0, 3]: written extent at pblk 300
+ * - [4, 5]: hole (DEL_RANGE)
+ * - [6, 9]: unwritten extent at pblk 310
+ */
+ ctx->journal.j_commit_sequence = 9;
+ ctx->running_txn.t_tid = 10;
+ ctx->journal.j_fc_off = 0;
+ sbi->s_fc_bytes = 0;
+
+ i_size_write(file1, 10 * TEST_BLK_SIZE);
+ file1->i_generation = 0x7777;
+ sync_inode_to_raw(f1_slot);
+
+ down_write(&ei1->i_data_sem);
+ ext4_es_insert_extent(file1, 0, 4, 300, EXTENT_STATUS_WRITTEN, false);
+ ext4_es_insert_extent(file1, 4, 2, 0, EXTENT_STATUS_HOLE, false);
+ ext4_es_insert_extent(file1, 6, 4, 310, EXTENT_STATUS_UNWRITTEN, false);
+ up_write(&ei1->i_data_sem);
+
+ ext4_fc_track_create(&ctx->handle, &d_f1);
+ ext4_fc_track_range(&ctx->handle, file1, 0, 9);
+
+ /* Execute real ext4_fc_commit() */
+ KUNIT_ASSERT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+ KUNIT_EXPECT_EQ(test, ctx->journal.j_fc_off, 1U);
+
+ /*
+ * Simulate pre-crash on-disk state before replay:
+ * - [0, 3] was mapped to old pblk 250 (to exercise pblk remap &
+ * old block bitmap freeing in ext4_fc_replay_add_range)
+ * - [4, 5] was mapped to pblk 260 (to exercise DEL_RANGE removal)
+ * - [6, 9] was mapped to pblk 310 as written (to exercise state toggle)
+ */
+ ext4_ext_tree_init(NULL, file1);
+ {
+ struct ext4_ext_path *path;
+ struct ext4_extent ex;
+
+ memset(&ex, 0, sizeof(ex));
+ ex.ee_block = cpu_to_le32(0);
+ ex.ee_len = cpu_to_le16(4);
+ ext4_ext_store_pblock(&ex, 250);
+ path = ext4_find_extent(file1, 0, NULL, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+ down_write(&ei1->i_data_sem);
+ path = ext4_ext_insert_extent(NULL, file1, path, &ex, 0);
+ up_write(&ei1->i_data_sem);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+ ext4_free_ext_path(path);
+
+ memset(&ex, 0, sizeof(ex));
+ ex.ee_block = cpu_to_le32(4);
+ ex.ee_len = cpu_to_le16(2);
+ ext4_ext_store_pblock(&ex, 260);
+ path = ext4_find_extent(file1, 4, NULL, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+ down_write(&ei1->i_data_sem);
+ path = ext4_ext_insert_extent(NULL, file1, path, &ex, 0);
+ up_write(&ei1->i_data_sem);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+ ext4_free_ext_path(path);
+
+ memset(&ex, 0, sizeof(ex));
+ ex.ee_block = cpu_to_le32(6);
+ ex.ee_len = cpu_to_le16(4);
+ ext4_ext_store_pblock(&ex, 310);
+ path = ext4_find_extent(file1, 6, NULL, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+ down_write(&ei1->i_data_sem);
+ path = ext4_ext_insert_extent(NULL, file1, path, &ex, 0);
+ up_write(&ei1->i_data_sem);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+ ext4_free_ext_path(path);
+ }
+
+ /* Run PASS_SCAN followed by PASS_REPLAY on the committed block */
+ ctx->nr_bb_marks = 0;
+ memset(&sbi->s_fc_replay_state, 0, sizeof(sbi->s_fc_replay_state));
+ KUNIT_ASSERT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_SCAN, 0, 10),
+ JBD2_FC_REPLAY_CONTINUE);
+ KUNIT_EXPECT_GT(test, sbi->s_fc_replay_state.fc_replay_num_tags, 0);
+ /* Both ADD_RANGE extents ([0,3]->300 and [6,9]->310) are excluded */
+ KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 300));
+ KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 312));
+
+ KUNIT_ASSERT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 0, 10),
+ JBD2_FC_REPLAY_CONTINUE);
+ KUNIT_ASSERT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 1, 10),
+ 0);
+
+ /* 1) Verify directory entry "e2e_file" was created in dir */
+ bh = ext4_find_entry_test(dir, &q_f1, &de, NULL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_FILE1_INO);
+ brelse(bh);
+
+ /* 2) Verify [0, 3] is mapped to new pblk 300 (written) */
+ map.m_lblk = 0;
+ map.m_len = 4;
+ KUNIT_EXPECT_EQ(test, ext4_ext_map_blocks(NULL, file1, &map, 0), 4);
+ KUNIT_EXPECT_EQ(test, map.m_pblk, 300ULL);
+ KUNIT_EXPECT_TRUE(test, (map.m_flags & EXT4_MAP_MAPPED) != 0);
+
+ /* 3) Verify [4, 5] hole was punched by DEL_RANGE */
+ map.m_lblk = 4;
+ map.m_len = 2;
+ KUNIT_EXPECT_EQ(test, ext4_ext_map_blocks(NULL, file1, &map, 0), 0);
+
+ /* 4) Verify [6, 9] is mapped to pblk 310 as unwritten */
+ map.m_lblk = 6;
+ map.m_len = 4;
+ KUNIT_EXPECT_EQ(test, ext4_ext_map_blocks(NULL, file1, &map, 0), 4);
+ KUNIT_EXPECT_EQ(test, map.m_pblk, 310ULL);
+ KUNIT_EXPECT_TRUE(test, (map.m_flags & EXT4_MAP_UNWRITTEN) != 0);
+
+ /* 5) Verify ext4_mb_mark_bb was invoked to free old blocks & mark new ones */
+ KUNIT_EXPECT_GT(test, ctx->nr_bb_marks, 0U);
+
+ /*
+ * 6) Error handling in ADD_RANGE and DEL_RANGE replay:
+ * - DEL_RANGE when ext4_map_blocks fails propagates error
+ * - ADD_RANGE when ext4_map_blocks, ext4_find_extent, or
+ * ext4_ext_insert_extent fails propagates error
+ * - ADD_RANGE / DEL_RANGE when ext4_iget fails returns 0
+ */
+ {
+ struct ext4_fc_del_range del_arg;
+ struct ext4_fc_add_range add_arg;
+ struct ext4_extent *ex = (struct ext4_extent *)add_arg.fc_ex;
+ u32 crc = 0;
+
+ /* DEL_RANGE: ext4_iget failure returns 0; ext4_map_blocks failure returns -EIO */
+ sbi->s_fc_bh = NULL;
+ sbi->s_fc_bytes = 0;
+ ctx->journal.j_fc_off = 0;
+ del_arg.fc_ino = cpu_to_le32(TEST_FILE1_INO);
+ del_arg.fc_lblk = cpu_to_le32(0);
+ del_arg.fc_len = cpu_to_le32(2);
+ KUNIT_ASSERT_TRUE(test,
+ ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_DEL_RANGE,
+ sizeof(del_arg),
+ (u8 *)&del_arg, &crc));
+
+ sbi->s_fc_replay_state.fc_replay_num_tags = 1;
+ ctx->fail_iget = -ENOENT;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 0, 10),
+ 0);
+ ctx->fail_iget = 0;
+
+ sbi->s_fc_replay_state.fc_replay_num_tags = 1;
+ ctx->fail_map_blocks = -EIO;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 0, 10),
+ -EIO);
+ ctx->fail_map_blocks = 0;
+
+ /* ADD_RANGE on unmapped range [20, 21]: test map_blocks, find_extent, insert_extent failures */
+ sbi->s_fc_bh = NULL;
+ sbi->s_fc_bytes = 0;
+ ctx->journal.j_fc_off = 0;
+ memset(&add_arg, 0, sizeof(add_arg));
+ add_arg.fc_ino = cpu_to_le32(TEST_FILE1_INO);
+ ex->ee_block = cpu_to_le32(20);
+ ex->ee_len = cpu_to_le16(2);
+ ext4_ext_store_pblock(ex, 400);
+ KUNIT_ASSERT_TRUE(test,
+ ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_ADD_RANGE,
+ sizeof(add_arg),
+ (u8 *)&add_arg, &crc));
+
+ sbi->s_fc_replay_state.fc_replay_num_tags = 1;
+ ctx->fail_map_blocks = -EIO;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 0, 10),
+ -EIO);
+ ctx->fail_map_blocks = 0;
+
+ sbi->s_fc_replay_state.fc_replay_num_tags = 1;
+ ctx->fail_find_extent = -EIO;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 0, 10),
+ -EIO);
+ ctx->fail_find_extent = 0;
+
+ sbi->s_fc_replay_state.fc_replay_num_tags = 1;
+ ctx->fail_insert_extent = -ENOSPC;
+ KUNIT_EXPECT_EQ(test,
+ ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+ PASS_REPLAY, 0, 10),
+ -ENOSPC);
+ ctx->fail_insert_extent = 0;
+ }
+
+ ext4_fc_replay_cleanup(sb);
+ KUNIT_EXPECT_FALSE(test, (sbi->s_mount_state & EXT4_FC_REPLAY) != 0);
+}
+
+static struct kunit_case fast_commit_test_cases[] = {
+ KUNIT_CASE(test_fc_init_and_eligibility),
+ KUNIT_CASE(test_fc_mark_ineligible),
+ KUNIT_CASE(test_fc_track_inode_and_range),
+ KUNIT_CASE(test_fc_track_dentry_and_del),
+ KUNIT_CASE(test_fc_reserve_space_and_tlv_writing),
+ KUNIT_CASE(test_fc_snapshot_inode_and_ranges),
+ KUNIT_CASE(test_fc_cleanup_and_stats),
+ KUNIT_CASE(test_fc_commit_paths),
+ KUNIT_CASE(test_fc_replay_regions_and_scan),
+ KUNIT_CASE(test_fc_replay_inode_and_dentries),
+ KUNIT_CASE(test_fc_replay_ranges_and_end_to_end),
+ {}
+};
+
+static struct kunit_suite fast_commit_test_suite = {
+ .name = "ext4_fast_commit_test",
+ .init = fast_commit_test_init,
+ .test_cases = fast_commit_test_cases,
+};
+
+kunit_test_suites(&fast_commit_test_suite);
+
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c
index 15bd040c2329..57d78cb3b55d 100644
--- a/fs/ext4/fast_commit.c
+++ b/fs/ext4/fast_commit.c
@@ -14,6 +14,7 @@
#include <linux/lockdep.h>
#include <linux/wait_bit.h>
+#include <kunit/static_stub.h>
/*
* Ext4 Fast Commits
* -----------------
@@ -661,11 +662,25 @@ void ext4_fc_track_range(handle_t *handle, struct inode *inode, ext4_lblk_t star
trace_ext4_fc_track_range(handle, inode, start, end, ret);
}
-static void ext4_fc_submit_bh(struct super_block *sb, bool is_tail)
+int ext4_fc_get_buf(journal_t *journal, struct buffer_head **bh)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_fc_get_buf, journal, bh);
+ return jbd2_fc_get_buf(journal, bh);
+}
+
+void ext4_fc_issue_flush(struct block_device *bdev)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_fc_issue_flush, bdev);
+ blkdev_issue_flush(bdev);
+}
+
+void ext4_fc_submit_bh(struct super_block *sb, bool is_tail)
{
blk_opf_t write_flags = JBD2_JOURNAL_REQ_FLAGS;
struct buffer_head *bh = EXT4_SB(sb)->s_fc_bh;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_fc_submit_bh, sb, is_tail);
+
/* Add REQ_FUA | REQ_PREFLUSH only its tail */
if (test_opt(sb, BARRIER) && is_tail)
write_flags |= REQ_FUA | REQ_PREFLUSH;
@@ -707,7 +722,7 @@ static u8 *ext4_fc_reserve_space(struct super_block *sb, int len, u32 *crc)
return NULL;
if (!sbi->s_fc_bh) {
- ret = jbd2_fc_get_buf(EXT4_SB(sb)->s_journal, &bh);
+ ret = ext4_fc_get_buf(EXT4_SB(sb)->s_journal, &bh);
if (ret)
return NULL;
sbi->s_fc_bh = bh;
@@ -737,7 +752,7 @@ static u8 *ext4_fc_reserve_space(struct super_block *sb, int len, u32 *crc)
ext4_fc_submit_bh(sb, false);
- ret = jbd2_fc_get_buf(EXT4_SB(sb)->s_journal, &bh);
+ ret = ext4_fc_get_buf(EXT4_SB(sb)->s_journal, &bh);
if (ret)
return NULL;
sbi->s_fc_bh = bh;
@@ -1426,7 +1441,7 @@ static int ext4_fc_perform_commit(journal_t *journal, tid_t commit_tid)
* issue a cache flush before we start writing fast commit blocks.
*/
if (journal->j_fs_dev != journal->j_dev)
- blkdev_issue_flush(journal->j_fs_dev);
+ ext4_fc_issue_flush(journal->j_fs_dev);
blk_start_plug(&plug);
alloc_ctx = ext4_fc_lock(sb);
@@ -2016,7 +2031,7 @@ static int ext4_fc_replay_inode(struct super_block *sb,
out:
iput(inode);
if (!ret)
- blkdev_issue_flush(sb->s_bdev);
+ ext4_fc_issue_flush(sb->s_bdev);
return ret;
}
@@ -2765,3 +2780,158 @@ void ext4_fc_destroy_dentry_cache(void)
kmem_cache_destroy(ext4_fc_range_cachep);
kmem_cache_destroy(ext4_fc_dentry_cachep);
}
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+void ext4_fc_set_snap_err_test(int *snap_err, int err)
+{
+ ext4_fc_set_snap_err(snap_err, err);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_set_snap_err_test);
+
+bool ext4_fc_disabled_test(struct super_block *sb)
+{
+ return ext4_fc_disabled(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_disabled_test);
+
+bool ext4_fc_eligible_test(struct super_block *sb)
+{
+ return ext4_fc_eligible(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_eligible_test);
+
+void ext4_fc_snap_stats_update_max_test(atomic64_t *stat, u64 value)
+{
+ ext4_fc_snap_stats_update_max(stat, value);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_snap_stats_update_max_test);
+
+void ext4_fc_wake_inode_state_test(struct inode *inode, int bit)
+{
+ ext4_fc_wake_inode_state(inode, bit);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_wake_inode_state_test);
+
+u8 *ext4_fc_reserve_space_test(struct super_block *sb, int len, u32 *crc)
+{
+ return ext4_fc_reserve_space(sb, len, crc);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_reserve_space_test);
+
+int ext4_fc_write_tail_test(struct super_block *sb, u32 crc)
+{
+ return ext4_fc_write_tail(sb, crc);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_write_tail_test);
+
+bool ext4_fc_add_tlv_test(struct super_block *sb, u16 tag, u16 len, u8 *val,
+ u32 *crc)
+{
+ return ext4_fc_add_tlv(sb, tag, len, val, crc);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_add_tlv_test);
+
+int ext4_fc_write_inode_test(struct inode *inode, u32 *crc)
+{
+ return ext4_fc_write_inode(inode, crc);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_write_inode_test);
+
+int ext4_fc_write_inode_data_test(struct inode *inode, u32 *crc)
+{
+ return ext4_fc_write_inode_data(inode, crc);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_write_inode_data_test);
+
+void ext4_fc_free_inode_snap_test(struct inode *inode)
+{
+ ext4_fc_free_inode_snap(inode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_free_inode_snap_test);
+
+int ext4_fc_snapshot_inode_test(struct inode *inode,
+ unsigned int nr_ranges_total,
+ unsigned int *nr_rangesp, int *snap_err)
+{
+ return ext4_fc_snapshot_inode(inode, nr_ranges_total, nr_rangesp,
+ snap_err);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_snapshot_inode_test);
+
+unsigned int ext4_fc_count_snapshot_inodes_test(struct super_block *sb)
+{
+ return ext4_fc_count_snapshot_inodes(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_count_snapshot_inodes_test);
+
+int ext4_fc_alloc_snapshot_inodes_test(struct super_block *sb,
+ struct inode ***inodesp,
+ unsigned int *nr_inodesp)
+{
+ return ext4_fc_alloc_snapshot_inodes(sb, inodesp, nr_inodesp);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_alloc_snapshot_inodes_test);
+
+int ext4_fc_snapshot_inodes_test(journal_t *journal, struct inode **inodes,
+ unsigned int inodes_size,
+ unsigned int *nr_inodesp,
+ unsigned int *nr_rangesp, int *snap_err)
+{
+ return ext4_fc_snapshot_inodes(journal, inodes, inodes_size,
+ nr_inodesp, nr_rangesp, snap_err);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_snapshot_inodes_test);
+
+void ext4_fc_update_stats_test(struct super_block *sb, int status,
+ u64 commit_time, int nblks, tid_t commit_tid)
+{
+ ext4_fc_update_stats(sb, status, commit_time, nblks, commit_tid);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_update_stats_test);
+
+void ext4_fc_cleanup_test(journal_t *journal, int full, tid_t tid)
+{
+ ext4_fc_cleanup(journal, full, tid);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_cleanup_test);
+
+int ext4_fc_record_modified_inode_test(struct super_block *sb, int ino)
+{
+ return ext4_fc_record_modified_inode(sb, ino);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_record_modified_inode_test);
+
+bool ext4_fc_value_len_isvalid_test(struct ext4_sb_info *sbi, int tag, int len)
+{
+ return ext4_fc_value_len_isvalid(sbi, tag, len);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_value_len_isvalid_test);
+
+int ext4_fc_replay_test(journal_t *journal, struct buffer_head *bh,
+ enum passtype pass, int off, tid_t expected_tid)
+{
+ return ext4_fc_replay(journal, bh, pass, off, expected_tid);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_replay_test);
+
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_get_buf);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_submit_bh);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_issue_flush);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_init_inode);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_del);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_mark_ineligible);
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_fc_track_unlink);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_track_unlink);
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_fc_track_link);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_track_link);
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_fc_track_create);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_track_create);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_track_inode);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_track_range);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_commit);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_record_regions);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_replay_check_excluded);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_replay_cleanup);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_init);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_info_show);
+#endif
diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c
index 02f3ef05acaf..25b387f3ce85 100644
--- a/fs/ext4/ialloc.c
+++ b/fs/ext4/ialloc.c
@@ -768,6 +768,8 @@ int ext4_mark_inode_used(struct super_block *sb, int ino)
int bit;
int err;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_mark_inode_used, sb, ino);
+
if (ino < EXT4_FIRST_INO(sb) || ino > max_ino)
return -EFSCORRUPTED;
@@ -1638,4 +1640,5 @@ int ext4_init_inode_table(struct super_block *sb, ext4_group_t group,
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_new_inode);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mark_inode_used);
#endif
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 105c731b95b1..67d37b39db93 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -5088,6 +5088,8 @@ int ext4_get_inode_loc_noio(struct inode *inode, struct ext4_iloc *iloc)
int inodes_per_block, inode_offset;
unsigned long ino = inode->i_ino;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_get_inode_loc_noio, inode, iloc);
+
iloc->bh = NULL;
if (ino < EXT4_ROOT_INO ||
ino > le32_to_cpu(EXT4_SB(sb)->s_es->s_inodes_count))
@@ -5129,6 +5131,8 @@ int ext4_get_inode_loc_noio(struct inode *inode, struct ext4_iloc *iloc)
int ext4_get_fc_inode_loc(struct super_block *sb, unsigned long ino,
struct ext4_iloc *iloc)
{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_get_fc_inode_loc, sb, ino, iloc);
+
return __ext4_get_inode_loc(sb, ino, NULL, iloc, NULL);
}
@@ -7002,6 +7006,8 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
}
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_inode_loc_noio);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_fc_inode_loc);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_get_block_prep);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_update_reserve_space);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_release_space);
diff --git a/fs/ext4/mballoc.c b/fs/ext4/mballoc.c
index 46ba07e017a3..3f06b0996063 100644
--- a/fs/ext4/mballoc.c
+++ b/fs/ext4/mballoc.c
@@ -4269,6 +4269,8 @@ void ext4_mb_mark_bb(struct super_block *sb, ext4_fsblk_t block,
int err = 0;
unsigned int clen, thisgrp_len;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_mb_mark_bb, sb, block, len, state);
+
while (len > 0) {
ext4_get_group_no_and_offset(sb, block, &group, &blkoff);
@@ -7474,7 +7476,6 @@ EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_group_number);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_release);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_block_bitmap_nowait);
EXPORT_SYMBOL_FOR_EXT4_TEST(mb_set_bits);
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_init_inode);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_mark_context);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_new_blocks);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_blocks);
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 10/27] ext4: expand KUnit tests for fs/ext4/inode.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (8 preceding siblings ...)
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:06 ` 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
` (16 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Expand the ext4_inode_test KUnit test suite in fs/ext4/inode-test.c from
1 parameterized test (16 timestamp decoding subtests) to 26 test cases
(41 subtests total) to comprehensively exercise fs/ext4/inode.c using an
in-memory VFS superblock, mock block device, and deterministic block pool
without requiring a mounted block device:
- Extra timestamp encoding and 64-bit round-trips across all 16 boundary
vectors, 256-byte inodes with 34-bit seconds + nanoseconds, and
128-byte old inodes storing signed 32-bit seconds with zeroed
nanoseconds.
- Raw inode CRC32C checksum computation and verification
(ext4_inode_csum_set and ext4_inode_csum_verify) across legacy
non-csum filesystems, 128-byte inodes (16-bit lo CRC), 256-byte inodes
with i_extra_isize = 0 vs >= 4 (32-bit CRC), and bit-flip corruptions.
- Inode block count encoding and decoding (ext4_inode_blocks_set and
ext4_inode_blocks) across 32-bit 512B sectors, 48-bit huge_file
sectors, and EXT4_HUGE_FILE_FL filesystem block units.
- VFS-to-raw inode serialization and writeback paths
(ext4_fill_raw_inode, ext4_reserve_inode_write, ext4_mark_iloc_dirty,
__ext4_mark_inode_dirty, ext4_dirty_inode, ext4_inode_attach_jinode,
ext4_write_inode, and ext4_sync_inode_metadata), including inline data
preservation, project IDs, lazytime neighbor timestamp flushes,
emergency read-only -EROFS handling, and buffer I/O error propagation.
- Dynamic extra-isize expansion (__ext4_expand_extra_isize,
ext4_try_to_expand_extra_isize, and ext4_expand_extra_isize), including
corrupted/invalid size guards, EXT4_STATE_NO_EXPAND, xattr_sem
contention, and shifting in-body extended attributes forward.
- On-disk inode location lookup (__ext4_get_inode_loc,
ext4_get_inode_loc, ext4_get_inode_loc_noio, and
ext4_get_fc_inode_loc), including zero-I/O uninitialized/unused inode
table optimization, neighbor bitmap occupancy checks, readahead window
calculation, and read -EIO handling.
- Inode loading and validation (__ext4_iget) across EXT4_IGET_NORMAL,
EXT4_IGET_SPECIAL, EXT4_IGET_HANDLE, EXT4_IGET_EA_INODE,
EXT4_IGET_NOWAIT, and EXT4_IGET_BAD, special device rdev encodings,
fast symlinks, and corruption rejections (bad CRC, unlinked ino, bad
mode, reserved ino < first_ino, bad extra_isize, conflicting flags,
oversize indirect i_size, bad i_file_acl, and bad extent header).
- Inode eviction (ext4_evict_inode) across live inodes, unlinked regular
files (truncating data extents, setting i_dtime, and freeing the
inode), and unlinked fast symlinks.
- Fast symlink classification, ext4_can_truncate, inode flag propagation
(ext4_set_inode_flags), address_space_operations selection
(ext4_set_aops), folio order caps (ext4_set_inode_mapping_order),
project ID lookup (ext4_get_projid), DIO alignment checks
(ext4_dio_alignment), and STATX_BTIME / STATX_DIOALIGN /
STATX_WRITE_ATOMIC / STATX_ATTR_* reporting in ext4_getattr and
ext4_file_getattr.
- Delayed allocation space accounting (ext4_da_reserve_space,
ext4_da_update_reserve_space, ext4_da_release_space, and
ext4_nonda_switch) and bigalloc shared-cluster pending reservations
(ext4_clu_alloc_state, ext4_insert_delayed_blocks, and
ext4_da_map_blocks).
- Logical-to-physical block mapping (ext4_map_blocks, ext4_get_block,
ext4_get_block_unwritten, ext4_da_get_block_prep, ext4_getblk,
ext4_bread, ext4_bread_batch, and ext4_walk_page_buffers) across ES
hits, extent allocation, unwritten extents, zeroout, and batch reads.
- iomap mapping callbacks (ext4_iomap_begin, ext4_iomap_ops, and
ext4_iomap_report_ops, including bigalloc multi-block IOMAP_ATOMIC
writes), buffered write_begin/write_end (ext4_da_aops and ext4_aops),
bmap, invalidate_folio, release_folio, and ext4_page_mkwrite.
- Transaction credit helpers (ext4_meta_trans_blocks,
ext4_chunk_trans_extent, and ext4_chunk_trans_blocks),
ext4_break_layouts, ext4_update_disksize_before_punch, partial-block
and EOF zeroing (ext4_zero_partial_blocks, ext4_block_zero_eof, and
ext4_truncate_page_cache_block_range), ext4_punch_hole, ext4_truncate,
and ext4_setattr.
Add thin KUNIT_STATIC_STUB_REDIRECT wrappers for buffer-cache lookup and
inode freeing (ext4_sb_getblk, ext4_sb_find_get_block,
ext4_sb_getblk_unmovable, ext4_read_bh_nowait, ext4_read_bh_lock,
ext4_sb_breadahead_unmovable, and ext4_free_inode) and export the tested
entry points and static-helper wrappers with EXPORT_SYMBOL_FOR_EXT4_TEST.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/ext4.h | 29 +
fs/ext4/ext4_jbd2.c | 2 +
fs/ext4/ialloc.c | 3 +
fs/ext4/inode-test.c | 3449 +++++++++++++++++++++++++++++++++++++++++-
fs/ext4/inode.c | 149 +-
fs/ext4/page-io.c | 1 +
fs/ext4/super.c | 19 +-
7 files changed, 3640 insertions(+), 12 deletions(-)
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index afa2db3b6d57..c4f685704031 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3258,6 +3258,35 @@ extern void ext4_da_update_reserve_space(struct inode *inode,
int used, int quota_claim);
extern int ext4_issue_zeroout(struct inode *inode, ext4_lblk_t lblk,
ext4_fsblk_t pblk, ext4_lblk_t len);
+struct buffer_head *ext4_sb_getblk(struct super_block *sb, ext4_fsblk_t block);
+struct buffer_head *ext4_sb_find_get_block(struct super_block *sb,
+ ext4_fsblk_t block);
+struct buffer_head *ext4_sb_getblk_unmovable(struct super_block *sb,
+ ext4_fsblk_t block);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+int ext4_inode_csum_verify_test(struct inode *inode, struct ext4_inode *raw,
+ struct ext4_inode_info *ei);
+int ext4_inode_blocks_set_test(struct ext4_inode *raw_inode,
+ struct ext4_inode_info *ei);
+blkcnt_t ext4_inode_blocks_test(struct ext4_inode *raw_inode,
+ struct ext4_inode_info *ei);
+int ext4_fill_raw_inode_test(struct inode *inode,
+ struct ext4_inode *raw_inode);
+int ext4_expand_extra_isize_test(struct inode *inode,
+ unsigned int new_extra_isize,
+ struct ext4_iloc *iloc,
+ handle_t *handle, int *no_expand);
+int ext4_try_to_expand_extra_isize_test(struct inode *inode,
+ unsigned int new_extra_isize,
+ struct ext4_iloc *iloc,
+ handle_t *handle);
+int ext4_da_reserve_space_test(struct inode *inode, int nr_resv);
+int ext4_clu_alloc_state_test(struct inode *inode, ext4_lblk_t lblk);
+int ext4_insert_delayed_blocks_test(struct inode *inode, ext4_lblk_t lblk,
+ ext4_lblk_t len);
+int ext4_da_map_blocks_test(struct inode *inode, struct ext4_map_blocks *map);
+int ext4_nonda_switch_test(struct super_block *sb);
+#endif
static inline bool is_special_ino(struct super_block *sb, unsigned long ino)
{
diff --git a/fs/ext4/ext4_jbd2.c b/fs/ext4/ext4_jbd2.c
index caba7ac68841..f18a01e6fd13 100644
--- a/fs/ext4/ext4_jbd2.c
+++ b/fs/ext4/ext4_jbd2.c
@@ -125,6 +125,7 @@ handle_t *__ext4_journal_start_sb(struct inode *inode,
return jbd2__journal_start(journal, blocks, rsv_blocks, revoke_creds,
GFP_NOFS, type, line);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_journal_start_sb);
int __ext4_journal_stop(const char *where, unsigned int line, handle_t *handle)
{
@@ -152,6 +153,7 @@ int __ext4_journal_stop(const char *where, unsigned int line, handle_t *handle)
__ext4_std_error(sb, where, line, err);
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_journal_stop);
handle_t *__ext4_journal_start_reserved(handle_t *handle, unsigned int line,
int type)
diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c
index 25b387f3ce85..02546b828836 100644
--- a/fs/ext4/ialloc.c
+++ b/fs/ext4/ialloc.c
@@ -250,6 +250,8 @@ void ext4_free_inode(handle_t *handle, struct inode *inode)
int fatal = 0, err, count, cleared;
struct ext4_group_info *grp;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_free_inode, handle, inode);
+
if (!sb) {
printk(KERN_ERR "EXT4-fs: %s:%d: inode on "
"nonexistent device\n", __func__, __LINE__);
@@ -363,6 +365,7 @@ void ext4_free_inode(handle_t *handle, struct inode *inode)
brelse(bitmap_bh);
ext4_std_error(sb, fatal);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_inode);
struct orlov_stats {
__u64 free_clusters;
diff --git a/fs/ext4/inode-test.c b/fs/ext4/inode-test.c
index 749af7ad4e09..e8d19c1d4270 100644
--- a/fs/ext4/inode-test.c
+++ b/fs/ext4/inode-test.c
@@ -1,14 +1,32 @@
// SPDX-License-Identifier: GPL-2.0
/*
- * KUnit test of ext4 inode that verify the seconds part of [a/c/m]
- * timestamps in ext4 inode structs are decoded correctly.
+ * KUnit tests for fs/ext4/inode.c
*/
#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/iomap.h>
+#include <linux/iversion.h>
+#include <linux/jbd2.h>
#include <linux/kernel.h>
+#include <linux/mm.h>
+#include <linux/pagemap.h>
+#include <linux/quotaops.h>
+#include <linux/rcupdate.h>
+#include <linux/slab.h>
+#include <linux/stat.h>
#include <linux/time64.h>
+#include <linux/writeback.h>
#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "ext4_extents.h"
+#include "xattr.h"
+#include "truncate.h"
/*
* For constructing the nonnegative timestamp lower bound value.
@@ -233,7 +251,6 @@ static time64_t get_32bit_time(const struct timestamp_expectation * const test)
return UPPER_MSB_0;
}
-
/*
* Test data is derived from the table in the Inode Timestamps section of
* Documentation/filesystems/ext4/inodes.rst.
@@ -267,8 +284,3432 @@ static void inode_test_xtimestamp_decoding(struct kunit *test)
test_param->extra_bits);
}
+/* -------------------------------------------------------------------------
+ * Mock filesystem, block pool, and VFS inode fixture for fs/ext4/inode.c
+ * -------------------------------------------------------------------------
+ */
+
+#define INODE_TEST_BLK_SIZE 4096
+#define INODE_TEST_BLK_BITS 12
+#define INODE_TEST_TOTAL_BLKS 32768
+#define INODE_TEST_INODE_SIZE 256
+#define INODE_TEST_EXTRA_ISIZE 32
+#define INODE_TEST_INODES_PER_BLK (INODE_TEST_BLK_SIZE / INODE_TEST_INODE_SIZE)
+#define INODE_TEST_INODES_PER_GRP 2048
+#define INODE_TEST_FIRST_INO 11
+#define INODE_TEST_PRIMARY_INO 12
+#define INODE_TEST_CSUM_SEED 0x5a3c9e71U
+
+#define INODE_TEST_IBITMAP_BLK 10
+#define INODE_TEST_ITABLE_BLK0 20
+#define INODE_TEST_ITABLE_BLK1 21
+#define INODE_TEST_META_BLK0 50
+#define INODE_TEST_META_BLK1 51
+#define INODE_TEST_DATA_BLK0 100
+#define INODE_TEST_DATA_NBLKS 8
+#define INODE_TEST_POOL_SIZE 16
+#define INODE_TEST_MAX_TRACKED_INODES 32
+
+struct inode_test_blk {
+ bool in_use;
+ bool in_cache;
+ ext4_fsblk_t blocknr;
+ struct buffer_head bh;
+ char cache_buf[INODE_TEST_BLK_SIZE] __aligned(8);
+ char disk_buf[INODE_TEST_BLK_SIZE] __aligned(8);
+};
+
+struct inode_test_ctx {
+ struct super_block *sb;
+ struct ext4_sb_info sbi;
+ struct ext4_super_block es;
+ struct ext4_group_desc gdp;
+ struct block_device bdev;
+ struct request_queue bdev_queue;
+ struct inode bd_inode;
+ struct buffer_head sb_bh;
+ struct dentry root_dentry;
+
+ struct ext4_inode_info ei;
+ struct ext4_inode_info scratch_ei;
+ union {
+ struct ext4_inode raw;
+ u8 bytes[INODE_TEST_INODE_SIZE];
+ } scratch_raw;
+ struct inode *tracked_inodes[INODE_TEST_MAX_TRACKED_INODES];
+ unsigned int nr_tracked_inodes;
+
+ struct inode_test_blk pool[INODE_TEST_POOL_SIZE];
+
+ bool fail_group_desc;
+ bool fail_getblk;
+ bool fail_read_eio;
+ int fail_zeroout_err;
+ int fail_alloc_err;
+ int fail_orphan_err;
+
+ ext4_fsblk_t next_alloc_pblk;
+ unsigned int alloc_calls;
+ unsigned int free_calls;
+ ext4_fsblk_t last_free_pblk;
+ unsigned long last_free_count;
+ unsigned long total_freed_blocks;
+ unsigned int zeroout_calls;
+ ext4_lblk_t last_zeroout_lblk;
+ ext4_fsblk_t last_zeroout_pblk;
+ ext4_lblk_t last_zeroout_len;
+ unsigned int read_nowait_calls;
+ unsigned int read_lock_calls;
+ unsigned int readahead_calls;
+ ext4_fsblk_t first_readahead_blk;
+ ext4_fsblk_t last_readahead_blk;
+ unsigned int orphan_add_calls;
+ unsigned int orphan_del_calls;
+ unsigned int free_inode_calls;
+};
+
+static unsigned long inode_test_shrinker_count(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return 0;
+}
+
+static unsigned long inode_test_shrinker_scan(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return SHRINK_STOP;
+}
+
+static void inode_test_init_ei_fields(struct ext4_inode_info *ei)
+{
+ inode_set_iversion(&ei->vfs_inode, 1);
+ ei->i_flags = 0;
+ ext4_clear_state_flags(ei);
+ spin_lock_init(&ei->i_raw_lock);
+ ei->i_prealloc_node = RB_ROOT;
+ atomic_set(&ei->i_prealloc_active, 0);
+ rwlock_init(&ei->i_prealloc_lock);
+ ext4_es_init_tree(&ei->i_es_tree);
+ rwlock_init(&ei->i_es_lock);
+ INIT_LIST_HEAD(&ei->i_es_list);
+ ei->i_es_all_nr = 0;
+ ei->i_es_shk_nr = 0;
+ ei->i_es_shrink_lblk = 0;
+ ei->i_es_seq = 0;
+ ei->i_reserved_data_blocks = 0;
+ spin_lock_init(&ei->i_block_reservation_lock);
+ ext4_init_pending_tree(&ei->i_pending_tree);
+#ifdef CONFIG_QUOTA
+ ei->i_reserved_quota = 0;
+ memset(&ei->i_dquot, 0, sizeof(ei->i_dquot));
+#endif
+ ei->jinode = NULL;
+ init_rwsem(&ei->xattr_sem);
+ INIT_LIST_HEAD(&ei->i_rsv_conversion_list);
+ spin_lock_init(&ei->i_completed_io_lock);
+ ei->i_sync_tid = 0;
+ ei->i_datasync_tid = 0;
+ INIT_WORK(&ei->i_rsv_conversion_work, ext4_end_io_rsv_work);
+ INIT_LIST_HEAD(&ei->i_orphan);
+ ext4_fc_init_inode(&ei->vfs_inode);
+ spin_lock_init(&ei->i_fc_lock);
+ ei->i_metadata_bhs = NULL;
+ ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+ ei->i_extra_isize = INODE_TEST_EXTRA_ISIZE;
+ ei->i_csum_seed = INODE_TEST_CSUM_SEED;
+}
+
+static void inode_test_cleanup_ext4_trees(struct inode *inode)
+{
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+ struct rb_node *node;
+
+ down_write(&ei->i_data_sem);
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ while ((node = rb_first(&ei->i_pending_tree.root)) != NULL) {
+ struct pending_reservation *pr;
+
+ pr = rb_entry(node, struct pending_reservation, rb_node);
+ ext4_remove_pending(inode, EXT4_C2B(sbi, pr->lclu));
+ }
+ up_write(&ei->i_data_sem);
+}
+
+static struct inode *inode_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_test_init_ei_fields(ei);
+ return &ei->vfs_inode;
+}
+
+static void inode_test_destroy_inode(struct inode *inode)
+{
+ inode_test_cleanup_ext4_trees(inode);
+ kfree(ext4_i_metadata_bhs(inode));
+ EXT4_I(inode)->i_metadata_bhs = NULL;
+}
+
+static void inode_test_free_inode(struct inode *inode)
+{
+ kfree(EXT4_I(inode));
+}
+
+static const struct super_operations inode_test_sops = {
+ .alloc_inode = inode_test_alloc_inode,
+ .evict_inode = ext4_evict_inode,
+ .destroy_inode = inode_test_destroy_inode,
+ .free_inode = inode_test_free_inode,
+ .drop_inode = inode_just_drop,
+};
+
+#ifdef CONFIG_QUOTA
+static qsize_t *inode_test_get_reserved_space(struct inode *inode)
+{
+ return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations inode_test_qops = {
+ .get_reserved_space = inode_test_get_reserved_space,
+};
+#endif
+
+static int inode_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static struct file_system_type inode_test_fs_type = {
+ .name = "ext4_inode_kunit",
+ .init_fs_context = inode_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static struct inode_test_blk *inode_test_find_blk(struct inode_test_ctx *ctx,
+ ext4_fsblk_t blocknr)
+{
+ int i;
+
+ for (i = 0; i < INODE_TEST_POOL_SIZE; i++) {
+ if (ctx->pool[i].in_use && ctx->pool[i].blocknr == blocknr)
+ return &ctx->pool[i];
+ }
+ return NULL;
+}
+
+static struct inode_test_blk *inode_test_find_blk_by_bh(
+ struct inode_test_ctx *ctx, struct buffer_head *bh)
+{
+ int i;
+
+ for (i = 0; i < INODE_TEST_POOL_SIZE; i++) {
+ if (ctx->pool[i].in_use && &ctx->pool[i].bh == bh)
+ return &ctx->pool[i];
+ }
+ return NULL;
+}
+
+static struct inode_test_blk *inode_test_setup_blk(struct inode_test_ctx *ctx,
+ int idx,
+ ext4_fsblk_t blocknr,
+ bool in_cache,
+ bool uptodate)
+{
+ struct inode_test_blk *slot = &ctx->pool[idx];
+
+ memset(slot, 0, sizeof(*slot));
+ slot->in_use = true;
+ slot->in_cache = in_cache;
+ slot->blocknr = blocknr;
+ slot->bh.b_bdev = &ctx->bdev;
+ slot->bh.b_blocknr = blocknr;
+ slot->bh.b_size = INODE_TEST_BLK_SIZE;
+ slot->bh.b_data = slot->cache_buf;
+ atomic_set(&slot->bh.b_count, 1);
+ if (uptodate)
+ set_buffer_uptodate(&slot->bh);
+ return slot;
+}
+
+/* --- Static Stubs --- */
+
+static struct ext4_group_desc *inode_test_get_group_desc_stub(
+ struct super_block *sb, ext4_group_t block_group,
+ struct buffer_head **bh)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct inode_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx || ctx->fail_group_desc || block_group != 0)
+ return NULL;
+ if (bh)
+ *bh = NULL;
+ return &ctx->gdp;
+}
+
+static struct buffer_head *inode_test_sb_getblk_stub(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct inode_test_ctx *ctx = test ? test->priv : NULL;
+ struct inode_test_blk *slot;
+
+ if (!ctx || ctx->fail_getblk)
+ return NULL;
+ slot = inode_test_find_blk(ctx, block);
+ if (!slot)
+ return NULL;
+ slot->in_cache = true;
+ get_bh(&slot->bh);
+ return &slot->bh;
+}
+
+static struct buffer_head *inode_test_sb_find_get_block_stub(
+ struct super_block *sb, ext4_fsblk_t block)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct inode_test_ctx *ctx = test ? test->priv : NULL;
+ struct inode_test_blk *slot;
+
+ if (!ctx)
+ return NULL;
+ slot = inode_test_find_blk(ctx, block);
+ if (!slot || !slot->in_cache)
+ return NULL;
+ get_bh(&slot->bh);
+ return &slot->bh;
+}
+
+static struct buffer_head *inode_test_sb_getblk_unmovable_stub(
+ struct super_block *sb, ext4_fsblk_t block)
+{
+ return inode_test_sb_getblk_stub(sb, block);
+}
+
+static struct buffer_head *inode_test_ext_getblk_stub(struct inode *inode,
+ ext4_fsblk_t pblk,
+ gfp_t gfp_flags)
+{
+ struct buffer_head *bh;
+
+ bh = inode_test_sb_getblk_stub(inode->i_sb, pblk);
+ if (bh) {
+ set_buffer_uptodate(bh);
+ set_buffer_verified(bh);
+ }
+ return bh;
+}
+
+static void inode_test_read_bh_nowait_stub(struct buffer_head *bh,
+ blk_opf_t op_flags,
+ bio_end_io_t end_io,
+ bool simu_fail)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct inode_test_ctx *ctx = test ? test->priv : NULL;
+ struct inode_test_blk *slot;
+
+ if (!ctx) {
+ unlock_buffer(bh);
+ return;
+ }
+ ctx->read_nowait_calls++;
+ if (ctx->fail_read_eio || simu_fail) {
+ clear_buffer_uptodate(bh);
+ unlock_buffer(bh);
+ return;
+ }
+ slot = inode_test_find_blk_by_bh(ctx, bh);
+ if (!slot)
+ slot = inode_test_find_blk(ctx, bh->b_blocknr);
+ if (slot) {
+ memcpy(bh->b_data, slot->disk_buf, bh->b_size);
+ set_buffer_uptodate(bh);
+ } else {
+ clear_buffer_uptodate(bh);
+ }
+ unlock_buffer(bh);
+}
+
+static int inode_test_read_bh_lock_stub(struct buffer_head *bh,
+ blk_opf_t op_flags, bool wait)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct inode_test_ctx *ctx = test ? test->priv : NULL;
+ struct inode_test_blk *slot;
+
+ if (!ctx)
+ return -EIO;
+ ctx->read_lock_calls++;
+ lock_buffer(bh);
+ if (ctx->fail_read_eio) {
+ clear_buffer_uptodate(bh);
+ unlock_buffer(bh);
+ return wait ? -EIO : 0;
+ }
+ slot = inode_test_find_blk_by_bh(ctx, bh);
+ if (!slot)
+ slot = inode_test_find_blk(ctx, bh->b_blocknr);
+ if (!slot) {
+ clear_buffer_uptodate(bh);
+ unlock_buffer(bh);
+ return wait ? -EIO : 0;
+ }
+ memcpy(bh->b_data, slot->disk_buf, bh->b_size);
+ set_buffer_uptodate(bh);
+ unlock_buffer(bh);
+ return 0;
+}
+
+static void inode_test_breadahead_stub(struct super_block *sb, sector_t block)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct inode_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return;
+ if (ctx->readahead_calls == 0)
+ ctx->first_readahead_blk = block;
+ ctx->last_readahead_blk = block;
+ ctx->readahead_calls++;
+}
+
+static ext4_fsblk_t inode_test_mb_new_blocks_stub(
+ handle_t *handle, struct ext4_allocation_request *ar, int *errp)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct inode_test_ctx *ctx = test ? test->priv : NULL;
+ ext4_fsblk_t pblk;
+
+ if (!ctx) {
+ *errp = -EIO;
+ return 0;
+ }
+ if (ctx->fail_alloc_err) {
+ *errp = ctx->fail_alloc_err;
+ return 0;
+ }
+ ctx->alloc_calls++;
+ pblk = ctx->next_alloc_pblk;
+ ctx->next_alloc_pblk += ar->len;
+ *errp = 0;
+ return pblk;
+}
+
+static void inode_test_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 inode_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (bh && (flags & EXT4_FREE_BLOCKS_FORGET))
+ brelse(bh);
+ if (!ctx)
+ return;
+ ctx->free_calls++;
+ ctx->last_free_pblk = block;
+ ctx->last_free_count = count;
+ ctx->total_freed_blocks += count;
+}
+
+static int inode_test_issue_zeroout_stub(struct inode *inode, ext4_lblk_t lblk,
+ ext4_fsblk_t pblk, ext4_lblk_t len)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct inode_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return -EIO;
+ ctx->zeroout_calls++;
+ ctx->last_zeroout_lblk = lblk;
+ ctx->last_zeroout_pblk = pblk;
+ ctx->last_zeroout_len = len;
+ return ctx->fail_zeroout_err;
+}
+
+static int inode_test_dirty_metadata_stub(const char *where, unsigned int line,
+ handle_t *handle,
+ struct inode *inode,
+ struct buffer_head *bh)
+{
+ if (bh) {
+ set_buffer_uptodate(bh);
+ set_buffer_dirty(bh);
+ }
+ return 0;
+}
+
+static int inode_test_ext_dirty_stub(const char *where, unsigned int line,
+ handle_t *handle, struct inode *inode,
+ struct ext4_ext_path *path)
+{
+ if (path && path->p_bh) {
+ ext4_extent_block_csum_set_test(inode,
+ ext_block_hdr(path->p_bh));
+ set_buffer_verified(path->p_bh);
+ }
+ return 0;
+}
+
+static int inode_test_orphan_add_stub(handle_t *handle, struct inode *inode)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct inode_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return -EIO;
+ if (ctx->fail_orphan_err)
+ return ctx->fail_orphan_err;
+ ctx->orphan_add_calls++;
+ return 0;
+}
+
+static int inode_test_orphan_del_stub(handle_t *handle, struct inode *inode)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct inode_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return -EIO;
+ ctx->orphan_del_calls++;
+ return 0;
+}
+
+static void inode_test_free_inode_stub(handle_t *handle, struct inode *inode)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct inode_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (ctx)
+ ctx->free_inode_calls++;
+ ext4_clear_inode(inode);
+}
+
+/* --- Fixture Setup & Cleanup --- */
+
+static void inode_test_track_iget_inode(struct kunit *test,
+ struct inode_test_ctx *ctx,
+ struct inode *inode)
+{
+ KUNIT_ASSERT_LT(test, ctx->nr_tracked_inodes,
+ (unsigned int)INODE_TEST_MAX_TRACKED_INODES);
+ ctx->tracked_inodes[ctx->nr_tracked_inodes++] = inode;
+}
+
+static void inode_test_put_tracked_inode(struct inode_test_ctx *ctx,
+ struct inode *inode)
+{
+ unsigned int i;
+
+ for (i = 0; i < ctx->nr_tracked_inodes; i++) {
+ if (ctx->tracked_inodes[i] == inode) {
+ ctx->tracked_inodes[i] =
+ ctx->tracked_inodes[--ctx->nr_tracked_inodes];
+ ctx->tracked_inodes[ctx->nr_tracked_inodes] = NULL;
+ iput(inode);
+ return;
+ }
+ }
+ iput(inode);
+}
+
+static void inode_test_reset_mock_inode(struct inode_test_ctx *ctx, umode_t mode)
+{
+ struct ext4_inode_info *ei = &ctx->ei;
+ struct inode *inode = &ei->vfs_inode;
+ struct mapping_metadata_bhs *mmb = ext4_i_metadata_bhs(inode);
+
+ truncate_inode_pages_final(&inode->i_data);
+ inode_io_list_del(inode);
+ ext4_fc_del(inode);
+ if (mmb)
+ mmb_invalidate(mmb);
+ inode_test_cleanup_ext4_trees(inode);
+ if (ei->jinode) {
+ jbd2_free_inode(ei->jinode);
+ ei->jinode = NULL;
+ }
+
+ inode_state_assign_raw(inode, 0);
+ inode->i_ino = INODE_TEST_PRIMARY_INO;
+ inode->i_mode = mode;
+ inode->i_blkbits = ctx->sb->s_blocksize_bits;
+ inode->i_flags = S_NOQUOTA;
+ set_nlink(inode, 1);
+ i_uid_write(inode, 0);
+ i_gid_write(inode, 0);
+ inode->i_size = 0;
+ inode->i_blocks = 0;
+ inode->i_generation = 1;
+ inode->i_rdev = 0;
+ ei->i_disksize = 0;
+ ei->i_flags = 0;
+ ei->i_file_acl = 0;
+ ei->i_dtime = 0;
+ ei->i_inline_off = 0;
+ ei->i_inline_size = 0;
+ ei->i_extra_isize = INODE_TEST_EXTRA_ISIZE;
+ ei->i_reserved_data_blocks = 0;
+ ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+ ext4_clear_state_flags(ei);
+ memset(ei->i_data, 0, sizeof(ei->i_data));
+
+ if (S_ISREG(mode) || S_ISDIR(mode)) {
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ext4_ext_tree_init(NULL, inode);
+ }
+ if (S_ISREG(mode))
+ ext4_set_aops(inode);
+}
+
+static void inode_test_cleanup(void *data)
+{
+ struct inode_test_ctx *ctx = data;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct mapping_metadata_bhs *mmb;
+ unsigned int i;
+
+ ctx->sbi.s_journal = NULL;
+
+ for (i = 0; i < ctx->nr_tracked_inodes; i++) {
+ struct inode *ti = ctx->tracked_inodes[i];
+
+ if (ti) {
+ spin_lock(&ti->i_lock);
+ inode_state_clear(ti, I_DIRTY_TIME);
+ spin_unlock(&ti->i_lock);
+ iput(ti);
+ ctx->tracked_inodes[i] = NULL;
+ }
+ }
+ ctx->nr_tracked_inodes = 0;
+
+ truncate_inode_pages_final(&inode->i_data);
+ inode_io_list_del(inode);
+ ext4_fc_del(inode);
+ mmb = ext4_i_metadata_bhs(inode);
+ if (mmb)
+ mmb_invalidate(mmb);
+ inode_test_cleanup_ext4_trees(inode);
+ if (ctx->ei.jinode) {
+ jbd2_free_inode(ctx->ei.jinode);
+ ctx->ei.jinode = NULL;
+ }
+
+ deactivate_super(ctx->sb);
+ rcu_barrier();
+
+ ext4_es_unregister_shrinker(&ctx->sbi);
+ percpu_counter_destroy(&ctx->sbi.s_dirtyclusters_counter);
+ percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+
+ kfree(mmb);
+ kfree(ctx);
+}
+
+static struct inode_test_ctx *inode_test_setup(struct kunit *test)
+{
+ struct inode_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ struct inode *inode;
+ int err, i;
+
+ ctx = kzalloc_obj(*ctx);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ test->priv = ctx;
+
+ fc = fs_context_for_mount(&inode_test_fs_type, 0);
+ if (IS_ERR(fc)) {
+ err = PTR_ERR(fc);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+ sb = sget_fc(fc, NULL, set_anon_super_fc);
+ put_fs_context(fc);
+ if (IS_ERR(sb)) {
+ err = PTR_ERR(sb);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+ ctx->sb = sb;
+ sbi = &ctx->sbi;
+
+ sbi->s_sb = sb;
+ sb->s_fs_info = sbi;
+ sb->s_op = &inode_test_sops;
+#ifdef CONFIG_QUOTA
+ sb->dq_op = &inode_test_qops;
+#endif
+ sb->s_flags |= SB_ACTIVE;
+ sb->s_blocksize = INODE_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = INODE_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_inode_kunit", sizeof(sb->s_id));
+
+ inode_init_once(&ctx->bd_inode);
+ ctx->bd_inode.i_sb = sb;
+ ctx->bd_inode.i_blkbits = INODE_TEST_BLK_BITS;
+ ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data;
+ ctx->bd_inode.i_data.host = &ctx->bd_inode;
+ ctx->bdev_queue.limits.logical_block_size = 512;
+ ctx->bdev_queue.limits.dma_alignment = 511;
+ ctx->bdev.bd_queue = &ctx->bdev_queue;
+ ctx->bdev.bd_mapping = &ctx->bd_inode.i_data;
+ bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+ sb->s_bdev = &ctx->bdev;
+
+ sbi->s_es = &ctx->es;
+ ctx->es.s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+ ctx->es.s_first_data_block = cpu_to_le32(1);
+ ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_LINUX);
+ ctx->es.s_inodes_count = cpu_to_le32(INODE_TEST_INODES_PER_GRP);
+ ctx->es.s_inodes_per_group = cpu_to_le32(INODE_TEST_INODES_PER_GRP);
+ ctx->es.s_first_ino = cpu_to_le32(INODE_TEST_FIRST_INO);
+ ctx->es.s_inode_size = cpu_to_le16(INODE_TEST_INODE_SIZE);
+ ctx->es.s_min_extra_isize = cpu_to_le16(INODE_TEST_EXTRA_ISIZE);
+ ctx->es.s_want_extra_isize = cpu_to_le16(INODE_TEST_EXTRA_ISIZE);
+ ctx->es.s_checksum_type = EXT4_CRC32C_CHKSUM;
+ ext4_blocks_count_set(&ctx->es, INODE_TEST_TOTAL_BLKS);
+ ext4_set_feature_extents(sb);
+ ext4_set_feature_extra_isize(sb);
+ ext4_set_feature_metadata_csum(sb);
+ ext4_set_feature_64bit(sb);
+ ext4_set_feature_huge_file(sb);
+ ext4_set_feature_large_file(sb);
+ ext4_set_feature_dir_index(sb);
+
+ ctx->sb_bh.b_data = (char *)&ctx->es;
+ ctx->sb_bh.b_size = sizeof(ctx->es);
+ ctx->sb_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->sb_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->sb_bh);
+ sbi->s_sbh = &ctx->sb_bh;
+
+ sbi->s_inode_size = INODE_TEST_INODE_SIZE;
+ sbi->s_inodes_per_block = INODE_TEST_INODES_PER_BLK;
+ sbi->s_inodes_per_group = INODE_TEST_INODES_PER_GRP;
+ sbi->s_first_ino = INODE_TEST_FIRST_INO;
+ sbi->s_want_extra_isize = INODE_TEST_EXTRA_ISIZE;
+ sbi->s_blocks_per_group = INODE_TEST_TOTAL_BLKS;
+ sbi->s_clusters_per_group = INODE_TEST_TOTAL_BLKS;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_cluster_bits = 0;
+ sbi->s_groups_count = 1;
+ sbi->s_gdb_count = 1;
+ sbi->s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_desc_per_block = INODE_TEST_BLK_SIZE / EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_bitmap_maxbytes = 0x1ff7ffff000ULL;
+ sbi->s_extent_max_zeroout_kb = 32;
+ sbi->s_csum_seed = INODE_TEST_CSUM_SEED;
+
+ ext4_inode_bitmap_set(sb, &ctx->gdp, INODE_TEST_IBITMAP_BLK);
+ ext4_inode_table_set(sb, &ctx->gdp, INODE_TEST_ITABLE_BLK0);
+
+ set_opt(sb, ERRORS_CONT);
+ set_opt(sb, ORDERED_DATA);
+ set_opt(sb, DELALLOC);
+ 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);
+ 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);
+
+ err = percpu_counter_init(&sbi->s_freeclusters_counter, 4096,
+ GFP_KERNEL);
+ if (err) {
+ deactivate_locked_super(sb);
+ kfree(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);
+ deactivate_locked_super(sb);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+ err = ext4_es_register_shrinker(sbi);
+ if (err) {
+ percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
+ percpu_counter_destroy(&sbi->s_freeclusters_counter);
+ deactivate_locked_super(sb);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+ sbi->s_es_shrinker->count_objects = inode_test_shrinker_count;
+ sbi->s_es_shrinker->scan_objects = inode_test_shrinker_scan;
+
+ /* Initialize mock block pool */
+ inode_test_setup_blk(ctx, 0, INODE_TEST_IBITMAP_BLK, true, true);
+ inode_test_setup_blk(ctx, 1, INODE_TEST_ITABLE_BLK0, true, true);
+ inode_test_setup_blk(ctx, 2, INODE_TEST_ITABLE_BLK1, true, true);
+ inode_test_setup_blk(ctx, 3, INODE_TEST_META_BLK0, true, true);
+ inode_test_setup_blk(ctx, 4, INODE_TEST_META_BLK1, true, true);
+ for (i = 0; i < INODE_TEST_DATA_NBLKS; i++)
+ inode_test_setup_blk(ctx, 5 + i, INODE_TEST_DATA_BLK0 + i,
+ true, true);
+ ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0;
+
+ /* Initialize standalone mock inode */
+ inode = &ctx->ei.vfs_inode;
+ init_rwsem(&ctx->ei.i_data_sem);
+ inode_init_once(inode);
+ inode->i_sb = sb;
+ inode->i_mapping = &inode->i_data;
+ inode->i_data.host = inode;
+ mapping_set_gfp_mask(&inode->i_data, GFP_HIGHUSER_MOVABLE);
+ init_rwsem(&inode->i_rwsem);
+ init_rwsem(&inode->i_data.invalidate_lock);
+ spin_lock_init(&inode->i_lock);
+ inode->__i_nlink = 1;
+ atomic_set(&inode->i_count, 1);
+ inode_test_init_ei_fields(&ctx->ei);
+
+ ctx->root_dentry.d_sb = sb;
+ ctx->root_dentry.d_inode = inode;
+
+ up_write(&sb->s_umount);
+
+ kunit_activate_static_stub(test, ext4_get_group_desc,
+ inode_test_get_group_desc_stub);
+ kunit_activate_static_stub(test, ext4_sb_getblk,
+ inode_test_sb_getblk_stub);
+ kunit_activate_static_stub(test, ext4_sb_find_get_block,
+ inode_test_sb_find_get_block_stub);
+ kunit_activate_static_stub(test, ext4_sb_getblk_unmovable,
+ inode_test_sb_getblk_unmovable_stub);
+ kunit_activate_static_stub(test, ext4_ext_getblk,
+ inode_test_ext_getblk_stub);
+ kunit_activate_static_stub(test, ext4_read_bh_nowait,
+ inode_test_read_bh_nowait_stub);
+ kunit_activate_static_stub(test, ext4_read_bh_lock,
+ inode_test_read_bh_lock_stub);
+ kunit_activate_static_stub(test, ext4_sb_breadahead_unmovable,
+ inode_test_breadahead_stub);
+ kunit_activate_static_stub(test, ext4_mb_new_blocks,
+ inode_test_mb_new_blocks_stub);
+ kunit_activate_static_stub(test, ext4_free_blocks,
+ inode_test_free_blocks_stub);
+ kunit_activate_static_stub(test, ext4_issue_zeroout,
+ inode_test_issue_zeroout_stub);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ inode_test_dirty_metadata_stub);
+ kunit_activate_static_stub(test, __ext4_ext_dirty,
+ inode_test_ext_dirty_stub);
+ kunit_activate_static_stub(test, ext4_orphan_add,
+ inode_test_orphan_add_stub);
+ kunit_activate_static_stub(test, ext4_orphan_del,
+ inode_test_orphan_del_stub);
+ kunit_activate_static_stub(test, ext4_free_inode,
+ inode_test_free_inode_stub);
+
+ err = kunit_add_action_or_reset(test, inode_test_cleanup, ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+
+ return ctx;
+}
+
+static struct ext4_inode *inode_test_raw_slot(struct inode_test_ctx *ctx,
+ unsigned long ino, bool disk)
+{
+ unsigned int grp_off = (ino - 1) % INODE_TEST_INODES_PER_GRP;
+ ext4_fsblk_t blk = INODE_TEST_ITABLE_BLK0 +
+ (grp_off / INODE_TEST_INODES_PER_BLK);
+ unsigned int byte_off = (grp_off % INODE_TEST_INODES_PER_BLK) *
+ INODE_TEST_INODE_SIZE;
+ struct inode_test_blk *slot = inode_test_find_blk(ctx, blk);
+ char *base = disk ? slot->disk_buf : slot->cache_buf;
+
+ return (struct ext4_inode *)(base + byte_off);
+}
+
+static void inode_test_stamp_raw_csum(struct inode_test_ctx *ctx,
+ unsigned long ino,
+ struct ext4_inode *raw)
+{
+ struct ext4_inode_info *tmp_ei = &ctx->scratch_ei;
+ struct inode *tmp_inode = &tmp_ei->vfs_inode;
+ __le32 inum = cpu_to_le32(ino);
+ __le32 gen = raw->i_generation;
+ u32 csum;
+
+ memset(tmp_ei, 0, sizeof(*tmp_ei));
+ tmp_inode->i_sb = ctx->sb;
+ tmp_inode->i_ino = ino;
+ tmp_ei->i_extra_isize = le16_to_cpu(raw->i_extra_isize);
+ csum = ext4_chksum(ctx->sbi.s_csum_seed, (u8 *)&inum, sizeof(inum));
+ tmp_ei->i_csum_seed = ext4_chksum(csum, (u8 *)&gen, sizeof(gen));
+ ext4_inode_csum_set(tmp_inode, raw, tmp_ei);
+}
+
+static void inode_test_prepare_valid_raw_inode(struct inode_test_ctx *ctx,
+ unsigned long ino, umode_t mode)
+{
+ struct inode_test_blk *slot;
+ struct ext4_inode *raw;
+ struct ext4_extent_header *eh;
+ unsigned int grp_off = (ino - 1) % INODE_TEST_INODES_PER_GRP;
+ ext4_fsblk_t blk = INODE_TEST_ITABLE_BLK0 +
+ (grp_off / INODE_TEST_INODES_PER_BLK);
+
+ slot = inode_test_find_blk(ctx, blk);
+ clear_buffer_uptodate(&slot->bh);
+ raw = inode_test_raw_slot(ctx, ino, true);
+ memset(raw, 0, INODE_TEST_INODE_SIZE);
+
+ raw->i_mode = cpu_to_le16(mode);
+ raw->i_links_count = cpu_to_le16(1);
+ raw->i_extra_isize = cpu_to_le16(INODE_TEST_EXTRA_ISIZE);
+ raw->i_generation = cpu_to_le32(7);
+ if (S_ISREG(mode) || S_ISDIR(mode)) {
+ raw->i_flags = cpu_to_le32(EXT4_EXTENTS_FL);
+ eh = (struct ext4_extent_header *)raw->i_block;
+ eh->eh_magic = EXT4_EXT_MAGIC;
+ eh->eh_entries = 0;
+ eh->eh_max = cpu_to_le16(4);
+ eh->eh_depth = 0;
+ }
+ inode_test_stamp_raw_csum(ctx, ino, raw);
+}
+
+/* -------------------------------------------------------------------------
+ * 1. Timestamp Encoding & Macro Round-Trip Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_xtimestamp_encoding_roundtrip(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct ext4_inode_info *ei = &ctx->ei;
+ struct inode *inode = &ei->vfs_inode;
+ struct ext4_inode *raw = &ctx->scratch_raw.raw;
+ struct timespec64 ts_in, ts_out;
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(test_data); i++) {
+ __le32 encoded_extra = ext4_encode_extra_time(test_data[i].expected);
+ __le32 encoded_base = cpu_to_le32((s32)test_data[i].expected.tv_sec);
+
+ KUNIT_EXPECT_EQ_MSG(test, le32_to_cpu(encoded_extra),
+ test_data[i].extra_bits,
+ "%s", test_data[i].test_case_name);
+ ts_out = ext4_decode_extra_time(encoded_base, encoded_extra);
+ KUNIT_EXPECT_EQ(test, ts_out.tv_sec,
+ test_data[i].expected.tv_sec);
+ KUNIT_EXPECT_EQ(test, ts_out.tv_nsec,
+ test_data[i].expected.tv_nsec);
+ }
+
+ /* 256-byte inode with extra_isize = 32: full 34-bit + nsec round-trip */
+ ts_in.tv_sec = 0x240001234LL;
+ ts_in.tv_nsec = 987654321L;
+ inode_set_atime_to_ts(inode, ts_in);
+ ts_in.tv_sec = 0x190005678LL;
+ ts_in.tv_nsec = 123456789L;
+ inode_set_mtime_to_ts(inode, ts_in);
+ ts_in.tv_sec = 0x310009abcLL;
+ ts_in.tv_nsec = 555666777L;
+ inode_set_ctime_to_ts(inode, ts_in);
+ ei->i_crtime.tv_sec = 0x22000def0LL;
+ ei->i_crtime.tv_nsec = 444333222L;
+
+ EXT4_INODE_SET_ATIME(inode, raw);
+ EXT4_INODE_SET_MTIME(inode, raw);
+ EXT4_INODE_SET_CTIME(inode, raw);
+ EXT4_EINODE_SET_XTIME(i_crtime, ei, raw);
+
+ inode_set_atime(inode, 0, 0);
+ inode_set_mtime(inode, 0, 0);
+ inode_set_ctime(inode, 0, 0);
+ ei->i_crtime = (struct timespec64){};
+
+ EXT4_INODE_GET_ATIME(inode, raw);
+ EXT4_INODE_GET_MTIME(inode, raw);
+ EXT4_INODE_GET_CTIME(inode, raw);
+ EXT4_EINODE_GET_XTIME(i_crtime, ei, raw);
+
+ KUNIT_EXPECT_EQ(test, inode_get_atime_sec(inode), 0x240001234LL);
+ KUNIT_EXPECT_EQ(test, inode_get_atime_nsec(inode), 987654321L);
+ KUNIT_EXPECT_EQ(test, inode_get_mtime_sec(inode), 0x190005678LL);
+ KUNIT_EXPECT_EQ(test, inode_get_mtime_nsec(inode), 123456789L);
+ KUNIT_EXPECT_EQ(test, inode_get_ctime_sec(inode), 0x310009abcLL);
+ KUNIT_EXPECT_EQ(test, inode_get_ctime_nsec(inode), 555666777L);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_crtime.tv_sec, 0x22000def0LL);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_crtime.tv_nsec, 444333222L);
+
+ /* 128-byte old inode: 32-bit signed seconds only, extra ignored, nsec=0 */
+ ctx->sbi.s_inode_size = EXT4_GOOD_OLD_INODE_SIZE;
+ ctx->ei.i_extra_isize = 0;
+ memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE);
+
+ inode_set_mtime(inode, S32_MAX, 500);
+ inode_set_atime(inode, S32_MIN, 700);
+ EXT4_INODE_SET_MTIME(inode, raw);
+ EXT4_INODE_SET_ATIME(inode, raw);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_mtime_extra), 0U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_atime_extra), 0U);
+
+ inode_set_mtime(inode, 0, 999);
+ inode_set_atime(inode, 0, 999);
+ EXT4_INODE_GET_MTIME(inode, raw);
+ EXT4_INODE_GET_ATIME(inode, raw);
+
+ KUNIT_EXPECT_EQ(test, inode_get_mtime_sec(inode), (time64_t)S32_MAX);
+ KUNIT_EXPECT_EQ(test, inode_get_mtime_nsec(inode), 0L);
+ KUNIT_EXPECT_EQ(test, inode_get_atime_sec(inode), (time64_t)S32_MIN);
+ KUNIT_EXPECT_EQ(test, inode_get_atime_nsec(inode), 0L);
+
+ ctx->sbi.s_inode_size = INODE_TEST_INODE_SIZE;
+ ctx->ei.i_extra_isize = INODE_TEST_EXTRA_ISIZE;
+}
+
+/* -------------------------------------------------------------------------
+ * 2. Inode Checksum Calculation & Verification Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_csum_set_and_verify(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_inode *raw = &ctx->scratch_raw.raw;
+
+ raw->i_mode = cpu_to_le16(S_IFREG | 0644);
+ raw->i_links_count = cpu_to_le16(1);
+ raw->i_extra_isize = cpu_to_le16(INODE_TEST_EXTRA_ISIZE);
+
+ /* Full 32-bit checksum (256B inode, extra_isize = 32) */
+ ext4_inode_csum_set(inode, raw, &ctx->ei);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 1);
+
+ /* Bit flip in low 16 bits fails */
+ raw->i_checksum_lo ^= cpu_to_le16(0x0001);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 0);
+ raw->i_checksum_lo ^= cpu_to_le16(0x0001);
+
+ /* Bit flip in high 16 bits fails */
+ raw->i_checksum_hi ^= cpu_to_le16(0x8000);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 0);
+ raw->i_checksum_hi ^= cpu_to_le16(0x8000);
+
+ /* Bit flip in raw inode body and in-body xattr tail (byte 200) fails */
+ raw->i_mode ^= cpu_to_le16(0x0002);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 0);
+ raw->i_mode ^= cpu_to_le16(0x0002);
+ ctx->scratch_raw.bytes[200] ^= 0x01;
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 0);
+ ctx->scratch_raw.bytes[200] ^= 0x01;
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 1);
+
+ /* Non-Linux creator OS bypasses checksum verification */
+ ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_HURD);
+ raw->i_checksum_lo ^= cpu_to_le16(0xffff);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 1);
+ ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_LINUX);
+
+ /* Disabled metadata_csum feature bypasses checksum verification */
+ ext4_clear_feature_metadata_csum(ctx->sb);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 1);
+ ext4_set_feature_metadata_csum(ctx->sb);
+
+ /* 256B inode with extra_isize = 0 (excludes i_checksum_hi): 16-bit */
+ memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE);
+ ctx->ei.i_extra_isize = 0;
+ raw->i_extra_isize = 0;
+ raw->i_checksum_hi = cpu_to_le16(0xbeef);
+ ext4_inode_csum_set(inode, raw, &ctx->ei);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_checksum_hi), 0xbeef);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 1);
+ raw->i_checksum_lo ^= cpu_to_le16(0x0010);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 0);
+
+ /* 256B inode with extra_isize = 4 (includes i_checksum_hi): 32-bit */
+ memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE);
+ ctx->ei.i_extra_isize = 4;
+ raw->i_extra_isize = cpu_to_le16(4);
+ ext4_inode_csum_set(inode, raw, &ctx->ei);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 1);
+ raw->i_checksum_hi ^= cpu_to_le16(0x0001);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 0);
+
+ /* 128B old inode: 16-bit checksum in i_checksum_lo only */
+ memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE);
+ ctx->sbi.s_inode_size = EXT4_GOOD_OLD_INODE_SIZE;
+ ctx->ei.i_extra_isize = 0;
+ ext4_inode_csum_set(inode, raw, &ctx->ei);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 1);
+ raw->i_checksum_lo ^= cpu_to_le16(0x0100);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 0);
+}
+
+/* -------------------------------------------------------------------------
+ * 3. i_blocks Encoding & Decoding Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_blocks_encode_decode(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_inode *raw = &ctx->scratch_raw.raw;
+ blkcnt_t large_sectors;
+ int err;
+
+ /* 32-bit sector count clears EXT4_INODE_HUGE_FILE */
+ ext4_set_inode_flag(inode, EXT4_INODE_HUGE_FILE);
+ inode->i_blocks = 0x12345678ULL;
+ err = ext4_inode_blocks_set_test(raw, &ctx->ei);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_HUGE_FILE));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_blocks_lo), 0x12345678U);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_blocks_high), 0);
+ KUNIT_EXPECT_EQ(test, ext4_inode_blocks_test(raw, &ctx->ei),
+ (blkcnt_t)0x12345678ULL);
+
+ /* > 32-bit without huge_file feature fails with -EFSCORRUPTED */
+ ext4_clear_feature_huge_file(ctx->sb);
+ inode->i_blocks = 0x100000000ULL;
+ err = ext4_inode_blocks_set_test(raw, &ctx->ei);
+ KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+ ext4_set_feature_huge_file(ctx->sb);
+
+ /* 48-bit sector count with huge_file feature (still sector units) */
+ ext4_set_inode_flag(inode, EXT4_INODE_HUGE_FILE);
+ inode->i_blocks = 0xabcd12345678ULL;
+ err = ext4_inode_blocks_set_test(raw, &ctx->ei);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_HUGE_FILE));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_blocks_lo), 0x12345678U);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_blocks_high), 0xabcd);
+ KUNIT_EXPECT_EQ(test, ext4_inode_blocks_test(raw, &ctx->ei),
+ (blkcnt_t)0xabcd12345678ULL);
+
+ /* > 48-bit sector count scales by fs-block units and sets HUGE_FILE */
+ large_sectors = 0x0002000000000008ULL; /* multiple of 8 (4K / 512B) */
+ inode->i_blocks = large_sectors;
+ err = ext4_inode_blocks_set_test(raw, &ctx->ei);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_HUGE_FILE));
+ KUNIT_EXPECT_EQ(test, ext4_inode_blocks_test(raw, &ctx->ei),
+ large_sectors);
+}
+
+/* -------------------------------------------------------------------------
+ * 4. Raw Inode Serialization (ext4_fill_raw_inode) Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_fill_raw_inode(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_inode *raw = &ctx->scratch_raw.raw;
+ dev_t small_dev, large_dev;
+ int err;
+
+ /* 32-bit uid/gid, 48-bit file_acl, 64-bit disksize & iversion, projid */
+ ext4_set_feature_project(ctx->sb);
+ i_uid_write(inode, 0x12345678U);
+ i_gid_write(inode, 0x9abcdef0U);
+ ctx->ei.i_projid = make_kprojid(&init_user_ns, 0x55aa1122U);
+ ctx->ei.i_file_acl = 0x000344556677ULL;
+ ctx->ei.i_disksize = 0x0000000511223344LL;
+ inode_set_iversion_queried(inode, 0x000000078899aabbULL);
+
+ err = ext4_fill_raw_inode_test(inode, raw);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_low), 0x5678);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_high), 0x1234);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_gid_low), 0xdef0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_gid_high), 0x9abc);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_projid), 0x55aa1122U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_file_acl_lo), 0x44556677U);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_file_acl_high), 0x0003);
+ KUNIT_EXPECT_EQ(test, ext4_isize(ctx->sb, raw), 0x0000000511223344LL);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_disk_version), 0x8899aabbU);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_version_hi), 0x00000007U);
+ KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei),
+ 1);
+
+ /* Non-default projid without PROJECT feature returns -EFSCORRUPTED */
+ ext4_clear_feature_project(ctx->sb);
+ err = ext4_fill_raw_inode_test(inode, raw);
+ KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+ ctx->ei.i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+
+ /* NO_UID32 masks uid/gid high bits; HURD_COMPAT skips file_acl_high */
+ memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE);
+ raw->osd2.hurd2.h_i_mode_high = cpu_to_le16(0x7777);
+ set_opt(ctx->sb, NO_UID32);
+ set_opt2(ctx->sb, HURD_COMPAT);
+ err = ext4_fill_raw_inode_test(inode, raw);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_high), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_gid_high), 0);
+ KUNIT_EXPECT_EQ(test, raw->osd2.hurd2.h_i_mode_high,
+ cpu_to_le16(0x7777));
+ clear_opt(ctx->sb, NO_UID32);
+ clear_opt2(ctx->sb, HURD_COMPAT);
+
+ /* Deleted inode (i_dtime != 0) clears uid_high/gid_high unless orphan */
+ ctx->ei.i_dtime = 100;
+ err = ext4_fill_raw_inode_test(inode, raw);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_high), 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_gid_high), 0);
+
+ ext4_set_inode_state(inode, EXT4_STATE_ORPHAN_FILE);
+ err = ext4_fill_raw_inode_test(inode, raw);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_high), 0x1234);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_gid_high), 0x9abc);
+ ext4_clear_inode_state(inode, EXT4_STATE_ORPHAN_FILE);
+ ctx->ei.i_dtime = 0;
+
+ /* EA_INODE stores raw iversion without shifting out flag bit */
+ ext4_set_inode_flag(inode, EXT4_INODE_EA_INODE);
+ inode_set_iversion_raw(inode, 0x00000002cafebabeULL);
+ err = ext4_fill_raw_inode_test(inode, raw);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_disk_version), 0xcafebabeU);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_version_hi), 0x00000002U);
+ ext4_clear_inode_flag(inode, EXT4_INODE_EA_INODE);
+
+ /* Inline-data inode preserves raw_inode->i_block contents */
+ memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE);
+ raw->i_block[0] = cpu_to_le32(0xdeadbeefU);
+ ctx->ei.i_data[0] = cpu_to_le32(0x11111111U);
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ctx->ei.i_inline_off = 164;
+ err = ext4_fill_raw_inode_test(inode, raw);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_block[0]), 0xdeadbeefU);
+ ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ctx->ei.i_inline_off = 0;
+
+ /* Special device inode encodes old vs new rdev into i_block[0]/[1] */
+ inode->i_mode = S_IFCHR | 0600;
+ small_dev = MKDEV(8, 3);
+ inode->i_rdev = small_dev;
+ err = ext4_fill_raw_inode_test(inode, raw);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, old_decode_dev(le32_to_cpu(raw->i_block[0])),
+ small_dev);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_block[1]), 0U);
+
+ large_dev = MKDEV(512, 1024);
+ inode->i_rdev = large_dev;
+ err = ext4_fill_raw_inode_test(inode, raw);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_block[0]), 0U);
+ KUNIT_EXPECT_EQ(test, new_decode_dev(le32_to_cpu(raw->i_block[1])),
+ large_dev);
+}
+
+/* -------------------------------------------------------------------------
+ * 5. Inode Dirtying, LARGE_FILE Promotion, and Lazytime Neighbor Flush
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_mark_dirty_and_update(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct inode_test_blk *itable = inode_test_find_blk(ctx,
+ INODE_TEST_ITABLE_BLK0);
+ struct ext4_inode *raw;
+ struct ext4_inode *neighbor_raw;
+ struct inode *neighbor;
+ struct ext4_iloc iloc = {};
+ handle_t *handle;
+ int ref_before, err;
+
+ /*
+ * 1. EXT4_STATE_NEW + LARGE_FILE feature promotion in
+ * ext4_reserve_inode_write() / ext4_mark_iloc_dirty().
+ */
+ ext4_clear_feature_large_file(ctx->sb);
+ ext4_set_inode_state(inode, EXT4_STATE_NEW);
+ ctx->ei.i_disksize = 0x100000000LL;
+ inode->i_size = ctx->ei.i_disksize;
+ raw = inode_test_raw_slot(ctx, INODE_TEST_PRIMARY_INO, false);
+ memset(raw, 0xff, INODE_TEST_INODE_SIZE);
+
+ handle = ext4_journal_start(inode, EXT4_HT_INODE, 2);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ err = ext4_reserve_inode_write(handle, inode, &iloc);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ err = ext4_mark_iloc_dirty(handle, inode, &iloc);
+ ext4_journal_stop(handle);
+
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_test_inode_state(inode, EXT4_STATE_NEW));
+ KUNIT_EXPECT_TRUE(test, ext4_has_feature_large_file(ctx->sb));
+ KUNIT_EXPECT_EQ(test, ext4_isize(ctx->sb, raw), 0x100000000LL);
+ KUNIT_EXPECT_TRUE(test,
+ !!(inode_state_read_once(inode) & I_METADATA_WRITEBACK));
+
+ /*
+ * 2. SB_LAZYTIME opportunistic neighbor timestamp flush + automatic
+ * extra_isize expansion via __ext4_mark_inode_dirty().
+ */
+ inode_test_prepare_valid_raw_inode(ctx, 13, S_IFREG | 0644);
+ memcpy(itable->cache_buf, itable->disk_buf, INODE_TEST_BLK_SIZE);
+ set_buffer_uptodate(&itable->bh);
+
+ neighbor = ext4_iget(ctx->sb, 13, EXT4_IGET_NORMAL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(neighbor));
+ inode_test_track_iget_inode(test, ctx, neighbor);
+
+ inode_set_mtime(neighbor, 0x55667788LL, 1234);
+ spin_lock(&neighbor->i_lock);
+ inode_state_set(neighbor, I_DIRTY_TIME);
+ spin_unlock(&neighbor->i_lock);
+
+ ctx->sb->s_flags |= SB_LAZYTIME;
+ ctx->ei.i_extra_isize = 16;
+ ctx->sbi.s_want_extra_isize = 32;
+
+ handle = ext4_journal_start(inode, EXT4_HT_INODE, 2);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ err = __ext4_mark_inode_dirty(handle, inode, __func__, __LINE__);
+ ext4_journal_stop(handle);
+ ctx->sb->s_flags &= ~SB_LAZYTIME;
+
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_extra_isize, (u16)32);
+ KUNIT_EXPECT_FALSE(test,
+ !!(inode_state_read_once(neighbor) & I_DIRTY_TIME));
+ neighbor_raw = inode_test_raw_slot(ctx, 13, false);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(neighbor_raw->i_mtime), 0x55667788U);
+ KUNIT_EXPECT_EQ(test,
+ ext4_inode_csum_verify_test(neighbor, neighbor_raw,
+ EXT4_I(neighbor)), 1);
+
+ /* 3. ext4_dirty_inode() updates raw inode on I_DIRTY_SYNC */
+ inode_set_ctime(inode, 0x12340000LL, 0);
+ ext4_dirty_inode(inode, I_DIRTY_SYNC);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_ctime), 0x12340000U);
+
+ /* 4. Emergency RO state returns -EROFS and drops iloc.bh cleanly */
+ err = ext4_get_inode_loc(inode, &iloc);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ ref_before = atomic_read(&itable->bh.b_count);
+ set_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+ err = ext4_mark_iloc_dirty(NULL, inode, &iloc);
+ clear_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+ KUNIT_EXPECT_EQ(test, err, -EROFS);
+ KUNIT_EXPECT_EQ(test, atomic_read(&itable->bh.b_count), ref_before - 1);
+}
+
+/* -------------------------------------------------------------------------
+ * 6. Extra Isize Expansion Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_expand_extra_isize(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_iloc iloc = {};
+ struct ext4_inode *raw;
+ struct ext4_xattr_ibody_header *ihdr;
+ struct ext4_xattr_entry *entry;
+ int no_expand = 0;
+ int err;
+
+ err = ext4_get_inode_loc(inode, &iloc);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ raw = ext4_raw_inode(&iloc);
+
+ /* Corrupted current i_extra_isize (unaligned or oversize) */
+ ctx->ei.i_extra_isize = 13;
+ err = ext4_expand_extra_isize_test(inode, 32, &iloc, NULL, &no_expand);
+ KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+
+ ctx->ei.i_extra_isize = 200;
+ err = ext4_expand_extra_isize_test(inode, 204, &iloc, NULL, &no_expand);
+ KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+
+ /* Invalid target new_extra_isize */
+ ctx->ei.i_extra_isize = 16;
+ KUNIT_EXPECT_EQ(test,
+ ext4_expand_extra_isize_test(inode, 12, &iloc, NULL,
+ &no_expand), -EINVAL);
+ KUNIT_EXPECT_EQ(test,
+ ext4_expand_extra_isize_test(inode, 200, &iloc, NULL,
+ &no_expand), -EINVAL);
+
+ /* Clean expansion without xattrs zeroes newly covered extra space */
+ memset((u8 *)raw + EXT4_GOOD_OLD_INODE_SIZE + 16, 0xaa, 32);
+ err = ext4_expand_extra_isize_test(inode, 32, &iloc, NULL, &no_expand);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_extra_isize, (u16)32);
+ KUNIT_EXPECT_EQ(test, *((u8 *)raw + EXT4_GOOD_OLD_INODE_SIZE + 16), 0);
+ KUNIT_EXPECT_EQ(test, *((u8 *)raw + EXT4_GOOD_OLD_INODE_SIZE + 31), 0);
+ KUNIT_EXPECT_EQ(test, *((u8 *)raw + EXT4_GOOD_OLD_INODE_SIZE + 32),
+ 0xaa);
+
+ /* Guard checks in ext4_try_to_expand_extra_isize() */
+ ctx->ei.i_extra_isize = 16;
+ ext4_set_inode_state(inode, EXT4_STATE_NO_EXPAND);
+ KUNIT_EXPECT_EQ(test,
+ ext4_try_to_expand_extra_isize_test(inode, 32, &iloc,
+ NULL), -EOVERFLOW);
+ ext4_clear_inode_state(inode, EXT4_STATE_NO_EXPAND);
+
+ ctx->sb->s_flags &= ~SB_ACTIVE;
+ KUNIT_EXPECT_EQ(test,
+ ext4_try_to_expand_extra_isize_test(inode, 32, &iloc,
+ NULL), -EBUSY);
+ ctx->sb->s_flags |= SB_ACTIVE;
+
+ down_read(&ctx->ei.xattr_sem);
+ err = ext4_try_to_expand_extra_isize_test(inode, 32, &iloc, NULL);
+ up_read(&ctx->ei.xattr_sem);
+ KUNIT_EXPECT_EQ(test, err, -EBUSY);
+
+ /* Expansion with valid in-body xattr shifts entry forward cleanly */
+ memset(raw, 0, INODE_TEST_INODE_SIZE);
+ ctx->ei.i_extra_isize = 16;
+ raw->i_extra_isize = cpu_to_le16(16);
+ ext4_set_inode_state(inode, EXT4_STATE_XATTR);
+ ihdr = IHDR(inode, raw);
+ ihdr->h_magic = cpu_to_le32(EXT4_XATTR_MAGIC);
+ entry = IFIRST(ihdr);
+ entry->e_name_len = 1;
+ entry->e_name_index = EXT4_XATTR_INDEX_USER;
+ entry->e_name[0] = 'k';
+ entry->e_value_size = cpu_to_le32(4);
+ entry->e_value_offs = cpu_to_le16(INODE_TEST_INODE_SIZE -
+ EXT4_GOOD_OLD_INODE_SIZE - 16 -
+ sizeof(__u32) - 4);
+ memcpy((char *)IFIRST(ihdr) + le16_to_cpu(entry->e_value_offs),
+ "val!", 4);
+ inode_test_stamp_raw_csum(ctx, inode->i_ino, raw);
+
+ /* ext4_expand_extra_isize() locks xattr_sem and consumes iloc.bh */
+ err = ext4_expand_extra_isize(inode, 32, &iloc);
+
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_extra_isize, (u16)32);
+ ihdr = IHDR(inode, raw);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ihdr->h_magic),
+ (u32)EXT4_XATTR_MAGIC);
+}
+
+/* -------------------------------------------------------------------------
+ * 7. Inode Location Lookup (get_inode_loc / noio / fc_inode_loc) Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_get_inode_loc_variants(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct inode_test_blk *itable = inode_test_find_blk(ctx,
+ INODE_TEST_ITABLE_BLK0);
+ struct inode_test_blk *ibitmap = inode_test_find_blk(ctx,
+ INODE_TEST_IBITMAP_BLK);
+ struct ext4_iloc iloc = {};
+ struct ext4_inode *raw;
+ int err;
+
+ /* Out-of-range inode numbers */
+ inode->i_ino = 1;
+ KUNIT_EXPECT_EQ(test, ext4_get_inode_loc(inode, &iloc), -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, ext4_get_inode_loc_noio(inode, &iloc),
+ -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, ext4_get_fc_inode_loc(ctx->sb, 99999, &iloc),
+ -EFSCORRUPTED);
+ inode->i_ino = INODE_TEST_PRIMARY_INO;
+
+ /* Missing group descriptor -> -EIO */
+ ctx->fail_group_desc = true;
+ KUNIT_EXPECT_EQ(test, ext4_get_inode_loc(inode, &iloc), -EIO);
+ KUNIT_EXPECT_EQ(test, ext4_get_inode_loc_noio(inode, &iloc), -EIO);
+ ctx->fail_group_desc = false;
+
+ /* Invalid inode table block in group descriptor -> -EFSCORRUPTED */
+ ext4_inode_table_set(ctx->sb, &ctx->gdp, 1);
+ KUNIT_EXPECT_EQ(test, ext4_get_inode_loc(inode, &iloc), -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, ext4_get_inode_loc_noio(inode, &iloc),
+ -EFSCORRUPTED);
+ ext4_inode_table_set(ctx->sb, &ctx->gdp, INODE_TEST_ITABLE_BLK0);
+
+ /* noio variant: uncached -> -EAGAIN, non-uptodate -> -EAGAIN, hit -> 0 */
+ itable->in_cache = false;
+ KUNIT_EXPECT_EQ(test, ext4_get_inode_loc_noio(inode, &iloc), -EAGAIN);
+ itable->in_cache = true;
+ clear_buffer_uptodate(&itable->bh);
+ KUNIT_EXPECT_EQ(test, ext4_get_inode_loc_noio(inode, &iloc), -EAGAIN);
+ KUNIT_EXPECT_EQ(test, atomic_read(&itable->bh.b_count), 1);
+
+ set_buffer_uptodate(&itable->bh);
+ err = ext4_get_inode_loc_noio(inode, &iloc);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, iloc.offset,
+ (unsigned long)((INODE_TEST_PRIMARY_INO - 1) *
+ INODE_TEST_INODE_SIZE));
+ brelse(iloc.bh);
+
+ /*
+ * Skip-I/O optimization: non-uptodate itable block + uptodate inode
+ * bitmap with ONLY primary inode bit set -> zero I/O reads!
+ */
+ clear_buffer_uptodate(&itable->bh);
+ memset(itable->disk_buf, 0x77, INODE_TEST_BLK_SIZE);
+ memset(ibitmap->cache_buf, 0, INODE_TEST_BLK_SIZE);
+ ext4_set_bit(INODE_TEST_PRIMARY_INO - 1, ibitmap->cache_buf);
+ set_buffer_uptodate(&ibitmap->bh);
+ ctx->read_nowait_calls = 0;
+ i_uid_write(inode, 0x4242);
+
+ err = ext4_get_inode_loc(inode, &iloc);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 0U);
+ raw = ext4_raw_inode(&iloc);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_low), 0x4242);
+ /* Neighbor slot was zeroed rather than read from 0x77 disk_buf */
+ KUNIT_EXPECT_EQ(test, (u8)itable->cache_buf[0], 0);
+ brelse(iloc.bh);
+
+ /*
+ * Neighbor inode bit also set in bitmap -> cannot skip I/O; performs
+ * disk read and triggers inode table readahead window calculation.
+ */
+ clear_buffer_uptodate(&itable->bh);
+ ext4_set_bit(INODE_TEST_PRIMARY_INO, ibitmap->cache_buf);
+ ctx->sbi.s_inode_readahead_blks = 4;
+ ctx->read_nowait_calls = 0;
+ ctx->readahead_calls = 0;
+
+ err = ext4_get_inode_loc(inode, &iloc);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 1U);
+ KUNIT_EXPECT_GT(test, ctx->readahead_calls, 0U);
+ KUNIT_EXPECT_EQ(test, ctx->first_readahead_blk,
+ (ext4_fsblk_t)INODE_TEST_ITABLE_BLK0);
+ KUNIT_EXPECT_EQ(test, (u8)itable->cache_buf[0], 0x77);
+ brelse(iloc.bh);
+ ctx->sbi.s_inode_readahead_blks = 0;
+
+ /* Disk read -EIO propagates cleanly with balanced bh refcount */
+ clear_buffer_uptodate(&itable->bh);
+ ctx->fail_read_eio = true;
+ err = ext4_get_fc_inode_loc(ctx->sb, INODE_TEST_PRIMARY_INO, &iloc);
+ ctx->fail_read_eio = false;
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_EQ(test, atomic_read(&itable->bh.b_count), 1);
+}
+
+/* -------------------------------------------------------------------------
+ * 8. __ext4_iget() Valid Modes, Caching, & Special Inode Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_iget_validation_and_modes(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct ext4_inode *raw;
+ struct inode *inode, *cached;
+
+ /* Illegal & special ino checks before VFS lookup */
+ KUNIT_EXPECT_EQ(test,
+ PTR_ERR(ext4_iget(ctx->sb, 0, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test,
+ PTR_ERR(ext4_iget(ctx->sb, 0, EXT4_IGET_HANDLE)),
+ -ESTALE);
+ KUNIT_EXPECT_EQ(test,
+ PTR_ERR(ext4_iget(ctx->sb, 8, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test,
+ PTR_ERR(ext4_iget(ctx->sb, 99999, EXT4_IGET_HANDLE)),
+ -ESTALE);
+
+ /* EXT4_IGET_NOWAIT returns -ENOENT when inode is not cached */
+ KUNIT_EXPECT_EQ(test,
+ PTR_ERR(ext4_iget(ctx->sb, 14, EXT4_IGET_NOWAIT)),
+ -ENOENT);
+
+ /* Valid regular file inode load & cached NOWAIT / check_igot_inode */
+ ext4_set_feature_project(ctx->sb);
+ inode_test_prepare_valid_raw_inode(ctx, 14, S_IFREG | 0640);
+ raw = inode_test_raw_slot(ctx, 14, true);
+ raw->i_uid_low = cpu_to_le16(0x2222);
+ raw->i_uid_high = cpu_to_le16(0x1111);
+ raw->i_gid_low = cpu_to_le16(0x4444);
+ raw->i_gid_high = cpu_to_le16(0x3333);
+ raw->i_projid = cpu_to_le32(77);
+ raw->i_size_lo = cpu_to_le32(8192);
+ raw->i_disk_version = cpu_to_le32(9);
+ raw->i_version_hi = cpu_to_le32(2);
+ inode_test_stamp_raw_csum(ctx, 14, raw);
+
+ inode = ext4_iget(ctx->sb, 14, EXT4_IGET_NORMAL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(inode));
+ inode_test_track_iget_inode(test, ctx, inode);
+ KUNIT_EXPECT_EQ(test, i_uid_read(inode), 0x11112222U);
+ KUNIT_EXPECT_EQ(test, i_gid_read(inode), 0x33334444U);
+ KUNIT_EXPECT_EQ(test, from_kprojid(&init_user_ns, EXT4_I(inode)->i_projid),
+ 77U);
+ KUNIT_EXPECT_EQ(test, i_size_read(inode), 8192LL);
+ KUNIT_EXPECT_EQ(test, inode_peek_iversion(inode),
+ (0x2ULL << 32) | 9ULL);
+
+ cached = ext4_iget(ctx->sb, 14, EXT4_IGET_NOWAIT);
+ KUNIT_ASSERT_PTR_EQ(test, cached, inode);
+ iput(cached);
+
+ /* Cached non-EA inode rejected when EXT4_IGET_EA_INODE requested */
+ cached = ext4_iget(ctx->sb, 14, EXT4_IGET_EA_INODE);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(cached), -EFSCORRUPTED);
+
+ /* Valid fast symlink sets cached i_link */
+ inode_test_prepare_valid_raw_inode(ctx, 15, S_IFLNK | 0777);
+ raw = inode_test_raw_slot(ctx, 15, true);
+ raw->i_flags = 0;
+ raw->i_size_lo = cpu_to_le32(11);
+ memcpy(raw->i_block, "target/path", 11);
+ inode_test_stamp_raw_csum(ctx, 15, raw);
+
+ inode = ext4_iget(ctx->sb, 15, EXT4_IGET_NORMAL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(inode));
+ inode_test_track_iget_inode(test, ctx, inode);
+ KUNIT_EXPECT_STREQ(test, inode->i_link, "target/path");
+
+ /* Valid character & block device special inodes (old & new rdev) */
+ inode_test_prepare_valid_raw_inode(ctx, 16, S_IFCHR | 0600);
+ raw = inode_test_raw_slot(ctx, 16, true);
+ raw->i_block[0] = cpu_to_le32(old_encode_dev(MKDEV(4, 64)));
+ inode_test_stamp_raw_csum(ctx, 16, raw);
+
+ inode = ext4_iget(ctx->sb, 16, EXT4_IGET_NORMAL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(inode));
+ inode_test_track_iget_inode(test, ctx, inode);
+ KUNIT_EXPECT_EQ(test, inode->i_rdev, MKDEV(4, 64));
+
+ inode_test_prepare_valid_raw_inode(ctx, 17, S_IFBLK | 0600);
+ raw = inode_test_raw_slot(ctx, 17, true);
+ raw->i_block[0] = 0;
+ raw->i_block[1] = cpu_to_le32(new_encode_dev(MKDEV(300, 700)));
+ inode_test_stamp_raw_csum(ctx, 17, raw);
+
+ inode = ext4_iget(ctx->sb, 17, EXT4_IGET_NORMAL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(inode));
+ inode_test_track_iget_inode(test, ctx, inode);
+ KUNIT_EXPECT_EQ(test, inode->i_rdev, MKDEV(300, 700));
+
+ /* Uninitialized EXT4_BOOT_LOADER_INO requires EXT4_IGET_BAD */
+ inode_test_prepare_valid_raw_inode(ctx, EXT4_BOOT_LOADER_INO, 0);
+ raw = inode_test_raw_slot(ctx, EXT4_BOOT_LOADER_INO, true);
+ raw->i_links_count = 0;
+ inode_test_stamp_raw_csum(ctx, EXT4_BOOT_LOADER_INO, raw);
+
+ inode = ext4_iget(ctx->sb, EXT4_BOOT_LOADER_INO, EXT4_IGET_SPECIAL);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(inode), -EFSCORRUPTED);
+
+ inode_test_prepare_valid_raw_inode(ctx, EXT4_BOOT_LOADER_INO, 0);
+ raw = inode_test_raw_slot(ctx, EXT4_BOOT_LOADER_INO, true);
+ raw->i_links_count = 0;
+ inode_test_stamp_raw_csum(ctx, EXT4_BOOT_LOADER_INO, raw);
+
+ inode = ext4_iget(ctx->sb, EXT4_BOOT_LOADER_INO,
+ EXT4_IGET_SPECIAL | EXT4_IGET_BAD);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(inode));
+ inode_test_track_iget_inode(test, ctx, inode);
+ KUNIT_EXPECT_TRUE(test, is_bad_inode(inode));
+ cached = ext4_iget(ctx->sb, EXT4_BOOT_LOADER_INO, EXT4_IGET_SPECIAL);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(cached), -EFSCORRUPTED);
+}
+
+/* -------------------------------------------------------------------------
+ * 9. __ext4_iget() On-Disk Corruption Rejection Matrix
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_iget_corruptions(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct ext4_inode *raw;
+ struct inode *inode;
+
+ /* 1. Unallocated inode with EXT4_IGET_HANDLE -> -ESTALE */
+ inode_test_prepare_valid_raw_inode(ctx, 18, 0);
+ raw = inode_test_raw_slot(ctx, 18, true);
+ raw->i_links_count = 0;
+ inode_test_stamp_raw_csum(ctx, 18, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 18, EXT4_IGET_HANDLE)),
+ -ESTALE);
+
+ /* 2. Unaligned i_extra_isize -> -EFSCORRUPTED */
+ inode_test_prepare_valid_raw_inode(ctx, 19, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 19, true);
+ raw->i_extra_isize = cpu_to_le16(5);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 19, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+
+ /* 3. Oversize i_extra_isize -> -EFSCORRUPTED */
+ inode_test_prepare_valid_raw_inode(ctx, 20, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 20, true);
+ raw->i_extra_isize = cpu_to_le16(132);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 20, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+
+ /* 4. Bad checksum -> -EFSBADCRC (and EXT4_FC_REPLAY bypasses it) */
+ inode_test_prepare_valid_raw_inode(ctx, 21, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 21, true);
+ raw->i_checksum_lo ^= cpu_to_le16(0x00ff);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 21, EXT4_IGET_NORMAL)),
+ -EFSBADCRC);
+
+ inode_test_prepare_valid_raw_inode(ctx, 21, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 21, true);
+ raw->i_checksum_lo ^= cpu_to_le16(0x00ff);
+ ctx->sbi.s_mount_state |= EXT4_FC_REPLAY;
+ inode = ext4_iget(ctx->sb, 21, EXT4_IGET_NORMAL);
+ ctx->sbi.s_mount_state &= ~EXT4_FC_REPLAY;
+ KUNIT_ASSERT_FALSE(test, IS_ERR(inode));
+ inode_test_track_iget_inode(test, ctx, inode);
+
+ /* 5. Deleted inode (nlink=0) -> -ESTALE normally, allowed in ORPHAN_FS */
+ inode_test_prepare_valid_raw_inode(ctx, 22, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 22, true);
+ raw->i_links_count = 0;
+ inode_test_stamp_raw_csum(ctx, 22, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 22, EXT4_IGET_NORMAL)),
+ -ESTALE);
+
+ inode_test_prepare_valid_raw_inode(ctx, 22, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 22, true);
+ raw->i_links_count = 0;
+ inode_test_stamp_raw_csum(ctx, 22, raw);
+ ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS;
+ inode = ext4_iget(ctx->sb, 22, EXT4_IGET_NORMAL);
+ ctx->sbi.s_mount_state &= ~EXT4_ORPHAN_FS;
+ KUNIT_ASSERT_FALSE(test, IS_ERR(inode));
+ inode_test_put_tracked_inode(ctx, inode);
+
+ /* 6. Unallocated special inode -> -EFSCORRUPTED */
+ inode_test_prepare_valid_raw_inode(ctx, 7, S_IFREG | 0600);
+ raw = inode_test_raw_slot(ctx, 7, true);
+ raw->i_links_count = 0;
+ inode_test_stamp_raw_csum(ctx, 7, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 7, EXT4_IGET_SPECIAL)),
+ -EFSCORRUPTED);
+
+ /* 7. Conflicting INLINE_DATA | EXTENTS flags -> -EFSCORRUPTED */
+ inode_test_prepare_valid_raw_inode(ctx, 23, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 23, true);
+ raw->i_flags = cpu_to_le32(EXT4_INLINE_DATA_FL | EXT4_EXTENTS_FL);
+ inode_test_stamp_raw_csum(ctx, 23, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 23, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+
+ /* 8. Indirect file i_size exceeding s_bitmap_maxbytes -> -EFSCORRUPTED */
+ inode_test_prepare_valid_raw_inode(ctx, 24, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 24, true);
+ raw->i_flags = 0;
+ memset(raw->i_block, 0, sizeof(raw->i_block));
+ raw->i_size_high = cpu_to_le32(0x1000);
+ inode_test_stamp_raw_csum(ctx, 24, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 24, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+
+ /* 9. Directory with INDEX flag when dir_index disabled -> -EFSCORRUPTED */
+ ext4_clear_feature_dir_index(ctx->sb);
+ inode_test_prepare_valid_raw_inode(ctx, 25, S_IFDIR | 0755);
+ raw = inode_test_raw_slot(ctx, 25, true);
+ raw->i_flags |= cpu_to_le32(EXT4_INDEX_FL);
+ inode_test_stamp_raw_csum(ctx, 25, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 25, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+ ext4_set_feature_dir_index(ctx->sb);
+
+ /* 10. Out-of-range i_file_acl block -> -EFSCORRUPTED */
+ inode_test_prepare_valid_raw_inode(ctx, 26, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 26, true);
+ raw->i_file_acl_lo = cpu_to_le32(INODE_TEST_TOTAL_BLKS + 100);
+ inode_test_stamp_raw_csum(ctx, 26, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 26, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+
+ /* 11. Corrupted extent header magic -> -EFSCORRUPTED */
+ inode_test_prepare_valid_raw_inode(ctx, 27, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 27, true);
+ raw->i_block[0] = 0;
+ inode_test_stamp_raw_csum(ctx, 27, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 27, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+
+ /* 12. Symlink with IMMUTABLE flag & fast symlink with embedded NUL */
+ inode_test_prepare_valid_raw_inode(ctx, 28, S_IFLNK | 0777);
+ raw = inode_test_raw_slot(ctx, 28, true);
+ raw->i_flags = cpu_to_le32(EXT4_IMMUTABLE_FL);
+ raw->i_size_lo = cpu_to_le32(3);
+ memcpy(raw->i_block, "abc", 3);
+ inode_test_stamp_raw_csum(ctx, 28, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 28, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+
+ inode_test_prepare_valid_raw_inode(ctx, 28, S_IFLNK | 0777);
+ raw = inode_test_raw_slot(ctx, 28, true);
+ raw->i_flags = 0;
+ raw->i_size_lo = cpu_to_le32(5);
+ memcpy(raw->i_block, "ab\0cd", 5);
+ inode_test_stamp_raw_csum(ctx, 28, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 28, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+
+ /* 13. Bogus i_mode (0) with nlink > 0 & CASEFOLD without feature */
+ inode_test_prepare_valid_raw_inode(ctx, 29, 0);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 29, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+
+ inode_test_prepare_valid_raw_inode(ctx, 30, S_IFDIR | 0755);
+ raw = inode_test_raw_slot(ctx, 30, true);
+ raw->i_flags |= cpu_to_le32(EXT4_CASEFOLD_FL);
+ inode_test_stamp_raw_csum(ctx, 30, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 30, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+
+ /* 14. EA_INODE opened without EXT4_IGET_EA_INODE: -ESTALE vs -EFSCORRUPTED */
+ inode_test_prepare_valid_raw_inode(ctx, 31, S_IFREG | 0600);
+ raw = inode_test_raw_slot(ctx, 31, true);
+ raw->i_flags |= cpu_to_le32(EXT4_EA_INODE_FL);
+ inode_test_stamp_raw_csum(ctx, 31, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 31, EXT4_IGET_HANDLE)),
+ -ESTALE);
+
+ inode_test_prepare_valid_raw_inode(ctx, 31, S_IFREG | 0600);
+ raw = inode_test_raw_slot(ctx, 31, true);
+ raw->i_flags |= cpu_to_le32(EXT4_EA_INODE_FL);
+ inode_test_stamp_raw_csum(ctx, 31, raw);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 31, EXT4_IGET_NORMAL)),
+ -EFSCORRUPTED);
+}
+
+/* -------------------------------------------------------------------------
+ * 10. Fast Symlink Classification & ext4_can_truncate() Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_fast_symlink_and_can_truncate(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+
+ /* Fast symlink classification with/without external xattr & EA_INODE */
+ inode_test_reset_mock_inode(ctx, S_IFLNK | 0777);
+ inode->i_size = 20;
+ inode->i_blocks = 0;
+ KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 1);
+
+ /* External xattr block accounts for 8 512B sectors when !EA_INODE */
+ ctx->ei.i_file_acl = INODE_TEST_META_BLK0;
+ inode->i_blocks = 8;
+ KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 1);
+ inode->i_blocks = 16;
+ KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 0);
+
+ /* With EA_INODE feature, fast symlink depends on 0 < i_size < 60 */
+ ext4_set_feature_ea_inode(ctx->sb);
+ inode->i_size = 0;
+ KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 0);
+ inode->i_size = 20;
+ KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 1);
+ inode->i_size = EXT4_N_BLOCKS * 4;
+ KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 0);
+ ext4_clear_feature_ea_inode(ctx->sb);
+
+ /* Inline-data symlink is not classified as a legacy fast symlink */
+ inode->i_size = 20;
+ inode->i_blocks = 8;
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ctx->ei.i_inline_off = 164;
+ KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 0);
+
+ /* ext4_can_truncate() across file types and fast vs slow symlinks */
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 1);
+
+ inode_test_reset_mock_inode(ctx, S_IFDIR | 0755);
+ KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 1);
+
+ inode_test_reset_mock_inode(ctx, S_IFIFO | 0600);
+ KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 0);
+
+ inode_test_reset_mock_inode(ctx, S_IFCHR | 0600);
+ KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 0);
+
+ inode_test_reset_mock_inode(ctx, S_IFLNK | 0777);
+ inode->i_blocks = 0;
+ KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 0);
+ inode->i_blocks = 8;
+ KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 1);
+}
+
+/* -------------------------------------------------------------------------
+ * 11. VFS Inode Flags, Address Space Ops, and Folio Order Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_flags_aops_mapping_order(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ const struct address_space_operations *da_aops, *nodelalloc_aops;
+ const struct address_space_operations *jdata_aops;
+
+ /* 1. ext4_set_inode_flags() flag translation & S_DAX policy */
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ ctx->ei.i_flags = EXT4_SYNC_FL | EXT4_APPEND_FL | EXT4_IMMUTABLE_FL |
+ EXT4_NOATIME_FL | EXT4_DIRSYNC_FL | EXT4_VERITY_FL;
+ ext4_set_inode_flags(inode, true);
+ KUNIT_EXPECT_EQ(test,
+ inode->i_flags & (S_SYNC | S_APPEND | S_IMMUTABLE |
+ S_NOATIME | S_DIRSYNC | S_VERITY),
+ (unsigned int)(S_SYNC | S_APPEND | S_IMMUTABLE |
+ S_NOATIME | S_DIRSYNC | S_VERITY));
+
+ ctx->ei.i_flags = 0;
+ ext4_set_inode_flags(inode, false);
+ KUNIT_EXPECT_EQ(test,
+ inode->i_flags & (S_SYNC | S_APPEND | S_IMMUTABLE |
+ S_NOATIME | S_DIRSYNC), 0U);
+
+ /* S_DAX enabled when BDEV_IS_DAX + DAX_ALWAYS on regular extent file */
+ set_bit(EXT4_FLAGS_BDEV_IS_DAX, &ctx->sbi.s_ext4_flags);
+ set_opt(ctx->sb, DAX_ALWAYS);
+ ext4_set_inode_flags(inode, true);
+ if (IS_ENABLED(CONFIG_FS_DAX))
+ KUNIT_EXPECT_TRUE(test, IS_DAX(inode));
+
+ /* DAX_NEVER and JOURNAL_DATA both suppress S_DAX */
+ clear_opt(ctx->sb, DAX_ALWAYS);
+ set_opt2(ctx->sb, DAX_NEVER);
+ inode->i_flags &= ~S_DAX;
+ ext4_set_inode_flags(inode, true);
+ KUNIT_EXPECT_FALSE(test, IS_DAX(inode));
+ clear_opt2(ctx->sb, DAX_NEVER);
+
+ clear_opt(ctx->sb, DELALLOC);
+ ctx->ei.i_flags = EXT4_DAX_FL | EXT4_JOURNAL_DATA_FL;
+ ctx->sbi.s_journal = (journal_t *)1;
+ ext4_set_inode_flags(inode, true);
+ ctx->sbi.s_journal = NULL;
+ set_opt(ctx->sb, DELALLOC);
+ KUNIT_EXPECT_FALSE(test, IS_DAX(inode));
+ clear_bit(EXT4_FLAGS_BDEV_IS_DAX, &ctx->sbi.s_ext4_flags);
+
+ /* 2. ext4_set_aops() dispatch across delalloc, ordered, journalled */
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ set_opt(ctx->sb, DELALLOC);
+ ext4_set_aops(inode);
+ da_aops = inode->i_mapping->a_ops;
+ KUNIT_EXPECT_NOT_NULL(test, da_aops);
+
+ clear_opt(ctx->sb, DELALLOC);
+ ext4_set_aops(inode);
+ nodelalloc_aops = inode->i_mapping->a_ops;
+ KUNIT_EXPECT_PTR_NE(test, da_aops, nodelalloc_aops);
+
+ ext4_set_inode_flag(inode, EXT4_INODE_JOURNAL_DATA);
+ ctx->sbi.s_journal = (journal_t *)1;
+ ext4_set_aops(inode);
+ ctx->sbi.s_journal = NULL;
+ jdata_aops = inode->i_mapping->a_ops;
+ KUNIT_EXPECT_PTR_NE(test, jdata_aops, da_aops);
+ KUNIT_EXPECT_PTR_NE(test, jdata_aops, nodelalloc_aops);
+ set_opt(ctx->sb, DELALLOC);
+
+ /* 3. ext4_set_inode_mapping_order() clamping */
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ ctx->sbi.s_min_folio_order = 0;
+ ctx->sbi.s_max_folio_order = 3;
+ ext4_set_inode_mapping_order(inode);
+ if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE)) {
+ KUNIT_EXPECT_EQ(test,
+ mapping_min_folio_order(inode->i_mapping), 0U);
+ KUNIT_EXPECT_EQ(test,
+ mapping_max_folio_order(inode->i_mapping), 3U);
+
+ ctx->sbi.s_min_folio_order = 1;
+ ext4_set_inode_flag(inode, EXT4_INODE_JOURNAL_DATA);
+ ext4_set_inode_mapping_order(inode);
+ KUNIT_EXPECT_EQ(test,
+ mapping_max_folio_order(inode->i_mapping), 1U);
+ }
+ ctx->sbi.s_min_folio_order = 0;
+ ctx->sbi.s_max_folio_order = 0;
+}
+
+/* -------------------------------------------------------------------------
+ * 12. Project ID, DIO Alignment, and getattr / file_getattr Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_projid_dio_and_getattr(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct path path = { .dentry = &ctx->root_dentry };
+ struct kstat stat = {};
+ kprojid_t projid = {};
+ int err;
+
+ /* ext4_get_projid(): -EOPNOTSUPP without feature, value with feature */
+ ctx->ei.i_projid = make_kprojid(&init_user_ns, 314);
+ ext4_clear_feature_project(ctx->sb);
+ KUNIT_EXPECT_EQ(test, ext4_get_projid(inode, &projid), -EOPNOTSUPP);
+ ext4_set_feature_project(ctx->sb);
+ KUNIT_EXPECT_EQ(test, ext4_get_projid(inode, &projid), 0);
+ KUNIT_EXPECT_EQ(test, from_kprojid(&init_user_ns, projid), 314U);
+
+ /* ext4_dio_alignment(): 1 default, blocksize encrypted, 0 jdata/inline */
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ KUNIT_EXPECT_EQ(test, ext4_dio_alignment(inode), 1U);
+
+ inode->i_flags |= S_ENCRYPTED;
+ KUNIT_EXPECT_EQ(test, ext4_dio_alignment(inode),
+ (u32)INODE_TEST_BLK_SIZE);
+ inode->i_flags &= ~S_ENCRYPTED;
+
+ clear_opt(ctx->sb, DELALLOC);
+ ext4_set_inode_flag(inode, EXT4_INODE_JOURNAL_DATA);
+ ctx->sbi.s_journal = (journal_t *)1;
+ KUNIT_EXPECT_EQ(test, ext4_dio_alignment(inode), 0U);
+ ctx->sbi.s_journal = NULL;
+ ext4_clear_inode_flag(inode, EXT4_INODE_JOURNAL_DATA);
+ set_opt(ctx->sb, DELALLOC);
+
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ctx->ei.i_inline_off = 164;
+ KUNIT_EXPECT_EQ(test, ext4_dio_alignment(inode), 0U);
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ctx->ei.i_inline_off = 0;
+
+ /* ext4_getattr() omits delalloc; ext4_file_getattr() includes it */
+ inode->i_size = 8192;
+ inode->i_blocks = 16;
+ ctx->ei.i_reserved_data_blocks = 2;
+ ctx->ei.i_crtime.tv_sec = 0x56781234LL;
+ ctx->ei.i_crtime.tv_nsec = 999L;
+ ctx->ei.i_flags |= EXT4_APPEND_FL | EXT4_IMMUTABLE_FL | EXT4_NODUMP_FL;
+ ctx->sbi.s_awu_min = 4096;
+ ctx->sbi.s_awu_max = 16384;
+
+ err = ext4_getattr(&nop_mnt_idmap, &path, &stat,
+ STATX_BASIC_STATS | STATX_BTIME |
+ STATX_DIOALIGN | STATX_WRITE_ATOMIC, 0);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, stat.blocks, 16ULL);
+
+ memset(&stat, 0, sizeof(stat));
+ err = ext4_file_getattr(&nop_mnt_idmap, &path, &stat,
+ STATX_BASIC_STATS | STATX_BTIME |
+ STATX_DIOALIGN | STATX_WRITE_ATOMIC, 0);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, !!(stat.result_mask & STATX_BTIME));
+ KUNIT_EXPECT_EQ(test, stat.btime.tv_sec, 0x56781234LL);
+ KUNIT_EXPECT_EQ(test, stat.btime.tv_nsec, 999L);
+ /* 16 on-disk sectors + 2 reserved 4K blocks (16 sectors) = 32 sectors */
+ KUNIT_EXPECT_EQ(test, stat.blocks, 32ULL);
+ KUNIT_EXPECT_EQ(test, stat.attributes & (STATX_ATTR_APPEND |
+ STATX_ATTR_IMMUTABLE |
+ STATX_ATTR_NODUMP),
+ (u64)(STATX_ATTR_APPEND | STATX_ATTR_IMMUTABLE |
+ STATX_ATTR_NODUMP));
+ KUNIT_EXPECT_EQ(test, stat.dio_mem_align, 512U);
+ KUNIT_EXPECT_EQ(test, stat.dio_offset_align, 512U);
+ KUNIT_EXPECT_EQ(test, stat.atomic_write_unit_min, 4096U);
+ KUNIT_EXPECT_EQ(test, stat.atomic_write_unit_max, 16384U);
+
+ /* Inline-data file reports simulated 512B sectors from i_size */
+ ctx->ei.i_reserved_data_blocks = 0;
+ inode->i_blocks = 0;
+ inode->i_size = 1200;
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ctx->ei.i_inline_off = 164;
+ memset(&stat, 0, sizeof(stat));
+ err = ext4_file_getattr(&nop_mnt_idmap, &path, &stat,
+ STATX_BASIC_STATS, 0);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, stat.blocks, 3ULL);
+ ctx->sbi.s_awu_min = 0;
+ ctx->sbi.s_awu_max = 0;
+}
+
+/* -------------------------------------------------------------------------
+ * 13. Delayed Allocation Reservation, Claim, Release & Nonda Switch Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_da_reserve_and_release(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ int err;
+
+ percpu_counter_set(&ctx->sbi.s_freeclusters_counter, 100);
+ percpu_counter_set(&ctx->sbi.s_dirtyclusters_counter, 0);
+
+ /* Reserve 5 clusters */
+ err = ext4_da_reserve_space_test(inode, 5);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 5U);
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum(&ctx->sbi.s_dirtyclusters_counter),
+ 5LL);
+
+ /* Claim 2 clusters via ext4_da_update_reserve_space() */
+ ext4_da_update_reserve_space(inode, 2, 1);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 3U);
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum(&ctx->sbi.s_dirtyclusters_counter),
+ 3LL);
+
+ /* Release 2 clusters via ext4_da_release_space() */
+ ext4_da_release_space(inode, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 3U);
+ ext4_da_release_space(inode, 2);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U);
+
+ /* Underflow in update_reserve_space clamps to 0 cleanly */
+ kunit_warning_suppress(test) {
+ ext4_da_update_reserve_space(inode, 4, 0);
+ }
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 0U);
+
+ /* Underflow in release_space clamps to 0 cleanly */
+ err = ext4_da_reserve_space_test(inode, 1);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ kunit_warning_suppress(test) {
+ ext4_da_release_space(inode, 3);
+ }
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 0U);
+
+ /* Exhausted free clusters -> -ENOSPC & ext4_nonda_switch() */
+ KUNIT_EXPECT_EQ(test, ext4_nonda_switch_test(ctx->sb), 0);
+ percpu_counter_set(&ctx->sbi.s_freeclusters_counter, 2);
+ percpu_counter_set(&ctx->sbi.s_dirtyclusters_counter, 4);
+ KUNIT_EXPECT_EQ(test, ext4_nonda_switch_test(ctx->sb), 1);
+
+ percpu_counter_set(&ctx->sbi.s_freeclusters_counter, 0);
+ percpu_counter_set(&ctx->sbi.s_dirtyclusters_counter, 0);
+ KUNIT_EXPECT_EQ(test, ext4_da_reserve_space_test(inode, 1), -ENOSPC);
+}
+
+/* -------------------------------------------------------------------------
+ * 14. Bigalloc Delayed Allocation & Cluster Sharing Accounting Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_enable_bigalloc(struct inode_test_ctx *ctx,
+ unsigned int cluster_ratio)
+{
+ ext4_set_feature_bigalloc(ctx->sb);
+ ctx->sbi.s_cluster_ratio = cluster_ratio;
+ ctx->sbi.s_cluster_bits = ilog2(cluster_ratio);
+ ctx->sbi.s_clusters_per_group =
+ ctx->sbi.s_blocks_per_group / cluster_ratio;
+}
+
+static void inode_test_insert_leaf_extent(struct inode *inode, ext4_lblk_t lblk,
+ u16 len, ext4_fsblk_t pblk,
+ bool unwritten)
+{
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ unsigned int idx = le16_to_cpu(eh->eh_entries);
+ struct ext4_extent *ex = EXT_FIRST_EXTENT(eh) + idx;
+
+ ex->ee_block = cpu_to_le32(lblk);
+ ex->ee_len = cpu_to_le16(len);
+ if (unwritten)
+ ext4_ext_mark_unwritten(ex);
+ ext4_ext_store_pblock(ex, pblk);
+ eh->eh_entries = cpu_to_le16(idx + 1);
+ ext4_es_remove_extent(inode, lblk, len);
+}
+
+static void inode_test_da_bigalloc_clusters(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_map_blocks map = {};
+ struct buffer_head bh = {};
+ int state_unalloc, state_delayed, state_mapped;
+ int err;
+
+ inode_test_enable_bigalloc(ctx, 4);
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ /* Pre-populate written extent at lblk 4 (lclu 1) -> pblk 104 */
+ inode_test_insert_leaf_extent(inode, 4, 1, INODE_TEST_DATA_BLK0 + 4,
+ false);
+ /* Pre-populate unwritten extent at lblk 8 (lclu 2) -> pblk 108 */
+ inode_test_insert_leaf_extent(inode, 8, 2, 108, true);
+
+ down_write(&ctx->ei.i_data_sem);
+ state_unalloc = ext4_clu_alloc_state_test(inode, 0);
+ state_mapped = ext4_clu_alloc_state_test(inode, 5);
+ up_write(&ctx->ei.i_data_sem);
+ KUNIT_EXPECT_EQ(test, state_unalloc, 0);
+ KUNIT_EXPECT_EQ(test, state_mapped, 2);
+
+ /* 1. First block in unallocated cluster 0 reserves 1 cluster */
+ memset(&bh, 0, sizeof(bh));
+ bh.b_size = INODE_TEST_BLK_SIZE;
+ err = ext4_da_get_block_prep(inode, 0, &bh, 1);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, buffer_delay(&bh));
+ KUNIT_EXPECT_TRUE(test, buffer_new(&bh));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U);
+
+ down_write(&ctx->ei.i_data_sem);
+ state_delayed = ext4_clu_alloc_state_test(inode, 2);
+ up_write(&ctx->ei.i_data_sem);
+ KUNIT_EXPECT_EQ(test, state_delayed, 1);
+
+ /* 2. Sibling block in cluster 0 shares existing delalloc reservation */
+ memset(&bh, 0, sizeof(bh));
+ bh.b_size = INODE_TEST_BLK_SIZE;
+ err = ext4_da_get_block_prep(inode, 1, &bh, 1);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, buffer_delay(&bh));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U);
+
+ /* Multi-block ext4_da_map_blocks() across [lblk 2..3] in cluster 0 */
+ map = (struct ext4_map_blocks){ .m_lblk = 2, .m_len = 2 };
+ err = ext4_da_map_blocks_test(inode, &map);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, map.m_len, 2U);
+ KUNIT_EXPECT_TRUE(test, !!(map.m_flags & EXT4_MAP_DELAYED));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U);
+
+ map = (struct ext4_map_blocks){ .m_lblk = 0, .m_len = 4 };
+ err = ext4_da_map_blocks_test(inode, &map);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, map.m_len, 4U);
+ KUNIT_EXPECT_TRUE(test, !!(map.m_flags & EXT4_MAP_DELAYED));
+
+ /*
+ * 3. Delayed block in cluster 1 (sharing written lblk 4) reserves 0 new
+ * clusters and records a pending reservation on cluster 1!
+ */
+ memset(&bh, 0, sizeof(bh));
+ bh.b_size = INODE_TEST_BLK_SIZE;
+ err = ext4_da_get_block_prep(inode, 5, &bh, 1);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, buffer_delay(&bh));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U);
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 4));
+
+ /* 4. Already-written block 4 maps directly without BH_Delay */
+ memset(&bh, 0, sizeof(bh));
+ bh.b_size = INODE_TEST_BLK_SIZE;
+ err = ext4_da_get_block_prep(inode, 4, &bh, 1);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, buffer_mapped(&bh));
+ KUNIT_EXPECT_FALSE(test, buffer_delay(&bh));
+ KUNIT_EXPECT_EQ(test, (ext4_fsblk_t)bh.b_blocknr,
+ (ext4_fsblk_t)(INODE_TEST_DATA_BLK0 + 4));
+
+ /*
+ * 5. Unwritten block 8 sets BH_Unwritten | BH_New | BH_Mapped;
+ * delayed sibling hole block 10 in the same cluster 2 records a
+ * pending reservation without reserving an extra cluster.
+ */
+ memset(&bh, 0, sizeof(bh));
+ bh.b_size = INODE_TEST_BLK_SIZE;
+ err = ext4_da_get_block_prep(inode, 8, &bh, 1);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, buffer_unwritten(&bh));
+ KUNIT_EXPECT_TRUE(test, buffer_new(&bh));
+ KUNIT_EXPECT_TRUE(test, buffer_mapped(&bh));
+ KUNIT_EXPECT_EQ(test, (ext4_fsblk_t)bh.b_blocknr, 108ULL);
+ KUNIT_EXPECT_FALSE(test, ext4_is_pending(inode, 8));
+
+ memset(&bh, 0, sizeof(bh));
+ bh.b_size = INODE_TEST_BLK_SIZE;
+ err = ext4_da_get_block_prep(inode, 10, &bh, 1);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, buffer_delay(&bh));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U);
+ KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 8));
+
+ /* Removing ES extents releases the 1 reserved cluster via da_release */
+ down_write(&ctx->ei.i_data_sem);
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ up_write(&ctx->ei.i_data_sem);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 0U);
+}
+
+/* -------------------------------------------------------------------------
+ * 15. ext4_map_blocks() Lookup, Caching, Allocation, Zeroout & Validity Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_map_blocks_paths(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_map_blocks map = {};
+ struct extent_status es = {};
+ handle_t *handle;
+ int ret;
+
+ /* 1. Out-of-range logical block -> -EFSCORRUPTED */
+ map.m_lblk = EXT_MAX_BLOCKS;
+ map.m_len = 1;
+ KUNIT_EXPECT_EQ(test, ext4_map_blocks(NULL, inode, &map, 0),
+ -EFSCORRUPTED);
+
+ /* 2. CACHED_NOWAIT miss returns 0 without querying extent tree */
+ inode_test_insert_leaf_extent(inode, 0, 2, INODE_TEST_DATA_BLK0, false);
+ map = (struct ext4_map_blocks){ .m_lblk = 0, .m_len = 2 };
+ ret = ext4_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CACHED_NOWAIT);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+
+ /* 3. ES miss queries extent tree, caches written extent; NOWAIT hits */
+ map = (struct ext4_map_blocks){ .m_lblk = 0, .m_len = 2 };
+ ret = ext4_map_blocks(NULL, inode, &map, 0);
+ KUNIT_EXPECT_EQ(test, ret, 2);
+ KUNIT_EXPECT_EQ(test, map.m_pblk, (ext4_fsblk_t)INODE_TEST_DATA_BLK0);
+ KUNIT_EXPECT_TRUE(test, !!(map.m_flags & EXT4_MAP_MAPPED));
+
+ map = (struct ext4_map_blocks){ .m_lblk = 1, .m_len = 1 };
+ ret = ext4_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CACHED_NOWAIT);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, map.m_pblk,
+ (ext4_fsblk_t)(INODE_TEST_DATA_BLK0 + 1));
+
+ /* 4. ES delayed & hole hits */
+ down_write(&ctx->ei.i_data_sem);
+ KUNIT_EXPECT_EQ(test, ext4_insert_delayed_blocks_test(inode, 4, 2), 0);
+ ext4_es_insert_extent(inode, 6, 2, ~0, EXTENT_STATUS_HOLE, 0);
+ up_write(&ctx->ei.i_data_sem);
+
+ map = (struct ext4_map_blocks){ .m_lblk = 4, .m_len = 2 };
+ ret = ext4_map_blocks(NULL, inode, &map, 0);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_TRUE(test, !!(map.m_flags & EXT4_MAP_DELAYED));
+
+ map = (struct ext4_map_blocks){ .m_lblk = 6, .m_len = 2 };
+ ret = ext4_map_blocks(NULL, inode, &map, 0);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, map.m_flags, 0U);
+
+ /* 5. Block allocation + EXT4_GET_BLOCKS_CREATE_ZERO & zeroout error */
+ ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0 + 2;
+ ctx->zeroout_calls = 0;
+ handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, 4);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ map = (struct ext4_map_blocks){ .m_lblk = 10, .m_len = 2 };
+ ret = ext4_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE_ZERO);
+ ext4_journal_stop(handle);
+
+ KUNIT_EXPECT_EQ(test, ret, 2);
+ KUNIT_EXPECT_EQ(test, map.m_pblk,
+ (ext4_fsblk_t)(INODE_TEST_DATA_BLK0 + 2));
+ KUNIT_EXPECT_TRUE(test, !!(map.m_flags & EXT4_MAP_NEW));
+ KUNIT_EXPECT_EQ(test, ctx->zeroout_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->last_zeroout_lblk, 10U);
+ KUNIT_EXPECT_EQ(test, ctx->last_zeroout_len, 2U);
+
+ /* Failed zeroout returns error BEFORE caching written extent in ES */
+ ctx->fail_zeroout_err = -EIO;
+ handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, 4);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ map = (struct ext4_map_blocks){ .m_lblk = 12, .m_len = 1 };
+ ret = ext4_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE_ZERO);
+ ext4_journal_stop(handle);
+ ctx->fail_zeroout_err = 0;
+
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 12, NULL, &es, NULL),
+ 1);
+ KUNIT_EXPECT_FALSE(test, ext4_es_is_written(&es));
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_hole(&es));
+
+ /* 6. check_block_validity() rejects out-of-bounds pblk in extent */
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ inode_test_insert_leaf_extent(inode, 0, 1, INODE_TEST_TOTAL_BLKS + 50,
+ false);
+ map = (struct ext4_map_blocks){ .m_lblk = 0, .m_len = 1 };
+ KUNIT_EXPECT_EQ(test, ext4_map_blocks(NULL, inode, &map, 0),
+ -EFSCORRUPTED);
+}
+
+/* -------------------------------------------------------------------------
+ * 16. ext4_get_block, getblk, bread, bread_batch & walk_page_buffers Tests
+ * -------------------------------------------------------------------------
+ */
+
+static int inode_test_walk_cb(handle_t *handle, struct inode *inode,
+ struct buffer_head *bh)
+{
+ int *visited = (int *)bh->b_private;
+
+ (*visited)++;
+ if (*visited < 0)
+ return -EINVAL;
+ return 0;
+}
+
+static void inode_test_get_block_bread_and_walk_buffers(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct inode_test_blk *dblk0 = inode_test_find_blk(ctx,
+ INODE_TEST_DATA_BLK0);
+ struct inode_test_blk *dblk1 = inode_test_find_blk(ctx,
+ INODE_TEST_DATA_BLK0 + 1);
+ struct buffer_head bh = {};
+ struct buffer_head *res_bh;
+ struct buffer_head *batch[3] = {};
+ struct buffer_head ring[4] = {};
+ int visited[4] = { 0, 0, 0, 0 };
+ int partial = 0;
+ handle_t *handle;
+ int i, err;
+
+ /* 1. ext4_get_block() inline -ERANGE, hole lookup, mapped & create */
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ctx->ei.i_inline_off = 164;
+ bh.b_size = INODE_TEST_BLK_SIZE;
+ KUNIT_EXPECT_EQ(test, ext4_get_block(inode, 0, &bh, 0), -ERANGE);
+ ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ctx->ei.i_inline_off = 0;
+
+ memset(&bh, 0, sizeof(bh));
+ bh.b_size = INODE_TEST_BLK_SIZE;
+ KUNIT_EXPECT_EQ(test, ext4_get_block(inode, 0, &bh, 0), 0);
+ KUNIT_EXPECT_FALSE(test, buffer_mapped(&bh));
+
+ ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0;
+ handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, 4);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ err = ext4_get_block_unwritten(inode, 0, &bh, 1);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, buffer_mapped(&bh));
+ KUNIT_EXPECT_TRUE(test, buffer_unwritten(&bh));
+ KUNIT_EXPECT_TRUE(test, buffer_new(&bh));
+
+ /* 2. ext4_getblk() hole -> NULL, CREATE new block zeroes & dirties bh */
+ res_bh = ext4_getblk(NULL, inode, 5, 0);
+ KUNIT_EXPECT_NULL(test, res_bh);
+
+ clear_buffer_uptodate(&dblk1->bh);
+ memset(dblk1->cache_buf, 0xcd, INODE_TEST_BLK_SIZE);
+ ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0 + 1;
+ handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, 4);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ res_bh = ext4_getblk(handle, inode, 1, EXT4_GET_BLOCKS_CREATE);
+ ext4_journal_stop(handle);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(res_bh));
+ KUNIT_EXPECT_TRUE(test, buffer_uptodate(res_bh));
+ KUNIT_EXPECT_EQ(test, (u8)dblk1->cache_buf[0], 0);
+ brelse(res_bh);
+
+ /* 3. ext4_bread() disk read & -EIO error cleanup */
+ inode_test_reset_mock_inode(ctx, S_IFDIR | 0755);
+ inode_test_insert_leaf_extent(inode, 0, 1, INODE_TEST_DATA_BLK0, false);
+ inode_test_insert_leaf_extent(inode, 2, 1, INODE_TEST_DATA_BLK0 + 1,
+ false);
+ clear_buffer_uptodate(&dblk0->bh);
+ memset(dblk0->disk_buf, 0x3c, INODE_TEST_BLK_SIZE);
+ ctx->read_lock_calls = 0;
+
+ res_bh = ext4_bread(NULL, inode, 0, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(res_bh));
+ KUNIT_EXPECT_EQ(test, ctx->read_lock_calls, 1U);
+ KUNIT_EXPECT_EQ(test, (u8)res_bh->b_data[0], 0x3c);
+ brelse(res_bh);
+
+ clear_buffer_uptodate(&dblk0->bh);
+ ctx->fail_read_eio = true;
+ res_bh = ext4_bread(NULL, inode, 0, 0);
+ ctx->fail_read_eio = false;
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res_bh), -EIO);
+ KUNIT_EXPECT_EQ(test, atomic_read(&dblk0->bh.b_count), 1);
+
+ /* 4. ext4_bread_batch() across [mapped, hole, mapped] & -EIO cleanup */
+ clear_buffer_uptodate(&dblk0->bh);
+ clear_buffer_uptodate(&dblk1->bh);
+ memset(dblk1->disk_buf, 0x99, INODE_TEST_BLK_SIZE);
+ err = ext4_bread_batch(inode, 0, 3, true, batch);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_NOT_NULL(test, batch[0]);
+ KUNIT_EXPECT_NULL(test, batch[1]);
+ KUNIT_EXPECT_NOT_NULL(test, batch[2]);
+ KUNIT_EXPECT_EQ(test, (u8)batch[2]->b_data[0], 0x99);
+ for (i = 0; i < 3; i++)
+ brelse(batch[i]);
+
+ memset(batch, 0, sizeof(batch));
+ clear_buffer_uptodate(&dblk0->bh);
+ ctx->fail_read_eio = true;
+ err = ext4_bread_batch(inode, 0, 3, true, batch);
+ ctx->fail_read_eio = false;
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_EQ(test, atomic_read(&dblk0->bh.b_count), 1);
+ KUNIT_EXPECT_EQ(test, atomic_read(&dblk1->bh.b_count), 1);
+
+ /* 5. ext4_walk_page_buffers() sub-page range, partial & error stop */
+ for (i = 0; i < 4; i++) {
+ ring[i].b_size = 1024;
+ ring[i].b_this_page = &ring[(i + 1) % 4];
+ ring[i].b_private = &visited[i];
+ if (i != 3)
+ set_buffer_uptodate(&ring[i]);
+ }
+ err = ext4_walk_page_buffers(NULL, inode, &ring[0], 1024, 3072,
+ &partial, inode_test_walk_cb);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, visited[0], 0);
+ KUNIT_EXPECT_EQ(test, visited[1], 1);
+ KUNIT_EXPECT_EQ(test, visited[2], 1);
+ KUNIT_EXPECT_EQ(test, visited[3], 0);
+ KUNIT_EXPECT_EQ(test, partial, 1);
+
+ set_buffer_uptodate(&ring[3]);
+ partial = 0;
+ err = ext4_walk_page_buffers(NULL, inode, &ring[0], 1024, 3072,
+ &partial, inode_test_walk_cb);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, partial, 0);
+
+ visited[1] = -2;
+ err = ext4_walk_page_buffers(NULL, inode, &ring[0], 1024, 3072,
+ NULL, inode_test_walk_cb);
+ KUNIT_EXPECT_EQ(test, err, -EINVAL);
+ KUNIT_EXPECT_EQ(test, visited[2], 2);
+}
+
+/* -------------------------------------------------------------------------
+ * 17. ext4_iomap_begin, ext4_iomap_report_ops, and Atomic Writes Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_iomap_and_atomic_writes(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct iomap iomap = {};
+ struct iomap srcmap = {};
+ struct iomap_iter iter = {};
+ int err;
+
+ /* 1. Offset past EXT4_MAX_LOGICAL_BLOCK -> -EINVAL */
+ err = ext4_iomap_begin(inode,
+ ((loff_t)EXT4_MAX_LOGICAL_BLOCK + 1) << 12,
+ 4096, 0, &iomap, &srcmap);
+ KUNIT_EXPECT_EQ(test, err, -EINVAL);
+
+ /* 2. Inline-data: iomap_begin warns & returns -ERANGE; report works */
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ctx->ei.i_inline_off = 164;
+ ctx->ei.i_inline_size = 60;
+ inode->i_size = 24;
+
+ kunit_warning_suppress(test) {
+ err = ext4_iomap_begin(inode, 0, 24, 0, &iomap, &srcmap);
+ }
+ KUNIT_EXPECT_EQ(test, err, -ERANGE);
+
+ iter = (struct iomap_iter){
+ .inode = inode, .pos = 0, .len = 24, .flags = IOMAP_REPORT,
+ };
+ err = ext4_iomap_report_ops.iomap_next(&iter, &iter.iomap,
+ &iter.srcmap);
+ KUNIT_EXPECT_GT(test, err, 0);
+ KUNIT_EXPECT_EQ(test, iter.iomap.type, (u16)IOMAP_INLINE);
+
+ iter = (struct iomap_iter){
+ .inode = inode, .pos = 60, .len = 24, .flags = IOMAP_REPORT,
+ };
+ err = ext4_iomap_report_ops.iomap_next(&iter, &iter.iomap,
+ &iter.srcmap);
+ KUNIT_EXPECT_EQ(test, err, -ENOENT);
+
+ /* 3. Indirect report past s_bitmap_maxbytes returns IOMAP_HOLE */
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ memset(ctx->ei.i_data, 0, sizeof(ctx->ei.i_data));
+ iter = (struct iomap_iter){
+ .inode = inode,
+ .pos = ctx->sbi.s_bitmap_maxbytes,
+ .len = 4096,
+ .flags = IOMAP_REPORT,
+ };
+ err = ext4_iomap_report_ops.iomap_next(&iter, &iter.iomap,
+ &iter.srcmap);
+ KUNIT_EXPECT_GT(test, err, 0);
+ KUNIT_EXPECT_EQ(test, iter.iomap.type, (u16)IOMAP_HOLE);
+
+ /* Indirect DIO write to hole inside i_size returns -ENOTBLK */
+ inode->i_size = 8192;
+ err = ext4_iomap_begin(inode, 0, 4096, IOMAP_WRITE | IOMAP_DIRECT,
+ &iomap, &srcmap);
+ KUNIT_EXPECT_EQ(test, err, -ENOTBLK);
+
+ /* 4. Read mappings (MAPPED, UNWRITTEN, DELALLOC, HOLE, DAX offset) */
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ inode->i_size = 16384;
+ ctx->sbi.s_dax_part_off = 0x200000ULL;
+ down_write(&ctx->ei.i_data_sem);
+ ext4_es_insert_extent(inode, 0, 1, INODE_TEST_DATA_BLK0,
+ EXTENT_STATUS_WRITTEN, 0);
+ ext4_es_insert_extent(inode, 1, 1, INODE_TEST_DATA_BLK0 + 1,
+ EXTENT_STATUS_UNWRITTEN, 0);
+ KUNIT_EXPECT_EQ(test, ext4_insert_delayed_blocks_test(inode, 2, 1), 0);
+ ext4_es_insert_extent(inode, 3, 1, ~0, EXTENT_STATUS_HOLE, 0);
+ up_write(&ctx->ei.i_data_sem);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_iomap_begin(inode, 0, 4096, IOMAP_DAX, &iomap,
+ &srcmap), 0);
+ KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_MAPPED);
+ KUNIT_EXPECT_EQ(test, iomap.addr,
+ ((u64)INODE_TEST_DATA_BLK0 << 12) +
+ (IS_ENABLED(CONFIG_FS_DAX) ? 0x200000ULL : 0ULL));
+ ctx->sbi.s_dax_part_off = 0;
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_iomap_begin(inode, 4096, 4096, 0, &iomap, &srcmap),
+ 0);
+ KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_UNWRITTEN);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_iomap_begin(inode, 8192, 4096, 0, &iomap, &srcmap),
+ 0);
+ KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_DELALLOC);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_iomap_begin(inode, 12288, 4096, 0, &iomap,
+ &srcmap), 0);
+ KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_HOLE);
+
+ iter = (struct iomap_iter){
+ .inode = inode, .pos = 0, .len = 4096, .flags = 0,
+ };
+ err = ext4_iomap_ops.iomap_next(&iter, &iter.iomap, &iter.srcmap);
+ KUNIT_EXPECT_GT(test, err, 0);
+ KUNIT_EXPECT_EQ(test, iter.iomap.type, (u16)IOMAP_MAPPED);
+ KUNIT_EXPECT_EQ(test, iter.iomap.addr,
+ (u64)INODE_TEST_DATA_BLK0 << 12);
+
+ /* 5. DIO write: NOWAIT -> -EAGAIN, inside i_size -> UNWRITTEN */
+ KUNIT_EXPECT_EQ(test,
+ ext4_iomap_begin(inode, 12288, 4096,
+ IOMAP_WRITE | IOMAP_DIRECT |
+ IOMAP_NOWAIT, &iomap, &srcmap),
+ -EAGAIN);
+
+ ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0 + 3;
+ KUNIT_EXPECT_EQ(test,
+ ext4_iomap_begin(inode, 12288, 4096,
+ IOMAP_WRITE | IOMAP_DIRECT, &iomap,
+ &srcmap), 0);
+ KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_UNWRITTEN);
+ KUNIT_EXPECT_TRUE(test, !!(iomap.flags & IOMAP_F_NEW));
+
+ /* 6. Bigalloc multi-block atomic write across mixed [written + hole] */
+ inode_test_enable_bigalloc(ctx, 4);
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ inode->i_size = 16384;
+ /* Pre-populate lblk 0..1 as written at pblk 100..101; lblk 2..3 hole */
+ inode_test_insert_leaf_extent(inode, 0, 2, INODE_TEST_DATA_BLK0, false);
+ ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0 + 2;
+ ctx->zeroout_calls = 0;
+
+ err = ext4_iomap_begin(inode, 0, 16384,
+ IOMAP_WRITE | IOMAP_DIRECT | IOMAP_ATOMIC,
+ &iomap, &srcmap);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_MAPPED);
+ KUNIT_EXPECT_EQ(test, iomap.length, 16384ULL);
+ KUNIT_EXPECT_EQ(test, iomap.addr, (u64)INODE_TEST_DATA_BLK0 << 12);
+ KUNIT_EXPECT_TRUE(test, !!(iomap.flags & IOMAP_F_ATOMIC_BIO));
+ KUNIT_EXPECT_GT(test, ctx->zeroout_calls, 0U);
+}
+
+/* -------------------------------------------------------------------------
+ * 18. Transaction Credits, Layout Breaking & Disksize Update Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_trans_credits_and_disksize(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ int ext_credits, ind_credits, chunk_ext, chunk_jdata;
+ int err;
+
+ /* 1. ext4_meta_trans_blocks() & ext4_chunk_trans_*() */
+ ctx->sbi.s_groups_count = 4;
+ ctx->sbi.s_gdb_count = 2;
+ ext_credits = ext4_meta_trans_blocks(inode, 8, 2, 1);
+ KUNIT_EXPECT_GT(test, ext_credits, 0);
+
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ind_credits = ext4_meta_trans_blocks(inode, 8, 2, 1);
+ KUNIT_EXPECT_GT(test, ind_credits, 0);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ctx->sbi.s_groups_count = 1;
+ ctx->sbi.s_gdb_count = 1;
+
+ chunk_ext = ext4_chunk_trans_extent(inode, 5);
+ KUNIT_EXPECT_EQ(test, ext4_chunk_trans_blocks(inode, 5), chunk_ext);
+
+ clear_opt(ctx->sb, DELALLOC);
+ ext4_set_inode_flag(inode, EXT4_INODE_JOURNAL_DATA);
+ ctx->sbi.s_journal = (journal_t *)1;
+ chunk_jdata = ext4_chunk_trans_extent(inode, 5);
+ ctx->sbi.s_journal = NULL;
+ ext4_clear_inode_flag(inode, EXT4_INODE_JOURNAL_DATA);
+ set_opt(ctx->sb, DELALLOC);
+ KUNIT_EXPECT_EQ(test, chunk_jdata, chunk_ext + 5);
+
+ /* 2. ext4_break_layouts() requires invalidate_lock held */
+ kunit_warning_suppress(test) {
+ err = ext4_break_layouts(inode);
+ }
+ KUNIT_EXPECT_EQ(test, err, -EINVAL);
+
+ filemap_invalidate_lock(inode->i_mapping);
+ err = ext4_break_layouts(inode);
+ filemap_invalidate_unlock(inode->i_mapping);
+ KUNIT_EXPECT_EQ(test, err, 0);
+
+ /* 3. ext4_update_disksize_before_punch() */
+ inode->i_size = 12000;
+ ctx->ei.i_disksize = 4096;
+
+ inode_lock(inode);
+ /* Punch past i_size is a no-op */
+ KUNIT_EXPECT_EQ(test,
+ ext4_update_disksize_before_punch(inode, 16384, 4096),
+ 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 4096LL);
+
+ /* Punch within [0, 8192) advances i_disksize to 8192 */
+ KUNIT_EXPECT_EQ(test,
+ ext4_update_disksize_before_punch(inode, 4096, 4096),
+ 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 8192LL);
+
+ /* Punch extending past i_size clamps i_disksize to i_size (12000) */
+ KUNIT_EXPECT_EQ(test,
+ ext4_update_disksize_before_punch(inode, 8192, 8192),
+ 0);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 12000LL);
+}
+
+/* -------------------------------------------------------------------------
+ * 19. Partial Block Zeroing, EOF Zeroing & Pagecache Truncate Range Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_zero_partial_and_eof(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct inode_test_blk *dblk0 = inode_test_find_blk(ctx,
+ INODE_TEST_DATA_BLK0);
+ struct inode_test_blk *dblk1 = inode_test_find_blk(ctx,
+ INODE_TEST_DATA_BLK0 + 1);
+ struct folio *folio;
+ unsigned int partial_zeroed = 0;
+ u8 *kaddr;
+ int err;
+
+ /*
+ * Map lblk 0 -> pblk 100 (written), leave lblk 1 as a hole, and map
+ * lblk 2 -> pblk 101 (written).
+ */
+ inode->i_size = 12288;
+ ctx->ei.i_disksize = 12288;
+ inode_test_insert_leaf_extent(inode, 0, 1, INODE_TEST_DATA_BLK0, false);
+ inode_test_insert_leaf_extent(inode, 2, 1, INODE_TEST_DATA_BLK0 + 1,
+ false);
+ memset(dblk0->disk_buf, 0x5a, INODE_TEST_BLK_SIZE);
+ memset(dblk1->disk_buf, 0xa5, INODE_TEST_BLK_SIZE);
+
+ /* 1. Single-block interior zeroing [512, 1536) sets START | END */
+ err = ext4_zero_partial_blocks(inode, 512, 1024, &partial_zeroed);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, partial_zeroed,
+ (unsigned int)(EXT4_PARTIAL_ZERO_START |
+ EXT4_PARTIAL_ZERO_END));
+
+ folio = filemap_get_folio(inode->i_mapping, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ kaddr = kmap_local_folio(folio, 0);
+ KUNIT_EXPECT_EQ(test, kaddr[511], 0x5a);
+ KUNIT_EXPECT_EQ(test, kaddr[512], 0x00);
+ KUNIT_EXPECT_EQ(test, kaddr[1535], 0x00);
+ KUNIT_EXPECT_EQ(test, kaddr[1536], 0x5a);
+ kunmap_local(kaddr);
+ folio_put(folio);
+
+ /* 2. Multi-block [3000, 5000): lblk 0 mapped, lblk 1 hole -> START */
+ partial_zeroed = 0;
+ err = ext4_zero_partial_blocks(inode, 3000, 2000, &partial_zeroed);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, partial_zeroed,
+ (unsigned int)EXT4_PARTIAL_ZERO_START);
+
+ /* Zero EOF tail [8500, 12288) in lblk 2 -> pblk 101 */
+ KUNIT_EXPECT_EQ(test, ext4_block_zero_eof(inode, 8192, 12288), 0);
+ KUNIT_EXPECT_EQ(test, ext4_block_zero_eof(inode, 9000, 8500), 0);
+
+ err = ext4_block_zero_eof(inode, 8500, 12288);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ folio = filemap_get_folio(inode->i_mapping, 8192 >> PAGE_SHIFT);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ kaddr = kmap_local_folio(folio, offset_in_folio(folio, 8192));
+ KUNIT_EXPECT_EQ(test, kaddr[307], 0xa5);
+ KUNIT_EXPECT_EQ(test, kaddr[308], 0x00);
+ KUNIT_EXPECT_EQ(test, kaddr[4095], 0x00);
+ kunmap_local(kaddr);
+ folio_put(folio);
+
+ /* 3. ext4_truncate_page_cache_block_range() evicts full folio 0 */
+ err = ext4_truncate_page_cache_block_range(inode, 0,
+ INODE_TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ err = ext4_truncate_page_cache_block_range(inode, 0,
+ max_t(loff_t, PAGE_SIZE, INODE_TEST_BLK_SIZE));
+ KUNIT_EXPECT_EQ(test, err, 0);
+ folio = filemap_get_folio(inode->i_mapping, 0);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(folio));
+ if (!IS_ERR(folio))
+ folio_put(folio);
+}
+
+/* -------------------------------------------------------------------------
+ * 20. ext4_punch_hole() & ext4_truncate() End-to-End Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_punch_hole_and_truncate(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct file *dummy_file;
+ struct extent_status es = {};
+ int err;
+
+ dummy_file = kunit_kzalloc(test, sizeof(*dummy_file), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, dummy_file);
+ dummy_file->f_inode = inode;
+ dummy_file->f_mapping = inode->i_mapping;
+
+ /* Set up 4-block written extent [lblk 0..3 -> pblk 100..103] */
+ inode->i_size = 4 * INODE_TEST_BLK_SIZE;
+ ctx->ei.i_disksize = inode->i_size;
+ inode->i_blocks = 32;
+ inode_test_insert_leaf_extent(inode, 0, 4, INODE_TEST_DATA_BLK0, false);
+ down_write(&ctx->ei.i_data_sem);
+ ext4_es_insert_extent(inode, 0, 4, INODE_TEST_DATA_BLK0,
+ EXTENT_STATUS_WRITTEN, 0);
+ up_write(&ctx->ei.i_data_sem);
+
+ /* 1. Punch past i_size is a no-op */
+ inode_lock(inode);
+ err = ext4_punch_hole(dummy_file, 5 * INODE_TEST_BLK_SIZE,
+ INODE_TEST_BLK_SIZE);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 0UL);
+
+ /* 2. Punch middle 2 blocks [4096, 12288) -> frees pblk 101..102 */
+ inode_lock(inode);
+ err = ext4_punch_hole(dummy_file, INODE_TEST_BLK_SIZE,
+ 2 * INODE_TEST_BLK_SIZE);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 2UL);
+ KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 1, NULL, &es, NULL),
+ 1);
+ KUNIT_EXPECT_TRUE(test, ext4_es_is_hole(&es));
+ KUNIT_EXPECT_EQ(test, es.es_len, 2U);
+
+ /* 3. Truncate down to 1 block (4096B) -> frees trailing pblk 103 */
+ ctx->total_freed_blocks = 0;
+ ctx->orphan_add_calls = 0;
+ ctx->orphan_del_calls = 0;
+ inode->i_size = INODE_TEST_BLK_SIZE;
+ inode_lock(inode);
+ err = ext4_truncate(inode);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->orphan_add_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->orphan_del_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 1UL);
+ KUNIT_EXPECT_EQ(test, ctx->last_free_pblk,
+ (ext4_fsblk_t)(INODE_TEST_DATA_BLK0 + 3));
+
+ /* 4. Truncate to 0 sets EXT4_STATE_DA_ALLOC_CLOSE & frees pblk 100 */
+ ctx->total_freed_blocks = 0;
+ inode->i_size = 0;
+ inode_lock(inode);
+ err = ext4_truncate(inode);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_state(inode,
+ EXT4_STATE_DA_ALLOC_CLOSE));
+ KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 1UL);
+ KUNIT_EXPECT_EQ(test, ctx->last_free_pblk,
+ (ext4_fsblk_t)INODE_TEST_DATA_BLK0);
+}
+
+/* -------------------------------------------------------------------------
+ * 21. ext4_inode_attach_jinode, ext4_write_inode & ext4_sync_inode_metadata
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_write_and_sync_metadata(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct inode_test_blk *itable = inode_test_find_blk(ctx,
+ INODE_TEST_ITABLE_BLK0);
+ struct writeback_control wbc = { .sync_mode = WB_SYNC_ALL };
+ struct ext4_inode *raw;
+ int err;
+
+ /* 1. ext4_inode_attach_jinode(): no-op without journal, allocs with */
+ KUNIT_EXPECT_EQ(test, ext4_inode_attach_jinode(inode), 0);
+ KUNIT_EXPECT_NULL(test, ctx->ei.jinode);
+
+ ctx->sbi.s_journal = (journal_t *)1;
+ err = ext4_inode_attach_jinode(inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_NOT_NULL(test, ctx->ei.jinode);
+ /* Idempotent when jinode is already attached */
+ KUNIT_EXPECT_EQ(test, ext4_inode_attach_jinode(inode), 0);
+ ctx->sbi.s_journal = NULL;
+ if (ctx->ei.jinode) {
+ jbd2_free_inode(ctx->ei.jinode);
+ ctx->ei.jinode = NULL;
+ }
+
+ /* 2. ext4_write_inode(): 0 in nojournal, -EROFS in emergency state */
+ KUNIT_EXPECT_EQ(test, ext4_write_inode(inode, &wbc), 0);
+ set_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+ KUNIT_EXPECT_EQ(test, ext4_write_inode(inode, &wbc), -EROFS);
+ clear_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+
+ /* 3. ext4_sync_inode_metadata(): warns & fails if journal present */
+ ctx->sbi.s_journal = (journal_t *)1;
+ kunit_warning_suppress(test) {
+ err = ext4_sync_inode_metadata(inode, &wbc);
+ }
+ ctx->sbi.s_journal = NULL;
+ KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+
+ /* Clean nojournal metadata sync flushes dirty raw inode & returns 0 */
+ i_uid_write(inode, 0x7788);
+ ext4_dirty_inode(inode, I_DIRTY_SYNC);
+ clear_buffer_mapped(&itable->bh);
+ clear_buffer_write_io_error(&itable->bh);
+ err = ext4_sync_inode_metadata(inode, &wbc);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ raw = inode_test_raw_slot(ctx, INODE_TEST_PRIMARY_INO, false);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_low), 0x7788);
+
+ /* Simulated buffer write error returns -EIO */
+ set_buffer_write_io_error(&itable->bh);
+ err = ext4_sync_inode_metadata(inode, &wbc);
+ clear_buffer_write_io_error(&itable->bh);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+}
+
+/* -------------------------------------------------------------------------
+ * 22. ext4_evict_inode() Live, Unlinked Regular & Fast Symlink Eviction Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_evict_inode_paths(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct ext4_inode *raw;
+ struct inode *ig;
+
+ /* 1. Live inode (i_nlink == 1): evicts cleanly without freeing inode */
+ inode_test_prepare_valid_raw_inode(ctx, 24, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 24, true);
+ raw->i_size_lo = cpu_to_le32(4096);
+ inode_test_stamp_raw_csum(ctx, 24, raw);
+ ig = ext4_iget(ctx->sb, 24, EXT4_IGET_NORMAL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ig));
+ ctx->free_inode_calls = 0;
+ ctx->total_freed_blocks = 0;
+ iput(ig);
+ KUNIT_EXPECT_EQ(test, ctx->free_inode_calls, 0U);
+ KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 0UL);
+
+ /*
+ * 2. Unlinked regular file (i_nlink == 0) with 2 mapped blocks:
+ * truncates data blocks, sets i_dtime, updates raw inode, and calls
+ * ext4_free_inode().
+ */
+ inode_test_prepare_valid_raw_inode(ctx, 25, S_IFREG | 0644);
+ raw = inode_test_raw_slot(ctx, 25, true);
+ raw->i_size_lo = cpu_to_le32(8192);
+ inode_test_stamp_raw_csum(ctx, 25, raw);
+ ig = ext4_iget(ctx->sb, 25, EXT4_IGET_NORMAL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ig));
+ inode_test_insert_leaf_extent(ig, 0, 2, INODE_TEST_DATA_BLK0, false);
+ ig->i_blocks = 16;
+ clear_nlink(ig);
+ ctx->free_inode_calls = 0;
+ ctx->total_freed_blocks = 0;
+ iput(ig);
+ KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 2UL);
+ KUNIT_EXPECT_EQ(test, ctx->free_inode_calls, 1U);
+ raw = inode_test_raw_slot(ctx, 25, false);
+ KUNIT_EXPECT_NE(test, le32_to_cpu(raw->i_dtime), 0U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_size_lo), 0U);
+
+ /*
+ * 3. Unlinked fast symlink (i_nlink == 0): clears inline i_data target
+ * without treating bytes as block pointers and frees the inode.
+ */
+ inode_test_prepare_valid_raw_inode(ctx, 26, S_IFLNK | 0777);
+ raw = inode_test_raw_slot(ctx, 26, true);
+ raw->i_flags = 0;
+ raw->i_size_lo = cpu_to_le32(11);
+ memcpy(raw->i_block, "target/path", 11);
+ inode_test_stamp_raw_csum(ctx, 26, raw);
+ ig = ext4_iget(ctx->sb, 26, EXT4_IGET_NORMAL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ig));
+ KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(ig), 1);
+ clear_nlink(ig);
+ ctx->free_inode_calls = 0;
+ ctx->total_freed_blocks = 0;
+ iput(ig);
+ KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 0UL);
+ KUNIT_EXPECT_EQ(test, ctx->free_inode_calls, 1U);
+}
+
+/* -------------------------------------------------------------------------
+ * 23. Buffered Write (ext4_da_aops & ext4_aops) and Folio Operations Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_buffered_write_and_folio_ops(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct address_space *mapping = inode->i_mapping;
+ struct folio *folio = NULL;
+ void *fsdata = NULL;
+ u8 *kaddr;
+ int ret;
+
+ /*
+ * 1. Delayed-allocation buffered write to hole [0, 128):
+ * reserves 1 delalloc block and updates i_size while deferring
+ * i_disksize update until writeback.
+ */
+ inode_lock(inode);
+ ret = mapping->a_ops->write_begin(NULL, mapping, 0, 128, &folio,
+ &fsdata);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_ASSERT_NOT_NULL(test, folio);
+ kaddr = kmap_local_folio(folio, 0);
+ memset(kaddr, 0xab, 128);
+ kunmap_local(kaddr);
+ ret = mapping->a_ops->write_end(NULL, mapping, 0, 128, 128, folio,
+ fsdata);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 128);
+ KUNIT_EXPECT_EQ(test, inode->i_size, 128LL);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 0LL);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U);
+
+ /*
+ * 2. Delayed-allocation write into an already-written block past i_size
+ * (placed on folio index 1 regardless of PAGE_SIZE) updates both
+ * i_size and i_disksize immediately.
+ */
+ {
+ loff_t wpos = max_t(loff_t, PAGE_SIZE, INODE_TEST_BLK_SIZE);
+ ext4_lblk_t wlblk = wpos >> INODE_TEST_BLK_BITS;
+
+ inode_test_insert_leaf_extent(inode, wlblk, 1,
+ INODE_TEST_DATA_BLK0 + 1, false);
+ inode_lock(inode);
+ ret = mapping->a_ops->write_begin(NULL, mapping, wpos, 64,
+ &folio, &fsdata);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ kaddr = kmap_local_folio(folio, offset_in_folio(folio, wpos));
+ memset(kaddr, 0xcd, 64);
+ kunmap_local(kaddr);
+ ret = mapping->a_ops->write_end(NULL, mapping, wpos, 64, 64,
+ folio, fsdata);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 64);
+ KUNIT_EXPECT_EQ(test, inode->i_size, wpos + 64);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, wpos + 64);
+
+ /* Full folio invalidation detaches buffers cleanly */
+ folio = filemap_lock_folio(mapping, wpos >> PAGE_SHIFT);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ KUNIT_EXPECT_NOT_NULL(test, folio_buffers(folio));
+ mapping->a_ops->invalidate_folio(folio, 0, folio_size(folio));
+ KUNIT_EXPECT_NULL(test, folio_buffers(folio));
+ folio_unlock(folio);
+ folio_put(folio);
+ }
+
+ /* 3. Non-delalloc buffered write (ext4_aops) allocates pblk directly */
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ clear_opt(ctx->sb, DELALLOC);
+ ext4_set_aops(inode);
+ ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0;
+ ctx->alloc_calls = 0;
+
+ inode_lock(inode);
+ ret = mapping->a_ops->write_begin(NULL, mapping, 0, 256, &folio,
+ &fsdata);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ kaddr = kmap_local_folio(folio, 0);
+ memset(kaddr, 0xef, 256);
+ kunmap_local(kaddr);
+ ret = mapping->a_ops->write_end(NULL, mapping, 0, 256, 256, folio,
+ fsdata);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 256);
+ KUNIT_EXPECT_EQ(test, ctx->alloc_calls, 1U);
+ KUNIT_EXPECT_EQ(test, inode->i_size, 256LL);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 256LL);
+
+ /* 4. a_ops->bmap, partial invalidate_folio & release_folio */
+ KUNIT_EXPECT_EQ(test, (ext4_fsblk_t)mapping->a_ops->bmap(mapping, 0),
+ (ext4_fsblk_t)INODE_TEST_DATA_BLK0);
+ KUNIT_EXPECT_EQ(test, (ext4_fsblk_t)mapping->a_ops->bmap(mapping, 5),
+ 0ULL);
+
+ folio = filemap_lock_folio(mapping, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ folio_set_checked(folio);
+ KUNIT_EXPECT_FALSE(test, mapping->a_ops->release_folio(folio,
+ GFP_KERNEL));
+ folio_clear_checked(folio);
+ /* Dirty buffer still pins folio buffers */
+ KUNIT_EXPECT_FALSE(test, mapping->a_ops->release_folio(folio,
+ GFP_KERNEL));
+ /* Partial invalidation preserves buffer ring */
+ mapping->a_ops->invalidate_folio(folio, 128, 128);
+ KUNIT_EXPECT_NOT_NULL(test, folio_buffers(folio));
+
+ clear_buffer_dirty(folio_buffers(folio));
+ folio_clear_dirty_for_io(folio);
+ KUNIT_EXPECT_TRUE(test, mapping->a_ops->release_folio(folio,
+ GFP_KERNEL));
+ KUNIT_EXPECT_NULL(test, folio_buffers(folio));
+ folio_unlock(folio);
+ folio_put(folio);
+ set_opt(ctx->sb, DELALLOC);
+}
+
+/* -------------------------------------------------------------------------
+ * 24. ext4_setattr() Permission, Mode, Extend & Shrink Truncate Tests
+ * -------------------------------------------------------------------------
+ */
+
+static void inode_test_setattr(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct inode_test_blk *dblk0 = inode_test_find_blk(ctx,
+ INODE_TEST_DATA_BLK0);
+ struct dentry dentry = { .d_sb = ctx->sb, .d_inode = inode };
+ struct iattr attr = {};
+ struct folio *folio;
+ u8 *kaddr;
+ u64 iv_before;
+ int err;
+
+ /* 1. S_IMMUTABLE and S_APPEND permission rejections */
+ inode->i_flags |= S_IMMUTABLE;
+ attr = (struct iattr){ .ia_valid = ATTR_MODE, .ia_mode = S_IFREG | 0600 };
+ inode_lock(inode);
+ KUNIT_EXPECT_EQ(test, ext4_setattr(&nop_mnt_idmap, &dentry, &attr),
+ -EPERM);
+ inode_unlock(inode);
+ inode->i_flags &= ~S_IMMUTABLE;
+
+ inode->i_flags |= S_APPEND;
+ inode_lock(inode);
+ KUNIT_EXPECT_EQ(test, ext4_setattr(&nop_mnt_idmap, &dentry, &attr),
+ -EPERM);
+ inode_unlock(inode);
+ inode->i_flags &= ~S_APPEND;
+
+ /* 2. Directory ATTR_SIZE -> -EINVAL; indirect > maxbytes -> -EFBIG */
+ inode->i_mode = S_IFDIR | 0755;
+ attr = (struct iattr){ .ia_valid = ATTR_SIZE, .ia_size = 0 };
+ inode_lock(inode);
+ KUNIT_EXPECT_EQ(test, ext4_setattr(&nop_mnt_idmap, &dentry, &attr),
+ -EINVAL);
+ inode_unlock(inode);
+
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ memset(ctx->ei.i_data, 0, sizeof(ctx->ei.i_data));
+ attr = (struct iattr){
+ .ia_valid = ATTR_SIZE,
+ .ia_size = ctx->sbi.s_bitmap_maxbytes + 4096,
+ };
+ inode_lock(inode);
+ KUNIT_EXPECT_EQ(test, ext4_setattr(&nop_mnt_idmap, &dentry, &attr),
+ -EFBIG);
+ inode_unlock(inode);
+
+ /* 3. ATTR_MODE chmod updates mode & increments iversion */
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ iv_before = inode_peek_iversion(inode);
+ attr = (struct iattr){ .ia_valid = ATTR_MODE, .ia_mode = S_IFREG | 0600 };
+ inode_lock(inode);
+ err = ext4_setattr(&nop_mnt_idmap, &dentry, &attr);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, inode->i_mode & 0777, 0600);
+ KUNIT_EXPECT_GT(test, inode_peek_iversion(inode), iv_before);
+
+ /*
+ * 4. ATTR_SIZE extend (2000 -> 8192) zeroes EOF tail [2000, 4096) in
+ * mapped block 0 and advances i_size and i_disksize.
+ */
+ inode->i_size = 2000;
+ ctx->ei.i_disksize = 2000;
+ inode->i_blocks = 16;
+ inode_test_insert_leaf_extent(inode, 0, 2, INODE_TEST_DATA_BLK0, false);
+ memset(dblk0->disk_buf, 0x77, INODE_TEST_BLK_SIZE);
+
+ attr = (struct iattr){ .ia_valid = ATTR_SIZE, .ia_size = 8192 };
+ inode_lock(inode);
+ err = ext4_setattr(&nop_mnt_idmap, &dentry, &attr);
+ inode_unlock(inode);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, inode->i_size, 8192LL);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 8192LL);
+
+ folio = filemap_get_folio(inode->i_mapping, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ kaddr = kmap_local_folio(folio, 0);
+ KUNIT_EXPECT_EQ(test, kaddr[1999], 0x77);
+ KUNIT_EXPECT_EQ(test, kaddr[2000], 0x00);
+ KUNIT_EXPECT_EQ(test, kaddr[4095], 0x00);
+ kunmap_local(kaddr);
+ folio_put(folio);
+
+ /* 5. ATTR_SIZE shrink (8192 -> 4096) frees trailing block 1 */
+ ctx->total_freed_blocks = 0;
+ attr = (struct iattr){ .ia_valid = ATTR_SIZE, .ia_size = 4096 };
+ inode_lock(inode);
+ err = ext4_setattr(&nop_mnt_idmap, &dentry, &attr);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, inode->i_size, 4096LL);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 4096LL);
+ KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 1UL);
+}
+
+/* -------------------------------------------------------------------------
+ * 25. ext4_page_mkwrite() Delalloc, Block-Alloc, Fast-Path & Truncate Tests
+ * -------------------------------------------------------------------------
+ */
+
+static vm_fault_t inode_test_call_page_mkwrite(struct vm_area_struct *vma,
+ struct folio *folio)
+{
+ struct vm_fault vmf = {
+ .vma = vma,
+ .page = folio_page(folio, 0),
+ };
+
+ return ext4_page_mkwrite(&vmf);
+}
+
+static void inode_test_page_mkwrite(struct kunit *test)
+{
+ struct inode_test_ctx *ctx = inode_test_setup(test);
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct file *dummy_file;
+ struct vm_area_struct *vma;
+ struct folio *folio;
+ vm_fault_t ret;
+
+ dummy_file = kunit_kzalloc(test, sizeof(*dummy_file), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, dummy_file);
+ dummy_file->f_inode = inode;
+ dummy_file->f_mapping = inode->i_mapping;
+ dummy_file->f_mode = FMODE_NOCMTIME;
+
+ vma = kunit_kzalloc(test, sizeof(*vma), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, vma);
+ vma->vm_file = dummy_file;
+
+ inode->i_size = 4096;
+ ctx->ei.i_disksize = 4096;
+ folio = __filemap_get_folio(inode->i_mapping, 0,
+ FGP_CREAT | FGP_LOCK, GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ folio_zero_segment(folio, 0, folio_size(folio));
+ folio_mark_uptodate(folio);
+ folio_unlock(folio);
+
+ /* 1. S_IMMUTABLE rejects write fault with VM_FAULT_SIGBUS */
+ inode->i_flags |= S_IMMUTABLE;
+ KUNIT_EXPECT_EQ(test, inode_test_call_page_mkwrite(vma, folio),
+ (vm_fault_t)VM_FAULT_SIGBUS);
+ inode->i_flags &= ~S_IMMUTABLE;
+
+ /* 2. Delalloc fault reserves 1 delayed block & returns LOCKED */
+ ret = inode_test_call_page_mkwrite(vma, folio);
+ KUNIT_EXPECT_EQ(test, ret, (vm_fault_t)VM_FAULT_LOCKED);
+ if (ret & VM_FAULT_LOCKED)
+ folio_unlock(folio);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U);
+ folio_put(folio);
+
+ /* 3. Non-delalloc fault allocates pblk; repeat fault hits fast-path */
+ inode_test_reset_mock_inode(ctx, S_IFREG | 0644);
+ clear_opt(ctx->sb, DELALLOC);
+ ext4_set_aops(inode);
+ inode->i_size = 4096;
+ ctx->ei.i_disksize = 4096;
+ ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0;
+ ctx->alloc_calls = 0;
+
+ folio = __filemap_get_folio(inode->i_mapping, 0,
+ FGP_CREAT | FGP_LOCK, GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ folio_zero_segment(folio, 0, folio_size(folio));
+ folio_mark_uptodate(folio);
+ folio_unlock(folio);
+
+ ret = inode_test_call_page_mkwrite(vma, folio);
+ KUNIT_EXPECT_EQ(test, ret, (vm_fault_t)VM_FAULT_LOCKED);
+ if (ret & VM_FAULT_LOCKED)
+ folio_unlock(folio);
+ KUNIT_EXPECT_EQ(test, ctx->alloc_calls, 1U);
+
+ /* Second fault sees all buffers mapped and skips journal_start/alloc */
+ ret = inode_test_call_page_mkwrite(vma, folio);
+ KUNIT_EXPECT_EQ(test, ret, (vm_fault_t)VM_FAULT_LOCKED);
+ if (ret & VM_FAULT_LOCKED)
+ folio_unlock(folio);
+ KUNIT_EXPECT_EQ(test, ctx->alloc_calls, 1U);
+
+ /* 4. Folio beyond truncated i_size returns VM_FAULT_NOPAGE */
+ folio_put(folio);
+ folio = __filemap_get_folio(inode->i_mapping, 2,
+ FGP_CREAT | FGP_LOCK, GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ folio_unlock(folio);
+ KUNIT_EXPECT_EQ(test, inode_test_call_page_mkwrite(vma, folio),
+ (vm_fault_t)VM_FAULT_NOPAGE);
+ folio_put(folio);
+ set_opt(ctx->sb, DELALLOC);
+}
+
static struct kunit_case ext4_inode_test_cases[] = {
KUNIT_CASE_PARAM(inode_test_xtimestamp_decoding, ext4_inode_gen_params),
+ KUNIT_CASE(inode_test_xtimestamp_encoding_roundtrip),
+ KUNIT_CASE(inode_test_csum_set_and_verify),
+ KUNIT_CASE(inode_test_blocks_encode_decode),
+ KUNIT_CASE(inode_test_fill_raw_inode),
+ KUNIT_CASE(inode_test_mark_dirty_and_update),
+ KUNIT_CASE(inode_test_expand_extra_isize),
+ KUNIT_CASE(inode_test_get_inode_loc_variants),
+ KUNIT_CASE(inode_test_iget_validation_and_modes),
+ KUNIT_CASE(inode_test_iget_corruptions),
+ KUNIT_CASE(inode_test_fast_symlink_and_can_truncate),
+ KUNIT_CASE(inode_test_flags_aops_mapping_order),
+ KUNIT_CASE(inode_test_projid_dio_and_getattr),
+ KUNIT_CASE(inode_test_da_reserve_and_release),
+ KUNIT_CASE(inode_test_da_bigalloc_clusters),
+ KUNIT_CASE(inode_test_map_blocks_paths),
+ KUNIT_CASE(inode_test_get_block_bread_and_walk_buffers),
+ KUNIT_CASE(inode_test_iomap_and_atomic_writes),
+ KUNIT_CASE(inode_test_trans_credits_and_disksize),
+ KUNIT_CASE(inode_test_zero_partial_and_eof),
+ KUNIT_CASE(inode_test_punch_hole_and_truncate),
+ KUNIT_CASE(inode_test_write_and_sync_metadata),
+ KUNIT_CASE(inode_test_evict_inode_paths),
+ KUNIT_CASE(inode_test_buffered_write_and_folio_ops),
+ KUNIT_CASE(inode_test_setattr),
+ KUNIT_CASE(inode_test_page_mkwrite),
{}
};
@@ -279,5 +3720,5 @@ static struct kunit_suite ext4_inode_test_suite = {
kunit_test_suites(&ext4_inode_test_suite);
-MODULE_DESCRIPTION("KUnit test of ext4 inode timestamp decoding");
+MODULE_DESCRIPTION("KUnit tests for ext4 inode operations");
MODULE_LICENSE("GPL v2");
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 67d37b39db93..98444d8ddb40 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -124,6 +124,7 @@ void ext4_inode_csum_set(struct inode *inode, struct ext4_inode *raw,
EXT4_FITS_IN_INODE(raw, ei, i_checksum_hi))
raw->i_checksum_hi = cpu_to_le16(csum >> 16);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_csum_set);
static inline int ext4_begin_ordered_truncate(struct inode *inode,
loff_t new_size)
@@ -162,6 +163,7 @@ int ext4_inode_is_fast_symlink(struct inode *inode)
return S_ISLNK(inode->i_mode) && inode->i_size &&
(inode->i_size < EXT4_N_BLOCKS * 4);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_is_fast_symlink);
/*
* Called at the last iput() if i_nlink is zero.
@@ -332,6 +334,7 @@ void ext4_evict_inode(struct inode *inode)
ext4_fc_mark_ineligible(inode->i_sb, EXT4_FC_REASON_NOMEM, NULL);
ext4_clear_inode(inode); /* We must guarantee clearing of inode... */
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_evict_inode);
#ifdef CONFIG_QUOTA
qsize_t *ext4_get_reserved_space(struct inode *inode)
@@ -946,6 +949,7 @@ int ext4_get_block(struct inode *inode, sector_t iblock,
return _ext4_get_block(inode, iblock, bh,
create ? EXT4_GET_BLOCKS_CREATE : 0);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_block);
/*
* Get block function used when preparing for buffered write if we require
@@ -972,10 +976,34 @@ int ext4_get_block_unwritten(struct inode *inode, sector_t iblock,
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_block_unwritten);
/* Maximum number of blocks we map for direct IO at once. */
#define DIO_MAX_BLOCKS 4096
+struct buffer_head *ext4_sb_getblk(struct super_block *sb, ext4_fsblk_t block)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_sb_getblk, sb, block);
+ return sb_getblk(sb, block);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_getblk);
+
+struct buffer_head *ext4_sb_find_get_block(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_sb_find_get_block, sb, block);
+ return sb_find_get_block(sb, block);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_find_get_block);
+
+struct buffer_head *ext4_sb_getblk_unmovable(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_sb_getblk_unmovable, sb, block);
+ return getblk_unmovable(sb->s_bdev, block, sb->s_blocksize);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_getblk_unmovable);
+
/*
* `handle' can be NULL if create is zero
*/
@@ -1005,15 +1033,14 @@ struct buffer_head *ext4_getblk(handle_t *handle, struct inode *inode,
return ERR_PTR(err);
if (nowait)
- return sb_find_get_block(inode->i_sb, map.m_pblk);
+ return ext4_sb_find_get_block(inode->i_sb, map.m_pblk);
/*
* Since bh could introduce extra ref count such as referred by
* journal_head etc. Try to avoid using __GFP_MOVABLE here
* as it may fail the migration when journal_head remains.
*/
- bh = getblk_unmovable(inode->i_sb->s_bdev, map.m_pblk,
- inode->i_sb->s_blocksize);
+ bh = ext4_sb_getblk_unmovable(inode->i_sb, map.m_pblk);
if (unlikely(!bh))
return ERR_PTR(-ENOMEM);
@@ -1074,6 +1101,7 @@ struct buffer_head *ext4_bread(handle_t *handle, struct inode *inode,
}
return bh;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_bread);
/* Read a contiguous batch of blocks. */
int ext4_bread_batch(struct inode *inode, ext4_lblk_t block, int bh_count,
@@ -1117,6 +1145,7 @@ int ext4_bread_batch(struct inode *inode, ext4_lblk_t block, int bh_count,
}
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_bread_batch);
int ext4_walk_page_buffers(handle_t *handle, struct inode *inode,
struct buffer_head *head,
@@ -1148,6 +1177,7 @@ int ext4_walk_page_buffers(handle_t *handle, struct inode *inode,
}
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_walk_page_buffers);
/*
* Helper for handling dirtying of journalled data. We also mark the folio as
@@ -3858,12 +3888,14 @@ int ext4_iomap_begin(struct inode *inode, loff_t offset, loff_t length,
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_iomap_begin);
static DEFINE_IOMAP_ITER_NEXT(ext4_iomap_next, ext4_iomap_begin);
const struct iomap_ops ext4_iomap_ops = {
.iomap_next = ext4_iomap_next,
};
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_iomap_ops);
static int ext4_iomap_begin_report(struct inode *inode, loff_t offset,
loff_t length, unsigned int flags,
@@ -3921,6 +3953,7 @@ static DEFINE_IOMAP_ITER_NEXT(ext4_iomap_next_report, ext4_iomap_begin_report);
const struct iomap_ops ext4_iomap_report_ops = {
.iomap_next = ext4_iomap_next_report,
};
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_iomap_report_ops);
/*
* For data=journal mode, folio should be marked dirty only when it was
@@ -4034,6 +4067,7 @@ void ext4_set_aops(struct inode *inode)
else
inode->i_mapping->a_ops = &ext4_aops;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_set_aops);
/*
* Here we can't skip an unwritten buffer even though it usually reads zero
@@ -4273,6 +4307,7 @@ int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end)
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_zero_eof);
/*
* Zero out the unaligned head and tail of the [lstart, lstart+length)
@@ -4333,6 +4368,7 @@ int ext4_zero_partial_blocks(struct inode *inode, loff_t lstart, loff_t length,
}
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_zero_partial_blocks);
int ext4_can_truncate(struct inode *inode)
{
@@ -4344,6 +4380,7 @@ int ext4_can_truncate(struct inode *inode)
return !ext4_inode_is_fast_symlink(inode);
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_can_truncate);
/*
* We have to make sure i_disksize gets properly updated before we truncate
@@ -4381,6 +4418,7 @@ int ext4_update_disksize_before_punch(struct inode *inode, loff_t offset,
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_update_disksize_before_punch);
static inline void ext4_truncate_folio(struct inode *inode,
loff_t start, loff_t end)
@@ -4443,6 +4481,7 @@ int ext4_truncate_page_cache_block_range(struct inode *inode,
truncate_pagecache_range(inode, start, end - 1);
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_truncate_page_cache_block_range);
static void ext4_wait_dax_page(struct inode *inode)
{
@@ -4458,6 +4497,7 @@ int ext4_break_layouts(struct inode *inode)
return dax_break_layout_inode(inode, ext4_wait_dax_page);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_break_layouts);
/*
* ext4_punch_hole: punches a hole in a file by releasing the blocks
@@ -4578,6 +4618,7 @@ int ext4_punch_hole(struct file *file, loff_t offset, loff_t length)
ext4_journal_stop(handle);
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_punch_hole);
int ext4_inode_attach_jinode(struct inode *inode)
{
@@ -4608,6 +4649,7 @@ int ext4_inode_attach_jinode(struct inode *inode)
jbd2_free_inode(jinode);
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_attach_jinode);
/*
* ext4_truncate()
@@ -4742,6 +4784,7 @@ int ext4_truncate(struct inode *inode)
trace_ext4_truncate_exit(inode);
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_truncate);
static inline u64 ext4_inode_peek_iversion(const struct inode *inode)
{
@@ -4933,7 +4976,7 @@ static int __ext4_get_inode_loc(struct super_block *sb, unsigned long ino,
}
block += (inode_offset / inodes_per_block);
- bh = sb_getblk(sb, block);
+ bh = ext4_sb_getblk(sb, block);
if (unlikely(!bh))
return -ENOMEM;
if (ext4_buffer_uptodate(bh))
@@ -4958,7 +5001,7 @@ static int __ext4_get_inode_loc(struct super_block *sb, unsigned long ino,
start = inode_offset & ~(inodes_per_block - 1);
/* Is the inode bitmap in cache? */
- bitmap_bh = sb_getblk(sb, ext4_inode_bitmap(sb, gdp));
+ bitmap_bh = ext4_sb_getblk(sb, ext4_inode_bitmap(sb, gdp));
if (unlikely(!bitmap_bh))
goto make_io;
@@ -5115,7 +5158,7 @@ int ext4_get_inode_loc_noio(struct inode *inode, struct ext4_iloc *iloc)
}
block += inode_offset / inodes_per_block;
- bh = sb_find_get_block(sb, block);
+ bh = ext4_sb_find_get_block(sb, block);
if (!bh)
return -EAGAIN;
if (!ext4_buffer_uptodate(bh)) {
@@ -5194,6 +5237,7 @@ void ext4_set_inode_flags(struct inode *inode, bool init)
S_SYNC|S_APPEND|S_IMMUTABLE|S_NOATIME|S_DIRSYNC|S_DAX|
S_ENCRYPTED|S_CASEFOLD|S_VERITY);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_set_inode_flags);
static blkcnt_t ext4_inode_blocks(struct ext4_inode *raw_inode,
struct ext4_inode_info *ei)
@@ -5250,6 +5294,7 @@ int ext4_get_projid(struct inode *inode, kprojid_t *projid)
*projid = EXT4_I(inode)->i_projid;
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_projid);
/*
* ext4 has self-managed i_version for ea inodes, it stores the lower 32bit of
@@ -5321,6 +5366,7 @@ void ext4_set_inode_mapping_order(struct inode *inode)
mapping_set_folio_order_range(inode->i_mapping, min_order, max_order);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_set_inode_mapping_order);
static int ext4_iget_match(struct inode *inode, u64 ino, void *data)
{
@@ -5928,6 +5974,7 @@ int ext4_write_inode(struct inode *inode, struct writeback_control *wbc)
return ext4_fc_commit(EXT4_SB(inode->i_sb)->s_journal,
EXT4_I(inode)->i_sync_tid);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_write_inode);
int ext4_sync_inode_metadata(struct inode *inode, struct writeback_control *wbc)
{
@@ -5958,6 +6005,7 @@ int ext4_sync_inode_metadata(struct inode *inode, struct writeback_control *wbc)
brelse(iloc.bh);
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sync_inode_metadata);
/*
* In data=journal mode ext4_journalled_invalidate_folio() may fail to invalidate
@@ -6274,6 +6322,7 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
error = rc;
return error;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_setattr);
u32 ext4_dio_alignment(struct inode *inode)
{
@@ -6287,6 +6336,7 @@ u32 ext4_dio_alignment(struct inode *inode)
return i_blocksize(inode);
return 1; /* use the iomap defaults */
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dio_alignment);
int ext4_getattr(struct mnt_idmap *idmap, const struct path *path,
struct kstat *stat, u32 request_mask, unsigned int query_flags)
@@ -6356,6 +6406,7 @@ int ext4_getattr(struct mnt_idmap *idmap, const struct path *path,
generic_fillattr(idmap, request_mask, inode, stat);
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_getattr);
int ext4_file_getattr(struct mnt_idmap *idmap,
const struct path *path, struct kstat *stat,
@@ -6390,6 +6441,7 @@ int ext4_file_getattr(struct mnt_idmap *idmap,
stat->blocks += delalloc_blocks << (inode->i_sb->s_blocksize_bits - 9);
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_file_getattr);
static int ext4_index_trans_blocks(struct inode *inode, int lblocks,
int pextents)
@@ -6442,6 +6494,7 @@ int ext4_meta_trans_blocks(struct inode *inode, int lblocks, int pextents,
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_meta_trans_blocks);
/*
* Calculate the journal credits for modifying the number of blocks
@@ -6459,6 +6512,7 @@ int ext4_chunk_trans_extent(struct inode *inode, int nrblocks)
ret += nrblocks;
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_chunk_trans_extent);
/*
* Calculate the journal credits for a chunk of data modification.
@@ -6473,6 +6527,7 @@ int ext4_chunk_trans_blocks(struct inode *inode, int nrblocks)
{
return ext4_meta_trans_blocks(inode, nrblocks, 1, 1);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_chunk_trans_blocks);
/*
* The caller must have previously called ext4_reserve_inode_write().
@@ -6550,6 +6605,7 @@ ext4_reserve_inode_write(handle_t *handle, struct inode *inode,
ext4_std_error(inode->i_sb, err);
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_reserve_inode_write);
static int __ext4_expand_extra_isize(struct inode *inode,
unsigned int new_extra_isize,
@@ -6701,6 +6757,7 @@ int ext4_expand_extra_isize(struct inode *inode,
ext4_journal_stop(handle);
return error;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_expand_extra_isize);
/*
* What we do here is to mark the in-core inode as clean with respect to inode
@@ -6767,6 +6824,7 @@ void ext4_dirty_inode(struct inode *inode, int flags)
ext4_mark_inode_dirty(handle, inode);
ext4_journal_stop(handle);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dirty_inode);
int ext4_change_inode_journal_flag(struct inode *inode, int val)
{
@@ -7004,8 +7062,87 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
sb_end_pagefault(inode->i_sb);
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_page_mkwrite);
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+int ext4_inode_csum_verify_test(struct inode *inode, struct ext4_inode *raw,
+ struct ext4_inode_info *ei)
+{
+ return ext4_inode_csum_verify(inode, raw, ei);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_csum_verify_test);
+
+int ext4_inode_blocks_set_test(struct ext4_inode *raw_inode,
+ struct ext4_inode_info *ei)
+{
+ return ext4_inode_blocks_set(raw_inode, ei);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_blocks_set_test);
+
+blkcnt_t ext4_inode_blocks_test(struct ext4_inode *raw_inode,
+ struct ext4_inode_info *ei)
+{
+ return ext4_inode_blocks(raw_inode, ei);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_blocks_test);
+
+int ext4_fill_raw_inode_test(struct inode *inode, struct ext4_inode *raw_inode)
+{
+ return ext4_fill_raw_inode(inode, raw_inode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fill_raw_inode_test);
+
+int ext4_expand_extra_isize_test(struct inode *inode,
+ unsigned int new_extra_isize,
+ struct ext4_iloc *iloc,
+ handle_t *handle, int *no_expand)
+{
+ return __ext4_expand_extra_isize(inode, new_extra_isize, iloc,
+ handle, no_expand);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_expand_extra_isize_test);
+
+int ext4_try_to_expand_extra_isize_test(struct inode *inode,
+ unsigned int new_extra_isize,
+ struct ext4_iloc *iloc,
+ handle_t *handle)
+{
+ return ext4_try_to_expand_extra_isize(inode, new_extra_isize,
+ *iloc, handle);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_try_to_expand_extra_isize_test);
+
+int ext4_da_reserve_space_test(struct inode *inode, int nr_resv)
+{
+ return ext4_da_reserve_space(inode, nr_resv);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_reserve_space_test);
+
+int ext4_clu_alloc_state_test(struct inode *inode, ext4_lblk_t lblk)
+{
+ return ext4_clu_alloc_state(inode, lblk);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_clu_alloc_state_test);
+
+int ext4_insert_delayed_blocks_test(struct inode *inode, ext4_lblk_t lblk,
+ ext4_lblk_t len)
+{
+ return ext4_insert_delayed_blocks(inode, lblk, len);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_insert_delayed_blocks_test);
+
+int ext4_da_map_blocks_test(struct inode *inode, struct ext4_map_blocks *map)
+{
+ return ext4_da_map_blocks(inode, map);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_map_blocks_test);
+
+int ext4_nonda_switch_test(struct super_block *sb)
+{
+ return ext4_nonda_switch(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_nonda_switch_test);
+
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_inode_loc_noio);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_fc_inode_loc);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_get_block_prep);
diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c
index 0236b6b9785a..7298bc81f240 100644
--- a/fs/ext4/page-io.c
+++ b/fs/ext4/page-io.c
@@ -296,6 +296,7 @@ void ext4_end_io_rsv_work(struct work_struct *work)
i_rsv_conversion_work);
ext4_do_flush_completed_IO(&ei->vfs_inode, &ei->i_rsv_conversion_list);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_end_io_rsv_work);
ext4_io_end_t *ext4_init_io_end(struct inode *inode, gfp_t flags)
{
diff --git a/fs/ext4/super.c b/fs/ext4/super.c
index 62ec5323bb25..74604a677ca4 100644
--- a/fs/ext4/super.c
+++ b/fs/ext4/super.c
@@ -185,6 +185,9 @@ static inline void __ext4_read_bh(struct buffer_head *bh, blk_opf_t op_flags,
void ext4_read_bh_nowait(struct buffer_head *bh, blk_opf_t op_flags,
bio_end_io_t end_io, bool simu_fail)
{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_read_bh_nowait, bh, op_flags,
+ end_io, simu_fail);
+
BUG_ON(!buffer_locked(bh));
if (ext4_buffer_uptodate(bh)) {
@@ -193,6 +196,7 @@ void ext4_read_bh_nowait(struct buffer_head *bh, blk_opf_t op_flags,
}
__ext4_read_bh(bh, op_flags, end_io, simu_fail);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_bh_nowait);
int ext4_read_bh(struct buffer_head *bh, blk_opf_t op_flags,
bio_end_io_t end_io, bool simu_fail)
@@ -214,6 +218,8 @@ int ext4_read_bh(struct buffer_head *bh, blk_opf_t op_flags,
int ext4_read_bh_lock(struct buffer_head *bh, blk_opf_t op_flags, bool wait)
{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_read_bh_lock, bh, op_flags, wait);
+
lock_buffer(bh);
if (!wait) {
ext4_read_bh_nowait(bh, op_flags, NULL, false);
@@ -221,6 +227,7 @@ int ext4_read_bh_lock(struct buffer_head *bh, blk_opf_t op_flags, bool wait)
}
return ext4_read_bh(bh, op_flags, NULL, false);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_bh_lock);
/*
* This works like __bread_gfp() except it uses ERR_PTR for error
@@ -282,8 +289,11 @@ struct buffer_head *ext4_sb_bread_nofail(struct super_block *sb,
void ext4_sb_breadahead_unmovable(struct super_block *sb, sector_t block)
{
- struct buffer_head *bh = bdev_getblk(sb->s_bdev, block,
- sb->s_blocksize, GFP_NOWAIT);
+ struct buffer_head *bh;
+
+ KUNIT_STATIC_STUB_REDIRECT(ext4_sb_breadahead_unmovable, sb, block);
+
+ bh = bdev_getblk(sb->s_bdev, block, sb->s_blocksize, GFP_NOWAIT);
if (likely(bh)) {
if (trylock_buffer(bh))
@@ -291,6 +301,7 @@ void ext4_sb_breadahead_unmovable(struct super_block *sb, sector_t block)
brelse(bh);
}
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_breadahead_unmovable);
static int ext4_verify_csum_type(struct super_block *sb,
struct ext4_super_block *es)
@@ -401,6 +412,7 @@ void ext4_inode_bitmap_set(struct super_block *sb,
if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
bg->bg_inode_bitmap_hi = cpu_to_le32(blk >> 32);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_bitmap_set);
void ext4_inode_table_set(struct super_block *sb,
struct ext4_group_desc *bg, ext4_fsblk_t blk)
@@ -409,6 +421,7 @@ void ext4_inode_table_set(struct super_block *sb,
if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
bg->bg_inode_table_hi = cpu_to_le32(blk >> 32);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_table_set);
void ext4_free_group_clusters_set(struct super_block *sb,
struct ext4_group_desc *bg, __u32 count)
@@ -1164,6 +1177,7 @@ void ext4_update_dynamic_rev(struct super_block *sb)
* can leave it up to e2fsck to clean up any inconsistencies there.
*/
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_update_dynamic_rev);
static inline struct inode *orphan_list_entry(struct list_head *l)
{
@@ -1581,6 +1595,7 @@ void ext4_clear_inode(struct inode *inode)
}
fscrypt_put_encryption_info(inode);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_clear_inode);
static struct inode *ext4_nfs_get_inode(struct super_block *sb,
u64 ino, u32 generation)
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 11/27] ext4: add KUnit tests for fs/ext4/ialloc.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (9 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 10/27] ext4: expand KUnit tests for fs/ext4/inode.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (15 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add the ext4_ialloc_test KUnit test suite in fs/ext4/ialloc-test.c (28
test cases, 33 subtests total) to comprehensively exercise inode bitmap
management, group selection heuristics, inode allocation, freeing,
orphan lookup, and lazy inode table initialization in fs/ext4/ialloc.c
using an in-memory VFS superblock, mock block device, and deterministic
block pool without requiring a mounted block device:
- Bitmap padding and asynchronous read completion helpers
(ext4_mark_bitmap_end and ext4_end_bitmap_read) across unaligned and
byte-aligned start/end bit boundaries, partial single-byte updates,
no-op guards, BLK_STS_OK completion, and BLK_STS_IOERR / BIO_QUIET
failure handling.
- Inode bitmap validation and loading (ext4_validate_inode_bitmap and
ext4_read_inode_bitmap), covering CRC32C verification, out-of-group
metadata block overlap detection, EXT4_FC_REPLAY and buffer_verified
short-circuits, cache hits vs. synchronous disk reads, lazy
EXT4_BG_INODE_UNINIT bitmap initialization, and fault injection across
ext4_get_group_desc, ext4_sb_getblk, and ext4_read_bh.
- Inode freeing (ext4_free_inode) across regular files and directories,
bitmap bit clearing, group descriptor, flex_bg, and percpu counter
updates, CRC32C recomputation, guard rejections (NULL i_sb, i_count >
1, i_nlink > 0, reserved ino < EXT4_FIRST_INO, and out-of-range ino),
double-free corruption marking, pre-corrupt bitmap skips (vs.
EXT4_FC_REPLAY), and journal get_write_access error handling.
- Orlov directory group allocator (find_group_orlov) across root and
EXT4_INODE_TOPDIR spreading (hashed qstr and NULL qstr), subdirectory
co-location, i_last_alloc_group hints, max_dirs / min_inodes /
min_clusters thresholds, flex_bg constituent group packing, and
two-pass linear fallback when percpu counters are inflated or all
groups are exhausted.
- Non-directory group allocator (find_group_other) with and without
flex_bg, covering parent group preference, last-allocated group
spreading, quadratic hash probing, cyclic linear search, and modulo
wraparound.
- Recently-deleted inode buffer-cache checks (recently_deleted) across
EXT4_BG_INODE_UNINIT, bg_itable_unused bounds, uncached or
non-uptodate inode table blocks, zero i_dtime, and clean (60s) vs.
dirty (360s) buffer_dirty() recency windows, plus find_inode_bit
scanning across non-journaled skipping, journaled (s_journal) bypass,
and recently_deleted_ino fallback.
- On-disk inode reservation during fast-commit replay
(ext4_mark_inode_used) across uninitialized EXT4_BG_INODE_UNINIT and
EXT4_BG_BLOCK_UNINIT groups, overhead cluster calculation,
bg_itable_unused updates, idempotency on already-set bits, and invalid
ino, inode/block bitmap read and wait, dirty-metadata, and
sync_dirty_buffer error paths.
- Extended attribute transaction credit estimation
(ext4_xattr_credits_for_new_inode) across POSIX default ACL
inheritance, security xattrs, and encryption contexts.
- Full new-inode allocation (__ext4_new_inode) across regular files,
directories, symlinks, and special files, flag inheritance and masking
(EXT4_FL_INHERITED and ext4_mask_flags), EXT4_EA_INODE_FL inline-data
suppression, UID/GID ownership overrides, SGID inheritance,
EXT4_INODE_PROJINHERIT project ID propagation, explicit goal and
s_inode_goal targeting, uninitialized group setup, corrupt/reserved
bit skipping, EXT4_FC_REPLAY goal allocation,
ext4_new_inode_start_handle, and error paths (-EPERM, emergency
-EIO/-EROFS, -ENOMEM, -ENOSPC, duplicate inode hash -EIO,
write-access and dirty-metadata failures, and ext4_init_security /
ext4_mark_inode_dirty rollback).
- Orphan inode lookup and validation (ext4_orphan_get), group free-inode
and used-directory summation (ext4_count_free_inodes and
ext4_count_dirs, including 64-bit high-16-bit fields), and lazy inode
table zeroing (ext4_init_inode_table) across already-zeroed, uninit,
partially used, full, bogus bg_itable_unused, and zeroout error cases.
Route buffer-cache lookups in ialloc.c through ext4_sb_getblk() and
ext4_sb_find_get_block(), extract ext4_itable_zeroout() with
KUNIT_STATIC_STUB_REDIRECT, add KUNIT_STATIC_STUB_REDIRECT hooks in
ext4_read_bh(), __ext4_journal_get_write_access(), and
ext4_init_security(), and export the tested entry points and
static-helper wrappers with EXPORT_SYMBOL_FOR_EXT4_TEST.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 2 +-
fs/ext4/bitmap.c | 5 +
fs/ext4/ext4.h | 21 +
fs/ext4/ext4_jbd2.c | 4 +
fs/ext4/ialloc-test.c | 2714 ++++++++++++++++++++++++++++++++++++++
fs/ext4/ialloc.c | 85 +-
fs/ext4/super.c | 17 +
fs/ext4/xattr_security.c | 4 +
8 files changed, 2844 insertions(+), 8 deletions(-)
create mode 100644 fs/ext4/ialloc-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 7bd91839c3ce..72198aff5b45 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -18,7 +18,7 @@ ext4-test-objs += inode-test.o mballoc-test.o \
extents-test.o hash-test.o \
extents_status-test.o xattr-test.o \
namei-test.o inline-test.o \
- fast_commit-test.o
+ fast_commit-test.o ialloc-test.o
obj-$(CONFIG_EXT4_KUNIT_TESTS) += ext4-test.o
ext4-$(CONFIG_FS_VERITY) += verity.o
ext4-$(CONFIG_FS_ENCRYPTION) += crypto.o
diff --git a/fs/ext4/bitmap.c b/fs/ext4/bitmap.c
index 87760fabdd2e..f9c22427891f 100644
--- a/fs/ext4/bitmap.c
+++ b/fs/ext4/bitmap.c
@@ -15,6 +15,7 @@ unsigned int ext4_count_free(char *bitmap, unsigned int numchars)
{
return numchars * BITS_PER_BYTE - memweight(bitmap, numchars);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_count_free);
int ext4_inode_bitmap_csum_verify(struct super_block *sb,
struct ext4_group_desc *gdp,
@@ -39,6 +40,7 @@ int ext4_inode_bitmap_csum_verify(struct super_block *sb,
return provided == calculated;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_bitmap_csum_verify);
void ext4_inode_bitmap_csum_set(struct super_block *sb,
struct ext4_group_desc *gdp,
@@ -57,6 +59,7 @@ void ext4_inode_bitmap_csum_set(struct super_block *sb,
if (sbi->s_desc_size >= EXT4_BG_INODE_BITMAP_CSUM_HI_END)
gdp->bg_inode_bitmap_csum_hi = cpu_to_le16(csum >> 16);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_bitmap_csum_set);
int ext4_block_bitmap_csum_verify(struct super_block *sb,
struct ext4_group_desc *gdp,
@@ -80,6 +83,7 @@ int ext4_block_bitmap_csum_verify(struct super_block *sb,
return provided == calculated;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_bitmap_csum_verify);
void ext4_block_bitmap_csum_set(struct super_block *sb,
struct ext4_group_desc *gdp,
@@ -97,3 +101,4 @@ void ext4_block_bitmap_csum_set(struct super_block *sb,
if (sbi->s_desc_size >= EXT4_BG_BLOCK_BITMAP_CSUM_HI_END)
gdp->bg_block_bitmap_csum_hi = cpu_to_le16(csum >> 16);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_bitmap_csum_set);
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index c4f685704031..9262ff021bda 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3065,9 +3065,30 @@ extern struct inode * ext4_orphan_get(struct super_block *, unsigned long);
extern unsigned long ext4_count_free_inodes(struct super_block *);
extern unsigned long ext4_count_dirs(struct super_block *);
extern void ext4_mark_bitmap_end(int start_bit, int end_bit, char *bitmap);
+int ext4_itable_zeroout(struct super_block *sb, ext4_fsblk_t blk, int num,
+ int barrier);
extern int ext4_init_inode_table(struct super_block *sb,
ext4_group_t group, int barrier);
void ext4_end_bitmap_read(struct bio *bio);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+int ext4_validate_inode_bitmap_test(struct super_block *sb,
+ struct ext4_group_desc *desc,
+ ext4_group_t block_group,
+ struct buffer_head *bh);
+struct buffer_head *ext4_read_inode_bitmap_test(struct super_block *sb,
+ ext4_group_t block_group);
+int ext4_find_group_orlov_test(struct super_block *sb, struct inode *parent,
+ ext4_group_t *group, umode_t mode,
+ const struct qstr *qstr);
+int ext4_find_group_other_test(struct super_block *sb, struct inode *parent,
+ ext4_group_t *group, umode_t mode);
+int ext4_recently_deleted_test(struct super_block *sb, ext4_group_t group,
+ int ino);
+int ext4_find_inode_bit_test(struct super_block *sb, ext4_group_t group,
+ struct buffer_head *bitmap, unsigned long *ino);
+int ext4_xattr_credits_for_new_inode_test(struct inode *dir, mode_t mode,
+ bool encrypt);
+#endif
/* fast_commit.c */
int ext4_fc_get_buf(journal_t *journal, struct buffer_head **bh);
diff --git a/fs/ext4/ext4_jbd2.c b/fs/ext4/ext4_jbd2.c
index f18a01e6fd13..fa65a6377ca0 100644
--- a/fs/ext4/ext4_jbd2.c
+++ b/fs/ext4/ext4_jbd2.c
@@ -248,6 +248,9 @@ int __ext4_journal_get_write_access(const char *where, unsigned int line,
{
int err;
+ KUNIT_STATIC_STUB_REDIRECT(__ext4_journal_get_write_access, where, line,
+ handle, sb, bh, trigger_type);
+
might_sleep();
if (ext4_handle_valid(handle)) {
@@ -267,6 +270,7 @@ int __ext4_journal_get_write_access(const char *where, unsigned int line,
&EXT4_SB(sb)->s_journal_triggers[trigger_type].tr_triggers);
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_journal_get_write_access);
/*
* The ext4 forget function must perform a revoke if we are freeing data
diff --git a/fs/ext4/ialloc-test.c b/fs/ext4/ialloc-test.c
new file mode 100644
index 000000000000..a3066b788ce6
--- /dev/null
+++ b/fs/ext4/ialloc-test.c
@@ -0,0 +1,2714 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/ialloc.c
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/bio.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/iversion.h>
+#include <linux/jbd2.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/pagemap.h>
+#include <linux/posix_acl.h>
+#include <linux/quotaops.h>
+#include <linux/rcupdate.h>
+#include <linux/slab.h>
+#include <linux/stat.h>
+#include <linux/timekeeping.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "ext4_extents.h"
+#include "xattr.h"
+#include "acl.h"
+
+/*
+ * Multi-group mock filesystem geometry:
+ * - 4 block groups, 1024-byte blocks (8192 bits per bitmap block)
+ * - 64 inodes per group (256 total inodes), 256-byte inodes (4 per block)
+ * - 16 inode table blocks per group, 1024 blocks (clusters) per group
+ * - s_first_data_block = 1, s_first_ino = 11 (inodes 1..10 reserved in bg 0)
+ * - sparse_super enabled: groups 0, 1, 3 have backup super + 1 gdb (2 base
+ * clusters), while group 2 has 0 base clusters (18 total overhead clusters,
+ * leaving 1024 - 18 = 1006 free clusters after init).
+ */
+#define IALLOC_TEST_NGROUPS 4U
+#define IALLOC_TEST_BLK_SIZE 1024U
+#define IALLOC_TEST_BLK_BITS 10U
+#define IALLOC_TEST_BLOCKS_PER_GRP 1024U
+#define IALLOC_TEST_FIRST_DATA_BLK 1U
+#define IALLOC_TEST_TOTAL_BLKS (IALLOC_TEST_FIRST_DATA_BLK + \
+ IALLOC_TEST_NGROUPS * IALLOC_TEST_BLOCKS_PER_GRP)
+#define IALLOC_TEST_INODE_SIZE 256U
+#define IALLOC_TEST_EXTRA_ISIZE 32U
+#define IALLOC_TEST_INODES_PER_BLK (IALLOC_TEST_BLK_SIZE / IALLOC_TEST_INODE_SIZE)
+#define IALLOC_TEST_INODES_PER_GRP 64U
+#define IALLOC_TEST_ITB_PER_GRP (IALLOC_TEST_INODES_PER_GRP / \
+ IALLOC_TEST_INODES_PER_BLK)
+#define IALLOC_TEST_TOTAL_INODES (IALLOC_TEST_NGROUPS * IALLOC_TEST_INODES_PER_GRP)
+#define IALLOC_TEST_FIRST_INO 11U
+#define IALLOC_TEST_BG0_FREE_INODES (IALLOC_TEST_INODES_PER_GRP - \
+ (IALLOC_TEST_FIRST_INO - 1U))
+#define IALLOC_TEST_DEFAULT_FREE_CLU 800U
+#define IALLOC_TEST_CSUM_SEED 0x6d4b29a3U
+
+#define IALLOC_TEST_GDESC_BLK 2ULL
+#define IALLOC_TEST_POOL_SIZE 32U
+#define IALLOC_TEST_MAX_TRACKED_INODES 32U
+
+struct ialloc_test_blk {
+ bool in_use;
+ bool in_cache;
+ bool marked_dirty;
+ ext4_fsblk_t blocknr;
+ struct buffer_head bh;
+ char cache_buf[IALLOC_TEST_BLK_SIZE] __aligned(8);
+ char disk_buf[IALLOC_TEST_BLK_SIZE] __aligned(8);
+};
+
+struct ialloc_test_ctx {
+ struct super_block *sb;
+ struct ext4_sb_info sbi;
+ struct ext4_super_block es;
+ struct blockgroup_lock bgl;
+ struct block_device bdev;
+ struct request_queue bdev_queue;
+ struct inode bd_inode;
+ struct buffer_head sb_bh;
+ struct buffer_head gdesc_bh;
+ char gdesc_buf[IALLOC_TEST_BLK_SIZE] __aligned(8);
+ bool gdesc_marked_dirty;
+
+ struct ext4_group_desc gdps[IALLOC_TEST_NGROUPS];
+ struct ext4_group_info grp_infos[IALLOC_TEST_NGROUPS];
+ struct ext4_group_info *grp_info_ptrs[16];
+ struct ext4_group_info **grp_info_slice[1];
+
+ struct flex_groups flex_grps[IALLOC_TEST_NGROUPS];
+ struct flex_groups *flex_grp_ptrs[IALLOC_TEST_NGROUPS];
+
+ struct dentry root_dentry;
+ struct inode *root_inode;
+
+ struct ext4_inode_info standalone_ei;
+ struct inode *tracked_inodes[IALLOC_TEST_MAX_TRACKED_INODES];
+ unsigned int nr_tracked_inodes;
+ bool evict_skip_free;
+
+ struct ialloc_test_blk pool[IALLOC_TEST_POOL_SIZE];
+
+ /* Fault injection and call tracking */
+ u32 fail_gdp_mask;
+ bool fail_getblk;
+ bool fail_read_eio;
+ bool fail_alloc_inode;
+ bool fail_sync_dirty_buffer;
+ int fail_bbitmap_err;
+ int fail_wait_bbitmap_err;
+ int fail_write_access_err;
+ int fail_write_access_on_call;
+ int fail_dirty_metadata_err;
+ int fail_dirty_metadata_on_call;
+ int fail_mark_inode_dirty_err;
+#ifdef CONFIG_EXT4_FS_SECURITY
+ int fail_init_security_err;
+#endif
+ int fail_itable_zeroout_err;
+ int fail_iget_err;
+ struct inode *iget_inode;
+
+ unsigned int read_bh_calls;
+ unsigned int write_access_calls;
+ unsigned int dirty_metadata_calls;
+ unsigned int zeroout_calls;
+ ext4_fsblk_t last_zeroout_blk;
+ int last_zeroout_num;
+ int last_zeroout_barrier;
+};
+
+static inline ext4_fsblk_t ialloc_test_group_start(ext4_group_t g)
+{
+ return IALLOC_TEST_FIRST_DATA_BLK +
+ (ext4_fsblk_t)g * IALLOC_TEST_BLOCKS_PER_GRP;
+}
+
+static inline ext4_fsblk_t ialloc_test_bbitmap_blk(ext4_group_t g)
+{
+ return ialloc_test_group_start(g) + 2;
+}
+
+static inline ext4_fsblk_t ialloc_test_ibitmap_blk(ext4_group_t g)
+{
+ return ialloc_test_group_start(g) + 3;
+}
+
+static inline ext4_fsblk_t ialloc_test_itable_blk(ext4_group_t g)
+{
+ return ialloc_test_group_start(g) + 4;
+}
+
+static inline void clear_bitmap_uptodate(struct buffer_head *bh)
+{
+ clear_bit(BH_BITMAP_UPTODATE, &bh->b_state);
+}
+
+static void ialloc_test_init_ei_fields(struct ext4_inode_info *ei)
+{
+ inode_set_iversion(&ei->vfs_inode, 1);
+ ei->i_flags = 0;
+ ext4_clear_state_flags(ei);
+ spin_lock_init(&ei->i_raw_lock);
+ ei->i_prealloc_node = RB_ROOT;
+ atomic_set(&ei->i_prealloc_active, 0);
+ rwlock_init(&ei->i_prealloc_lock);
+ ext4_es_init_tree(&ei->i_es_tree);
+ rwlock_init(&ei->i_es_lock);
+ INIT_LIST_HEAD(&ei->i_es_list);
+ ei->i_es_all_nr = 0;
+ ei->i_es_shk_nr = 0;
+ ei->i_es_shrink_lblk = 0;
+ ei->i_es_seq = 0;
+ ei->i_reserved_data_blocks = 0;
+ spin_lock_init(&ei->i_block_reservation_lock);
+ ext4_init_pending_tree(&ei->i_pending_tree);
+#ifdef CONFIG_QUOTA
+ ei->i_reserved_quota = 0;
+ memset(&ei->i_dquot, 0, sizeof(ei->i_dquot));
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+ ei->i_crypt_info = NULL;
+#endif
+ ei->jinode = NULL;
+ init_rwsem(&ei->xattr_sem);
+ INIT_LIST_HEAD(&ei->i_rsv_conversion_list);
+ spin_lock_init(&ei->i_completed_io_lock);
+ ei->i_sync_tid = 0;
+ ei->i_datasync_tid = 0;
+ INIT_LIST_HEAD(&ei->i_orphan);
+ ext4_fc_init_inode(&ei->vfs_inode);
+ spin_lock_init(&ei->i_fc_lock);
+ ei->i_metadata_bhs = NULL;
+ ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+ ei->i_extra_isize = IALLOC_TEST_EXTRA_ISIZE;
+ ei->i_csum_seed = IALLOC_TEST_CSUM_SEED;
+ ei->i_last_alloc_group = ~0U;
+}
+
+static struct inode *ialloc_test_alloc_inode(struct super_block *sb)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+ struct ext4_inode_info *ei;
+
+ if (ctx && ctx->fail_alloc_inode)
+ return NULL;
+
+ ei = kzalloc_obj(*ei);
+ if (!ei)
+ return NULL;
+
+ init_rwsem(&ei->i_data_sem);
+ inode_init_once(&ei->vfs_inode);
+ ialloc_test_init_ei_fields(ei);
+ return &ei->vfs_inode;
+}
+
+static void ialloc_test_evict_inode(struct inode *inode)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+ truncate_inode_pages_final(&inode->i_data);
+ if (ctx && !ctx->evict_skip_free &&
+ !inode->i_nlink && !is_bad_inode(inode)) {
+ ext4_free_inode(NULL, inode);
+ return;
+ }
+ ext4_clear_inode(inode);
+}
+
+static void ialloc_test_destroy_inode(struct inode *inode)
+{
+ kfree(ext4_i_metadata_bhs(inode));
+ EXT4_I(inode)->i_metadata_bhs = NULL;
+}
+
+static void ialloc_test_free_inode_mem(struct inode *inode)
+{
+ kfree(EXT4_I(inode));
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **ialloc_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+
+static qsize_t *ialloc_test_get_reserved_space(struct inode *inode)
+{
+ return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations ialloc_test_qops = {
+ .get_reserved_space = ialloc_test_get_reserved_space,
+};
+#endif
+
+static const struct super_operations ialloc_test_sops = {
+ .alloc_inode = ialloc_test_alloc_inode,
+ .evict_inode = ialloc_test_evict_inode,
+ .destroy_inode = ialloc_test_destroy_inode,
+ .free_inode = ialloc_test_free_inode_mem,
+ .drop_inode = inode_just_drop,
+#ifdef CONFIG_QUOTA
+ .get_dquots = ialloc_test_get_dquots,
+#endif
+};
+
+static int ialloc_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static struct file_system_type ialloc_test_fs_type = {
+ .name = "ext4_ialloc_kunit",
+ .init_fs_context = ialloc_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static struct ialloc_test_blk *ialloc_test_find_blk(struct ialloc_test_ctx *ctx,
+ ext4_fsblk_t blocknr)
+{
+ unsigned int i;
+
+ for (i = 0; i < IALLOC_TEST_POOL_SIZE; i++) {
+ if (ctx->pool[i].in_use && ctx->pool[i].blocknr == blocknr)
+ return &ctx->pool[i];
+ }
+ return NULL;
+}
+
+static struct ialloc_test_blk *ialloc_test_find_blk_by_bh(
+ struct ialloc_test_ctx *ctx, struct buffer_head *bh)
+{
+ unsigned int i;
+
+ for (i = 0; i < IALLOC_TEST_POOL_SIZE; i++) {
+ if (ctx->pool[i].in_use && &ctx->pool[i].bh == bh)
+ return &ctx->pool[i];
+ }
+ return NULL;
+}
+
+static struct ialloc_test_blk *ialloc_test_setup_blk(struct ialloc_test_ctx *ctx,
+ ext4_fsblk_t blocknr,
+ bool in_cache,
+ bool uptodate)
+{
+ struct ialloc_test_blk *slot = ialloc_test_find_blk(ctx, blocknr);
+ unsigned int i;
+
+ if (!slot) {
+ for (i = 0; i < IALLOC_TEST_POOL_SIZE; i++) {
+ if (!ctx->pool[i].in_use) {
+ slot = &ctx->pool[i];
+ break;
+ }
+ }
+ }
+ if (!slot)
+ return NULL;
+
+ memset(slot, 0, sizeof(*slot));
+ slot->in_use = true;
+ slot->in_cache = in_cache;
+ slot->blocknr = blocknr;
+ slot->bh.b_bdev = &ctx->bdev;
+ slot->bh.b_blocknr = blocknr;
+ slot->bh.b_size = IALLOC_TEST_BLK_SIZE;
+ slot->bh.b_data = slot->cache_buf;
+ atomic_set(&slot->bh.b_count, 1);
+ if (uptodate)
+ set_buffer_uptodate(&slot->bh);
+ return slot;
+}
+
+/* --- Static Stubs --- */
+
+static struct ext4_group_desc *ialloc_test_get_group_desc_stub(
+ struct super_block *sb, ext4_group_t block_group,
+ struct buffer_head **bh)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx || block_group >= EXT4_SB(sb)->s_groups_count)
+ return NULL;
+ if (block_group < 32 && (ctx->fail_gdp_mask & BIT(block_group)))
+ return NULL;
+ if (bh)
+ *bh = &ctx->gdesc_bh;
+ return &ctx->gdps[block_group];
+}
+
+static struct buffer_head *ialloc_test_sb_getblk_stub(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+ struct ialloc_test_blk *slot;
+
+ if (!ctx || ctx->fail_getblk)
+ return NULL;
+ slot = ialloc_test_find_blk(ctx, block);
+ if (!slot)
+ return NULL;
+ slot->in_cache = true;
+ get_bh(&slot->bh);
+ return &slot->bh;
+}
+
+static struct buffer_head *ialloc_test_sb_find_get_block_stub(
+ struct super_block *sb, ext4_fsblk_t block)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+ struct ialloc_test_blk *slot;
+
+ if (!ctx)
+ return NULL;
+ slot = ialloc_test_find_blk(ctx, block);
+ if (!slot || !slot->in_cache)
+ return NULL;
+ get_bh(&slot->bh);
+ return &slot->bh;
+}
+
+static int ialloc_test_read_bh_stub(struct buffer_head *bh, blk_opf_t op_flags,
+ bio_end_io_t end_io, bool simu_fail)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+ struct ialloc_test_blk *slot;
+ bool fail;
+
+ if (!ctx) {
+ unlock_buffer(bh);
+ return -EIO;
+ }
+ ctx->read_bh_calls++;
+ slot = ialloc_test_find_blk_by_bh(ctx, bh);
+ fail = ctx->fail_read_eio || simu_fail || !slot;
+
+ if (!fail)
+ memcpy(bh->b_data, slot->disk_buf, bh->b_size);
+
+ if (end_io) {
+ struct bio *bio = bio_alloc(NULL, 0, REQ_OP_READ, GFP_KERNEL);
+
+ if (bio) {
+ bio->bi_private = bh;
+ bio->bi_status = fail ? BLK_STS_IOERR : BLK_STS_OK;
+ end_io(bio);
+ return fail ? -EIO : 0;
+ }
+ }
+
+ if (fail) {
+ clear_buffer_uptodate(bh);
+ } else {
+ set_buffer_uptodate(bh);
+ set_bitmap_uptodate(bh);
+ }
+ unlock_buffer(bh);
+ return fail ? -EIO : 0;
+}
+
+static struct buffer_head *ialloc_test_read_bbitmap_nowait_stub(
+ struct super_block *sb, ext4_group_t block_group,
+ bool ignore_locked)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+ struct ialloc_test_blk *slot;
+
+ if (!ctx)
+ return ERR_PTR(-EIO);
+ if (ctx->fail_bbitmap_err)
+ return ERR_PTR(ctx->fail_bbitmap_err);
+ slot = ialloc_test_find_blk(ctx, ialloc_test_bbitmap_blk(block_group));
+ if (!slot)
+ return ERR_PTR(-ENOMEM);
+ set_buffer_uptodate(&slot->bh);
+ set_bitmap_uptodate(&slot->bh);
+ set_buffer_verified(&slot->bh);
+ get_bh(&slot->bh);
+ return &slot->bh;
+}
+
+static int ialloc_test_wait_bbitmap_stub(struct super_block *sb,
+ ext4_group_t block_group,
+ struct buffer_head *bh)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return -EIO;
+ return ctx->fail_wait_bbitmap_err;
+}
+
+static int ialloc_test_get_write_access_stub(
+ const char *where, unsigned int line, handle_t *handle,
+ struct super_block *sb, struct buffer_head *bh,
+ enum ext4_journal_trigger_type trigger_type)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return 0;
+ ctx->write_access_calls++;
+ if (ctx->fail_write_access_err &&
+ (!ctx->fail_write_access_on_call ||
+ ctx->write_access_calls == ctx->fail_write_access_on_call))
+ return ctx->fail_write_access_err;
+ return 0;
+}
+
+static int ialloc_test_dirty_metadata_stub(const char *where, unsigned int line,
+ handle_t *handle,
+ struct inode *inode,
+ struct buffer_head *bh)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+ struct ialloc_test_blk *slot;
+
+ if (!ctx)
+ return 0;
+ ctx->dirty_metadata_calls++;
+ if (ctx->fail_dirty_metadata_err &&
+ (!ctx->fail_dirty_metadata_on_call ||
+ ctx->dirty_metadata_calls == ctx->fail_dirty_metadata_on_call))
+ return ctx->fail_dirty_metadata_err;
+
+ if (bh) {
+ set_buffer_uptodate(bh);
+ if (bh == &ctx->gdesc_bh) {
+ ctx->gdesc_marked_dirty = true;
+ } else {
+ slot = ialloc_test_find_blk_by_bh(ctx, bh);
+ if (slot)
+ slot->marked_dirty = true;
+ }
+ if (ctx->fail_sync_dirty_buffer) {
+ clear_buffer_mapped(bh);
+ set_buffer_dirty(bh);
+ } else {
+ clear_buffer_dirty(bh);
+ }
+ }
+ return 0;
+}
+
+static int ialloc_test_mark_inode_dirty_stub(handle_t *handle,
+ struct inode *inode,
+ const char *func,
+ unsigned int line)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return 0;
+ return ctx->fail_mark_inode_dirty_err;
+}
+
+#ifdef CONFIG_EXT4_FS_SECURITY
+static int ialloc_test_init_security_stub(handle_t *handle,
+ struct inode *inode,
+ struct inode *dir,
+ const struct qstr *qstr)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return 0;
+ return ctx->fail_init_security_err;
+}
+#endif
+
+static int ialloc_test_itable_zeroout_stub(struct super_block *sb,
+ ext4_fsblk_t blk, int num,
+ int barrier)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return -EIO;
+ ctx->zeroout_calls++;
+ ctx->last_zeroout_blk = blk;
+ ctx->last_zeroout_num = num;
+ ctx->last_zeroout_barrier = barrier;
+ return ctx->fail_itable_zeroout_err;
+}
+
+static struct inode *ialloc_test_iget_stub(struct super_block *sb,
+ unsigned long ino,
+ ext4_iget_flags flags,
+ const char *function,
+ unsigned int line)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct ialloc_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return ERR_PTR(-EIO);
+ if (ctx->fail_iget_err)
+ return ERR_PTR(ctx->fail_iget_err);
+ if (!ctx->iget_inode)
+ return ERR_PTR(-ENOENT);
+ ihold(ctx->iget_inode);
+ return ctx->iget_inode;
+}
+
+/* --- Fixture Helpers & Cleanup --- */
+
+static void ialloc_test_track_inode(struct kunit *test,
+ struct ialloc_test_ctx *ctx,
+ struct inode *inode)
+{
+ if (inode_state_read_once(inode) & I_NEW)
+ unlock_new_inode(inode);
+ KUNIT_ASSERT_LT(test, ctx->nr_tracked_inodes,
+ IALLOC_TEST_MAX_TRACKED_INODES);
+ ctx->tracked_inodes[ctx->nr_tracked_inodes++] = inode;
+}
+
+static void ialloc_test_untrack_inode(struct ialloc_test_ctx *ctx,
+ struct inode *inode)
+{
+ unsigned int i;
+
+ for (i = 0; i < ctx->nr_tracked_inodes; i++) {
+ if (ctx->tracked_inodes[i] == inode) {
+ ctx->tracked_inodes[i] =
+ ctx->tracked_inodes[--ctx->nr_tracked_inodes];
+ ctx->tracked_inodes[ctx->nr_tracked_inodes] = NULL;
+ return;
+ }
+ }
+}
+
+static void ialloc_test_sync_ibitmap_csum(struct ialloc_test_ctx *ctx,
+ ext4_group_t g, bool sync_disk)
+{
+ struct ialloc_test_blk *slot;
+
+ slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(g));
+ if (!slot)
+ return;
+ ext4_inode_bitmap_csum_set(ctx->sb, &ctx->gdps[g], &slot->bh);
+ ext4_group_desc_csum_set(ctx->sb, g, &ctx->gdps[g]);
+ if (sync_disk)
+ memcpy(slot->disk_buf, slot->cache_buf, IALLOC_TEST_BLK_SIZE);
+}
+
+static void ialloc_test_enable_flex_bg(struct ialloc_test_ctx *ctx,
+ unsigned int log_groups_per_flex)
+{
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ ext4_group_t g, fg;
+
+ ext4_set_feature_flex_bg(sb);
+ sbi->s_log_groups_per_flex = log_groups_per_flex;
+ for (g = 0; g < IALLOC_TEST_NGROUPS; g++) {
+ atomic_set(&ctx->flex_grps[g].free_inodes, 0);
+ atomic_set(&ctx->flex_grps[g].used_dirs, 0);
+ atomic64_set(&ctx->flex_grps[g].free_clusters, 0);
+ }
+ for (g = 0; g < IALLOC_TEST_NGROUPS; g++) {
+ fg = ext4_flex_group(sbi, g);
+ atomic_add(ext4_free_inodes_count(sb, &ctx->gdps[g]),
+ &ctx->flex_grps[fg].free_inodes);
+ atomic_add(ext4_used_dirs_count(sb, &ctx->gdps[g]),
+ &ctx->flex_grps[fg].used_dirs);
+ atomic64_add(ext4_free_group_clusters(sb, &ctx->gdps[g]),
+ &ctx->flex_grps[fg].free_clusters);
+ }
+}
+
+static void ialloc_test_prep_standalone_free_inode(struct ialloc_test_ctx *ctx,
+ unsigned long ino,
+ umode_t mode)
+{
+ struct ext4_inode_info *ei = &ctx->standalone_ei;
+ struct inode *inode = &ei->vfs_inode;
+
+ memset(ei, 0, sizeof(*ei));
+ init_rwsem(&ei->i_data_sem);
+ inode_init_once(inode);
+ inode->i_sb = ctx->sb;
+ inode->i_ino = ino;
+ inode->i_mode = mode;
+ inode->i_blkbits = ctx->sb->s_blocksize_bits;
+ inode->i_flags = S_NOQUOTA;
+ inode->i_mapping = &inode->i_data;
+ inode->i_data.host = inode;
+ mapping_set_gfp_mask(&inode->i_data, GFP_HIGHUSER_MOVABLE);
+ init_rwsem(&inode->i_rwsem);
+ init_rwsem(&inode->i_data.invalidate_lock);
+ spin_lock_init(&inode->i_lock);
+ inode_state_assign_raw(inode, I_FREEING);
+ inode->__i_nlink = 0;
+ atomic_set(&inode->i_count, 1);
+ ialloc_test_init_ei_fields(ei);
+}
+
+static void ialloc_test_cleanup(void *data)
+{
+ struct ialloc_test_ctx *ctx = data;
+ handle_t *handle;
+ unsigned int i;
+
+ ctx->evict_skip_free = true;
+ ctx->sbi.s_journal = NULL;
+
+ while ((handle = ext4_journal_current_handle()) != NULL)
+ ext4_journal_stop(handle);
+
+ for (i = 0; i < ctx->nr_tracked_inodes; i++) {
+ struct inode *ti = ctx->tracked_inodes[i];
+
+ if (ti) {
+ if (inode_state_read_once(ti) & I_NEW)
+ unlock_new_inode(ti);
+ spin_lock(&ti->i_lock);
+ inode_state_clear(ti, I_DIRTY_TIME);
+ spin_unlock(&ti->i_lock);
+ set_nlink(ti, 1);
+ iput(ti);
+ ctx->tracked_inodes[i] = NULL;
+ }
+ }
+ ctx->nr_tracked_inodes = 0;
+
+ ctx->sb->s_root = NULL;
+ if (ctx->root_inode) {
+ set_nlink(ctx->root_inode, 2);
+ iput(ctx->root_inode);
+ ctx->root_inode = NULL;
+ }
+
+ deactivate_super(ctx->sb);
+ rcu_barrier();
+
+ percpu_counter_destroy(&ctx->sbi.s_dirs_counter);
+ percpu_counter_destroy(&ctx->sbi.s_freeinodes_counter);
+ percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+
+ kfree(ctx);
+}
+
+static int ialloc_test_init(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ ext4_group_t g;
+ unsigned int bit;
+ s64 total_free_inodes = 0;
+ s64 total_free_clusters = 0;
+ int err;
+
+ ctx = kzalloc_obj(*ctx);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ test->priv = ctx;
+
+ fc = fs_context_for_mount(&ialloc_test_fs_type, 0);
+ if (IS_ERR(fc)) {
+ err = PTR_ERR(fc);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+ sb = sget_fc(fc, NULL, set_anon_super_fc);
+ put_fs_context(fc);
+ if (IS_ERR(sb)) {
+ err = PTR_ERR(sb);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+ ctx->sb = sb;
+ sbi = &ctx->sbi;
+
+ sbi->s_sb = sb;
+ sb->s_fs_info = sbi;
+ sb->s_op = &ialloc_test_sops;
+#ifdef CONFIG_QUOTA
+ sb->dq_op = &ialloc_test_qops;
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
+#endif
+ sb->s_flags |= SB_ACTIVE;
+ sb->s_blocksize = IALLOC_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = IALLOC_TEST_BLK_BITS;
+ sb->s_maxbytes = MAX_LFS_FILESIZE;
+ sb->s_time_gran = 1;
+ sb->s_xattr = ext4_xattr_handlers;
+ strscpy(sb->s_id, "ext4_ialloc_kunit", sizeof(sb->s_id));
+
+ inode_init_once(&ctx->bd_inode);
+ ctx->bd_inode.i_sb = sb;
+ ctx->bd_inode.i_blkbits = IALLOC_TEST_BLK_BITS;
+ ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data;
+ ctx->bd_inode.i_data.host = &ctx->bd_inode;
+ ctx->bdev_queue.limits.logical_block_size = 512;
+ ctx->bdev_queue.limits.dma_alignment = 511;
+ ctx->bdev.bd_queue = &ctx->bdev_queue;
+ ctx->bdev.bd_mapping = &ctx->bd_inode.i_data;
+ bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+ sb->s_bdev = &ctx->bdev;
+
+ bgl_lock_init(&ctx->bgl);
+ sbi->s_blockgroup_lock = &ctx->bgl;
+
+ sbi->s_es = &ctx->es;
+ ctx->es.s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+ ctx->es.s_first_data_block = cpu_to_le32(IALLOC_TEST_FIRST_DATA_BLK);
+ ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_LINUX);
+ ctx->es.s_inodes_count = cpu_to_le32(IALLOC_TEST_TOTAL_INODES);
+ ctx->es.s_inodes_per_group = cpu_to_le32(IALLOC_TEST_INODES_PER_GRP);
+ ctx->es.s_blocks_per_group = cpu_to_le32(IALLOC_TEST_BLOCKS_PER_GRP);
+ ctx->es.s_clusters_per_group = cpu_to_le32(IALLOC_TEST_BLOCKS_PER_GRP);
+ ctx->es.s_first_ino = cpu_to_le32(IALLOC_TEST_FIRST_INO);
+ ctx->es.s_inode_size = cpu_to_le16(IALLOC_TEST_INODE_SIZE);
+ ctx->es.s_min_extra_isize = cpu_to_le16(IALLOC_TEST_EXTRA_ISIZE);
+ ctx->es.s_want_extra_isize = cpu_to_le16(IALLOC_TEST_EXTRA_ISIZE);
+ ctx->es.s_checksum_type = EXT4_CRC32C_CHKSUM;
+ ext4_blocks_count_set(&ctx->es, IALLOC_TEST_TOTAL_BLKS);
+
+ ext4_set_feature_sparse_super(sb);
+ ext4_set_feature_extents(sb);
+ ext4_set_feature_extra_isize(sb);
+ ext4_set_feature_metadata_csum(sb);
+ ext4_set_feature_64bit(sb);
+ ext4_set_feature_dir_index(sb);
+
+ ctx->sb_bh.b_data = (char *)&ctx->es;
+ ctx->sb_bh.b_size = sizeof(ctx->es);
+ ctx->sb_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->sb_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->sb_bh);
+ sbi->s_sbh = &ctx->sb_bh;
+
+ ctx->gdesc_bh.b_data = ctx->gdesc_buf;
+ ctx->gdesc_bh.b_size = IALLOC_TEST_BLK_SIZE;
+ ctx->gdesc_bh.b_blocknr = IALLOC_TEST_GDESC_BLK;
+ ctx->gdesc_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->gdesc_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->gdesc_bh);
+
+ sbi->s_inode_size = IALLOC_TEST_INODE_SIZE;
+ sbi->s_inodes_per_block = IALLOC_TEST_INODES_PER_BLK;
+ sbi->s_inodes_per_group = IALLOC_TEST_INODES_PER_GRP;
+ sbi->s_itb_per_group = IALLOC_TEST_ITB_PER_GRP;
+ sbi->s_first_ino = IALLOC_TEST_FIRST_INO;
+ sbi->s_want_extra_isize = IALLOC_TEST_EXTRA_ISIZE;
+ sbi->s_blocks_per_group = IALLOC_TEST_BLOCKS_PER_GRP;
+ sbi->s_clusters_per_group = IALLOC_TEST_BLOCKS_PER_GRP;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_cluster_bits = 0;
+ sbi->s_groups_count = IALLOC_TEST_NGROUPS;
+ sbi->s_gdb_count = 1;
+ sbi->s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_desc_per_block = IALLOC_TEST_BLK_SIZE / EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_desc_per_block_bits = ilog2(sbi->s_desc_per_block);
+ sbi->s_bitmap_maxbytes = 0x1ff7ffff000ULL;
+ sbi->s_csum_seed = IALLOC_TEST_CSUM_SEED;
+ sbi->s_hash_seed[0] = 0x12345678U;
+ sbi->s_hash_seed[1] = 0x9abcdef0U;
+ sbi->s_hash_seed[2] = 0x13579bdfU;
+ sbi->s_hash_seed[3] = 0x2468ace0U;
+
+ set_opt(sb, ERRORS_CONT);
+ set_opt(sb, ORDERED_DATA);
+ spin_lock_init(&sbi->s_bdev_wb_lock);
+ spin_lock_init(&sbi->s_error_lock);
+ spin_lock_init(&sbi->s_md_lock);
+ mutex_init(&sbi->s_fc_lock);
+ INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_MAIN]);
+ INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_STAGING]);
+ INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_MAIN]);
+ INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_STAGING]);
+
+ ratelimit_state_init(&sbi->s_err_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+ ratelimit_state_init(&sbi->s_warning_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+ ratelimit_state_init(&sbi->s_msg_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+
+ /* Wire RCU-indexed group_info and flex_groups tables */
+ for (g = 0; g < IALLOC_TEST_NGROUPS; g++) {
+ struct ialloc_test_blk *iblk, *bblk;
+ u32 free_inos = (g == 0) ? IALLOC_TEST_BG0_FREE_INODES :
+ IALLOC_TEST_INODES_PER_GRP;
+
+ init_rwsem(&ctx->grp_infos[g].alloc_sem);
+ ctx->grp_infos[g].bb_state = 0;
+ ctx->grp_info_ptrs[g] = &ctx->grp_infos[g];
+ ctx->flex_grp_ptrs[g] = &ctx->flex_grps[g];
+
+ ext4_block_bitmap_set(sb, &ctx->gdps[g],
+ ialloc_test_bbitmap_blk(g));
+ ext4_inode_bitmap_set(sb, &ctx->gdps[g],
+ ialloc_test_ibitmap_blk(g));
+ ext4_inode_table_set(sb, &ctx->gdps[g],
+ ialloc_test_itable_blk(g));
+ ext4_free_inodes_set(sb, &ctx->gdps[g], free_inos);
+ ext4_used_dirs_set(sb, &ctx->gdps[g], (g == 0) ? 1U : 0U);
+ ext4_itable_unused_set(sb, &ctx->gdps[g], free_inos);
+ ext4_free_group_clusters_set(sb, &ctx->gdps[g],
+ IALLOC_TEST_DEFAULT_FREE_CLU);
+
+ iblk = ialloc_test_setup_blk(ctx, ialloc_test_ibitmap_blk(g),
+ true, true);
+ bblk = ialloc_test_setup_blk(ctx, ialloc_test_bbitmap_blk(g),
+ true, true);
+ if (!iblk || !bblk) {
+ deactivate_locked_super(sb);
+ kfree(ctx);
+ KUNIT_FAIL_AND_ABORT(test, "block pool setup failed");
+ }
+
+ /* Mark reserved bits 0..9 in group 0 and end padding 64..8191 */
+ if (g == 0) {
+ for (bit = 0; bit < IALLOC_TEST_FIRST_INO - 1; bit++)
+ ext4_set_bit(bit, iblk->cache_buf);
+ }
+ ext4_mark_bitmap_end(IALLOC_TEST_INODES_PER_GRP,
+ IALLOC_TEST_BLK_SIZE * 8,
+ iblk->cache_buf);
+ set_bitmap_uptodate(&iblk->bh);
+ set_buffer_verified(&iblk->bh);
+
+ ext4_block_bitmap_csum_set(sb, &ctx->gdps[g], &bblk->bh);
+ memcpy(bblk->disk_buf, bblk->cache_buf, IALLOC_TEST_BLK_SIZE);
+ set_bitmap_uptodate(&bblk->bh);
+ set_buffer_verified(&bblk->bh);
+
+ ialloc_test_sync_ibitmap_csum(ctx, g, true);
+
+ total_free_inodes += free_inos;
+ total_free_clusters += IALLOC_TEST_DEFAULT_FREE_CLU;
+ }
+ ctx->grp_info_slice[0] = ctx->grp_info_ptrs;
+ rcu_assign_pointer(sbi->s_group_info, ctx->grp_info_slice);
+ rcu_assign_pointer(sbi->s_flex_groups, ctx->flex_grp_ptrs);
+
+ err = percpu_counter_init(&sbi->s_freeclusters_counter,
+ total_free_clusters, GFP_KERNEL);
+ if (err) {
+ deactivate_locked_super(sb);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+ err = percpu_counter_init(&sbi->s_freeinodes_counter,
+ total_free_inodes, GFP_KERNEL);
+ if (err) {
+ percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+ deactivate_locked_super(sb);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+ err = percpu_counter_init(&sbi->s_dirs_counter, 1, GFP_KERNEL);
+ if (err) {
+ percpu_counter_destroy(&ctx->sbi.s_freeinodes_counter);
+ percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+ deactivate_locked_super(sb);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+
+ /* Create hashed VFS root directory inode (EXT4_ROOT_INO = 2) */
+ ctx->root_inode = new_inode(sb);
+ if (!ctx->root_inode) {
+ percpu_counter_destroy(&ctx->sbi.s_dirs_counter);
+ percpu_counter_destroy(&ctx->sbi.s_freeinodes_counter);
+ percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+ deactivate_locked_super(sb);
+ kfree(ctx);
+ KUNIT_FAIL_AND_ABORT(test, "failed to allocate root_inode");
+ }
+ ctx->root_inode->i_ino = EXT4_ROOT_INO;
+ ctx->root_inode->i_mode = S_IFDIR | 0755;
+ set_nlink(ctx->root_inode, 2);
+ EXT4_I(ctx->root_inode)->i_block_group = 0;
+ insert_inode_locked(ctx->root_inode);
+ unlock_new_inode(ctx->root_inode);
+
+ ctx->root_dentry.d_sb = sb;
+ ctx->root_dentry.d_inode = ctx->root_inode;
+ sb->s_root = &ctx->root_dentry;
+
+ up_write(&sb->s_umount);
+
+ err = kunit_add_action_or_reset(test, ialloc_test_cleanup, ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ kunit_activate_static_stub(test, ext4_get_group_desc,
+ ialloc_test_get_group_desc_stub);
+ kunit_activate_static_stub(test, ext4_sb_getblk,
+ ialloc_test_sb_getblk_stub);
+ kunit_activate_static_stub(test, ext4_sb_find_get_block,
+ ialloc_test_sb_find_get_block_stub);
+ kunit_activate_static_stub(test, ext4_read_bh,
+ ialloc_test_read_bh_stub);
+ kunit_activate_static_stub(test, ext4_read_block_bitmap_nowait,
+ ialloc_test_read_bbitmap_nowait_stub);
+ kunit_activate_static_stub(test, ext4_wait_block_bitmap,
+ ialloc_test_wait_bbitmap_stub);
+ kunit_activate_static_stub(test, __ext4_journal_get_write_access,
+ ialloc_test_get_write_access_stub);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ ialloc_test_dirty_metadata_stub);
+ kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+ ialloc_test_mark_inode_dirty_stub);
+#ifdef CONFIG_EXT4_FS_SECURITY
+ kunit_activate_static_stub(test, ext4_init_security,
+ ialloc_test_init_security_stub);
+#endif
+ kunit_activate_static_stub(test, ext4_itable_zeroout,
+ ialloc_test_itable_zeroout_stub);
+ kunit_activate_static_stub(test, __ext4_iget,
+ ialloc_test_iget_stub);
+
+ return 0;
+}
+
+/* =========================================================================
+ * 1. Bitmap Helpers: ext4_mark_bitmap_end & ext4_end_bitmap_read
+ * =========================================================================
+ */
+
+struct mark_bitmap_end_param {
+ const char *desc;
+ int start_bit;
+ int end_bit;
+};
+
+static const struct mark_bitmap_end_param mark_bitmap_end_params[] = {
+ { "unaligned_start_to_aligned_end", 67, 128 },
+ { "byte_aligned_memset_only", 64, 128 },
+ { "single_partial_byte_no_memset", 3, 8 },
+ { "unaligned_within_second_byte", 10, 16 },
+ { "equal_start_and_end_noop", 64, 64 },
+ { "start_greater_than_end_noop", 75, 64 },
+};
+
+static void mark_bitmap_end_param_to_desc(const struct mark_bitmap_end_param *p,
+ char *desc)
+{
+ strscpy(desc, p->desc, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(mark_bitmap_end, mark_bitmap_end_params,
+ mark_bitmap_end_param_to_desc);
+
+static void ialloc_test_mark_bitmap_end(struct kunit *test)
+{
+ const struct mark_bitmap_end_param *p = test->param_value;
+ char bitmap[24] = {};
+ int bit;
+
+ ext4_mark_bitmap_end(p->start_bit, p->end_bit, bitmap);
+
+ for (bit = 0; bit < (int)sizeof(bitmap) * 8; bit++) {
+ bool expected = (p->start_bit < p->end_bit) &&
+ (bit >= p->start_bit && bit < p->end_bit);
+
+ KUNIT_EXPECT_EQ_MSG(test, (bool)ext4_test_bit(bit, bitmap),
+ expected, "%s: unexpected value at bit %d",
+ p->desc, bit);
+ }
+}
+
+static void ialloc_test_end_bitmap_read_ok(struct kunit *test)
+{
+ struct buffer_head bh = {};
+ struct bio *bio;
+
+ atomic_set(&bh.b_count, 1);
+ lock_buffer(&bh);
+
+ bio = bio_alloc(NULL, 0, REQ_OP_READ, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, bio);
+ bio->bi_private = &bh;
+ bio->bi_status = BLK_STS_OK;
+
+ ext4_end_bitmap_read(bio);
+
+ KUNIT_EXPECT_FALSE(test, buffer_locked(&bh));
+ KUNIT_EXPECT_TRUE(test, buffer_uptodate(&bh));
+ KUNIT_EXPECT_TRUE(test, bitmap_uptodate(&bh));
+}
+
+static void ialloc_test_end_bitmap_read_ioerr(struct kunit *test)
+{
+ struct buffer_head bh = {};
+ struct bio *bio;
+
+ atomic_set(&bh.b_count, 1);
+ lock_buffer(&bh);
+
+ bio = bio_alloc(NULL, 0, REQ_OP_READ, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, bio);
+ bio->bi_private = &bh;
+ bio->bi_status = BLK_STS_IOERR;
+ bio_set_flag(bio, BIO_QUIET);
+
+ ext4_end_bitmap_read(bio);
+
+ KUNIT_EXPECT_FALSE(test, buffer_locked(&bh));
+ KUNIT_EXPECT_FALSE(test, buffer_uptodate(&bh));
+ KUNIT_EXPECT_FALSE(test, bitmap_uptodate(&bh));
+ KUNIT_EXPECT_TRUE(test, test_bit(BH_Quiet, &bh.b_state));
+}
+
+/* =========================================================================
+ * 2. Inode Bitmap Validation: ext4_validate_inode_bitmap_test
+ * =========================================================================
+ */
+
+static void ialloc_test_validate_ibitmap_csum(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *slot;
+ s64 free_before;
+ int err;
+
+ slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ /* 64-bit group descriptor valid CRC */
+ clear_buffer_verified(&slot->bh);
+ err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[1], 1, &slot->bh);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, (bool)buffer_verified(&slot->bh));
+ KUNIT_EXPECT_FALSE(test,
+ (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[1]));
+
+ /* 32-bit group descriptor valid CRC (low 16-bit checksum only) */
+ ext4_clear_feature_64bit(sb);
+ ctx->sbi.s_desc_size = EXT4_MIN_DESC_SIZE;
+ ext4_inode_bitmap_csum_set(sb, &ctx->gdps[1], &slot->bh);
+ clear_buffer_verified(&slot->bh);
+ err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[1], 1, &slot->bh);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, (bool)buffer_verified(&slot->bh));
+
+ /* Restore 64-bit descriptor and corrupt bitmap byte */
+ ext4_set_feature_64bit(sb);
+ ctx->sbi.s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+ ialloc_test_sync_ibitmap_csum(ctx, 1, false);
+ clear_buffer_verified(&slot->bh);
+ slot->cache_buf[0] ^= 0x01;
+ free_before = percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter);
+
+ err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[1], 1, &slot->bh);
+ KUNIT_EXPECT_EQ(test, err, -EFSBADCRC);
+ KUNIT_EXPECT_FALSE(test, (bool)buffer_verified(&slot->bh));
+ KUNIT_EXPECT_TRUE(test,
+ (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[1]));
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter),
+ free_before - IALLOC_TEST_INODES_PER_GRP);
+}
+
+static void ialloc_test_validate_ibitmap_short_circuits(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *slot;
+ int err;
+
+ slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(2));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ /* Corrupt byte, but EXT4_FC_REPLAY skips validation */
+ clear_buffer_verified(&slot->bh);
+ slot->cache_buf[0] ^= 0xff;
+ ctx->sbi.s_mount_state |= EXT4_FC_REPLAY;
+ err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[2], 2, &slot->bh);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test, (bool)buffer_verified(&slot->bh));
+ ctx->sbi.s_mount_state &= ~EXT4_FC_REPLAY;
+
+ /* Already buffer_verified short-circuits before checking CRC */
+ set_buffer_verified(&slot->bh);
+ err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[2], 2, &slot->bh);
+ KUNIT_EXPECT_EQ(test, err, 0);
+
+ /* Out-of-range group (NULL ext4_get_group_info) -> -EFSCORRUPTED */
+ clear_buffer_verified(&slot->bh);
+ err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[2],
+ IALLOC_TEST_NGROUPS + 1,
+ &slot->bh);
+ KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+
+ /* Pre-marked IBITMAP_CORRUPT -> -EFSCORRUPTED */
+ set_bit(EXT4_GROUP_INFO_IBITMAP_CORRUPT_BIT,
+ &ctx->grp_infos[2].bb_state);
+ err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[2], 2, &slot->bh);
+ KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+
+#ifdef CONFIG_EXT4_DEBUG
+ slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(3));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+ clear_buffer_verified(&slot->bh);
+ ctx->sbi.s_simulate_fail = EXT4_SIM_IBITMAP_CRC;
+ err = ext4_validate_inode_bitmap_test(sb, &ctx->gdps[3], 3, &slot->bh);
+ KUNIT_EXPECT_EQ(test, err, -EFSBADCRC);
+ KUNIT_EXPECT_EQ(test, ctx->sbi.s_simulate_fail, 0UL);
+#endif
+}
+
+/* =========================================================================
+ * 3. Reading Inode Bitmaps: ext4_read_inode_bitmap_test
+ * =========================================================================
+ */
+
+static void ialloc_test_read_ibitmap_disk_and_cache(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *slot;
+ struct buffer_head *bh;
+
+ slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ /* Put distinctive valid bitmap on disk and invalidate cache flags */
+ ext4_set_bit(5, slot->disk_buf);
+ memcpy(slot->cache_buf, slot->disk_buf, IALLOC_TEST_BLK_SIZE);
+ ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+ memset(slot->cache_buf, 0, IALLOC_TEST_BLK_SIZE);
+ clear_bitmap_uptodate(&slot->bh);
+ clear_buffer_uptodate(&slot->bh);
+ clear_buffer_verified(&slot->bh);
+
+ bh = ext4_read_inode_bitmap_test(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, ctx->read_bh_calls, 1U);
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(5, bh->b_data));
+ KUNIT_EXPECT_TRUE(test, (bool)bitmap_uptodate(bh));
+ KUNIT_EXPECT_TRUE(test, (bool)buffer_uptodate(bh));
+ KUNIT_EXPECT_TRUE(test, (bool)buffer_verified(bh));
+ brelse(bh);
+ KUNIT_EXPECT_EQ(test, atomic_read(&slot->bh.b_count), 1);
+
+ /* Second read hits bitmap_uptodate fast path with zero I/O */
+ bh = ext4_read_inode_bitmap_test(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, ctx->read_bh_calls, 1U);
+ brelse(bh);
+
+ /* buffer_uptodate && !bitmap_uptodate promotes bitmap_uptodate without I/O */
+ clear_bitmap_uptodate(&slot->bh);
+ clear_buffer_verified(&slot->bh);
+ bh = ext4_read_inode_bitmap_test(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, ctx->read_bh_calls, 1U);
+ KUNIT_EXPECT_TRUE(test, (bool)bitmap_uptodate(bh));
+ KUNIT_EXPECT_TRUE(test, (bool)buffer_verified(bh));
+ brelse(bh);
+ KUNIT_EXPECT_EQ(test, atomic_read(&slot->bh.b_count), 1);
+}
+
+static void ialloc_test_read_ibitmap_uninit(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *slot0, *slot1;
+ struct buffer_head *bh;
+ unsigned int bit;
+
+ /* Group 1 with EXT4_BG_INODE_UNINIT initializes in memory without I/O */
+ slot1 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, slot1);
+ memset(slot1->cache_buf, 0xaa, IALLOC_TEST_BLK_SIZE);
+ clear_bitmap_uptodate(&slot1->bh);
+ clear_buffer_uptodate(&slot1->bh);
+ clear_buffer_verified(&slot1->bh);
+ ctx->gdps[1].bg_flags |= cpu_to_le16(EXT4_BG_INODE_UNINIT);
+
+ bh = ext4_read_inode_bitmap_test(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, ctx->read_bh_calls, 0U);
+ KUNIT_EXPECT_TRUE(test, (bool)bitmap_uptodate(bh));
+ KUNIT_EXPECT_TRUE(test, (bool)buffer_uptodate(bh));
+ KUNIT_EXPECT_TRUE(test, (bool)buffer_verified(bh));
+ for (bit = 0; bit < IALLOC_TEST_INODES_PER_GRP; bit++)
+ KUNIT_EXPECT_FALSE_MSG(test, (bool)ext4_test_bit(bit, bh->b_data),
+ "expected bit %u cleared", bit);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)ext4_test_bit(IALLOC_TEST_INODES_PER_GRP,
+ bh->b_data));
+ KUNIT_EXPECT_TRUE(test,
+ (bool)ext4_test_bit(IALLOC_TEST_BLK_SIZE * 8 - 1,
+ bh->b_data));
+ brelse(bh);
+
+ /* Group 0 marked EXT4_BG_INODE_UNINIT is rejected as corrupt */
+ slot0 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(0));
+ KUNIT_ASSERT_NOT_NULL(test, slot0);
+ clear_bitmap_uptodate(&slot0->bh);
+ clear_buffer_uptodate(&slot0->bh);
+ ctx->gdps[0].bg_flags |= cpu_to_le16(EXT4_BG_INODE_UNINIT);
+
+ bh = ext4_read_inode_bitmap_test(sb, 0);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, atomic_read(&slot0->bh.b_count), 1);
+}
+
+static void ialloc_test_read_ibitmap_errors(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *slot;
+ struct buffer_head *bh;
+
+ /* NULL group descriptor */
+ ctx->fail_gdp_mask = BIT(1);
+ bh = ext4_read_inode_bitmap_test(sb, 1);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSCORRUPTED);
+ ctx->fail_gdp_mask = 0;
+
+ /* Invalid bitmap_blk <= s_first_data_block (1) */
+ ext4_inode_bitmap_set(sb, &ctx->gdps[1], IALLOC_TEST_FIRST_DATA_BLK);
+ bh = ext4_read_inode_bitmap_test(sb, 1);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[1]));
+
+ /* Invalid bitmap_blk >= ext4_blocks_count(es) */
+ ctx->grp_infos[1].bb_state = 0;
+ ext4_inode_bitmap_set(sb, &ctx->gdps[1], IALLOC_TEST_TOTAL_BLKS);
+ bh = ext4_read_inode_bitmap_test(sb, 1);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[1]));
+ ctx->grp_infos[1].bb_state = 0;
+ ext4_inode_bitmap_set(sb, &ctx->gdps[1], ialloc_test_ibitmap_blk(1));
+
+ /* ext4_sb_getblk allocation failure */
+ ctx->fail_getblk = true;
+ bh = ext4_read_inode_bitmap_test(sb, 1);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -ENOMEM);
+ ctx->fail_getblk = false;
+
+ /* Disk read I/O failure */
+ slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(2));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+ clear_bitmap_uptodate(&slot->bh);
+ clear_buffer_uptodate(&slot->bh);
+ ctx->fail_read_eio = true;
+ bh = ext4_read_inode_bitmap_test(sb, 2);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EIO);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[2]));
+ KUNIT_EXPECT_EQ(test, atomic_read(&slot->bh.b_count), 1);
+ ctx->fail_read_eio = false;
+
+ /* Bad CRC after successful disk read */
+ slot = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(3));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+ slot->disk_buf[0] ^= 0x80;
+ clear_bitmap_uptodate(&slot->bh);
+ clear_buffer_uptodate(&slot->bh);
+ clear_buffer_verified(&slot->bh);
+ bh = ext4_read_inode_bitmap_test(sb, 3);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSBADCRC);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[3]));
+ KUNIT_EXPECT_EQ(test, atomic_read(&slot->bh.b_count), 1);
+}
+
+/* =========================================================================
+ * 4. Freeing Inodes: ext4_free_inode
+ * =========================================================================
+ */
+
+static void ialloc_test_free_inode_reg_and_dir(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *iblk;
+ s64 free_before, dirs_before;
+
+ iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, iblk);
+
+ /* Mark bit 2 (ino 67, reg) and bit 3 (ino 68, dir) allocated in group 1 */
+ ext4_set_bit(2, iblk->cache_buf);
+ ext4_set_bit(3, iblk->cache_buf);
+ ext4_free_inodes_set(sb, &ctx->gdps[1], 62);
+ ext4_used_dirs_set(sb, &ctx->gdps[1], 1);
+ percpu_counter_add(&ctx->sbi.s_freeinodes_counter, -2);
+ percpu_counter_add(&ctx->sbi.s_dirs_counter, 1);
+ ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+ ialloc_test_enable_flex_bg(ctx, 1);
+
+ free_before = percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter);
+ dirs_before = percpu_counter_sum_positive(&ctx->sbi.s_dirs_counter);
+
+ /* Free regular file ino 67 (group 1, bit 2, flex_group 0) */
+ ialloc_test_prep_standalone_free_inode(ctx, 67, S_IFREG | 0644);
+ ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+
+ KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(2, iblk->cache_buf));
+ KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[1]), 63U);
+ KUNIT_EXPECT_EQ(test, ext4_used_dirs_count(sb, &ctx->gdps[1]), 1U);
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter),
+ free_before + 1);
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum_positive(&ctx->sbi.s_dirs_counter),
+ dirs_before);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->flex_grps[0].free_inodes),
+ (int)(IALLOC_TEST_BG0_FREE_INODES + 63));
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->flex_grps[0].used_dirs), 2);
+ KUNIT_EXPECT_TRUE(test, iblk->marked_dirty);
+ KUNIT_EXPECT_TRUE(test, ctx->gdesc_marked_dirty);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)ext4_inode_bitmap_csum_verify(sb, &ctx->gdps[1],
+ &iblk->bh));
+ KUNIT_EXPECT_TRUE(test,
+ (bool)ext4_group_desc_csum_verify(sb, 1,
+ &ctx->gdps[1]));
+
+ /* Free directory ino 68 (group 1, bit 3, flex_group 0) */
+ ialloc_test_prep_standalone_free_inode(ctx, 68, S_IFDIR | 0755);
+ ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+
+ KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(3, iblk->cache_buf));
+ KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[1]), 64U);
+ KUNIT_EXPECT_EQ(test, ext4_used_dirs_count(sb, &ctx->gdps[1]), 0U);
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter),
+ free_before + 2);
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum_positive(&ctx->sbi.s_dirs_counter),
+ dirs_before - 1);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->flex_grps[0].used_dirs), 1);
+}
+
+static void ialloc_test_free_inode_guards(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *iblk0, *iblk1;
+ struct inode *inode = &ctx->standalone_ei.vfs_inode;
+
+ iblk0 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(0));
+ iblk1 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, iblk0);
+ KUNIT_ASSERT_NOT_NULL(test, iblk1);
+
+ ext4_set_bit(0, iblk1->cache_buf); /* ino 65 */
+ ext4_free_inodes_set(sb, &ctx->gdps[1], 63);
+ ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+
+ /* NULL i_sb is ignored */
+ ialloc_test_prep_standalone_free_inode(ctx, 65, S_IFREG | 0644);
+ inode->i_sb = NULL;
+ ext4_free_inode(NULL, inode);
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(0, iblk1->cache_buf));
+
+ /* i_count > 1 is rejected before clear_inode */
+ ialloc_test_prep_standalone_free_inode(ctx, 65, S_IFREG | 0644);
+ atomic_set(&inode->i_count, 2);
+ ext4_free_inode(NULL, inode);
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(0, iblk1->cache_buf));
+ KUNIT_EXPECT_FALSE(test, !!(inode_state_read_once(inode) & I_CLEAR));
+
+ /* i_nlink > 0 is rejected before clear_inode */
+ ialloc_test_prep_standalone_free_inode(ctx, 65, S_IFREG | 0644);
+ set_nlink(inode, 1);
+ ext4_free_inode(NULL, inode);
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(0, iblk1->cache_buf));
+ KUNIT_EXPECT_FALSE(test, !!(inode_state_read_once(inode) & I_CLEAR));
+
+ /* Reserved ino < EXT4_FIRST_INO(sb) clears VFS inode then errors out */
+ ialloc_test_prep_standalone_free_inode(ctx, 5, S_IFREG | 0644);
+ ext4_free_inode(NULL, inode);
+ KUNIT_EXPECT_TRUE(test, !!(inode_state_read_once(inode) & I_CLEAR));
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(4, iblk0->cache_buf));
+
+ /* Out-of-range ino > s_inodes_count clears VFS inode then errors out */
+ ialloc_test_prep_standalone_free_inode(ctx, IALLOC_TEST_TOTAL_INODES + 1,
+ S_IFREG | 0644);
+ ext4_free_inode(NULL, inode);
+ KUNIT_EXPECT_TRUE(test, !!(inode_state_read_once(inode) & I_CLEAR));
+}
+
+static void ialloc_test_free_inode_corrupt_and_journal_errors(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *iblk1, *iblk2, *iblk3;
+
+ /* 1. Double-free (bit already clear) marks group bitmap corrupt */
+ iblk1 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, iblk1);
+ ialloc_test_prep_standalone_free_inode(ctx, 66, S_IFREG | 0644);
+ ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[1]));
+ KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[1]), 64U);
+
+ /* 2. Pre-corrupt group skips bitmap clear unless EXT4_FC_REPLAY */
+ iblk2 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(2));
+ KUNIT_ASSERT_NOT_NULL(test, iblk2);
+ ext4_set_bit(0, iblk2->cache_buf); /* ino 129 */
+ ext4_free_inodes_set(sb, &ctx->gdps[2], 63);
+ ialloc_test_sync_ibitmap_csum(ctx, 2, true);
+ set_bit(EXT4_GROUP_INFO_IBITMAP_CORRUPT_BIT,
+ &ctx->grp_infos[2].bb_state);
+
+ ialloc_test_prep_standalone_free_inode(ctx, 129, S_IFREG | 0644);
+ ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(0, iblk2->cache_buf));
+
+ ctx->sbi.s_mount_state |= EXT4_FC_REPLAY;
+ ialloc_test_prep_standalone_free_inode(ctx, 129, S_IFREG | 0644);
+ ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+ ctx->sbi.s_mount_state &= ~EXT4_FC_REPLAY;
+ KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(0, iblk2->cache_buf));
+ KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[2]), 64U);
+
+ /* 3. Write-access failure on bitmap_bh (1st) vs bh2 (2nd) */
+ iblk3 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(3));
+ KUNIT_ASSERT_NOT_NULL(test, iblk3);
+ ext4_set_bit(0, iblk3->cache_buf); /* ino 193 */
+ ext4_free_inodes_set(sb, &ctx->gdps[3], 63);
+ ialloc_test_sync_ibitmap_csum(ctx, 3, true);
+
+ ctx->write_access_calls = 0;
+ ctx->fail_write_access_err = -EIO;
+ ctx->fail_write_access_on_call = 1;
+ ialloc_test_prep_standalone_free_inode(ctx, 193, S_IFREG | 0644);
+ ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(0, iblk3->cache_buf));
+ KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[3]), 63U);
+
+ ctx->write_access_calls = 0;
+ ctx->fail_write_access_on_call = 2;
+ iblk3->marked_dirty = false;
+ ialloc_test_prep_standalone_free_inode(ctx, 193, S_IFREG | 0644);
+ ext4_free_inode(NULL, &ctx->standalone_ei.vfs_inode);
+ KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(0, iblk3->cache_buf));
+ KUNIT_EXPECT_TRUE(test, iblk3->marked_dirty);
+ KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[3]), 63U);
+ ctx->fail_write_access_err = 0;
+ ctx->fail_write_access_on_call = 0;
+}
+
+/* =========================================================================
+ * 5. Orlov Directory Allocator: ext4_find_group_orlov_test
+ * =========================================================================
+ */
+
+static void ialloc_test_orlov_topdir_spread(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct qstr qname = QSTR_INIT("projects", 8);
+ struct inode *subdir;
+ ext4_group_t grp = 0;
+ int ret;
+
+ /*
+ * Configure group 2 as the unique above-average group with the lowest
+ * directory count (0 used_dirs, 64 free_inodes, 900 free_clusters).
+ */
+ ext4_used_dirs_set(sb, &ctx->gdps[0], 5);
+ ext4_used_dirs_set(sb, &ctx->gdps[1], 4);
+ ext4_used_dirs_set(sb, &ctx->gdps[2], 0);
+ ext4_used_dirs_set(sb, &ctx->gdps[3], 6);
+ ext4_free_group_clusters_set(sb, &ctx->gdps[2], 900);
+
+ ret = ext4_find_group_orlov_test(sb, ctx->root_inode, &grp,
+ S_IFDIR | 0755, &qname);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 2U);
+
+ /* qstr == NULL uses random start, still converges on best group 2 */
+ grp = 0;
+ ret = ext4_find_group_orlov_test(sb, ctx->root_inode, &grp,
+ S_IFDIR | 0755, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 2U);
+
+ /* Non-root directory with EXT4_INODE_TOPDIR also uses topdir spread */
+ subdir = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, subdir);
+ subdir->i_ino = 65;
+ subdir->i_mode = S_IFDIR | 0755;
+ EXT4_I(subdir)->i_block_group = 1;
+ ext4_set_inode_flag(subdir, EXT4_INODE_TOPDIR);
+ ialloc_test_track_inode(test, ctx, subdir);
+
+ grp = 0;
+ ret = ext4_find_group_orlov_test(sb, subdir, &grp, S_IFDIR | 0755,
+ &qname);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 2U);
+}
+
+static void ialloc_test_orlov_subdir_and_thresholds(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *parent;
+ ext4_group_t grp = 0;
+ int ret;
+
+ parent = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, parent);
+ parent->i_ino = 65;
+ parent->i_mode = S_IFDIR | 0755;
+ EXT4_I(parent)->i_block_group = 1;
+ ialloc_test_track_inode(test, ctx, parent);
+
+ /* Healthy parent_group (1) is preferred */
+ ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 1U);
+
+ /* i_last_alloc_group overrides parent_group */
+ EXT4_I(parent)->i_last_alloc_group = 3;
+ ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 3U);
+ EXT4_I(parent)->i_last_alloc_group = ~0U;
+
+ /* Group 1 exceeds max_dirs -> advances cyclically to group 2 */
+ ext4_used_dirs_set(sb, &ctx->gdps[1], 40);
+ ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 2U);
+ ext4_used_dirs_set(sb, &ctx->gdps[1], 0);
+
+ /* Group 1 below min_inodes -> advances to group 2 */
+ ext4_free_inodes_set(sb, &ctx->gdps[1], 2);
+ ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 2U);
+ ext4_free_inodes_set(sb, &ctx->gdps[1], IALLOC_TEST_INODES_PER_GRP);
+
+ /* Group 1 below min_clusters -> advances to group 2 */
+ ext4_free_group_clusters_set(sb, &ctx->gdps[1], 10);
+ ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 2U);
+}
+
+static void ialloc_test_orlov_flex_bg_and_fallback(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *parent;
+ ext4_group_t grp = 0;
+ ext4_group_t g;
+ int ret;
+
+ parent = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, parent);
+ parent->i_ino = 12;
+ parent->i_mode = S_IFDIR | 0755;
+ EXT4_I(parent)->i_block_group = 0;
+ ialloc_test_track_inode(test, ctx, parent);
+
+ /*
+ * Flex size 2: flex 0 = {0, 1} has excessive dirs; flex 1 = {2, 3}
+ * is healthy, with group 2 full (0 free inodes) and group 3 having 64
+ * free inodes -> packs into first available constituent group 3.
+ */
+ ext4_used_dirs_set(sb, &ctx->gdps[0], 30);
+ ext4_used_dirs_set(sb, &ctx->gdps[1], 30);
+ ext4_free_inodes_set(sb, &ctx->gdps[2], 0);
+ ext4_free_inodes_set(sb, &ctx->gdps[3], 64);
+ percpu_counter_set(&ctx->sbi.s_dirs_counter, 60);
+ percpu_counter_set(&ctx->sbi.s_freeinodes_counter,
+ IALLOC_TEST_BG0_FREE_INODES + 64 + 0 + 64);
+ ialloc_test_enable_flex_bg(ctx, 1);
+
+ ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 3U);
+
+ /* Disable flex_bg and test fallback_retry with inflated counter */
+ ctx->sbi.s_log_groups_per_flex = 0;
+ ext4_clear_feature_flex_bg(sb);
+ for (g = 0; g < IALLOC_TEST_NGROUPS; g++) {
+ ext4_used_dirs_set(sb, &ctx->gdps[g], 50);
+ ext4_free_inodes_set(sb, &ctx->gdps[g], (g == 3) ? 1U : 0U);
+ }
+ /* Counter claims 200 free inodes (avefreei = 50), only bg 3 has 1 */
+ percpu_counter_set(&ctx->sbi.s_freeinodes_counter, 200);
+
+ ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 3U);
+
+ /* All groups completely full -> returns -1 */
+ ext4_free_inodes_set(sb, &ctx->gdps[3], 0);
+ ret = ext4_find_group_orlov_test(sb, parent, &grp, S_IFDIR | 0755, NULL);
+ KUNIT_EXPECT_EQ(test, ret, -1);
+}
+
+/* =========================================================================
+ * 6. Non-Directory Group Allocator: ext4_find_group_other_test
+ * =========================================================================
+ */
+
+static void ialloc_test_find_group_other_non_flex(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *parent;
+ ext4_group_t grp = 0;
+ ext4_group_t g;
+ int ret;
+
+ parent = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, parent);
+ parent->i_ino = 65;
+ parent->i_mode = S_IFDIR | 0755;
+ EXT4_I(parent)->i_block_group = 1;
+ ialloc_test_track_inode(test, ctx, parent);
+
+ /* 1. Parent group 1 has both free inodes and free clusters */
+ ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 1U);
+
+ /*
+ * 2. Quadratic hash probe when parent group 1 has 0 free inodes:
+ * start = (1 + 65) % 4 = 2; step i = 1 -> (2 + 1) % 4 = 3.
+ */
+ ext4_free_inodes_set(sb, &ctx->gdps[1], 0);
+ ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 3U);
+
+ /*
+ * 3. Linear fallback when no group has free clusters:
+ * starts at parent_group = 1, ++*group checks 2 first.
+ */
+ for (g = 0; g < IALLOC_TEST_NGROUPS; g++)
+ ext4_free_group_clusters_set(sb, &ctx->gdps[g], 0);
+ ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 2U);
+
+ /* 4. Returns -1 when all groups have 0 free inodes */
+ for (g = 0; g < IALLOC_TEST_NGROUPS; g++)
+ ext4_free_inodes_set(sb, &ctx->gdps[g], 0);
+ ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+ KUNIT_EXPECT_EQ(test, ret, -1);
+}
+
+static void ialloc_test_find_group_other_flex_bg(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *parent;
+ ext4_group_t grp = 0;
+ int ret;
+
+ parent = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, parent);
+ parent->i_ino = 65;
+ parent->i_mode = S_IFDIR | 0755;
+ EXT4_I(parent)->i_block_group = 1;
+ ialloc_test_track_inode(test, ctx, parent);
+
+ /* Flex size 2: parent in bg 1 (flex 0 = {0, 1}) packs into bg 0 first */
+ ialloc_test_enable_flex_bg(ctx, 1);
+ ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 0U);
+
+ /* Flex 0 full, i_last_alloc_group = 3 (flex 1 = {2, 3}) retries flex 1 -> bg 2 */
+ ext4_free_inodes_set(sb, &ctx->gdps[0], 0);
+ ext4_free_inodes_set(sb, &ctx->gdps[1], 0);
+ ialloc_test_enable_flex_bg(ctx, 1);
+ EXT4_I(parent)->i_last_alloc_group = 3;
+ ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 2U);
+
+ /* Flex 0 full and i_last_alloc_group unset -> Orlov fallback picks bg 2 */
+ EXT4_I(parent)->i_last_alloc_group = ~0U;
+ ret = ext4_find_group_other_test(sb, parent, &grp, S_IFREG | 0644);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, grp, 2U);
+}
+
+/* =========================================================================
+ * 7. Recently Deleted & Bit Search: recently_deleted & find_inode_bit
+ * =========================================================================
+ */
+
+static void ialloc_test_recently_deleted(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *it_slot;
+ struct ext4_inode *raw;
+ u32 now;
+
+ /* 1. EXT4_BG_INODE_UNINIT returns 0 immediately */
+ ctx->gdps[1].bg_flags |= cpu_to_le16(EXT4_BG_INODE_UNINIT);
+ KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 0), 0);
+ ctx->gdps[1].bg_flags &= ~cpu_to_le16(EXT4_BG_INODE_UNINIT);
+
+ /* 2. Inode >= (64 - itable_unused) was never used -> returns 0 */
+ ext4_itable_unused_set(sb, &ctx->gdps[1], 54); /* 0..9 used */
+ KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 10), 0);
+
+ /* 3. Uncached inode table block -> returns 0 */
+ it_slot = ialloc_test_setup_blk(ctx, ialloc_test_itable_blk(1),
+ false, true);
+ KUNIT_ASSERT_NOT_NULL(test, it_slot);
+ raw = (struct ext4_inode *)(it_slot->cache_buf +
+ 2 * IALLOC_TEST_INODE_SIZE);
+ now = ktime_get_real_seconds();
+ raw->i_dtime = cpu_to_le32(now - 10);
+ KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 0);
+
+ /* 4. Cached but !buffer_uptodate -> returns 0 */
+ it_slot->in_cache = true;
+ clear_buffer_uptodate(&it_slot->bh);
+ KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 0);
+ set_buffer_uptodate(&it_slot->bh);
+
+ /* 5. dtime == 0 -> returns 0 */
+ raw->i_dtime = 0;
+ KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 0);
+
+ /* 6. Clean buffer uses 60s window; dirty buffer extends to 360s */
+ now = ktime_get_real_seconds();
+ raw->i_dtime = cpu_to_le32(now - 20);
+ KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 1);
+
+ raw->i_dtime = cpu_to_le32(now - 120);
+ KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 0);
+
+ set_buffer_dirty(&it_slot->bh);
+ KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 1);
+
+ raw->i_dtime = cpu_to_le32(now - 400);
+ KUNIT_EXPECT_EQ(test, ext4_recently_deleted_test(sb, 1, 2), 0);
+ clear_buffer_dirty(&it_slot->bh);
+}
+
+static void ialloc_test_find_inode_bit(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *iblk, *it_slot;
+ struct ext4_inode *raw1;
+ unsigned long ino;
+ unsigned int bit;
+ int ret;
+
+ iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, iblk);
+ ext4_itable_unused_set(sb, &ctx->gdps[1], 0);
+
+ /* Mark all bits except bit 1 (recently deleted) and bit 6 (old) */
+ for (bit = 0; bit < IALLOC_TEST_INODES_PER_GRP; bit++) {
+ if (bit != 1 && bit != 6)
+ ext4_set_bit(bit, iblk->cache_buf);
+ }
+ it_slot = ialloc_test_setup_blk(ctx, ialloc_test_itable_blk(1),
+ true, true);
+ KUNIT_ASSERT_NOT_NULL(test, it_slot);
+ raw1 = (struct ext4_inode *)(it_slot->cache_buf + IALLOC_TEST_INODE_SIZE);
+ raw1->i_dtime = cpu_to_le32((u32)ktime_get_real_seconds() - 10);
+
+ /* No-journal mode skips recently deleted bit 1 in favor of bit 6 */
+ ino = 0;
+ ret = ext4_find_inode_bit_test(sb, 1, &iblk->bh, &ino);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, ino, 6UL);
+
+ /* Journaled mode ignores recently_deleted and picks bit 1 directly */
+ ctx->sbi.s_journal = (journal_t *)1UL;
+ ino = 0;
+ ret = ext4_find_inode_bit_test(sb, 1, &iblk->bh, &ino);
+ ctx->sbi.s_journal = NULL;
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, ino, 1UL);
+
+ /* Fallback: when bit 6 is also allocated, returns recently deleted bit 1 */
+ ext4_set_bit(6, iblk->cache_buf);
+ ino = 0;
+ ret = ext4_find_inode_bit_test(sb, 1, &iblk->bh, &ino);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, ino, 1UL);
+
+ /* Full bitmap returns 0 */
+ ext4_set_bit(1, iblk->cache_buf);
+ ino = 0;
+ ret = ext4_find_inode_bit_test(sb, 1, &iblk->bh, &ino);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+}
+
+/* =========================================================================
+ * 8. Marking Specific Inodes Used: ext4_mark_inode_used
+ * =========================================================================
+ */
+
+static void ialloc_test_mark_inode_used_uninit(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *iblk, *bblk;
+ int err;
+
+ /*
+ * Configure group 2 with both EXT4_BG_INODE_UNINIT and
+ * EXT4_BG_BLOCK_UNINIT. Group 2 has no backup superblock, so overhead
+ * is 1 (bbitmap) + 1 (ibitmap) + 16 (itable) = 18 clusters -> 1006 free.
+ */
+ iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(2));
+ bblk = ialloc_test_find_blk(ctx, ialloc_test_bbitmap_blk(2));
+ KUNIT_ASSERT_NOT_NULL(test, iblk);
+ KUNIT_ASSERT_NOT_NULL(test, bblk);
+
+ clear_bitmap_uptodate(&iblk->bh);
+ clear_buffer_uptodate(&iblk->bh);
+ clear_buffer_verified(&iblk->bh);
+ ctx->gdps[2].bg_flags = cpu_to_le16(EXT4_BG_INODE_UNINIT |
+ EXT4_BG_BLOCK_UNINIT);
+ ext4_free_group_clusters_set(sb, &ctx->gdps[2], 0);
+ ext4_itable_unused_set(sb, &ctx->gdps[2], 64);
+ ext4_free_inodes_set(sb, &ctx->gdps[2], 64);
+ ext4_group_desc_csum_set(sb, 2, &ctx->gdps[2]);
+
+ /* Inode 135 is in group 2, bit 6 ((135 - 1) % 64 == 6) */
+ err = ext4_mark_inode_used(sb, 135);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test,
+ le16_to_cpu(ctx->gdps[2].bg_flags) &
+ (EXT4_BG_INODE_UNINIT | EXT4_BG_BLOCK_UNINIT), 0);
+ KUNIT_EXPECT_EQ(test, ext4_free_group_clusters(sb, &ctx->gdps[2]),
+ 1006U);
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(6, iblk->cache_buf));
+ KUNIT_EXPECT_EQ(test, ext4_itable_unused_count(sb, &ctx->gdps[2]), 57U);
+ KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[2]), 63U);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)ext4_block_bitmap_csum_verify(sb, &ctx->gdps[2],
+ &bblk->bh));
+ KUNIT_EXPECT_TRUE(test,
+ (bool)ext4_inode_bitmap_csum_verify(sb, &ctx->gdps[2],
+ &iblk->bh));
+ KUNIT_EXPECT_TRUE(test,
+ (bool)ext4_group_desc_csum_verify(sb, 2,
+ &ctx->gdps[2]));
+
+ /* Idempotent when bit 6 is already set */
+ err = ext4_mark_inode_used(sb, 135);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[2]), 63U);
+
+ /* Lower bit 2 (ino 131) decrements free_inodes without raising itable_unused */
+ err = ext4_mark_inode_used(sb, 131);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(2, iblk->cache_buf));
+ KUNIT_EXPECT_EQ(test, ext4_itable_unused_count(sb, &ctx->gdps[2]), 57U);
+ KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[2]), 62U);
+}
+
+static void ialloc_test_mark_inode_used_errors(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *iblk;
+
+ /* Bounds checks: below EXT4_FIRST_INO and above max_ino */
+ KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 10), -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test,
+ ext4_mark_inode_used(sb, IALLOC_TEST_TOTAL_INODES + 1),
+ -EFSCORRUPTED);
+
+ /* Inode bitmap read I/O failure */
+ iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, iblk);
+ clear_bitmap_uptodate(&iblk->bh);
+ clear_buffer_uptodate(&iblk->bh);
+ ctx->fail_read_eio = true;
+ KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 65), -EIO);
+ ctx->fail_read_eio = false;
+
+ /* Dirty metadata error on group 2 (ino 129) */
+ ctx->fail_dirty_metadata_err = -EIO;
+ KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 129), -EIO);
+ ctx->fail_dirty_metadata_err = 0;
+
+ /* sync_dirty_buffer I/O error on unmapped dirty buffer (ino 130) */
+ ctx->fail_sync_dirty_buffer = true;
+ KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 130), -EIO);
+ ctx->fail_sync_dirty_buffer = false;
+
+ /* Block bitmap read and wait failures during EXT4_BG_BLOCK_UNINIT init */
+ ctx->gdps[3].bg_flags |= cpu_to_le16(EXT4_BG_BLOCK_UNINIT);
+ ext4_group_desc_csum_set(sb, 3, &ctx->gdps[3]);
+ ctx->fail_bbitmap_err = -EIO;
+ KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 193), -EIO);
+ ctx->fail_bbitmap_err = 0;
+
+ ctx->fail_wait_bbitmap_err = -EFSCORRUPTED;
+ KUNIT_EXPECT_EQ(test, ext4_mark_inode_used(sb, 194), -EFSCORRUPTED);
+ ctx->fail_wait_bbitmap_err = 0;
+}
+
+/* =========================================================================
+ * 9. Xattr Credits & Inode Allocation: __ext4_new_inode
+ * =========================================================================
+ */
+
+#ifdef CONFIG_EXT4_FS_POSIX_ACL
+static struct posix_acl *ialloc_test_err_get_inode_acl(struct inode *inode,
+ int type, bool rcu)
+{
+ return ERR_PTR(-EIO);
+}
+
+static const struct inode_operations ialloc_test_err_acl_iops = {
+ .get_inode_acl = ialloc_test_err_get_inode_acl,
+};
+#endif
+
+static void ialloc_test_xattr_credits_for_new_inode(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ int base, enc_creds, base_ea, enc_ea_creds;
+
+ base = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+ S_IFREG | 0644, false);
+ KUNIT_ASSERT_GE(test, base, 0);
+ if (!IS_ENABLED(CONFIG_SECURITY))
+ KUNIT_EXPECT_EQ(test, base, 0);
+
+ /* Without ea_inode, encryption xattr adds 7 credits */
+ enc_creds = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+ S_IFREG | 0644, true);
+ KUNIT_EXPECT_EQ(test, enc_creds, base + 7);
+
+ /* With ea_inode on 1024B blocks, 40B fscrypt context adds 17 credits */
+ ext4_set_feature_ea_inode(sb);
+ base_ea = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+ S_IFREG | 0644, false);
+ enc_ea_creds = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+ S_IFREG | 0644,
+ true);
+ KUNIT_EXPECT_EQ(test, enc_ea_creds, base_ea + 17);
+ ext4_clear_feature_ea_inode(sb);
+
+#ifdef CONFIG_EXT4_FS_POSIX_ACL
+ {
+ struct posix_acl *acl;
+ const struct inode_operations *orig_iops;
+ int reg_acl, dir_acl, err_acl;
+
+ acl = posix_acl_alloc(1, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, acl);
+ set_cached_acl(ctx->root_inode, ACL_TYPE_DEFAULT, acl);
+ posix_acl_release(acl);
+
+ reg_acl = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+ S_IFREG | 0644,
+ false);
+ dir_acl = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+ S_IFDIR | 0755,
+ false);
+ KUNIT_EXPECT_EQ(test, reg_acl, base + 7);
+ KUNIT_EXPECT_EQ(test, dir_acl, base + 14);
+ forget_cached_acl(ctx->root_inode, ACL_TYPE_DEFAULT);
+
+ orig_iops = ctx->root_inode->i_op;
+ ctx->root_inode->i_op = &ialloc_test_err_acl_iops;
+ sb->s_flags |= SB_POSIXACL;
+ err_acl = ext4_xattr_credits_for_new_inode_test(ctx->root_inode,
+ S_IFREG | 0644,
+ false);
+ sb->s_flags &= ~SB_POSIXACL;
+ ctx->root_inode->i_op = orig_iops;
+ KUNIT_EXPECT_EQ(test, err_acl, -EIO);
+ }
+#endif
+}
+
+static void ialloc_test_new_inode_types_and_flags(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ handle_t *nojournal;
+ struct ialloc_test_blk *iblk0;
+ struct inode *reg, *dir, *lnk, *fifo, *ea_ino;
+ __le32 inum_le, gen_le;
+ u32 expected_seed;
+ s64 free_after_reg;
+
+ nojournal = ext4_journal_start_sb(sb, EXT4_HT_INODE, 2);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(nojournal));
+
+ ext4_set_feature_inline_data(sb);
+ ialloc_test_enable_flex_bg(ctx, 1);
+ EXT4_I(ctx->root_inode)->i_flags = EXT4_SYNC_FL | EXT4_NODUMP_FL |
+ EXT4_DIRSYNC_FL | EXT4_TOPDIR_FL |
+ EXT4_EXTENTS_FL;
+
+ /* 1. Regular file in group 0 (first free bit is 10 -> ino 11) */
+ reg = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644,
+ NULL, 0, NULL, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(reg));
+ ialloc_test_track_inode(test, ctx, reg);
+ KUNIT_EXPECT_EQ(test, reg->i_ino, 11UL);
+ KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(reg, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_EQ(test, ext_inode_hdr(reg)->eh_magic, EXT4_EXT_MAGIC);
+ KUNIT_EXPECT_TRUE(test, ext4_test_inode_state(reg, EXT4_STATE_NEW));
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_state(reg, EXT4_STATE_MAY_INLINE_DATA));
+ KUNIT_EXPECT_EQ(test,
+ EXT4_I(reg)->i_flags &
+ (EXT4_SYNC_FL | EXT4_NODUMP_FL | EXT4_DIRSYNC_FL |
+ EXT4_TOPDIR_FL),
+ (u32)(EXT4_SYNC_FL | EXT4_NODUMP_FL));
+ KUNIT_EXPECT_EQ(test, EXT4_I(reg)->i_extra_isize,
+ (u16)IALLOC_TEST_EXTRA_ISIZE);
+
+ inum_le = cpu_to_le32(reg->i_ino);
+ gen_le = cpu_to_le32(reg->i_generation);
+ expected_seed = ext4_chksum(ctx->sbi.s_csum_seed, (u8 *)&inum_le,
+ sizeof(inum_le));
+ expected_seed = ext4_chksum(expected_seed, (u8 *)&gen_le,
+ sizeof(gen_le));
+ KUNIT_EXPECT_EQ(test, EXT4_I(reg)->i_csum_seed, expected_seed);
+
+ /*
+ * 2. Directory inherits DIRSYNC (unlike regular files), still strips
+ * non-inheritable TOPDIR, and increments directory counters.
+ */
+ dir = ext4_new_inode(nojournal, ctx->root_inode, S_IFDIR | 0755,
+ NULL, 12, NULL, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(dir));
+ ialloc_test_track_inode(test, ctx, dir);
+ KUNIT_EXPECT_EQ(test, dir->i_ino, 12UL);
+ KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(dir, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_EQ(test,
+ EXT4_I(dir)->i_flags &
+ (EXT4_SYNC_FL | EXT4_NODUMP_FL | EXT4_DIRSYNC_FL |
+ EXT4_TOPDIR_FL),
+ (u32)(EXT4_SYNC_FL | EXT4_NODUMP_FL |
+ EXT4_DIRSYNC_FL));
+ KUNIT_EXPECT_EQ(test, ext4_used_dirs_count(sb, &ctx->gdps[0]), 2U);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->flex_grps[0].used_dirs), 2);
+
+ /* 3. Symlink gets EXT4_INODE_EXTENTS; FIFO does not */
+ lnk = ext4_new_inode(nojournal, ctx->root_inode, S_IFLNK | 0777,
+ NULL, 13, NULL, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(lnk));
+ ialloc_test_track_inode(test, ctx, lnk);
+ KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(lnk, EXT4_INODE_EXTENTS));
+
+ fifo = ext4_new_inode(nojournal, ctx->root_inode, S_IFIFO | 0600,
+ NULL, 14, NULL, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fifo));
+ ialloc_test_track_inode(test, ctx, fifo);
+ KUNIT_EXPECT_FALSE(test, ext4_test_inode_flag(fifo, EXT4_INODE_EXTENTS));
+
+ /* 4. EXT4_EA_INODE_FL suppresses EXT4_STATE_MAY_INLINE_DATA */
+ ea_ino = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0600,
+ NULL, 15, NULL, EXT4_EA_INODE_FL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ea_ino));
+ ialloc_test_track_inode(test, ctx, ea_ino);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(ea_ino,
+ EXT4_STATE_MAY_INLINE_DATA));
+
+ /* 5. Full lifecycle round-trip: unlink + iput frees ino 11 back */
+ iblk0 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(0));
+ KUNIT_ASSERT_NOT_NULL(test, iblk0);
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(10, iblk0->cache_buf));
+ free_after_reg = percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter);
+
+ ialloc_test_untrack_inode(ctx, reg);
+ clear_nlink(reg);
+ iput(reg);
+
+ KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(10, iblk0->cache_buf));
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum_positive(&ctx->sbi.s_freeinodes_counter),
+ free_after_reg + 1);
+
+ ext4_journal_stop(nojournal);
+}
+
+static void ialloc_test_new_inode_ownership_and_projid(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ handle_t *nojournal;
+ uid_t explicit_owner[2] = { 1001, 2002 };
+ struct inode *ino_own, *ino_grpid, *ino_sgid;
+
+ nojournal = ext4_journal_start_sb(sb, EXT4_HT_INODE, 2);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(nojournal));
+
+ i_gid_write(ctx->root_inode, 3003);
+ ext4_set_feature_project(sb);
+ EXT4_I(ctx->root_inode)->i_projid = make_kprojid(&init_user_ns, 42);
+ ext4_set_inode_flag(ctx->root_inode, EXT4_INODE_PROJINHERIT);
+
+ /* Explicit owner overrides uid/gid and inherits projid 42 */
+ ino_own = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0640,
+ NULL, 0, explicit_owner, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ino_own));
+ ialloc_test_track_inode(test, ctx, ino_own);
+ KUNIT_EXPECT_EQ(test, i_uid_read(ino_own), 1001U);
+ KUNIT_EXPECT_EQ(test, i_gid_read(ino_own), 2002U);
+ KUNIT_EXPECT_EQ(test,
+ from_kprojid(&init_user_ns, EXT4_I(ino_own)->i_projid),
+ 42U);
+
+ /* Without PROJINHERIT on parent, child gets EXT4_DEF_PROJID (0) and GRPID gid */
+ ext4_clear_inode_flag(ctx->root_inode, EXT4_INODE_PROJINHERIT);
+ set_opt(sb, GRPID);
+ ino_grpid = ext4_new_inode(nojournal, ctx->root_inode, S_IFDIR | 0750,
+ NULL, 0, NULL, 0);
+ clear_opt(sb, GRPID);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ino_grpid));
+ ialloc_test_track_inode(test, ctx, ino_grpid);
+ KUNIT_EXPECT_EQ(test, i_gid_read(ino_grpid), 3003U);
+ KUNIT_EXPECT_FALSE(test, !!(ino_grpid->i_mode & S_ISGID));
+ KUNIT_EXPECT_EQ(test,
+ from_kprojid(&init_user_ns, EXT4_I(ino_grpid)->i_projid),
+ 0U);
+
+ /* Standard inode_init_owner propagates parent's S_ISGID to child dir */
+ ctx->root_inode->i_mode |= S_ISGID;
+ ino_sgid = ext4_new_inode(nojournal, ctx->root_inode, S_IFDIR | 0755,
+ NULL, 0, NULL, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ino_sgid));
+ ialloc_test_track_inode(test, ctx, ino_sgid);
+ KUNIT_EXPECT_EQ(test, i_gid_read(ino_sgid), 3003U);
+ KUNIT_EXPECT_TRUE(test, !!(ino_sgid->i_mode & S_ISGID));
+
+ ext4_journal_stop(nojournal);
+}
+
+static void ialloc_test_new_inode_goal_uninit_and_corrupt_skip(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ handle_t *nojournal;
+ struct ialloc_test_blk *iblk0, *iblk2;
+ struct inode *ino_goal, *ino_sb_goal, *ino_skip, *ino_started;
+ struct inode *ino_replay;
+
+ nojournal = ext4_journal_start_sb(sb, EXT4_HT_INODE, 2);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(nojournal));
+
+ /* 1. Explicit goal = 140 in uninitialized group 2 (bit 11) */
+ iblk2 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(2));
+ KUNIT_ASSERT_NOT_NULL(test, iblk2);
+ clear_bitmap_uptodate(&iblk2->bh);
+ clear_buffer_uptodate(&iblk2->bh);
+ clear_buffer_verified(&iblk2->bh);
+ ctx->gdps[2].bg_flags = cpu_to_le16(EXT4_BG_INODE_UNINIT |
+ EXT4_BG_BLOCK_UNINIT);
+ ext4_group_desc_csum_set(sb, 2, &ctx->gdps[2]);
+
+ ino_goal = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644,
+ NULL, 140, NULL, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ino_goal));
+ ialloc_test_track_inode(test, ctx, ino_goal);
+ KUNIT_EXPECT_EQ(test, ino_goal->i_ino, 140UL);
+ KUNIT_EXPECT_EQ(test,
+ le16_to_cpu(ctx->gdps[2].bg_flags) &
+ (EXT4_BG_INODE_UNINIT | EXT4_BG_BLOCK_UNINIT), 0);
+ KUNIT_EXPECT_EQ(test, ext4_free_group_clusters(sb, &ctx->gdps[2]),
+ 1006U);
+ KUNIT_EXPECT_EQ(test, ext4_itable_unused_count(sb, &ctx->gdps[2]), 52U);
+
+ /* 2. sbi->s_inode_goal = 200 (group 3, bit 7) used when goal == 0 */
+ ctx->sbi.s_inode_goal = 200;
+ ino_sb_goal = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644,
+ NULL, 0, NULL, 0);
+ ctx->sbi.s_inode_goal = 0;
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ino_sb_goal));
+ ialloc_test_track_inode(test, ctx, ino_sb_goal);
+ KUNIT_EXPECT_EQ(test, ino_sb_goal->i_ino, 200UL);
+
+ /*
+ * 3. Reserved bit 3 (ino 4 < EXT4_FIRST_INO) cleared in group 0:
+ * marks group 0 IBITMAP_CORRUPT and advances to group 1 (ino 65).
+ */
+ iblk0 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(0));
+ KUNIT_ASSERT_NOT_NULL(test, iblk0);
+ ext4_clear_bit(3, iblk0->cache_buf);
+ ialloc_test_sync_ibitmap_csum(ctx, 0, true);
+
+ ino_skip = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644,
+ NULL, 4, NULL, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ino_skip));
+ ialloc_test_track_inode(test, ctx, ino_skip);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[0]));
+ KUNIT_EXPECT_EQ(test, ino_skip->i_ino, 65UL);
+
+ ext4_journal_stop(nojournal);
+
+ /* 4. ext4_new_inode_start_handle (handle == NULL) starts handle */
+ KUNIT_ASSERT_NULL(test, ext4_journal_current_handle());
+ ino_started = ext4_new_inode_start_handle(&nop_mnt_idmap,
+ ctx->root_inode,
+ S_IFREG | 0644, NULL, 66,
+ NULL, EXT4_HT_INODE, 2);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ino_started));
+ ialloc_test_track_inode(test, ctx, ino_started);
+ KUNIT_EXPECT_EQ(test, ino_started->i_ino, 66UL);
+ KUNIT_ASSERT_NOT_NULL(test, ext4_journal_current_handle());
+ ext4_journal_stop(ext4_journal_current_handle());
+ KUNIT_EXPECT_NULL(test, ext4_journal_current_handle());
+
+ /*
+ * 5. EXT4_FC_REPLAY with handle == NULL allocates exact goal ino 15 in
+ * group 0 despite IBITMAP_CORRUPT and without starting a handle.
+ */
+ ext4_set_bit(3, iblk0->cache_buf);
+ ialloc_test_sync_ibitmap_csum(ctx, 0, true);
+ ctx->sbi.s_mount_state |= EXT4_FC_REPLAY;
+ ino_replay = ext4_new_inode(NULL, ctx->root_inode, S_IFREG | 0644,
+ NULL, 15, NULL, 0);
+ ctx->sbi.s_mount_state &= ~EXT4_FC_REPLAY;
+ KUNIT_ASSERT_FALSE(test, IS_ERR(ino_replay));
+ ialloc_test_track_inode(test, ctx, ino_replay);
+ KUNIT_EXPECT_EQ(test, ino_replay->i_ino, 15UL);
+ KUNIT_EXPECT_NULL(test, ext4_journal_current_handle());
+}
+
+static void ialloc_test_new_inode_errors(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ handle_t *nojournal;
+ struct inode *unlinked_dir, *dup_existing, *res;
+ struct ialloc_test_blk *iblk;
+ ext4_group_t g;
+
+ nojournal = ext4_journal_start_sb(sb, EXT4_HT_INODE, 2);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(nojournal));
+
+ /* 1. NULL dir or unlinked dir -> -EPERM */
+ res = ext4_new_inode(nojournal, NULL, S_IFREG | 0644, NULL, 0, NULL, 0);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EPERM);
+
+ unlinked_dir = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, unlinked_dir);
+ unlinked_dir->i_ino = 50;
+ unlinked_dir->i_mode = S_IFDIR | 0755;
+ clear_nlink(unlinked_dir);
+ ialloc_test_track_inode(test, ctx, unlinked_dir);
+ res = ext4_new_inode(nojournal, unlinked_dir, S_IFREG | 0644, NULL, 0,
+ NULL, 0);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EPERM);
+
+ /* 2. Emergency shutdown (-EIO) and emergency RO (-EROFS) */
+ set_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 0, NULL, 0);
+ clear_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+
+ set_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 0, NULL, 0);
+ clear_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EROFS);
+
+ /* 3. VFS alloc_inode failure -> -ENOMEM */
+ ctx->fail_alloc_inode = true;
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 0, NULL, 0);
+ ctx->fail_alloc_inode = false;
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -ENOMEM);
+
+ /* 4. Journal write-access and dirty-metadata failures in group 1 */
+ iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, iblk);
+
+ ctx->write_access_calls = 0;
+ ctx->fail_write_access_err = -EIO;
+ ctx->fail_write_access_on_call = 1;
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 70, NULL, 0);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+ KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(5, iblk->cache_buf));
+
+ ctx->write_access_calls = 0;
+ ctx->fail_write_access_on_call = 2;
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 71, NULL, 0);
+ ctx->fail_write_access_err = 0;
+ ctx->fail_write_access_on_call = 0;
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(6, iblk->cache_buf));
+ ext4_clear_bit(6, iblk->cache_buf);
+ ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+
+ ctx->dirty_metadata_calls = 0;
+ ctx->fail_dirty_metadata_err = -EIO;
+ ctx->fail_dirty_metadata_on_call = 1;
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 71, NULL, 0);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+ ext4_clear_bit(6, iblk->cache_buf);
+ ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+
+ ctx->dirty_metadata_calls = 0;
+ ctx->fail_dirty_metadata_on_call = 2;
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 71, NULL, 0);
+ ctx->fail_dirty_metadata_err = 0;
+ ctx->fail_dirty_metadata_on_call = 0;
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+ ext4_clear_bit(6, iblk->cache_buf);
+ ext4_free_inodes_set(sb, &ctx->gdps[1], 64);
+ ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+
+#ifdef CONFIG_EXT4_FS_SECURITY
+ /* 5. ext4_init_security failure rolls back allocated inode bit */
+ ctx->fail_init_security_err = -EACCES;
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 70, NULL, 0);
+ ctx->fail_init_security_err = 0;
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EACCES);
+ KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(5, iblk->cache_buf));
+ KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[1]), 64U);
+#endif
+
+ /* 6. ext4_mark_inode_dirty failure rolls back allocated inode bit */
+ ctx->fail_mark_inode_dirty_err = -EIO;
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 70, NULL, 0);
+ ctx->fail_mark_inode_dirty_err = 0;
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+ KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(5, iblk->cache_buf));
+ KUNIT_EXPECT_EQ(test, ext4_free_inodes_count(sb, &ctx->gdps[1]), 64U);
+
+ /*
+ * 7. Doubly-allocated inode (insert_inode_locked collision):
+ * Pre-hash ino 140 in VFS cache, leave only bg 2 non-empty with bit 11
+ * clear -> marks bg 2 IBITMAP_CORRUPT and returns -EIO.
+ */
+ dup_existing = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, dup_existing);
+ dup_existing->i_ino = 140;
+ dup_existing->i_mode = S_IFREG | 0644;
+ KUNIT_ASSERT_EQ(test, insert_inode_locked(dup_existing), 0);
+ ialloc_test_track_inode(test, ctx, dup_existing);
+
+ ext4_free_inodes_set(sb, &ctx->gdps[0], 0);
+ ext4_free_inodes_set(sb, &ctx->gdps[1], 0);
+ ext4_free_inodes_set(sb, &ctx->gdps[3], 0);
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 140, NULL, 0);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -EIO);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[2]));
+
+ /* 8. Descriptor claims free inodes in bg 3, but bitmap is all ones -> -ENOSPC */
+ iblk = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(3));
+ KUNIT_ASSERT_NOT_NULL(test, iblk);
+ memset(iblk->cache_buf, 0xff, IALLOC_TEST_BLK_SIZE);
+ ialloc_test_sync_ibitmap_csum(ctx, 3, true);
+ ext4_free_inodes_set(sb, &ctx->gdps[2], 0);
+ ext4_free_inodes_set(sb, &ctx->gdps[3], 5);
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 193, NULL, 0);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -ENOSPC);
+
+ /* 9. All groups report 0 free inodes -> -ENOSPC before loop */
+ for (g = 0; g < IALLOC_TEST_NGROUPS; g++)
+ ext4_free_inodes_set(sb, &ctx->gdps[g], 0);
+ res = ext4_new_inode(nojournal, ctx->root_inode, S_IFREG | 0644, NULL,
+ 0, NULL, 0);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(res), -ENOSPC);
+
+ ext4_journal_stop(nojournal);
+}
+
+/* =========================================================================
+ * 10. Orphans, Counters, & Lazy Init: ext4_orphan_get, ext4_count_*,
+ * ext4_init_inode_table
+ * =========================================================================
+ */
+
+static void ialloc_test_orphan_get(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ialloc_test_blk *iblk1;
+ struct inode *orphan, *got;
+
+ /* Bounds checks */
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_orphan_get(sb, 5)), -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test,
+ PTR_ERR(ext4_orphan_get(sb, IALLOC_TEST_TOTAL_INODES + 1)),
+ -EFSCORRUPTED);
+
+ /* Clear bit in bitmap -> -EFSCORRUPTED */
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_orphan_get(sb, 65)), -EFSCORRUPTED);
+
+ /* Mark bit 0 (ino 65) allocated in group 1 */
+ iblk1 = ialloc_test_find_blk(ctx, ialloc_test_ibitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, iblk1);
+ ext4_set_bit(0, iblk1->cache_buf);
+ ialloc_test_sync_ibitmap_csum(ctx, 1, true);
+
+ /* Bitmap read error propagates */
+ clear_bitmap_uptodate(&iblk1->bh);
+ clear_buffer_uptodate(&iblk1->bh);
+ ctx->fail_read_eio = true;
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_orphan_get(sb, 65)), -EIO);
+ ctx->fail_read_eio = false;
+ set_buffer_uptodate(&iblk1->bh);
+ set_bitmap_uptodate(&iblk1->bh);
+ set_buffer_verified(&iblk1->bh);
+
+ /* ext4_iget error propagates */
+ ctx->fail_iget_err = -EIO;
+ KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_orphan_get(sb, 65)), -EIO);
+ ctx->fail_iget_err = 0;
+
+ /* Prepare mock orphan inode */
+ orphan = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, orphan);
+ orphan->i_ino = 65;
+ ialloc_test_track_inode(test, ctx, orphan);
+ ctx->iget_inode = orphan;
+
+ /* 1. Valid unlinked orphan (i_nlink == 0, NEXT_ORPHAN <= max_ino) */
+ orphan->i_mode = S_IFREG | 0644;
+ clear_nlink(orphan);
+ orphan->i_blocks = 8;
+ NEXT_ORPHAN(orphan) = 0;
+ got = ext4_orphan_get(sb, 65);
+ KUNIT_ASSERT_PTR_EQ(test, got, orphan);
+ iput(got);
+
+ /* 2. Valid live truncating regular file orphan (i_nlink == 1) */
+ set_nlink(orphan, 1);
+ ext4_set_inode_flag(orphan, EXT4_INODE_EXTENTS);
+ got = ext4_orphan_get(sb, 65);
+ KUNIT_ASSERT_PTR_EQ(test, got, orphan);
+ iput(got);
+
+ /* 3. Live non-truncatable FIFO (i_nlink == 1) -> -EFSCORRUPTED */
+ orphan->i_mode = S_IFIFO | 0600;
+ got = ext4_orphan_get(sb, 65);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(got), -EFSCORRUPTED);
+
+ /* 4. Unlinked orphan with NEXT_ORPHAN > max_ino zeroes i_blocks */
+ orphan->i_mode = S_IFREG | 0644;
+ clear_nlink(orphan);
+ orphan->i_blocks = 24;
+ NEXT_ORPHAN(orphan) = IALLOC_TEST_TOTAL_INODES + 50;
+ got = ext4_orphan_get(sb, 65);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(got), -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, orphan->i_blocks, (blkcnt_t)0);
+
+ /* 5. Unlinked bad_inode zeroes i_blocks and returns -EFSCORRUPTED */
+ NEXT_ORPHAN(orphan) = 0;
+ orphan->i_blocks = 16;
+ make_bad_inode(orphan);
+ got = ext4_orphan_get(sb, 65);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(got), -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, orphan->i_blocks, (blkcnt_t)0);
+}
+
+static void ialloc_test_count_free_inodes_and_dirs(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+
+ /* Include 64-bit high-16-bit values (0x10000 + low) and a NULL gdp */
+ ext4_free_inodes_set(sb, &ctx->gdps[0], 10);
+ ext4_free_inodes_set(sb, &ctx->gdps[1], 0x10020);
+ ext4_free_inodes_set(sb, &ctx->gdps[2], 999); /* masked out below */
+ ext4_free_inodes_set(sb, &ctx->gdps[3], 5);
+
+ ext4_used_dirs_set(sb, &ctx->gdps[0], 2);
+ ext4_used_dirs_set(sb, &ctx->gdps[1], 0x20003);
+ ext4_used_dirs_set(sb, &ctx->gdps[2], 777); /* masked out below */
+ ext4_used_dirs_set(sb, &ctx->gdps[3], 4);
+
+ ctx->fail_gdp_mask = BIT(2);
+
+ KUNIT_EXPECT_EQ(test, ext4_count_free_inodes(sb), 0x1002fUL);
+ KUNIT_EXPECT_EQ(test, ext4_count_dirs(sb), 0x20009UL);
+}
+
+static void ialloc_test_init_inode_table(struct kunit *test)
+{
+ struct ialloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ int err;
+
+ /* 1. Already EXT4_BG_INODE_ZEROED is a no-op */
+ ctx->gdps[1].bg_flags = cpu_to_le16(EXT4_BG_INODE_ZEROED);
+ err = ext4_init_inode_table(sb, 1, 1);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->zeroout_calls, 0U);
+
+ /* 2. Uninitialized group 1 zeroes all 16 blocks with barrier = 1 */
+ ctx->gdps[1].bg_flags = cpu_to_le16(EXT4_BG_INODE_UNINIT);
+ err = ext4_init_inode_table(sb, 1, 1);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->zeroout_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->last_zeroout_blk, ialloc_test_itable_blk(1));
+ KUNIT_EXPECT_EQ(test, ctx->last_zeroout_num, 16);
+ KUNIT_EXPECT_EQ(test, ctx->last_zeroout_barrier, 1);
+ KUNIT_EXPECT_TRUE(test,
+ !!(le16_to_cpu(ctx->gdps[1].bg_flags) &
+ EXT4_BG_INODE_ZEROED));
+ KUNIT_EXPECT_TRUE(test,
+ (bool)ext4_group_desc_csum_verify(sb, 1,
+ &ctx->gdps[1]));
+
+ /*
+ * 3. Initialized group 2 with itable_unused = 54 -> used_inos = 10 ->
+ * used_blks = DIV_ROUND_UP(10, 4) = 3 -> zeroes remaining 13 blocks.
+ */
+ ctx->gdps[2].bg_flags = 0;
+ ext4_itable_unused_set(sb, &ctx->gdps[2], 54);
+ err = ext4_init_inode_table(sb, 2, 0);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->zeroout_calls, 2U);
+ KUNIT_EXPECT_EQ(test, ctx->last_zeroout_blk,
+ ialloc_test_itable_blk(2) + 3);
+ KUNIT_EXPECT_EQ(test, ctx->last_zeroout_num, 13);
+ KUNIT_EXPECT_EQ(test, ctx->last_zeroout_barrier, 0);
+ KUNIT_EXPECT_TRUE(test,
+ !!(le16_to_cpu(ctx->gdps[2].bg_flags) &
+ EXT4_BG_INODE_ZEROED));
+
+ /* 4. Full inode table (itable_unused = 0 -> num = 0) skips zeroout, sets ZEROED */
+ ctx->gdps[3].bg_flags = 0;
+ ext4_itable_unused_set(sb, &ctx->gdps[3], 0);
+ err = ext4_init_inode_table(sb, 3, 1);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->zeroout_calls, 2U);
+ KUNIT_EXPECT_TRUE(test,
+ !!(le16_to_cpu(ctx->gdps[3].bg_flags) &
+ EXT4_BG_INODE_ZEROED));
+
+ /* 5. Corrupt itable_unused > inodes_per_group returns 1 without ZEROED */
+ ctx->gdps[3].bg_flags = 0;
+ ext4_itable_unused_set(sb, &ctx->gdps[3], 100);
+ err = ext4_init_inode_table(sb, 3, 0);
+ KUNIT_EXPECT_EQ(test, err, 1);
+ KUNIT_EXPECT_FALSE(test,
+ !!(le16_to_cpu(ctx->gdps[3].bg_flags) &
+ EXT4_BG_INODE_ZEROED));
+
+ /* 6. Group 0 with used_inos (4) < EXT4_FIRST_INO (11) returns 1 */
+ ctx->gdps[0].bg_flags = 0;
+ ext4_itable_unused_set(sb, &ctx->gdps[0], 60);
+ err = ext4_init_inode_table(sb, 0, 0);
+ KUNIT_EXPECT_EQ(test, err, 1);
+ KUNIT_EXPECT_FALSE(test,
+ !!(le16_to_cpu(ctx->gdps[0].bg_flags) &
+ EXT4_BG_INODE_ZEROED));
+
+ /* 7. Zeroout I/O failure (-EIO) and emergency RO (-EROFS) */
+ ctx->gdps[3].bg_flags = cpu_to_le16(EXT4_BG_INODE_UNINIT);
+ ctx->fail_itable_zeroout_err = -EIO;
+ err = ext4_init_inode_table(sb, 3, 0);
+ ctx->fail_itable_zeroout_err = 0;
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_FALSE(test,
+ !!(le16_to_cpu(ctx->gdps[3].bg_flags) &
+ EXT4_BG_INODE_ZEROED));
+
+ set_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+ err = ext4_init_inode_table(sb, 3, 0);
+ clear_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags);
+ KUNIT_EXPECT_EQ(test, err, -EROFS);
+}
+
+static struct kunit_case ialloc_test_cases[] = {
+ KUNIT_CASE_PARAM(ialloc_test_mark_bitmap_end,
+ mark_bitmap_end_gen_params),
+ KUNIT_CASE(ialloc_test_end_bitmap_read_ok),
+ KUNIT_CASE(ialloc_test_end_bitmap_read_ioerr),
+ KUNIT_CASE(ialloc_test_validate_ibitmap_csum),
+ KUNIT_CASE(ialloc_test_validate_ibitmap_short_circuits),
+ KUNIT_CASE(ialloc_test_read_ibitmap_disk_and_cache),
+ KUNIT_CASE(ialloc_test_read_ibitmap_uninit),
+ KUNIT_CASE(ialloc_test_read_ibitmap_errors),
+ KUNIT_CASE(ialloc_test_free_inode_reg_and_dir),
+ KUNIT_CASE(ialloc_test_free_inode_guards),
+ KUNIT_CASE(ialloc_test_free_inode_corrupt_and_journal_errors),
+ KUNIT_CASE(ialloc_test_orlov_topdir_spread),
+ KUNIT_CASE(ialloc_test_orlov_subdir_and_thresholds),
+ KUNIT_CASE(ialloc_test_orlov_flex_bg_and_fallback),
+ KUNIT_CASE(ialloc_test_find_group_other_non_flex),
+ KUNIT_CASE(ialloc_test_find_group_other_flex_bg),
+ KUNIT_CASE(ialloc_test_recently_deleted),
+ KUNIT_CASE(ialloc_test_find_inode_bit),
+ KUNIT_CASE(ialloc_test_mark_inode_used_uninit),
+ KUNIT_CASE(ialloc_test_mark_inode_used_errors),
+ KUNIT_CASE(ialloc_test_xattr_credits_for_new_inode),
+ KUNIT_CASE(ialloc_test_new_inode_types_and_flags),
+ KUNIT_CASE(ialloc_test_new_inode_ownership_and_projid),
+ KUNIT_CASE(ialloc_test_new_inode_goal_uninit_and_corrupt_skip),
+ KUNIT_CASE(ialloc_test_new_inode_errors),
+ KUNIT_CASE(ialloc_test_orphan_get),
+ KUNIT_CASE(ialloc_test_count_free_inodes_and_dirs),
+ KUNIT_CASE(ialloc_test_init_inode_table),
+ {}
+};
+
+static struct kunit_suite ialloc_test_suite = {
+ .name = "ext4_ialloc_test",
+ .init = ialloc_test_init,
+ .test_cases = ialloc_test_cases,
+};
+
+kunit_test_suites(&ialloc_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 inode allocation and deallocation");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c
index 02546b828836..66215b635932 100644
--- a/fs/ext4/ialloc.c
+++ b/fs/ext4/ialloc.c
@@ -66,6 +66,7 @@ void ext4_mark_bitmap_end(int start_bit, int end_bit, char *bitmap)
if (i < end_bit)
memset(bitmap + (i >> 3), 0xff, (end_bit - i) >> 3);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mark_bitmap_end);
void ext4_end_bitmap_read(struct bio *bio)
{
@@ -78,6 +79,7 @@ void ext4_end_bitmap_read(struct bio *bio)
}
unlock_buffer(bh);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_end_bitmap_read);
static int ext4_validate_inode_bitmap(struct super_block *sb,
struct ext4_group_desc *desc,
@@ -144,7 +146,7 @@ ext4_read_inode_bitmap(struct super_block *sb, ext4_group_t block_group)
EXT4_GROUP_INFO_IBITMAP_CORRUPT);
return ERR_PTR(-EFSCORRUPTED);
}
- bh = sb_getblk(sb, bitmap_blk);
+ bh = ext4_sb_getblk(sb, bitmap_blk);
if (unlikely(!bh)) {
ext4_warning(sb, "Cannot read inode bitmap - "
"block_group = %u, inode_bitmap = %llu",
@@ -698,7 +700,7 @@ static int recently_deleted(struct super_block *sb, ext4_group_t group, int ino)
ino >= EXT4_INODES_PER_GROUP(sb) - ext4_itable_unused_count(sb, gdp)))
return 0;
- bh = sb_find_get_block(sb, ext4_inode_table(sb, gdp) +
+ bh = ext4_sb_find_get_block(sb, ext4_inode_table(sb, gdp) +
(ino / inodes_per_block));
if (!bh || !buffer_uptodate(bh))
/*
@@ -878,6 +880,7 @@ int ext4_mark_inode_used(struct super_block *sb, int ino)
brelse(inode_bitmap_bh);
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mark_inode_used);
static int ext4_xattr_credits_for_new_inode(struct inode *dir, mode_t mode,
bool encrypt)
@@ -1383,6 +1386,7 @@ struct inode *__ext4_new_inode(struct mnt_idmap *idmap,
brelse(inode_bitmap_bh);
return ERR_PTR(err);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_new_inode);
/* Verify that we are loading a valid orphan from disk */
struct inode *ext4_orphan_get(struct super_block *sb, unsigned long ino)
@@ -1456,6 +1460,7 @@ struct inode *ext4_orphan_get(struct super_block *sb, unsigned long ino)
brelse(bitmap_bh);
return ERR_PTR(err);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_orphan_get);
unsigned long ext4_count_free_inodes(struct super_block *sb)
{
@@ -1506,6 +1511,7 @@ unsigned long ext4_count_free_inodes(struct super_block *sb)
return desc_count;
#endif
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_count_free_inodes);
/* Called at mount-time, super-block is locked */
unsigned long ext4_count_dirs(struct super_block * sb)
@@ -1521,6 +1527,23 @@ unsigned long ext4_count_dirs(struct super_block * sb)
}
return count;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_count_dirs);
+
+int ext4_itable_zeroout(struct super_block *sb, ext4_fsblk_t blk, int num,
+ int barrier)
+{
+ int ret;
+
+ KUNIT_STATIC_STUB_REDIRECT(ext4_itable_zeroout, sb, blk, num, barrier);
+
+ ret = sb_issue_zeroout(sb, blk, num, GFP_NOFS);
+ if (ret < 0)
+ return ret;
+ if (barrier)
+ blkdev_issue_flush(sb->s_bdev);
+ return ret;
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_itable_zeroout);
/*
* Zeroes not yet zeroed inode table - just write zeroes through the whole
@@ -1617,11 +1640,9 @@ int ext4_init_inode_table(struct super_block *sb, ext4_group_t group,
ext4_debug("going to zero out inode table in group %d\n",
group);
- ret = sb_issue_zeroout(sb, blk, num, GFP_NOFS);
+ ret = ext4_itable_zeroout(sb, blk, num, barrier);
if (ret < 0)
goto err_out;
- if (barrier)
- blkdev_issue_flush(sb->s_bdev);
skip_zeroout:
ext4_lock_group(sb, group);
@@ -1640,8 +1661,58 @@ int ext4_init_inode_table(struct super_block *sb, ext4_group_t group,
out:
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_inode_table);
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
-EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_new_inode);
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mark_inode_used);
+int ext4_validate_inode_bitmap_test(struct super_block *sb,
+ struct ext4_group_desc *desc,
+ ext4_group_t block_group,
+ struct buffer_head *bh)
+{
+ return ext4_validate_inode_bitmap(sb, desc, block_group, bh);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_validate_inode_bitmap_test);
+
+struct buffer_head *ext4_read_inode_bitmap_test(struct super_block *sb,
+ ext4_group_t block_group)
+{
+ return ext4_read_inode_bitmap(sb, block_group);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_inode_bitmap_test);
+
+int ext4_find_group_orlov_test(struct super_block *sb, struct inode *parent,
+ ext4_group_t *group, umode_t mode,
+ const struct qstr *qstr)
+{
+ return find_group_orlov(sb, parent, group, mode, qstr);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_group_orlov_test);
+
+int ext4_find_group_other_test(struct super_block *sb, struct inode *parent,
+ ext4_group_t *group, umode_t mode)
+{
+ return find_group_other(sb, parent, group, mode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_group_other_test);
+
+int ext4_recently_deleted_test(struct super_block *sb, ext4_group_t group,
+ int ino)
+{
+ return recently_deleted(sb, group, ino);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_recently_deleted_test);
+
+int ext4_find_inode_bit_test(struct super_block *sb, ext4_group_t group,
+ struct buffer_head *bitmap, unsigned long *ino)
+{
+ return find_inode_bit(sb, group, bitmap, ino);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_inode_bit_test);
+
+int ext4_xattr_credits_for_new_inode_test(struct inode *dir, mode_t mode,
+ bool encrypt)
+{
+ return ext4_xattr_credits_for_new_inode(dir, mode, encrypt);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_xattr_credits_for_new_inode_test);
#endif
diff --git a/fs/ext4/super.c b/fs/ext4/super.c
index 74604a677ca4..88bd28cedd99 100644
--- a/fs/ext4/super.c
+++ b/fs/ext4/super.c
@@ -201,6 +201,9 @@ EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_bh_nowait);
int ext4_read_bh(struct buffer_head *bh, blk_opf_t op_flags,
bio_end_io_t end_io, bool simu_fail)
{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_read_bh, bh, op_flags,
+ end_io, simu_fail);
+
BUG_ON(!buffer_locked(bh));
if (ext4_buffer_uptodate(bh)) {
@@ -215,6 +218,7 @@ int ext4_read_bh(struct buffer_head *bh, blk_opf_t op_flags,
return 0;
return -EIO;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_bh);
int ext4_read_bh_lock(struct buffer_head *bh, blk_opf_t op_flags, bool wait)
{
@@ -348,6 +352,7 @@ ext4_fsblk_t ext4_block_bitmap(struct super_block *sb,
(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
(ext4_fsblk_t)le32_to_cpu(bg->bg_block_bitmap_hi) << 32 : 0);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_bitmap);
ext4_fsblk_t ext4_inode_bitmap(struct super_block *sb,
struct ext4_group_desc *bg)
@@ -356,6 +361,7 @@ ext4_fsblk_t ext4_inode_bitmap(struct super_block *sb,
(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
(ext4_fsblk_t)le32_to_cpu(bg->bg_inode_bitmap_hi) << 32 : 0);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_bitmap);
ext4_fsblk_t ext4_inode_table(struct super_block *sb,
struct ext4_group_desc *bg)
@@ -364,6 +370,7 @@ ext4_fsblk_t ext4_inode_table(struct super_block *sb,
(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
(ext4_fsblk_t)le32_to_cpu(bg->bg_inode_table_hi) << 32 : 0);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_table);
__u32 ext4_free_group_clusters(struct super_block *sb,
struct ext4_group_desc *bg)
@@ -380,6 +387,7 @@ __u32 ext4_free_inodes_count(struct super_block *sb,
(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
(__u32)le16_to_cpu(READ_ONCE(bg->bg_free_inodes_count_hi)) << 16 : 0);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_inodes_count);
__u32 ext4_used_dirs_count(struct super_block *sb,
struct ext4_group_desc *bg)
@@ -388,6 +396,7 @@ __u32 ext4_used_dirs_count(struct super_block *sb,
(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
(__u32)le16_to_cpu(bg->bg_used_dirs_count_hi) << 16 : 0);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_used_dirs_count);
__u32 ext4_itable_unused_count(struct super_block *sb,
struct ext4_group_desc *bg)
@@ -396,6 +405,7 @@ __u32 ext4_itable_unused_count(struct super_block *sb,
(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
(__u32)le16_to_cpu(bg->bg_itable_unused_hi) << 16 : 0);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_itable_unused_count);
void ext4_block_bitmap_set(struct super_block *sb,
struct ext4_group_desc *bg, ext4_fsblk_t blk)
@@ -404,6 +414,7 @@ void ext4_block_bitmap_set(struct super_block *sb,
if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
bg->bg_block_bitmap_hi = cpu_to_le32(blk >> 32);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_bitmap_set);
void ext4_inode_bitmap_set(struct super_block *sb,
struct ext4_group_desc *bg, ext4_fsblk_t blk)
@@ -438,6 +449,7 @@ void ext4_free_inodes_set(struct super_block *sb,
if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
WRITE_ONCE(bg->bg_free_inodes_count_hi, cpu_to_le16(count >> 16));
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_inodes_set);
void ext4_used_dirs_set(struct super_block *sb,
struct ext4_group_desc *bg, __u32 count)
@@ -446,6 +458,7 @@ void ext4_used_dirs_set(struct super_block *sb,
if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
bg->bg_used_dirs_count_hi = cpu_to_le16(count >> 16);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_used_dirs_set);
void ext4_itable_unused_set(struct super_block *sb,
struct ext4_group_desc *bg, __u32 count)
@@ -454,6 +467,7 @@ void ext4_itable_unused_set(struct super_block *sb,
if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
bg->bg_itable_unused_hi = cpu_to_le16(count >> 16);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_itable_unused_set);
static void __ext4_update_tstamp(__le32 *lo, __u8 *hi, time64_t now)
{
@@ -1152,6 +1166,7 @@ void ext4_mark_group_bitmap_corrupted(struct super_block *sb,
}
}
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mark_group_bitmap_corrupted);
void ext4_update_dynamic_rev(struct super_block *sb)
{
@@ -3332,6 +3347,7 @@ int ext4_group_desc_csum_verify(struct super_block *sb, __u32 block_group,
return 1;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_group_desc_csum_verify);
void ext4_group_desc_csum_set(struct super_block *sb, __u32 block_group,
struct ext4_group_desc *gdp)
@@ -3340,6 +3356,7 @@ void ext4_group_desc_csum_set(struct super_block *sb, __u32 block_group,
return;
gdp->bg_checksum = ext4_group_desc_csum(sb, block_group, gdp);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_group_desc_csum_set);
/* Called at mount-time, super-block is locked */
static int ext4_check_descriptors(struct super_block *sb,
diff --git a/fs/ext4/xattr_security.c b/fs/ext4/xattr_security.c
index 776cf11d24ca..9b6c7126a971 100644
--- a/fs/ext4/xattr_security.c
+++ b/fs/ext4/xattr_security.c
@@ -4,6 +4,7 @@
* Handler for storing security labels as extended attributes.
*/
+#include <kunit/static_stub.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/security.h>
@@ -55,9 +56,12 @@ int
ext4_init_security(handle_t *handle, struct inode *inode, struct inode *dir,
const struct qstr *qstr)
{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_init_security,
+ handle, inode, dir, qstr);
return security_inode_init_security(inode, dir, qstr,
&ext4_initxattrs, handle);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_security);
const struct xattr_handler ext4_xattr_security_handler = {
.prefix = XATTR_SECURITY_PREFIX,
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 12/27] ext4: add KUnit tests for fs/ext4/balloc.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (10 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 11/27] ext4: add KUnit tests for fs/ext4/ialloc.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (14 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add the ext4_balloc_test KUnit test suite in fs/ext4/balloc-test.c (29
test cases, 43 subtests total) to comprehensively exercise block bitmap
management, group descriptor and overhead calculations, cluster pool
reservation, allocation retry policy, metadata block allocation, and
goal block selection in fs/ext4/balloc.c using an in-memory VFS
superblock, mock block device, and deterministic block pool without
requiring a mounted block device:
- Block-to-group and cluster-offset mapping (ext4_get_group_number and
ext4_get_group_no_and_offset) across 1K and 4K block sizes, standard
shift-based vs. non-standard do_div group geometries, bigalloc
multi-block clusters, s_first_data_block offsets, >32-bit physical
block numbers, and optional NULL output pointers.
- Backup superblock placement (ext4_bg_has_super) across dense
filesystems, sparse_super powers of 3, 5, and 7, and sparse_super2
explicit s_backup_bgs configurations.
- Group descriptor table and base metadata block counts
(ext4_bg_num_gdb and ext4_num_base_meta_blocks) across traditional
GDT layouts with s_reserved_gdt_blocks and hybrid meta_bg filesystems
spanning pre-s_first_meta_bg and meta_bg descriptor groups.
- Group overhead and post-init free cluster accounting
(ext4_free_clusters_after_init, ext4_num_overhead_clusters, and
num_clusters_in_group) across standard groups, partial final groups,
external flex_bg metadata, spanning inode tables, and bigalloc
shared-cluster layouts.
- Group descriptor and group info lookup (ext4_get_group_desc and
ext4_get_group_info) across multi-block descriptor tables, bounds
checks, missing descriptor buffers, and NULL RCU slice guards.
- Block bitmap validation, reading, prefetching, and lazy initialization
(ext4_read_block_bitmap_nowait, ext4_wait_block_bitmap, and
ext4_read_block_bitmap), covering CRC32C verification, in-group and
out-of-group metadata bit validation with and without flex_bg, bitmap
tail padding checks, EXT4_FC_REPLAY and buffer_verified
short-circuits, cold disk reads vs. cached buffer promotions,
REQ_RAHEAD locked-buffer prefetching, EXT4_BG_BLOCK_UNINIT lazy bitmap
initialization (CRC32C, legacy CRC16 GDT_CSUM, and unchecksummed
fallback), group 0 and oversized base-metadata corruption guards, and
simulated getblk / I/O errors.
- Free cluster reservation and pool accounting
(ext4_claim_free_clusters and ext4_has_free_clusters) across
above-watermark fast paths, unprivileged limits, bigalloc r_blocks
scaling, root-reserved pool access via s_resuid, non-root s_resgid
supplementary group membership, EXT4_MB_USE_ROOT_BLOCKS, and
CAP_SYS_RESOURCE, plus EXT4_MB_USE_RESERVED internal pool access.
- Allocation retry policy (ext4_should_retry_alloc) across non-journaled
mounts, pending mballoc free retries up to the 3-attempt limit with
s_sra_exceeded_retry_limit accounting, free-space rechecks, and
s_discard_work flushing under s_retry_alloc_pending.
- Metadata block allocation (ext4_new_meta_blocks), free cluster
summation (ext4_count_free_clusters across corrupt bitmaps, 64-bit
high free-cluster counts, and unloaded descriptor blocks), and inode
goal block selection (ext4_inode_to_goal_block across flex_bg
directory vs. regular file placement, delalloc, and nodelalloc PID
colouring).
Route the block bitmap buffer lookup in ext4_read_block_bitmap_nowait()
through ext4_sb_getblk() so KUnit tests can intercept buffer-cache
lookups via its existing static stub hook, and export the remaining
non-static balloc.c entry points with EXPORT_SYMBOL_FOR_EXT4_TEST.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 3 +-
fs/ext4/balloc-test.c | 2359 +++++++++++++++++++++++++++++++++++++++++
fs/ext4/balloc.c | 11 +-
3 files changed, 2371 insertions(+), 2 deletions(-)
create mode 100644 fs/ext4/balloc-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 72198aff5b45..7d0ae7fd4448 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -18,7 +18,8 @@ ext4-test-objs += inode-test.o mballoc-test.o \
extents-test.o hash-test.o \
extents_status-test.o xattr-test.o \
namei-test.o inline-test.o \
- fast_commit-test.o ialloc-test.o
+ fast_commit-test.o ialloc-test.o \
+ balloc-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/balloc-test.c b/fs/ext4/balloc-test.c
new file mode 100644
index 000000000000..f3f3479d6afb
--- /dev/null
+++ b/fs/ext4/balloc-test.c
@@ -0,0 +1,2359 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/balloc.c
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/bio.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/capability.h>
+#include <linux/cred.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/jbd2.h>
+#include <linux/kernel.h>
+#include <linux/log2.h>
+#include <linux/module.h>
+#include <linux/quotaops.h>
+#include <linux/rcupdate.h>
+#include <linux/slab.h>
+#include <linux/stat.h>
+#include <linux/uidgid.h>
+#include <linux/workqueue.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "mballoc.h"
+
+/*
+ * Default multi-group mock filesystem geometry:
+ * - 4 active block groups (capacity up to 32 groups across 2 GDT blocks)
+ * - 1024-byte blocks (8192 bits per bitmap block, 16 64-byte GDs per block)
+ * - s_first_data_block = 1, 1024 blocks (clusters) per group (total 4097)
+ * - 64 inodes per group, 16 inode table blocks per group
+ * - sparse_super + metadata_csum + 64bit enabled, s_gdb_count = 1:
+ * - Groups 0, 1, 3 have super + 1 gdb (2 base blocks) + bbitmap + ibitmap +
+ * 16 itable blocks = 20 overhead clusters -> 1004 free clusters.
+ * - Group 2 has 0 base blocks + bbitmap + ibitmap + 16 itable blocks =
+ * 18 overhead clusters -> 1006 free clusters.
+ */
+#define BALLOC_TEST_NGROUPS 4U
+#define BALLOC_TEST_MAX_GROUPS 32U
+#define BALLOC_TEST_GDESC_BLKS 2U
+#define BALLOC_TEST_BLK_SIZE 1024U
+#define BALLOC_TEST_BLK_BITS 10U
+#define BALLOC_TEST_BLOCKS_PER_GRP 1024U
+#define BALLOC_TEST_FIRST_DATA_BLK 1U
+#define BALLOC_TEST_TOTAL_BLKS (BALLOC_TEST_FIRST_DATA_BLK + \
+ BALLOC_TEST_NGROUPS * \
+ BALLOC_TEST_BLOCKS_PER_GRP)
+#define BALLOC_TEST_INODE_SIZE 256U
+#define BALLOC_TEST_INODES_PER_GRP 64U
+#define BALLOC_TEST_ITB_PER_GRP 16U
+#define BALLOC_TEST_CSUM_SEED 0x5a3c91e7U
+#define BALLOC_TEST_GDESC_BLK0 2ULL
+#define BALLOC_TEST_POOL_SIZE 16U
+
+struct balloc_test_blk {
+ bool in_use;
+ ext4_fsblk_t blocknr;
+ struct buffer_head bh;
+ char cache_buf[BALLOC_TEST_BLK_SIZE] __aligned(8);
+ char disk_buf[BALLOC_TEST_BLK_SIZE] __aligned(8);
+};
+
+struct balloc_test_ctx {
+ struct super_block *sb;
+ struct ext4_sb_info sbi;
+ struct ext4_super_block es;
+ struct blockgroup_lock bgl;
+ struct block_device bdev;
+ struct request_queue bdev_queue;
+ struct inode bd_inode;
+ struct buffer_head sb_bh;
+
+ struct ext4_group_desc gdps[BALLOC_TEST_MAX_GROUPS] __aligned(8);
+ struct buffer_head gdesc_bh[BALLOC_TEST_GDESC_BLKS];
+ struct buffer_head *gdesc_bh_ptrs[BALLOC_TEST_GDESC_BLKS];
+
+ struct ext4_group_info grp_infos[BALLOC_TEST_MAX_GROUPS];
+ struct ext4_group_info *grp_info_slice0[16];
+ struct ext4_group_info *grp_info_slice1[16];
+ struct ext4_group_info **grp_info_slices[BALLOC_TEST_GDESC_BLKS];
+
+ struct inode *test_inode;
+ journal_t journal;
+
+ struct balloc_test_blk pool[BALLOC_TEST_POOL_SIZE];
+
+ bool fcc_inited;
+ bool dcc_inited;
+ bool fic_inited;
+ bool sra_inited;
+
+ /* Credential override tracking for deterministic cleanup */
+ const struct cred *saved_cred;
+ struct cred *override_cred;
+
+ /* Discard work tracking */
+ unsigned int discard_work_runs;
+ int discard_saw_retry_pending;
+ s64 discard_release_clusters;
+
+ /* Stub fault injection and call capture */
+ bool fail_getblk;
+ bool fail_read_eio;
+ unsigned int read_nowait_calls;
+ blk_opf_t last_read_op_flags;
+
+ unsigned int mb_new_blocks_calls;
+ struct ext4_allocation_request last_ar;
+ ext4_fsblk_t mock_alloc_blk;
+ unsigned int mock_alloc_len;
+ int mock_alloc_err;
+};
+
+static inline ext4_fsblk_t balloc_test_group_start(ext4_group_t g)
+{
+ return BALLOC_TEST_FIRST_DATA_BLK +
+ (ext4_fsblk_t)g * BALLOC_TEST_BLOCKS_PER_GRP;
+}
+
+static inline unsigned int balloc_test_default_base_blks(ext4_group_t g)
+{
+ return (g == 2) ? 0U : 2U;
+}
+
+static inline ext4_fsblk_t balloc_test_bbitmap_blk(ext4_group_t g)
+{
+ return balloc_test_group_start(g) + balloc_test_default_base_blks(g);
+}
+
+static inline ext4_fsblk_t balloc_test_ibitmap_blk(ext4_group_t g)
+{
+ return balloc_test_bbitmap_blk(g) + 1;
+}
+
+static inline ext4_fsblk_t balloc_test_itable_blk(ext4_group_t g)
+{
+ return balloc_test_bbitmap_blk(g) + 2;
+}
+
+static inline void clear_bitmap_uptodate(struct buffer_head *bh)
+{
+ clear_bit(BH_BITMAP_UPTODATE, &bh->b_state);
+}
+
+static struct inode *balloc_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);
+ init_rwsem(&ei->xattr_sem);
+ INIT_LIST_HEAD(&ei->i_orphan);
+ inode_init_once(&ei->vfs_inode);
+ ext4_fc_init_inode(&ei->vfs_inode);
+ spin_lock_init(&ei->i_raw_lock);
+ spin_lock_init(&ei->i_block_reservation_lock);
+#ifdef CONFIG_QUOTA
+ memset(&ei->i_dquot, 0, sizeof(ei->i_dquot));
+#endif
+ return &ei->vfs_inode;
+}
+
+static void balloc_test_free_inode_mem(struct inode *inode)
+{
+ kfree(EXT4_I(inode));
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **balloc_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+
+static qsize_t *balloc_test_get_reserved_space(struct inode *inode)
+{
+ return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations balloc_test_qops = {
+ .get_reserved_space = balloc_test_get_reserved_space,
+};
+#endif
+
+static const struct super_operations balloc_test_sops = {
+ .alloc_inode = balloc_test_alloc_inode,
+ .free_inode = balloc_test_free_inode_mem,
+ .drop_inode = inode_just_drop,
+#ifdef CONFIG_QUOTA
+ .get_dquots = balloc_test_get_dquots,
+#endif
+};
+
+static int balloc_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static struct file_system_type balloc_test_fs_type = {
+ .name = "ext4_balloc_kunit",
+ .init_fs_context = balloc_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static struct balloc_test_blk *balloc_test_find_blk(struct balloc_test_ctx *ctx,
+ ext4_fsblk_t blocknr)
+{
+ unsigned int i;
+
+ for (i = 0; i < BALLOC_TEST_POOL_SIZE; i++) {
+ if (ctx->pool[i].in_use && ctx->pool[i].blocknr == blocknr)
+ return &ctx->pool[i];
+ }
+ return NULL;
+}
+
+static struct balloc_test_blk *
+balloc_test_find_blk_by_bh(struct balloc_test_ctx *ctx, struct buffer_head *bh)
+{
+ unsigned int i;
+
+ for (i = 0; i < BALLOC_TEST_POOL_SIZE; i++) {
+ if (ctx->pool[i].in_use && &ctx->pool[i].bh == bh)
+ return &ctx->pool[i];
+ }
+ return NULL;
+}
+
+static struct balloc_test_blk *
+balloc_test_setup_blk(struct balloc_test_ctx *ctx, ext4_fsblk_t blocknr,
+ bool uptodate)
+{
+ struct balloc_test_blk *slot = balloc_test_find_blk(ctx, blocknr);
+ unsigned int i;
+
+ if (!slot) {
+ for (i = 0; i < BALLOC_TEST_POOL_SIZE; i++) {
+ if (!ctx->pool[i].in_use) {
+ slot = &ctx->pool[i];
+ break;
+ }
+ }
+ }
+ if (!slot)
+ return NULL;
+
+ memset(slot, 0, sizeof(*slot));
+ slot->in_use = true;
+ slot->blocknr = blocknr;
+ slot->bh.b_bdev = &ctx->bdev;
+ slot->bh.b_blocknr = blocknr;
+ slot->bh.b_size = BALLOC_TEST_BLK_SIZE;
+ slot->bh.b_data = slot->cache_buf;
+ atomic_set(&slot->bh.b_count, 1);
+ if (uptodate)
+ set_buffer_uptodate(&slot->bh);
+ return slot;
+}
+
+static void balloc_test_sync_bbitmap_csum(struct balloc_test_ctx *ctx,
+ ext4_group_t g, bool sync_disk)
+{
+ struct ext4_group_desc *gdp = &ctx->gdps[g];
+ struct balloc_test_blk *slot;
+
+ slot = balloc_test_find_blk(ctx, ext4_block_bitmap(ctx->sb, gdp));
+ if (slot) {
+ ext4_block_bitmap_csum_set(ctx->sb, gdp, &slot->bh);
+ if (sync_disk)
+ memcpy(slot->disk_buf, slot->cache_buf,
+ BALLOC_TEST_BLK_SIZE);
+ }
+ ext4_group_desc_csum_set(ctx->sb, g, gdp);
+}
+
+static void balloc_test_discard_work_fn(struct work_struct *work)
+{
+ struct ext4_sb_info *sbi =
+ container_of(work, struct ext4_sb_info, s_discard_work);
+ struct balloc_test_ctx *ctx =
+ container_of(sbi, struct balloc_test_ctx, sbi);
+
+ ctx->discard_work_runs++;
+ ctx->discard_saw_retry_pending = atomic_read(&sbi->s_retry_alloc_pending);
+ if (ctx->discard_release_clusters)
+ percpu_counter_add(&sbi->s_freeclusters_counter,
+ ctx->discard_release_clusters);
+}
+
+static void balloc_test_restore_creds(struct balloc_test_ctx *ctx)
+{
+ if (ctx->saved_cred) {
+ revert_creds(ctx->saved_cred);
+ ctx->saved_cred = NULL;
+ }
+ if (ctx->override_cred) {
+ put_cred(ctx->override_cred);
+ ctx->override_cred = NULL;
+ }
+}
+
+static void balloc_test_override_creds(struct kunit *test,
+ struct balloc_test_ctx *ctx,
+ uid_t uid, gid_t gid, bool cap_sys_res,
+ int supp_gid)
+{
+ struct cred *cred;
+
+ balloc_test_restore_creds(ctx);
+
+ cred = prepare_creds();
+ KUNIT_ASSERT_NOT_NULL(test, cred);
+ ctx->override_cred = cred;
+
+ cred->uid = KUIDT_INIT(uid);
+ cred->euid = KUIDT_INIT(uid);
+ cred->suid = KUIDT_INIT(uid);
+ cred->fsuid = KUIDT_INIT(uid);
+ cred->gid = KGIDT_INIT(gid);
+ cred->egid = KGIDT_INIT(gid);
+ cred->sgid = KGIDT_INIT(gid);
+ cred->fsgid = KGIDT_INIT(gid);
+ cap_clear(cred->cap_effective);
+ cap_clear(cred->cap_permitted);
+ cap_clear(cred->cap_inheritable);
+ cap_clear(cred->cap_ambient);
+ if (cap_sys_res) {
+ cap_raise(cred->cap_permitted, CAP_SYS_RESOURCE);
+ cap_raise(cred->cap_effective, CAP_SYS_RESOURCE);
+ }
+#ifdef CONFIG_MULTIUSER
+ {
+ struct group_info *gi = groups_alloc(supp_gid >= 0 ? 1 : 0);
+
+ KUNIT_ASSERT_NOT_NULL(test, gi);
+ if (supp_gid >= 0) {
+ gi->gid[0] = KGIDT_INIT((gid_t)supp_gid);
+ groups_sort(gi);
+ }
+ set_groups(cred, gi);
+ put_group_info(gi);
+ }
+#endif
+ ctx->saved_cred = override_creds(cred);
+}
+
+/* --- Static Stubs --- */
+
+static struct buffer_head *balloc_test_sb_getblk_stub(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct balloc_test_ctx *ctx = test ? test->priv : NULL;
+ struct balloc_test_blk *slot;
+
+ if (!ctx || ctx->fail_getblk)
+ return NULL;
+ slot = balloc_test_find_blk(ctx, block);
+ if (!slot)
+ return NULL;
+ get_bh(&slot->bh);
+ return &slot->bh;
+}
+
+static void balloc_test_read_bh_nowait_stub(struct buffer_head *bh,
+ blk_opf_t op_flags,
+ bio_end_io_t end_io,
+ bool simu_fail)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct balloc_test_ctx *ctx = test ? test->priv : NULL;
+ struct balloc_test_blk *slot;
+ bool fail;
+
+ if (!ctx) {
+ unlock_buffer(bh);
+ return;
+ }
+ ctx->read_nowait_calls++;
+ ctx->last_read_op_flags = op_flags;
+
+ if (simu_fail) {
+ clear_buffer_uptodate(bh);
+ unlock_buffer(bh);
+ return;
+ }
+
+ clear_buffer_verified(bh);
+ slot = balloc_test_find_blk_by_bh(ctx, bh);
+ fail = ctx->fail_read_eio || !slot;
+ if (!fail)
+ memcpy(bh->b_data, slot->disk_buf, bh->b_size);
+
+ if (end_io) {
+ struct bio *bio = bio_alloc(NULL, 0, REQ_OP_READ, GFP_KERNEL);
+
+ if (bio) {
+ bio->bi_private = bh;
+ bio->bi_status = fail ? BLK_STS_IOERR : BLK_STS_OK;
+ end_io(bio);
+ return;
+ }
+ }
+
+ if (fail) {
+ clear_buffer_uptodate(bh);
+ } else {
+ set_buffer_uptodate(bh);
+ set_bitmap_uptodate(bh);
+ }
+ unlock_buffer(bh);
+}
+
+static ext4_fsblk_t
+balloc_test_mb_new_blocks_stub(handle_t *handle,
+ struct ext4_allocation_request *ar, int *errp)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct balloc_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx) {
+ *errp = -EIO;
+ return 0;
+ }
+ ctx->mb_new_blocks_calls++;
+ ctx->last_ar = *ar;
+ *errp = ctx->mock_alloc_err;
+ if (ctx->mock_alloc_err)
+ return 0;
+ if (ctx->mock_alloc_len)
+ ar->len = ctx->mock_alloc_len;
+ return ctx->mock_alloc_blk;
+}
+
+/* --- Fixture Setup & Teardown --- */
+
+static void balloc_test_cleanup(struct balloc_test_ctx *ctx)
+{
+ unsigned int i;
+
+ balloc_test_restore_creds(ctx);
+
+ for (i = 0; i < BALLOC_TEST_POOL_SIZE; i++) {
+ if (ctx->pool[i].in_use && buffer_locked(&ctx->pool[i].bh))
+ unlock_buffer(&ctx->pool[i].bh);
+ }
+
+ cancel_work_sync(&ctx->sbi.s_discard_work);
+ ctx->sbi.s_journal = NULL;
+
+ if (ctx->test_inode) {
+ spin_lock(&ctx->test_inode->i_lock);
+ inode_state_clear(ctx->test_inode, I_DIRTY_ALL);
+ spin_unlock(&ctx->test_inode->i_lock);
+ iput(ctx->test_inode);
+ ctx->test_inode = NULL;
+ }
+
+ if (ctx->sb) {
+ deactivate_super(ctx->sb);
+ rcu_barrier();
+ ctx->sb = NULL;
+ }
+
+ if (ctx->sra_inited)
+ percpu_counter_destroy(&ctx->sbi.s_sra_exceeded_retry_limit);
+ if (ctx->fic_inited)
+ percpu_counter_destroy(&ctx->sbi.s_freeinodes_counter);
+ if (ctx->dcc_inited)
+ percpu_counter_destroy(&ctx->sbi.s_dirtyclusters_counter);
+ if (ctx->fcc_inited)
+ percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+
+ kfree(ctx);
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(balloc_test_cleanup_action, balloc_test_cleanup,
+ struct balloc_test_ctx *);
+
+static int balloc_test_init(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ ext4_group_t g;
+ s64 total_free_clusters = 0;
+ s64 total_free_inodes = 0;
+ int err;
+
+ ctx = kzalloc_obj(*ctx);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ test->priv = ctx;
+
+ INIT_WORK(&ctx->sbi.s_discard_work, balloc_test_discard_work_fn);
+ rwlock_init(&ctx->journal.j_state_lock);
+
+ err = kunit_add_action_or_reset(test, balloc_test_cleanup_action, ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ fc = fs_context_for_mount(&balloc_test_fs_type, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fc));
+ sb = sget_fc(fc, NULL, set_anon_super_fc);
+ put_fs_context(fc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(sb));
+
+ sbi = &ctx->sbi;
+ sbi->s_sb = sb;
+ sb->s_fs_info = sbi;
+ sb->s_op = &balloc_test_sops;
+#ifdef CONFIG_QUOTA
+ sb->dq_op = &balloc_test_qops;
+#endif
+ sb->s_flags |= SB_ACTIVE;
+ sb->s_blocksize = BALLOC_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = BALLOC_TEST_BLK_BITS;
+ sb->s_maxbytes = MAX_LFS_FILESIZE;
+ strscpy(sb->s_id, "ext4_balloc_kunit", sizeof(sb->s_id));
+
+ inode_init_once(&ctx->bd_inode);
+ ctx->bd_inode.i_sb = sb;
+ ctx->bd_inode.i_blkbits = BALLOC_TEST_BLK_BITS;
+ ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data;
+ ctx->bd_inode.i_data.host = &ctx->bd_inode;
+ ctx->bdev_queue.limits.logical_block_size = 512;
+ ctx->bdev_queue.limits.dma_alignment = 511;
+ ctx->bdev.bd_queue = &ctx->bdev_queue;
+ ctx->bdev.bd_mapping = &ctx->bd_inode.i_data;
+ bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+ sb->s_bdev = &ctx->bdev;
+
+ up_write(&sb->s_umount);
+ ctx->sb = sb;
+
+ bgl_lock_init(&ctx->bgl);
+ sbi->s_blockgroup_lock = &ctx->bgl;
+
+ sbi->s_es = &ctx->es;
+ ctx->es.s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+ ctx->es.s_first_data_block = cpu_to_le32(BALLOC_TEST_FIRST_DATA_BLK);
+ ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ ctx->es.s_inodes_count = cpu_to_le32(BALLOC_TEST_NGROUPS *
+ BALLOC_TEST_INODES_PER_GRP);
+ ctx->es.s_inodes_per_group = cpu_to_le32(BALLOC_TEST_INODES_PER_GRP);
+ ctx->es.s_blocks_per_group = cpu_to_le32(BALLOC_TEST_BLOCKS_PER_GRP);
+ ctx->es.s_clusters_per_group = cpu_to_le32(BALLOC_TEST_BLOCKS_PER_GRP);
+ ctx->es.s_inode_size = cpu_to_le16(BALLOC_TEST_INODE_SIZE);
+ ctx->es.s_checksum_type = EXT4_CRC32C_CHKSUM;
+ ext4_blocks_count_set(&ctx->es, BALLOC_TEST_TOTAL_BLKS);
+
+ ext4_set_feature_sparse_super(sb);
+ ext4_set_feature_extents(sb);
+ ext4_set_feature_metadata_csum(sb);
+ ext4_set_feature_64bit(sb);
+
+ ctx->sb_bh.b_data = (char *)&ctx->es;
+ ctx->sb_bh.b_size = sizeof(ctx->es);
+ ctx->sb_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->sb_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->sb_bh);
+ sbi->s_sbh = &ctx->sb_bh;
+
+ for (g = 0; g < BALLOC_TEST_GDESC_BLKS; g++) {
+ ctx->gdesc_bh[g].b_data = (char *)&ctx->gdps[g * 16];
+ ctx->gdesc_bh[g].b_size = BALLOC_TEST_BLK_SIZE;
+ ctx->gdesc_bh[g].b_blocknr = BALLOC_TEST_GDESC_BLK0 + g;
+ ctx->gdesc_bh[g].b_bdev = &ctx->bdev;
+ atomic_set(&ctx->gdesc_bh[g].b_count, 1);
+ set_buffer_uptodate(&ctx->gdesc_bh[g]);
+ ctx->gdesc_bh_ptrs[g] = &ctx->gdesc_bh[g];
+ }
+ rcu_assign_pointer(sbi->s_group_desc, ctx->gdesc_bh_ptrs);
+
+ sbi->s_inode_size = BALLOC_TEST_INODE_SIZE;
+ sbi->s_inodes_per_block = BALLOC_TEST_BLK_SIZE / BALLOC_TEST_INODE_SIZE;
+ sbi->s_inodes_per_group = BALLOC_TEST_INODES_PER_GRP;
+ sbi->s_itb_per_group = BALLOC_TEST_ITB_PER_GRP;
+ sbi->s_blocks_per_group = BALLOC_TEST_BLOCKS_PER_GRP;
+ sbi->s_clusters_per_group = BALLOC_TEST_BLOCKS_PER_GRP;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_cluster_bits = 0;
+ sbi->s_groups_count = BALLOC_TEST_NGROUPS;
+ sbi->s_gdb_count = 1;
+ sbi->s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_desc_per_block = BALLOC_TEST_BLK_SIZE / EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_desc_per_block_bits = ilog2(sbi->s_desc_per_block);
+ sbi->s_csum_seed = BALLOC_TEST_CSUM_SEED;
+ sbi->s_resuid = make_kuid(&init_user_ns, EXT4_DEF_RESUID);
+ sbi->s_resgid = make_kgid(&init_user_ns, EXT4_DEF_RESGID);
+
+ set_opt(sb, ERRORS_CONT);
+ set_opt(sb, DELALLOC);
+ spin_lock_init(&sbi->s_bdev_wb_lock);
+ spin_lock_init(&sbi->s_error_lock);
+ spin_lock_init(&sbi->s_md_lock);
+
+ 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);
+
+ for (g = 0; g < BALLOC_TEST_MAX_GROUPS; g++) {
+ init_rwsem(&ctx->grp_infos[g].alloc_sem);
+ ctx->grp_infos[g].bb_state = 0;
+ if (g < 16)
+ ctx->grp_info_slice0[g] = &ctx->grp_infos[g];
+ else
+ ctx->grp_info_slice1[g - 16] = &ctx->grp_infos[g];
+ }
+ ctx->grp_info_slices[0] = ctx->grp_info_slice0;
+ ctx->grp_info_slices[1] = ctx->grp_info_slice1;
+ rcu_assign_pointer(sbi->s_group_info, ctx->grp_info_slices);
+
+ for (g = 0; g < BALLOC_TEST_NGROUPS; g++) {
+ struct balloc_test_blk *bblk;
+ unsigned int overhead = balloc_test_default_base_blks(g) +
+ 2U + BALLOC_TEST_ITB_PER_GRP;
+ u32 free_clu = BALLOC_TEST_BLOCKS_PER_GRP - overhead;
+ unsigned int bit;
+
+ ext4_block_bitmap_set(sb, &ctx->gdps[g],
+ balloc_test_bbitmap_blk(g));
+ ext4_inode_bitmap_set(sb, &ctx->gdps[g],
+ balloc_test_ibitmap_blk(g));
+ ext4_inode_table_set(sb, &ctx->gdps[g],
+ balloc_test_itable_blk(g));
+ ext4_free_group_clusters_set(sb, &ctx->gdps[g], free_clu);
+ ext4_free_inodes_set(sb, &ctx->gdps[g],
+ BALLOC_TEST_INODES_PER_GRP);
+ ctx->grp_infos[g].bb_free = free_clu;
+
+ bblk = balloc_test_setup_blk(ctx, balloc_test_bbitmap_blk(g),
+ true);
+ KUNIT_ASSERT_NOT_NULL(test, bblk);
+
+ for (bit = 0; bit < overhead; bit++)
+ ext4_set_bit(bit, bblk->cache_buf);
+ ext4_mark_bitmap_end(BALLOC_TEST_BLOCKS_PER_GRP,
+ BALLOC_TEST_BLK_SIZE * 8,
+ bblk->cache_buf);
+ set_bitmap_uptodate(&bblk->bh);
+ set_buffer_verified(&bblk->bh);
+ balloc_test_sync_bbitmap_csum(ctx, g, true);
+
+ total_free_clusters += free_clu;
+ total_free_inodes += BALLOC_TEST_INODES_PER_GRP;
+ }
+
+ err = percpu_counter_init(&sbi->s_freeclusters_counter,
+ total_free_clusters, GFP_KERNEL);
+ if (!err)
+ ctx->fcc_inited = true;
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ err = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0, GFP_KERNEL);
+ if (!err)
+ ctx->dcc_inited = true;
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ err = percpu_counter_init(&sbi->s_freeinodes_counter,
+ total_free_inodes, GFP_KERNEL);
+ if (!err)
+ ctx->fic_inited = true;
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ err = percpu_counter_init(&sbi->s_sra_exceeded_retry_limit, 0,
+ GFP_KERNEL);
+ if (!err)
+ ctx->sra_inited = true;
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ ctx->test_inode = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->test_inode);
+ ctx->test_inode->i_ino = EXT4_FIRST_INO(sb);
+ ctx->test_inode->i_mode = S_IFREG | 0644;
+ ctx->test_inode->i_blkbits = BALLOC_TEST_BLK_BITS;
+ inode_set_bytes(ctx->test_inode, 0);
+ EXT4_I(ctx->test_inode)->i_block_group = 0;
+
+ kunit_activate_static_stub(test, ext4_sb_getblk,
+ balloc_test_sb_getblk_stub);
+ kunit_activate_static_stub(test, ext4_read_bh_nowait,
+ balloc_test_read_bh_nowait_stub);
+ kunit_activate_static_stub(test, ext4_mb_new_blocks,
+ balloc_test_mb_new_blocks_stub);
+
+ return 0;
+}
+
+/* =========================================================================
+ * 1. Group Number & Offset Calculation
+ * =========================================================================
+ */
+
+struct group_offset_param {
+ const char *desc;
+ unsigned char blocksize_bits;
+ unsigned int cluster_bits;
+ u32 blocks_per_group;
+ u32 first_data_block;
+ bool std_group_size;
+ ext4_fsblk_t block;
+ ext4_group_t expected_group;
+ ext4_grpblk_t expected_offset;
+};
+
+static const struct group_offset_param group_offset_params[] = {
+ {
+ .desc = "std_1k_bg0_first_block",
+ .blocksize_bits = 10,
+ .cluster_bits = 0,
+ .blocks_per_group = 8192,
+ .first_data_block = 1,
+ .std_group_size = true,
+ .block = 1,
+ .expected_group = 0,
+ .expected_offset = 0,
+ },
+ {
+ .desc = "std_1k_bg0_last_block",
+ .blocksize_bits = 10,
+ .cluster_bits = 0,
+ .blocks_per_group = 8192,
+ .first_data_block = 1,
+ .std_group_size = true,
+ .block = 8192,
+ .expected_group = 0,
+ .expected_offset = 8191,
+ },
+ {
+ .desc = "std_1k_bg1_first_block",
+ .blocksize_bits = 10,
+ .cluster_bits = 0,
+ .blocks_per_group = 8192,
+ .first_data_block = 1,
+ .std_group_size = true,
+ .block = 8193,
+ .expected_group = 1,
+ .expected_offset = 0,
+ },
+ {
+ .desc = "std_1k_bg3_interior_block",
+ .blocksize_bits = 10,
+ .cluster_bits = 0,
+ .blocks_per_group = 8192,
+ .first_data_block = 1,
+ .std_group_size = true,
+ .block = 25000,
+ .expected_group = 3,
+ .expected_offset = 423,
+ },
+ {
+ .desc = "std_4k_bg0_last_block",
+ .blocksize_bits = 12,
+ .cluster_bits = 0,
+ .blocks_per_group = 32768,
+ .first_data_block = 0,
+ .std_group_size = true,
+ .block = 32767,
+ .expected_group = 0,
+ .expected_offset = 32767,
+ },
+ {
+ .desc = "std_4k_bg1_first_block",
+ .blocksize_bits = 12,
+ .cluster_bits = 0,
+ .blocks_per_group = 32768,
+ .first_data_block = 0,
+ .std_group_size = true,
+ .block = 32768,
+ .expected_group = 1,
+ .expected_offset = 0,
+ },
+ {
+ .desc = "std_4k_above_32bit_blocknr",
+ .blocksize_bits = 12,
+ .cluster_bits = 0,
+ .blocks_per_group = 32768,
+ .first_data_block = 0,
+ .std_group_size = true,
+ .block = 0x100008005ULL,
+ .expected_group = 131073,
+ .expected_offset = 5,
+ },
+ {
+ .desc = "nonstd_bg0_last_block",
+ .blocksize_bits = 10,
+ .cluster_bits = 0,
+ .blocks_per_group = 1000,
+ .first_data_block = 1,
+ .std_group_size = false,
+ .block = 1000,
+ .expected_group = 0,
+ .expected_offset = 999,
+ },
+ {
+ .desc = "nonstd_bg1_first_block",
+ .blocksize_bits = 10,
+ .cluster_bits = 0,
+ .blocks_per_group = 1000,
+ .first_data_block = 1,
+ .std_group_size = false,
+ .block = 1001,
+ .expected_group = 1,
+ .expected_offset = 0,
+ },
+ {
+ .desc = "nonstd_bg2_interior_block",
+ .blocksize_bits = 10,
+ .cluster_bits = 0,
+ .blocks_per_group = 1000,
+ .first_data_block = 1,
+ .std_group_size = false,
+ .block = 2346,
+ .expected_group = 2,
+ .expected_offset = 345,
+ },
+ {
+ .desc = "bigalloc_std_cluster_start",
+ .blocksize_bits = 12,
+ .cluster_bits = 3,
+ .blocks_per_group = 262144,
+ .first_data_block = 0,
+ .std_group_size = true,
+ .block = 16,
+ .expected_group = 0,
+ .expected_offset = 2,
+ },
+ {
+ .desc = "bigalloc_std_cluster_end_same_offset",
+ .blocksize_bits = 12,
+ .cluster_bits = 3,
+ .blocks_per_group = 262144,
+ .first_data_block = 0,
+ .std_group_size = true,
+ .block = 23,
+ .expected_group = 0,
+ .expected_offset = 2,
+ },
+ {
+ .desc = "bigalloc_std_next_cluster",
+ .blocksize_bits = 12,
+ .cluster_bits = 3,
+ .blocks_per_group = 262144,
+ .first_data_block = 0,
+ .std_group_size = true,
+ .block = 24,
+ .expected_group = 0,
+ .expected_offset = 3,
+ },
+ {
+ .desc = "bigalloc_std_bg2_offset",
+ .blocksize_bits = 12,
+ .cluster_bits = 3,
+ .blocks_per_group = 262144,
+ .first_data_block = 0,
+ .std_group_size = true,
+ .block = 524303,
+ .expected_group = 2,
+ .expected_offset = 1,
+ },
+ {
+ .desc = "bigalloc_nonstd_bg3_offset",
+ .blocksize_bits = 10,
+ .cluster_bits = 2,
+ .blocks_per_group = 600,
+ .first_data_block = 1,
+ .std_group_size = false,
+ .block = 1820,
+ .expected_group = 3,
+ .expected_offset = 4,
+ },
+};
+
+static void group_offset_param_get_desc(const struct group_offset_param *p,
+ char *desc)
+{
+ strscpy(desc, p->desc, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(group_offset, group_offset_params,
+ group_offset_param_get_desc);
+
+static void test_group_no_and_offset_matrix(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ const struct group_offset_param *p = test->param_value;
+ ext4_group_t grp = 0xdeadbeefU;
+ ext4_grpblk_t off = 0x7654321;
+
+ sb->s_blocksize_bits = p->blocksize_bits;
+ sb->s_blocksize = 1UL << p->blocksize_bits;
+ sbi->s_cluster_bits = p->cluster_bits;
+ sbi->s_cluster_ratio = 1U << p->cluster_bits;
+ sbi->s_blocks_per_group = p->blocks_per_group;
+ sbi->s_clusters_per_group = p->blocks_per_group >> p->cluster_bits;
+ ctx->es.s_first_data_block = cpu_to_le32(p->first_data_block);
+ if (p->std_group_size)
+ set_opt2(sb, STD_GROUP_SIZE);
+ else
+ clear_opt2(sb, STD_GROUP_SIZE);
+
+ KUNIT_EXPECT_EQ(test, ext4_get_group_number(sb, p->block),
+ p->expected_group);
+
+ ext4_get_group_no_and_offset(sb, p->block, &grp, &off);
+ KUNIT_EXPECT_EQ(test, grp, p->expected_group);
+ KUNIT_EXPECT_EQ(test, off, p->expected_offset);
+}
+
+static void test_group_no_and_offset_optional_out_ptrs(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ ext4_group_t grp = 999;
+ ext4_grpblk_t off = 999;
+
+ /* Block 2060 = first_data_block (1) + 2 * 1024 + 11 */
+ ext4_get_group_no_and_offset(sb, 2060, &grp, NULL);
+ KUNIT_EXPECT_EQ(test, grp, 2U);
+
+ ext4_get_group_no_and_offset(sb, 2060, NULL, &off);
+ KUNIT_EXPECT_EQ(test, off, 11);
+
+ /* Both output pointers NULL must be a safe no-op */
+ ext4_get_group_no_and_offset(sb, 2060, NULL, NULL);
+}
+
+/* =========================================================================
+ * 2. Backup Superblock Placement (ext4_bg_has_super)
+ * =========================================================================
+ */
+
+static void test_bg_has_super_dense_all_groups(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ static const ext4_group_t groups[] = { 0, 1, 2, 3, 4, 6, 8, 15, 22 };
+ unsigned int i;
+
+ ext4_clear_feature_sparse_super(sb);
+ ext4_clear_feature_sparse_super2(sb);
+
+ for (i = 0; i < ARRAY_SIZE(groups); i++)
+ KUNIT_EXPECT_EQ_MSG(test, ext4_bg_has_super(sb, groups[i]), 1,
+ "dense group %u should have super",
+ groups[i]);
+}
+
+static void test_bg_has_super_sparse_super_powers(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ static const ext4_group_t with_super[] = {
+ 0, 1, 3, 9, 27, 81, 5, 25, 125, 7, 49, 343
+ };
+ static const ext4_group_t without_super[] = {
+ 2, 4, 6, 8, 10, 11, 15, 21, 35, 45, 63, 77
+ };
+ unsigned int i;
+
+ ext4_set_feature_sparse_super(sb);
+ ext4_clear_feature_sparse_super2(sb);
+
+ for (i = 0; i < ARRAY_SIZE(with_super); i++)
+ KUNIT_EXPECT_EQ_MSG(test, ext4_bg_has_super(sb, with_super[i]),
+ 1, "sparse_super group %u expected 1",
+ with_super[i]);
+
+ for (i = 0; i < ARRAY_SIZE(without_super); i++)
+ KUNIT_EXPECT_EQ_MSG(test,
+ ext4_bg_has_super(sb, without_super[i]), 0,
+ "sparse_super group %u expected 0",
+ without_super[i]);
+}
+
+static void test_bg_has_super_sparse_super2_explicit_backups(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+
+ ext4_set_feature_sparse_super2(sb);
+ ctx->es.s_backup_bgs[0] = cpu_to_le32(6);
+ ctx->es.s_backup_bgs[1] = cpu_to_le32(42);
+
+ KUNIT_EXPECT_EQ(test, ext4_bg_has_super(sb, 0), 1);
+ KUNIT_EXPECT_EQ(test, ext4_bg_has_super(sb, 6), 1);
+ KUNIT_EXPECT_EQ(test, ext4_bg_has_super(sb, 42), 1);
+ KUNIT_EXPECT_EQ(test, ext4_bg_has_super(sb, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_bg_has_super(sb, 3), 0);
+ KUNIT_EXPECT_EQ(test, ext4_bg_has_super(sb, 5), 0);
+ KUNIT_EXPECT_EQ(test, ext4_bg_has_super(sb, 7), 0);
+
+ /* Single backup group configured (s_backup_bgs[1] == 0) */
+ ctx->es.s_backup_bgs[0] = cpu_to_le32(12);
+ ctx->es.s_backup_bgs[1] = cpu_to_le32(0);
+ KUNIT_EXPECT_EQ(test, ext4_bg_has_super(sb, 0), 1);
+ KUNIT_EXPECT_EQ(test, ext4_bg_has_super(sb, 12), 1);
+ KUNIT_EXPECT_EQ(test, ext4_bg_has_super(sb, 6), 0);
+ KUNIT_EXPECT_EQ(test, ext4_bg_has_super(sb, 42), 0);
+}
+
+/* =========================================================================
+ * 3. GDT & Base Metadata Blocks (ext4_bg_num_gdb, ext4_num_base_meta_blocks)
+ * =========================================================================
+ */
+
+static void test_bg_num_gdb_and_base_meta_non_metabg(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+
+ ext4_clear_feature_meta_bg(sb);
+ ext4_set_feature_sparse_super(sb);
+ ctx->sbi.s_gdb_count = 4;
+ ctx->es.s_reserved_gdt_blocks = cpu_to_le16(7);
+
+ /* Groups with super: 1 super + 4 gdb + 7 reserved_gdt = 12 blocks */
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 0), 4UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 0), 12U);
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 3), 4UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 3), 12U);
+
+ /* Groups without super: 0 gdb and reserved_gdt is not present */
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 2), 0UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 2), 0U);
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 15), 0UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 15), 0U);
+}
+
+static void test_bg_num_gdb_and_base_meta_hybrid_metabg(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+
+ /*
+ * s_desc_per_block = 16, s_first_meta_bg = 2:
+ * - Groups 0..31 (metagroups 0..1) use traditional GDT layout with
+ * s_first_meta_bg (2) GDT blocks + s_reserved_gdt_blocks (9) when
+ * backup super is present.
+ * - Groups >= 32 (metagroups >= 2) place 1 descriptor block in the
+ * 1st, 2nd, and last group of each 16-group metagroup, with no
+ * reserved GDT blocks.
+ */
+ ext4_set_feature_meta_bg(sb);
+ ext4_set_feature_sparse_super(sb);
+ ctx->sbi.s_gdb_count = 5;
+ ctx->es.s_first_meta_bg = cpu_to_le32(2);
+ ctx->es.s_reserved_gdt_blocks = cpu_to_le16(9);
+
+ /* Pre-meta_bg groups with super: 1 + 2 + 9 = 12 */
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 0), 2UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 0), 12U);
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 27), 2UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 27), 12U);
+
+ /* Pre-meta_bg groups without super */
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 2), 0UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 2), 0U);
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 31), 0UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 31), 0U);
+
+ /* Metagroup 2 (groups 32..47): first=32, second=33, last=47 (no super) */
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 32), 1UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 32), 1U);
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 33), 1UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 33), 1U);
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 47), 1UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 47), 1U);
+
+ /* Interior meta_bg group with neither super nor meta gdb */
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 34), 0UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 34), 0U);
+
+ /* Group 125 (5^3 in metagroup 112..127): super=1, meta_gdb=0 -> 1 */
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 125), 0UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 125), 1U);
+
+ /* Group 49 (7^2, second of 48..63): super=1, meta_gdb=1 -> 2 */
+ KUNIT_EXPECT_EQ(test, ext4_bg_num_gdb(sb, 49), 1UL);
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(sb, 49), 2U);
+}
+
+/* =========================================================================
+ * 4. Overhead & Free Clusters After Init (ext4_free_clusters_after_init)
+ * =========================================================================
+ */
+
+static void
+test_free_clusters_after_init_standard_and_partial_last(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 0, &ctx->gdps[0]),
+ 1004U);
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 2, &ctx->gdps[2]),
+ 1006U);
+
+ /* Shrink total blocks so last group 3 has only 300 blocks (20 overhead) */
+ ext4_blocks_count_set(&ctx->es, balloc_test_group_start(3) + 300);
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 3, &ctx->gdps[3]),
+ 280U);
+ /* Non-last group 1 still uses full s_blocks_per_group (1024) */
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 1, &ctx->gdps[1]),
+ 1004U);
+}
+
+static void
+test_free_clusters_after_init_external_and_spanning_meta(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ ext4_fsblk_t g2_start = balloc_test_group_start(2);
+
+ /*
+ * Group 2 has 0 base metadata blocks. Relocate its bitmaps and inode
+ * table into group 0 -> 0 overhead clusters in group 2 -> 1024 free.
+ */
+ ext4_block_bitmap_set(sb, &ctx->gdps[2], balloc_test_group_start(0) + 50);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[2], balloc_test_group_start(0) + 51);
+ ext4_inode_table_set(sb, &ctx->gdps[2], balloc_test_group_start(0) + 52);
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 2, &ctx->gdps[2]),
+ 1024U);
+
+ /*
+ * Inode table of 16 blocks starting 6 blocks before group 2 overlaps
+ * 10 blocks [g2_start .. g2_start + 9] inside group 2 -> 1014 free.
+ */
+ ext4_inode_table_set(sb, &ctx->gdps[2], g2_start - 6);
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 2, &ctx->gdps[2]),
+ 1014U);
+
+ /*
+ * Inode table starting 5 blocks before end of group 2 overlaps 5
+ * blocks [g2_end - 4 .. g2_end] inside group 2 -> 1019 free.
+ */
+ ext4_inode_table_set(sb, &ctx->gdps[2],
+ g2_start + BALLOC_TEST_BLOCKS_PER_GRP - 5);
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 2, &ctx->gdps[2]),
+ 1019U);
+}
+
+static void
+test_free_clusters_after_init_bigalloc_cluster_sharing(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ ext4_fsblk_t g1_start = balloc_test_group_start(1);
+ ext4_fsblk_t g2_start = balloc_test_group_start(2);
+
+ /* 4 blocks per cluster -> 1024 / 4 = 256 clusters per full group */
+ sbi->s_cluster_bits = 2;
+ sbi->s_cluster_ratio = 4;
+ sbi->s_clusters_per_group = 256;
+ sbi->s_itb_per_group = 8;
+
+ /*
+ * Case 1 (border overlap with base_clusters):
+ * Group 1 has 2 base blocks (offsets 0..1 -> base_clusters = 1, clu 0).
+ * Place 8-block itable at offsets 2..9 (clusters 0..2, overlapping
+ * border clu 0), bbitmap at offset 12 (clu 3), ibitmap at offset 16
+ * (clu 4) -> 5 unique overhead clusters (0..4) -> 251 free clusters.
+ */
+ ext4_inode_table_set(sb, &ctx->gdps[1], g1_start + 2);
+ ext4_block_bitmap_set(sb, &ctx->gdps[1], g1_start + 12);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[1], g1_start + 16);
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 1, &ctx->gdps[1]),
+ 251U);
+
+ /*
+ * Case 2 (bitmaps inside base_clusters):
+ * Base blocks at offsets 0..1 (clu 0), bbitmap at offset 2 (clu 0),
+ * ibitmap at offset 3 (clu 0), 8-block itable at offsets 4..11
+ * (clusters 1..2) -> 3 overhead clusters (0..2) -> 253 free clusters.
+ */
+ ext4_block_bitmap_set(sb, &ctx->gdps[1], g1_start + 2);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[1], g1_start + 3);
+ ext4_inode_table_set(sb, &ctx->gdps[1], g1_start + 4);
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 1, &ctx->gdps[1]),
+ 253U);
+
+ /*
+ * Case 3 (bbitmap and ibitmap sharing the same non-base cluster):
+ * Group 2 has 0 base clusters. bbitmap at offset 0 (clu 0), ibitmap at
+ * offset 1 (clu 0), 8-block itable at offsets 4..11 (clusters 1..2) ->
+ * 3 overhead clusters -> 253 free clusters.
+ */
+ ext4_block_bitmap_set(sb, &ctx->gdps[2], g2_start + 0);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[2], g2_start + 1);
+ ext4_inode_table_set(sb, &ctx->gdps[2], g2_start + 4);
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 2, &ctx->gdps[2]),
+ 253U);
+
+ /*
+ * Case 4 (bitmaps sharing tail cluster of inode table):
+ * Group 2 with 6-block itable at offsets 0..5 (clusters 0..1), bbitmap
+ * at offset 6 (clu 1), ibitmap at offset 7 (clu 1) -> 2 overhead
+ * clusters (0..1) -> 254 free clusters.
+ */
+ sbi->s_itb_per_group = 6;
+ ext4_inode_table_set(sb, &ctx->gdps[2], g2_start + 0);
+ ext4_block_bitmap_set(sb, &ctx->gdps[2], g2_start + 6);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[2], g2_start + 7);
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 2, &ctx->gdps[2]),
+ 254U);
+
+ /*
+ * Case 5 (partial last group rounding up blocks to clusters):
+ * Group 3 with 301 blocks -> EXT4_NUM_B2C(4, 301) = 76 clusters;
+ * external metadata -> 0 overhead in group 3 -> 76 free clusters.
+ */
+ ext4_blocks_count_set(&ctx->es, balloc_test_group_start(3) + 301);
+ ext4_block_bitmap_set(sb, &ctx->gdps[3], balloc_test_group_start(0) + 40);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[3], balloc_test_group_start(0) + 44);
+ ext4_inode_table_set(sb, &ctx->gdps[3], balloc_test_group_start(0) + 48);
+ /* Group 3 has 2 base blocks = 1 base cluster -> 76 - 1 = 75 free */
+ KUNIT_EXPECT_EQ(test,
+ ext4_free_clusters_after_init(sb, 3, &ctx->gdps[3]),
+ 75U);
+}
+
+/* =========================================================================
+ * 5. Group Descriptor & Group Info Lookup
+ * =========================================================================
+ */
+
+static void test_get_group_desc_lookup_and_bounds(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct buffer_head *bh = NULL;
+ struct ext4_group_desc *desc;
+
+ /* Expand to 20 groups spanning gdesc_bh[0] (0..15) and gdesc_bh[1] (16..19) */
+ ctx->sbi.s_groups_count = 20;
+
+ desc = ext4_get_group_desc(sb, 0, &bh);
+ KUNIT_EXPECT_PTR_EQ(test, desc, &ctx->gdps[0]);
+ KUNIT_EXPECT_PTR_EQ(test, bh, &ctx->gdesc_bh[0]);
+
+ desc = ext4_get_group_desc(sb, 15, &bh);
+ KUNIT_EXPECT_PTR_EQ(test, desc, &ctx->gdps[15]);
+ KUNIT_EXPECT_PTR_EQ(test, bh, &ctx->gdesc_bh[0]);
+
+ desc = ext4_get_group_desc(sb, 16, &bh);
+ KUNIT_EXPECT_PTR_EQ(test, desc, &ctx->gdps[16]);
+ KUNIT_EXPECT_PTR_EQ(test, bh, &ctx->gdesc_bh[1]);
+
+ desc = ext4_get_group_desc(sb, 19, NULL);
+ KUNIT_EXPECT_PTR_EQ(test, desc, &ctx->gdps[19]);
+
+ /* Out-of-range block_group >= s_groups_count returns NULL */
+ ctx->sbi.s_mount_state &= ~EXT4_ERROR_FS;
+ KUNIT_EXPECT_NULL(test, ext4_get_group_desc(sb, 20, &bh));
+ KUNIT_EXPECT_TRUE(test, !!(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+
+ /* Missing group descriptor buffer_head slot returns NULL */
+ ctx->gdesc_bh_ptrs[1] = NULL;
+ ctx->sbi.s_mount_state &= ~EXT4_ERROR_FS;
+ KUNIT_EXPECT_NULL(test, ext4_get_group_desc(sb, 16, &bh));
+ KUNIT_EXPECT_TRUE(test, !!(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+}
+
+static void test_get_group_info_lookup_and_null_guards(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+
+ ctx->sbi.s_groups_count = 20;
+
+ KUNIT_EXPECT_PTR_EQ(test, ext4_get_group_info(sb, 0),
+ &ctx->grp_infos[0]);
+ KUNIT_EXPECT_PTR_EQ(test, ext4_get_group_info(sb, 15),
+ &ctx->grp_infos[15]);
+ KUNIT_EXPECT_PTR_EQ(test, ext4_get_group_info(sb, 16),
+ &ctx->grp_infos[16]);
+ KUNIT_EXPECT_PTR_EQ(test, ext4_get_group_info(sb, 19),
+ &ctx->grp_infos[19]);
+
+ /* Out-of-bounds group returns NULL */
+ KUNIT_EXPECT_NULL(test, ext4_get_group_info(sb, 20));
+
+ /* NULL second-level slice returns NULL only for that slice */
+ ctx->grp_info_slices[1] = NULL;
+ KUNIT_EXPECT_PTR_EQ(test, ext4_get_group_info(sb, 15),
+ &ctx->grp_infos[15]);
+ KUNIT_EXPECT_NULL(test, ext4_get_group_info(sb, 16));
+
+ /* NULL s_group_info table returns NULL */
+ rcu_assign_pointer(ctx->sbi.s_group_info, NULL);
+ KUNIT_EXPECT_NULL(test, ext4_get_group_info(sb, 0));
+}
+
+/* =========================================================================
+ * 6. Block Bitmap Validation (CSUM, Metadata Self-Checks, Padding, Flags)
+ * =========================================================================
+ */
+
+static void test_read_block_bitmap_validates_csum(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct balloc_test_blk *slot;
+ struct buffer_head *bh;
+ s64 free_before;
+
+ slot = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ /* 1. Unverified buffer with valid CRC32C verifies and sets BH_Verified */
+ clear_buffer_verified(&slot->bh);
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_TRUE(test, buffer_verified(bh));
+ KUNIT_EXPECT_FALSE(test, EXT4_MB_GRP_BBITMAP_CORRUPT(&ctx->grp_infos[1]));
+ put_bh(bh);
+
+#ifdef CONFIG_EXT4_DEBUG
+ /* Simulated CRC failure triggers -EFSBADCRC and resets s_simulate_fail */
+ clear_buffer_verified(&slot->bh);
+ ctx->sbi.s_simulate_fail = EXT4_SIM_BBITMAP_CRC;
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSBADCRC);
+ KUNIT_EXPECT_EQ(test, ctx->sbi.s_simulate_fail, 0UL);
+ ctx->grp_infos[1].bb_state = 0;
+ percpu_counter_add(&ctx->sbi.s_freeclusters_counter,
+ ctx->grp_infos[1].bb_free);
+#endif
+
+ /* 2. Corrupting free-space bit without updating csum -> -EFSBADCRC */
+ free_before = percpu_counter_sum(&ctx->sbi.s_freeclusters_counter);
+ clear_buffer_verified(&slot->bh);
+ slot->cache_buf[40] ^= 0x01;
+
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSBADCRC);
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_BBITMAP_CORRUPT(&ctx->grp_infos[1]));
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum(&ctx->sbi.s_freeclusters_counter),
+ free_before - ctx->grp_infos[1].bb_free);
+
+ /*
+ * 3. Subsequent read on already-corrupt group returns -EFSCORRUPTED
+ * immediately without subtracting bb_free a second time.
+ */
+ slot->cache_buf[40] ^= 0x01;
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, PTR_ERR(bh), -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum(&ctx->sbi.s_freeclusters_counter),
+ free_before - ctx->grp_infos[1].bb_free);
+}
+
+static void test_read_block_bitmap_validates_metadata_bits(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct balloc_test_blk *slot;
+ struct buffer_head *bh;
+ static const unsigned int meta_bits[] = {
+ 0, /* block bitmap in group 2 */
+ 1, /* inode bitmap in group 2 */
+ 2, /* first inode table block in group 2 */
+ 10, /* middle inode table block in group 2 */
+ 17, /* last inode table block in group 2 */
+ };
+ unsigned int i;
+
+ slot = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(2));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ /* Clearing any in-group metadata bit (with valid CRC) -> -EFSCORRUPTED */
+ for (i = 0; i < ARRAY_SIZE(meta_bits); i++) {
+ ctx->grp_infos[2].bb_state = 0;
+ clear_buffer_verified(&slot->bh);
+ ext4_clear_bit(meta_bits[i], slot->cache_buf);
+ balloc_test_sync_bbitmap_csum(ctx, 2, false);
+
+ bh = ext4_read_block_bitmap(sb, 2);
+ KUNIT_EXPECT_EQ_MSG(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED,
+ "cleared bit %u should fail validation",
+ meta_bits[i]);
+ KUNIT_EXPECT_TRUE_MSG(test,
+ EXT4_MB_GRP_BBITMAP_CORRUPT(&ctx->grp_infos[2]),
+ "bit %u should mark BBITMAP_CORRUPT",
+ meta_bits[i]);
+
+ ext4_set_bit(meta_bits[i], slot->cache_buf);
+ }
+
+ /* Out-of-group block bitmap (< start and >= end) without FLEX_BG */
+ {
+ struct balloc_test_blk *slot1, *slot3;
+
+ slot1 = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(1));
+ slot3 = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(3));
+ KUNIT_ASSERT_NOT_NULL(test, slot1);
+ KUNIT_ASSERT_NOT_NULL(test, slot3);
+
+ ctx->grp_infos[2].bb_state = 0;
+ clear_buffer_verified(&slot1->bh);
+ ext4_block_bitmap_set(sb, &ctx->gdps[2],
+ balloc_test_bbitmap_blk(1));
+ balloc_test_sync_bbitmap_csum(ctx, 2, false);
+ bh = ext4_read_block_bitmap(sb, 2);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+
+ ctx->grp_infos[2].bb_state = 0;
+ clear_buffer_verified(&slot3->bh);
+ ext4_block_bitmap_set(sb, &ctx->gdps[2],
+ balloc_test_bbitmap_blk(3));
+ balloc_test_sync_bbitmap_csum(ctx, 2, false);
+ bh = ext4_read_block_bitmap(sb, 2);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+
+ ext4_block_bitmap_set(sb, &ctx->gdps[2],
+ balloc_test_bbitmap_blk(2));
+ balloc_test_sync_bbitmap_csum(ctx, 1, false);
+ balloc_test_sync_bbitmap_csum(ctx, 3, false);
+ }
+
+ /* Out-of-group inode bitmap without FLEX_BG -> -EFSCORRUPTED */
+ ctx->grp_infos[2].bb_state = 0;
+ clear_buffer_verified(&slot->bh);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[2], balloc_test_group_start(0) + 5);
+ balloc_test_sync_bbitmap_csum(ctx, 2, false);
+ bh = ext4_read_block_bitmap(sb, 2);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[2], balloc_test_ibitmap_blk(2));
+
+ /* Inode table before start or past end of group without FLEX_BG */
+ ctx->grp_infos[2].bb_state = 0;
+ clear_buffer_verified(&slot->bh);
+ ext4_inode_table_set(sb, &ctx->gdps[2], balloc_test_group_start(1) + 50);
+ balloc_test_sync_bbitmap_csum(ctx, 2, false);
+ bh = ext4_read_block_bitmap(sb, 2);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+
+ ctx->grp_infos[2].bb_state = 0;
+ clear_buffer_verified(&slot->bh);
+ ext4_inode_table_set(sb, &ctx->gdps[2],
+ balloc_test_group_start(2) +
+ BALLOC_TEST_BLOCKS_PER_GRP - 8);
+ balloc_test_sync_bbitmap_csum(ctx, 2, false);
+ bh = ext4_read_block_bitmap(sb, 2);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+
+ /* With FLEX_BG enabled, out-of-group metadata skips per-group bit check */
+ ext4_set_feature_flex_bg(sb);
+ ctx->grp_infos[2].bb_state = 0;
+ clear_buffer_verified(&slot->bh);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[2], balloc_test_group_start(0) + 5);
+ ext4_inode_table_set(sb, &ctx->gdps[2], balloc_test_group_start(0) + 20);
+ ext4_clear_bit(1, slot->cache_buf);
+ ext4_clear_bit(2, slot->cache_buf);
+ balloc_test_sync_bbitmap_csum(ctx, 2, false);
+
+ bh = ext4_read_block_bitmap(sb, 2);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_TRUE(test, buffer_verified(bh));
+ put_bh(bh);
+}
+
+static void test_read_block_bitmap_validates_padding(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct balloc_test_blk *slot;
+ struct buffer_head *bh;
+
+ slot = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(3));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ /* 1. Zero bit at first padding bit (1024) -> -EFSCORRUPTED */
+ clear_buffer_verified(&slot->bh);
+ ext4_clear_bit(1024, slot->cache_buf);
+ balloc_test_sync_bbitmap_csum(ctx, 3, false);
+ bh = ext4_read_block_bitmap(sb, 3);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_BBITMAP_CORRUPT(&ctx->grp_infos[3]));
+ ext4_set_bit(1024, slot->cache_buf);
+
+ /* 2. Zero bit at very last bit of bitmap block (8191) -> -EFSCORRUPTED */
+ ctx->grp_infos[3].bb_state = 0;
+ clear_buffer_verified(&slot->bh);
+ ext4_clear_bit(8191, slot->cache_buf);
+ balloc_test_sync_bbitmap_csum(ctx, 3, false);
+ bh = ext4_read_block_bitmap(sb, 3);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+ ext4_set_bit(8191, slot->cache_buf);
+
+ /* 3. Partial last group (300 blocks): unset bit 300 in padding fails */
+ ctx->grp_infos[3].bb_state = 0;
+ clear_buffer_verified(&slot->bh);
+ ext4_blocks_count_set(&ctx->es, balloc_test_group_start(3) + 300);
+ balloc_test_sync_bbitmap_csum(ctx, 3, false);
+ bh = ext4_read_block_bitmap(sb, 3);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+
+ /* Setting padding bits 300..1023 makes the partial last group valid */
+ ctx->grp_infos[3].bb_state = 0;
+ clear_buffer_verified(&slot->bh);
+ ext4_mark_bitmap_end(300, BALLOC_TEST_BLK_SIZE * 8, slot->cache_buf);
+ balloc_test_sync_bbitmap_csum(ctx, 3, false);
+ bh = ext4_read_block_bitmap(sb, 3);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_TRUE(test, buffer_verified(bh));
+ put_bh(bh);
+
+ /* 4. Full 8192-cluster group (bitmap_size == offset) has no padding */
+ ctx->sbi.s_blocks_per_group = 8192;
+ ctx->sbi.s_clusters_per_group = 8192;
+ ctx->es.s_blocks_per_group = cpu_to_le32(8192);
+ ctx->es.s_clusters_per_group = cpu_to_le32(8192);
+ ext4_blocks_count_set(&ctx->es, 1 + 4 * 8192);
+
+ slot = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(0));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+ clear_buffer_verified(&slot->bh);
+ ext4_clear_bit(8191, slot->cache_buf);
+ balloc_test_sync_bbitmap_csum(ctx, 0, false);
+
+ bh = ext4_read_block_bitmap(sb, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_TRUE(test, buffer_verified(bh));
+ put_bh(bh);
+}
+
+static void test_read_block_bitmap_validation_short_circuits(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct balloc_test_blk *slot;
+ struct buffer_head *bh;
+
+ slot = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ /*
+ * 1. EXT4_FC_REPLAY skips csum and corrupt-flag checks without setting
+ * BH_Verified.
+ */
+ clear_buffer_verified(&slot->bh);
+ slot->cache_buf[50] ^= 0xff;
+ set_bit(EXT4_GROUP_INFO_BBITMAP_CORRUPT_BIT,
+ &ctx->grp_infos[1].bb_state);
+ ctx->sbi.s_mount_state |= EXT4_FC_REPLAY;
+
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_FALSE(test, buffer_verified(bh));
+ put_bh(bh);
+
+ ctx->sbi.s_mount_state &= ~EXT4_FC_REPLAY;
+ slot->cache_buf[50] ^= 0xff;
+
+ /* 2. Already buffer_verified(bh) short-circuits before checking grp */
+ set_buffer_verified(&slot->bh);
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ put_bh(bh);
+
+ /* 3. NULL ext4_group_info when !buffer_verified returns -EFSCORRUPTED */
+ clear_buffer_verified(&slot->bh);
+ ctx->grp_infos[1].bb_state = 0;
+ rcu_assign_pointer(ctx->sbi.s_group_info, NULL);
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+}
+
+/* =========================================================================
+ * 7. Block Bitmap Reading, Prefetching, and Lazy Initialization
+ * =========================================================================
+ */
+
+static void test_read_block_bitmap_disk_and_cache_paths(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct balloc_test_blk *slot;
+ struct buffer_head *bh;
+
+ slot = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ /* 1. Cold read from disk populates cache, runs end_io, and validates */
+ memset(slot->cache_buf, 0, sizeof(slot->cache_buf));
+ clear_buffer_uptodate(&slot->bh);
+ clear_bitmap_uptodate(&slot->bh);
+ clear_buffer_verified(&slot->bh);
+
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->last_read_op_flags,
+ (blk_opf_t)(REQ_META | REQ_PRIO));
+ KUNIT_EXPECT_TRUE(test, buffer_uptodate(bh));
+ KUNIT_EXPECT_TRUE(test, (bool)bitmap_uptodate(bh));
+ KUNIT_EXPECT_TRUE(test, buffer_verified(bh));
+ KUNIT_EXPECT_FALSE(test, buffer_new(bh));
+ put_bh(bh);
+
+ /* 2. Warm read hits bitmap_uptodate(bh) without issuing I/O */
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 1U);
+ put_bh(bh);
+
+ /*
+ * 3. Initialized group with BH_Uptodate set and BH_BITMAP_UPTODATE
+ * clear promotes BH_BITMAP_UPTODATE and validates without disk I/O.
+ */
+ clear_bitmap_uptodate(&slot->bh);
+ clear_buffer_verified(&slot->bh);
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 1U);
+ KUNIT_EXPECT_TRUE(test, (bool)bitmap_uptodate(bh));
+ KUNIT_EXPECT_TRUE(test, buffer_verified(bh));
+ put_bh(bh);
+}
+
+static void test_read_block_bitmap_nowait_prefetch_locked(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct balloc_test_blk *slot;
+ struct buffer_head *bh;
+ int ref_before;
+
+ slot = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(2));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ /* 1. Locked buffer with ignore_locked=true returns NULL without leaking ref */
+ ref_before = atomic_read(&slot->bh.b_count);
+ lock_buffer(&slot->bh);
+ bh = ext4_read_block_bitmap_nowait(sb, 2, true);
+ unlock_buffer(&slot->bh);
+
+ KUNIT_EXPECT_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, atomic_read(&slot->bh.b_count), ref_before);
+ KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 0U);
+
+ /* 2. Uncached unlocked buffer with ignore_locked=true sets REQ_RAHEAD */
+ clear_buffer_uptodate(&slot->bh);
+ clear_bitmap_uptodate(&slot->bh);
+ clear_buffer_verified(&slot->bh);
+
+ bh = ext4_read_block_bitmap_nowait(sb, 2, true);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->last_read_op_flags,
+ (blk_opf_t)(REQ_META | REQ_PRIO | REQ_RAHEAD));
+ KUNIT_EXPECT_EQ(test, ext4_wait_block_bitmap(sb, 2, bh), 0);
+ put_bh(bh);
+}
+
+static void test_read_block_bitmap_uninit_group(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct balloc_test_blk *slot;
+ struct buffer_head *bh;
+ unsigned int bit;
+
+ slot = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ /*
+ * Mark group 1 EXT4_BG_BLOCK_UNINIT and poison cache_buf with 0xa5.
+ * Lazy init must zero the bitmap, set metadata bits 0..19, leave
+ * data bits 20..1023 free, set padding bits 1024..8191, and skip I/O.
+ */
+ memset(slot->cache_buf, 0xa5, sizeof(slot->cache_buf));
+ clear_buffer_uptodate(&slot->bh);
+ clear_bitmap_uptodate(&slot->bh);
+ clear_buffer_verified(&slot->bh);
+ ctx->gdps[1].bg_flags = cpu_to_le16(EXT4_BG_BLOCK_UNINIT);
+ ext4_group_desc_csum_set(sb, 1, &ctx->gdps[1]);
+
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 0U);
+ KUNIT_EXPECT_TRUE(test, buffer_uptodate(bh));
+ KUNIT_EXPECT_TRUE(test, (bool)bitmap_uptodate(bh));
+ KUNIT_EXPECT_TRUE(test, buffer_verified(bh));
+
+ for (bit = 0; bit < 20; bit++)
+ KUNIT_EXPECT_TRUE_MSG(test,
+ (bool)ext4_test_bit(bit, bh->b_data),
+ "metadata bit %u must be set", bit);
+ KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(20, bh->b_data));
+ KUNIT_EXPECT_FALSE(test, (bool)ext4_test_bit(1023, bh->b_data));
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(1024, bh->b_data));
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(8191, bh->b_data));
+
+ KUNIT_EXPECT_EQ(test, ext4_count_free(bh->b_data, 1024 / 8),
+ ext4_free_clusters_after_init(sb, 1, &ctx->gdps[1]));
+ put_bh(bh);
+
+ /*
+ * Legacy GDT_CSUM (CRC16 without metadata_csum) with FLEX_BG external
+ * metadata on group 2 (0 base blocks, bitmaps & itable in group 3)
+ * initializes all 1024 data bits in group 2 to free without I/O.
+ */
+ {
+ struct balloc_test_blk *ext_slot;
+ ext4_fsblk_t ext_bbitmap = balloc_test_group_start(3) + 50;
+
+ ext4_clear_feature_metadata_csum(sb);
+ ext4_set_feature_gdt_csum(sb);
+ ext4_set_feature_flex_bg(sb);
+
+ ext_slot = balloc_test_setup_blk(ctx, ext_bbitmap, false);
+ KUNIT_ASSERT_NOT_NULL(test, ext_slot);
+ memset(ext_slot->cache_buf, 0x5a, sizeof(ext_slot->cache_buf));
+
+ ext4_block_bitmap_set(sb, &ctx->gdps[2], ext_bbitmap);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[2], ext_bbitmap + 1);
+ ext4_inode_table_set(sb, &ctx->gdps[2], ext_bbitmap + 2);
+ ctx->gdps[2].bg_flags = cpu_to_le16(EXT4_BG_BLOCK_UNINIT);
+ ext4_group_desc_csum_set(sb, 2, &ctx->gdps[2]);
+
+ bh = ext4_read_block_bitmap(sb, 2);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 0U);
+ KUNIT_EXPECT_EQ(test, ext4_count_free(bh->b_data, 1024 / 8),
+ 1024U);
+ KUNIT_EXPECT_TRUE(test, (bool)ext4_test_bit(1024, bh->b_data));
+ put_bh(bh);
+
+ ext4_clear_feature_flex_bg(sb);
+ }
+
+ /*
+ * Without GDT checksums (neither metadata_csum nor gdt_csum),
+ * EXT4_BG_BLOCK_UNINIT is ignored and bitmap is read from disk.
+ */
+ ext4_clear_feature_metadata_csum(sb);
+ ext4_clear_feature_gdt_csum(sb);
+ clear_buffer_uptodate(&slot->bh);
+ clear_bitmap_uptodate(&slot->bh);
+ clear_buffer_verified(&slot->bh);
+
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(bh));
+ KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 1U);
+ put_bh(bh);
+}
+
+static void test_read_block_bitmap_uninit_errors(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct balloc_test_blk *slot0, *slot1;
+ struct buffer_head *bh;
+ s64 free_clu_before, free_ino_before;
+
+ slot0 = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(0));
+ slot1 = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(1));
+ KUNIT_ASSERT_NOT_NULL(test, slot0);
+ KUNIT_ASSERT_NOT_NULL(test, slot1);
+
+ /* 1. Group 0 marked EXT4_BG_BLOCK_UNINIT is rejected -> -EFSCORRUPTED */
+ clear_buffer_uptodate(&slot0->bh);
+ clear_bitmap_uptodate(&slot0->bh);
+ ctx->gdps[0].bg_flags = cpu_to_le16(EXT4_BG_BLOCK_UNINIT);
+ ext4_group_desc_csum_set(sb, 0, &ctx->gdps[0]);
+
+ bh = ext4_read_block_bitmap(sb, 0);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+
+ /*
+ * 2. Group 1 marked EXT4_BG_BLOCK_UNINIT with bad GDT checksum fails
+ * with -EFSBADCRC and marks BOTH block and inode bitmaps corrupt.
+ */
+ free_clu_before = percpu_counter_sum(&ctx->sbi.s_freeclusters_counter);
+ free_ino_before = percpu_counter_sum(&ctx->sbi.s_freeinodes_counter);
+ clear_buffer_uptodate(&slot1->bh);
+ clear_bitmap_uptodate(&slot1->bh);
+ ctx->gdps[1].bg_flags = cpu_to_le16(EXT4_BG_BLOCK_UNINIT);
+ ctx->gdps[1].bg_checksum = cpu_to_le16(0xdead);
+
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSBADCRC);
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_BBITMAP_CORRUPT(&ctx->grp_infos[1]));
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_IBITMAP_CORRUPT(&ctx->grp_infos[1]));
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum(&ctx->sbi.s_freeclusters_counter),
+ free_clu_before - ctx->grp_infos[1].bb_free);
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum(&ctx->sbi.s_freeinodes_counter),
+ free_ino_before - BALLOC_TEST_INODES_PER_GRP);
+
+ /*
+ * 3. Base metadata clusters exceeding bitmap block size ((bit_max >> 3)
+ * >= bh->b_size) returns -EFSCORRUPTED instead of overflowing bh.
+ */
+ ctx->grp_infos[1].bb_state = 0;
+ clear_buffer_uptodate(&slot1->bh);
+ clear_bitmap_uptodate(&slot1->bh);
+ ctx->sbi.s_gdb_count = BALLOC_TEST_BLK_SIZE * 8;
+ ext4_group_desc_csum_set(sb, 1, &ctx->gdps[1]);
+
+ bh = ext4_read_block_bitmap(sb, 1);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+}
+
+static void test_read_and_wait_block_bitmap_errors(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct balloc_test_blk *slot;
+ struct buffer_head *bh;
+ int ref_before;
+
+ /* 1. Out-of-range block group -> -EFSCORRUPTED */
+ bh = ext4_read_block_bitmap(sb, 99);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+
+ /* 2. Block bitmap <= s_first_data_block (1) -> -EFSCORRUPTED + corrupt */
+ ext4_block_bitmap_set(sb, &ctx->gdps[2], BALLOC_TEST_FIRST_DATA_BLK);
+ bh = ext4_read_block_bitmap(sb, 2);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_BBITMAP_CORRUPT(&ctx->grp_infos[2]));
+
+ /* 3. Block bitmap >= ext4_blocks_count(es) -> -EFSCORRUPTED + corrupt */
+ ctx->grp_infos[2].bb_state = 0;
+ ext4_block_bitmap_set(sb, &ctx->gdps[2], BALLOC_TEST_TOTAL_BLKS);
+ bh = ext4_read_block_bitmap(sb, 2);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_BBITMAP_CORRUPT(&ctx->grp_infos[2]));
+ ext4_block_bitmap_set(sb, &ctx->gdps[2], balloc_test_bbitmap_blk(2));
+
+ /* 4. ext4_sb_getblk allocation failure -> -ENOMEM without corrupt flag */
+ ctx->grp_infos[2].bb_state = 0;
+ ctx->fail_getblk = true;
+ bh = ext4_read_block_bitmap(sb, 2);
+ ctx->fail_getblk = false;
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -ENOMEM);
+ KUNIT_EXPECT_FALSE(test, EXT4_MB_GRP_BBITMAP_CORRUPT(&ctx->grp_infos[2]));
+
+ /* 5. Async read I/O failure -> -EIO, marks corrupt, releases bh ref */
+ slot = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(2));
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+ ref_before = atomic_read(&slot->bh.b_count);
+ clear_buffer_uptodate(&slot->bh);
+ clear_bitmap_uptodate(&slot->bh);
+ clear_buffer_verified(&slot->bh);
+ ctx->fail_read_eio = true;
+
+ bh = ext4_read_block_bitmap(sb, 2);
+ ctx->fail_read_eio = false;
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EIO);
+ KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_BBITMAP_CORRUPT(&ctx->grp_infos[2]));
+ KUNIT_EXPECT_EQ(test, atomic_read(&slot->bh.b_count), ref_before);
+
+#ifdef CONFIG_EXT4_DEBUG
+ ctx->grp_infos[2].bb_state = 0;
+ clear_buffer_uptodate(&slot->bh);
+ clear_bitmap_uptodate(&slot->bh);
+ ctx->sbi.s_simulate_fail = EXT4_SIM_BBITMAP_EIO;
+ bh = ext4_read_block_bitmap(sb, 2);
+ KUNIT_EXPECT_EQ(test, PTR_ERR_OR_ZERO(bh), -EIO);
+ KUNIT_EXPECT_EQ(test, ctx->sbi.s_simulate_fail, 0UL);
+#endif
+
+ /* 6. ext4_wait_block_bitmap guards on BH_New and group descriptor */
+ clear_buffer_new(&slot->bh);
+ KUNIT_EXPECT_EQ(test, ext4_wait_block_bitmap(sb, 99, &slot->bh), 0);
+ set_buffer_new(&slot->bh);
+ KUNIT_EXPECT_EQ(test, ext4_wait_block_bitmap(sb, 99, &slot->bh),
+ -EFSCORRUPTED);
+ clear_buffer_new(&slot->bh);
+}
+
+/* =========================================================================
+ * 8. Free Cluster Reservation & Pool Accounting (ext4_claim_free_clusters)
+ * =========================================================================
+ */
+
+static void balloc_test_set_cluster_pools(struct balloc_test_ctx *ctx,
+ s64 free_clu, s64 dirty_clu,
+ u64 r_blocks, s64 resv_clu)
+{
+ percpu_counter_set(&ctx->sbi.s_freeclusters_counter, free_clu);
+ percpu_counter_set(&ctx->sbi.s_dirtyclusters_counter, dirty_clu);
+ ctx->es.s_r_blocks_count_lo = cpu_to_le32(lower_32_bits(r_blocks));
+ ctx->es.s_r_blocks_count_hi = cpu_to_le32(upper_32_bits(r_blocks));
+ atomic64_set(&ctx->sbi.s_resv_clusters, resv_clu);
+}
+
+static void
+test_claim_free_clusters_unprivileged_and_bigalloc(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+
+ sbi->s_resuid = KUIDT_INIT(500);
+ sbi->s_resgid = GLOBAL_ROOT_GID;
+ balloc_test_override_creds(test, ctx, 1000, 1000, false, -1);
+
+ /* Above-watermark fast path */
+ balloc_test_set_cluster_pools(ctx, 100000, 10, 30, 15);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 25, 0), 0);
+ KUNIT_EXPECT_EQ(test, percpu_counter_sum(&sbi->s_dirtyclusters_counter),
+ 35LL);
+
+ /*
+ * Exact unprivileged limit:
+ * free=100, dirty=20, r_blocks=30, resv=15 -> usable = 100 - 20 - 45 = 35
+ */
+ balloc_test_set_cluster_pools(ctx, 100, 20, 30, 15);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 36, 0), -ENOSPC);
+ KUNIT_EXPECT_EQ(test, percpu_counter_sum(&sbi->s_dirtyclusters_counter),
+ 20LL);
+
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 35, 0), 0);
+ KUNIT_EXPECT_EQ(test, percpu_counter_sum(&sbi->s_dirtyclusters_counter),
+ 55LL);
+
+ /*
+ * Bigalloc (cluster_bits = 2 -> 4 blocks/cluster):
+ * r_blocks_count = 120 blocks -> 30 reserved clusters -> usable = 35.
+ */
+ sbi->s_cluster_bits = 2;
+ sbi->s_cluster_ratio = 4;
+ balloc_test_set_cluster_pools(ctx, 100, 20, 120, 15);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 36, 0), -ENOSPC);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 35, 0), 0);
+ KUNIT_EXPECT_EQ(test, percpu_counter_sum(&sbi->s_dirtyclusters_counter),
+ 55LL);
+
+ balloc_test_restore_creds(ctx);
+}
+
+static void test_claim_free_clusters_root_and_reserved_pools(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+
+ /*
+ * Pool layout: free=100, dirty=20, r_blocks=30, resv=15:
+ * - Unprivileged limit: 35 clusters (100 - 20 - 30 - 15)
+ * - Root-pool limit: 65 clusters (100 - 20 - 15; resv protected)
+ * - Internal resv limit: 80 clusters (100 - 20; EXT4_MB_USE_RESERVED)
+ */
+ sbi->s_resuid = KUIDT_INIT(500);
+ sbi->s_resgid = GLOBAL_ROOT_GID;
+
+ /* 1. Matching s_resuid grants root pool (65), still protects resv (66) */
+ balloc_test_override_creds(test, ctx, 500, 1000, false, -1);
+ balloc_test_set_cluster_pools(ctx, 100, 20, 30, 15);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 66, 0), -ENOSPC);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 65, 0), 0);
+
+ /*
+ * 2. s_resgid == GLOBAL_ROOT_GID (0) does NOT grant root blocks via
+ * group membership even when caller's fsgid is 0.
+ */
+ balloc_test_override_creds(test, ctx, 1000, 0, false, -1);
+ balloc_test_set_cluster_pools(ctx, 100, 20, 30, 15);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 36, 0), -ENOSPC);
+
+#ifdef CONFIG_MULTIUSER
+ /* 3. Non-root s_resgid grants root pool only when caller is in group */
+ sbi->s_resgid = KGIDT_INIT(2000);
+ balloc_test_override_creds(test, ctx, 1000, 1000, false, -1);
+ balloc_test_set_cluster_pools(ctx, 100, 20, 30, 15);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 36, 0), -ENOSPC);
+
+ balloc_test_override_creds(test, ctx, 1000, 1000, false, 2000);
+ balloc_test_set_cluster_pools(ctx, 100, 20, 30, 15);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 66, 0), -ENOSPC);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 65, 0), 0);
+ sbi->s_resgid = GLOBAL_ROOT_GID;
+#endif
+
+ /* 4. EXT4_MB_USE_ROOT_BLOCKS grants root pool to unprivileged caller */
+ balloc_test_override_creds(test, ctx, 1000, 1000, false, -1);
+ balloc_test_set_cluster_pools(ctx, 100, 20, 30, 15);
+ KUNIT_EXPECT_EQ(test,
+ ext4_claim_free_clusters(sbi, 66,
+ EXT4_MB_USE_ROOT_BLOCKS),
+ -ENOSPC);
+ KUNIT_EXPECT_EQ(test,
+ ext4_claim_free_clusters(sbi, 65,
+ EXT4_MB_USE_ROOT_BLOCKS),
+ 0);
+
+ /* 5. CAP_SYS_RESOURCE grants root pool regardless of uid/gid/flags */
+ balloc_test_override_creds(test, ctx, 1000, 1000, true, -1);
+ balloc_test_set_cluster_pools(ctx, 100, 20, 30, 15);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 66, 0), -ENOSPC);
+ KUNIT_EXPECT_EQ(test, ext4_claim_free_clusters(sbi, 65, 0), 0);
+
+ /* 6. EXT4_MB_USE_RESERVED dips into s_resv_clusters up to 80 clusters */
+ balloc_test_override_creds(test, ctx, 1000, 1000, false, -1);
+ balloc_test_set_cluster_pools(ctx, 100, 20, 30, 15);
+ KUNIT_EXPECT_EQ(test,
+ ext4_claim_free_clusters(sbi, 81, EXT4_MB_USE_RESERVED),
+ -ENOSPC);
+ KUNIT_EXPECT_EQ(test,
+ ext4_claim_free_clusters(sbi, 80, EXT4_MB_USE_RESERVED),
+ 0);
+ KUNIT_EXPECT_EQ(test, percpu_counter_sum(&sbi->s_dirtyclusters_counter),
+ 100LL);
+
+ balloc_test_restore_creds(ctx);
+}
+
+/* =========================================================================
+ * 9. Allocation Retry Policy (ext4_should_retry_alloc)
+ * =========================================================================
+ */
+
+static void
+test_should_retry_alloc_no_journal_and_retry_limit(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ int retries = 1;
+
+ /* Without a journal, returns 0 without incrementing *retries */
+ sbi->s_journal = NULL;
+ KUNIT_EXPECT_EQ(test, ext4_should_retry_alloc(sb, &retries), 0);
+ KUNIT_EXPECT_EQ(test, retries, 1);
+
+ /* With journal and pending mb frees, retries 1..3 succeed, 4th stops */
+ sbi->s_journal = &ctx->journal;
+ atomic_set(&sbi->s_mb_free_pending, 4);
+ retries = 0;
+
+ KUNIT_EXPECT_EQ(test, ext4_should_retry_alloc(sb, &retries), 1);
+ KUNIT_EXPECT_EQ(test, retries, 1);
+ KUNIT_EXPECT_EQ(test, ext4_should_retry_alloc(sb, &retries), 1);
+ KUNIT_EXPECT_EQ(test, retries, 2);
+ KUNIT_EXPECT_EQ(test, ext4_should_retry_alloc(sb, &retries), 1);
+ KUNIT_EXPECT_EQ(test, retries, 3);
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum(&sbi->s_sra_exceeded_retry_limit),
+ 0LL);
+
+ KUNIT_EXPECT_EQ(test, ext4_should_retry_alloc(sb, &retries), 0);
+ KUNIT_EXPECT_EQ(test, retries, 4);
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum(&sbi->s_sra_exceeded_retry_limit),
+ 1LL);
+}
+
+static void
+test_should_retry_alloc_free_space_and_discard_flush(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ int retries;
+
+ sbi->s_journal = &ctx->journal;
+ atomic_set(&sbi->s_mb_free_pending, 0);
+ clear_opt(sb, DISCARD);
+ balloc_test_override_creds(test, ctx, 1000, 1000, false, -1);
+
+ /* 1. No pending frees, no discard, 0 free clusters -> returns 0 */
+ balloc_test_set_cluster_pools(ctx, 10, 10, 0, 0);
+ retries = 0;
+ KUNIT_EXPECT_EQ(test, ext4_should_retry_alloc(sb, &retries), 0);
+ KUNIT_EXPECT_EQ(test, retries, 1);
+ KUNIT_EXPECT_EQ(test, ctx->discard_work_runs, 0U);
+
+ /* 2. Free cluster available -> returns 1 */
+ balloc_test_set_cluster_pools(ctx, 11, 10, 0, 0);
+ retries = 0;
+ KUNIT_EXPECT_EQ(test, ext4_should_retry_alloc(sb, &retries), 1);
+
+ /*
+ * 3. With DISCARD enabled, flushes s_discard_work while holding
+ * s_retry_alloc_pending == 1 and rechecks free clusters afterward.
+ */
+ set_opt(sb, DISCARD);
+ balloc_test_set_cluster_pools(ctx, 10, 10, 0, 0);
+ ctx->discard_release_clusters = 8;
+ schedule_work(&sbi->s_discard_work);
+
+ retries = 0;
+ KUNIT_EXPECT_EQ(test, ext4_should_retry_alloc(sb, &retries), 1);
+ KUNIT_EXPECT_EQ(test, ctx->discard_work_runs, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->discard_saw_retry_pending, 1);
+ KUNIT_EXPECT_EQ(test, atomic_read(&sbi->s_retry_alloc_pending), 0);
+
+ balloc_test_restore_creds(ctx);
+}
+
+/* =========================================================================
+ * 10. Metadata Block Allocation (ext4_new_meta_blocks)
+ * =========================================================================
+ */
+
+static void test_new_meta_blocks_alloc_and_delalloc_quota(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct inode *inode = ctx->test_inode;
+ unsigned long count;
+ ext4_fsblk_t blk;
+ int err = -EINVAL;
+
+ /* 1. count == NULL defaults ar.len to 1; non-delalloc skips quota */
+ ctx->mock_alloc_blk = 1234ULL;
+ ctx->mock_alloc_len = 0;
+ ctx->mock_alloc_err = 0;
+ inode_set_bytes(inode, 0);
+
+ blk = ext4_new_meta_blocks(NULL, inode, 500ULL, EXT4_MB_USE_ROOT_BLOCKS,
+ NULL, &err);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, blk, 1234ULL);
+ KUNIT_EXPECT_PTR_EQ(test, ctx->last_ar.inode, inode);
+ KUNIT_EXPECT_EQ(test, ctx->last_ar.goal, 500ULL);
+ KUNIT_EXPECT_EQ(test, ctx->last_ar.len, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->last_ar.flags,
+ (unsigned int)EXT4_MB_USE_ROOT_BLOCKS);
+ KUNIT_EXPECT_EQ(test, inode_get_bytes(inode), 0LL);
+
+ /*
+ * 2. Partial allocation (requested 8 clusters, granted 3) with
+ * EXT4_MB_DELALLOC_RESERVED and bigalloc (4 blocks/cluster = 4096B
+ * per cluster) updates *count to 3 and charges 12288 quota bytes.
+ */
+ ctx->sbi.s_cluster_bits = 2;
+ ctx->sbi.s_cluster_ratio = 4;
+ ctx->mock_alloc_blk = 2400ULL;
+ ctx->mock_alloc_len = 3;
+ ctx->mock_alloc_err = 0;
+ count = 8;
+
+ blk = ext4_new_meta_blocks(NULL, inode, 2048ULL,
+ EXT4_MB_DELALLOC_RESERVED, &count, &err);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, blk, 2400ULL);
+ KUNIT_EXPECT_EQ(test, count, 3UL);
+ KUNIT_EXPECT_EQ(test, inode_get_bytes(inode), 12288LL);
+
+ /* 3. Error from ext4_mb_new_blocks does not charge quota bytes */
+ ctx->mock_alloc_err = -ENOSPC;
+ count = 4;
+ blk = ext4_new_meta_blocks(NULL, inode, 2048ULL,
+ EXT4_MB_DELALLOC_RESERVED, &count, &err);
+ KUNIT_EXPECT_EQ(test, err, -ENOSPC);
+ KUNIT_EXPECT_EQ(test, blk, 0ULL);
+ KUNIT_EXPECT_EQ(test, inode_get_bytes(inode), 12288LL);
+}
+
+/* =========================================================================
+ * 11. Free Cluster Summation (ext4_count_free_clusters)
+ * =========================================================================
+ */
+
+static void
+test_count_free_clusters_corrupt_and_64bit_groups(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct balloc_test_blk *slot2;
+
+ slot2 = balloc_test_find_blk(ctx, balloc_test_bbitmap_blk(2));
+ KUNIT_ASSERT_NOT_NULL(test, slot2);
+
+ /* Default 4 groups: 1004 + 1004 + 1006 + 1004 = 4018 free clusters */
+ KUNIT_EXPECT_EQ(test, ext4_count_free_clusters(sb), 4018ULL);
+
+ /* Corrupt bitmap flag on group 2 excludes its 1006 clusters -> 3012 */
+ clear_buffer_verified(&slot2->bh);
+ set_bit(EXT4_GROUP_INFO_BBITMAP_CORRUPT_BIT,
+ &ctx->grp_infos[2].bb_state);
+ KUNIT_EXPECT_EQ(test, ext4_count_free_clusters(sb), 3012ULL);
+
+ /* NULL s_group_info still sums all valid group descriptors -> 4018 */
+ set_buffer_verified(&slot2->bh);
+ rcu_assign_pointer(ctx->sbi.s_group_info, NULL);
+ KUNIT_EXPECT_EQ(test, ext4_count_free_clusters(sb), 4018ULL);
+ rcu_assign_pointer(ctx->sbi.s_group_info, ctx->grp_info_slices);
+ ctx->grp_infos[2].bb_state = 0;
+
+#ifndef EXT4FS_DEBUG
+ /* 64-bit group descriptors with bg_free_blocks_count_hi > 0 */
+ ctx->sbi.s_groups_count = 2;
+ ext4_free_group_clusters_set(sb, &ctx->gdps[0], 0x10005U);
+ ext4_free_group_clusters_set(sb, &ctx->gdps[1], 0x20003U);
+ KUNIT_EXPECT_EQ(test, ext4_count_free_clusters(sb), 0x30008ULL);
+
+ /* Unloaded group descriptor block is skipped cleanly */
+ ctx->sbi.s_groups_count = 20;
+ ctx->gdesc_bh_ptrs[1] = NULL;
+ KUNIT_EXPECT_EQ(test, ext4_count_free_clusters(sb),
+ 0x30008ULL + 1006ULL + 1004ULL);
+#endif
+}
+
+/* =========================================================================
+ * 12. Inode Goal Block Selection (ext4_inode_to_goal_block)
+ * =========================================================================
+ */
+
+static void test_inode_to_goal_block_flex_bg_and_delalloc(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *inode = ctx->test_inode;
+
+ ctx->sbi.s_groups_count = 8;
+ ext4_blocks_count_set(&ctx->es, 1 + 8 * BALLOC_TEST_BLOCKS_PER_GRP);
+ set_opt(sb, DELALLOC);
+
+ /* flex_size = 2 (< EXT4_FLEX_SIZE_DIR_ALLOC_SCHEME = 4): stays in bg 3 */
+ ctx->sbi.s_log_groups_per_flex = 1;
+ EXT4_I(inode)->i_block_group = 3;
+ inode->i_mode = S_IFDIR | 0755;
+ KUNIT_EXPECT_EQ(test, ext4_inode_to_goal_block(inode),
+ balloc_test_group_start(3));
+ inode->i_mode = S_IFREG | 0644;
+ KUNIT_EXPECT_EQ(test, ext4_inode_to_goal_block(inode),
+ balloc_test_group_start(3));
+
+ /*
+ * flex_size = 4 (== EXT4_FLEX_SIZE_DIR_ALLOC_SCHEME):
+ * - Directories and special/symlink inodes map to 1st group of flex_bg
+ * - Regular files map to 2nd group of flex_bg (flex_start + 1)
+ */
+ ctx->sbi.s_log_groups_per_flex = 2;
+
+ EXT4_I(inode)->i_block_group = 3;
+ inode->i_mode = S_IFDIR | 0755;
+ KUNIT_EXPECT_EQ(test, ext4_inode_to_goal_block(inode),
+ balloc_test_group_start(0));
+ inode->i_mode = S_IFLNK | 0777;
+ KUNIT_EXPECT_EQ(test, ext4_inode_to_goal_block(inode),
+ balloc_test_group_start(0));
+ inode->i_mode = S_IFREG | 0644;
+ KUNIT_EXPECT_EQ(test, ext4_inode_to_goal_block(inode),
+ balloc_test_group_start(1));
+
+ EXT4_I(inode)->i_block_group = 6;
+ inode->i_mode = S_IFDIR | 0755;
+ KUNIT_EXPECT_EQ(test, ext4_inode_to_goal_block(inode),
+ balloc_test_group_start(4));
+ inode->i_mode = S_IFREG | 0644;
+ KUNIT_EXPECT_EQ(test, ext4_inode_to_goal_block(inode),
+ balloc_test_group_start(5));
+}
+
+static void test_inode_to_goal_block_nodelalloc_colour(struct kunit *test)
+{
+ struct balloc_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *inode = ctx->test_inode;
+ ext4_fsblk_t g0_start = balloc_test_group_start(0);
+ ext4_fsblk_t g3_start = balloc_test_group_start(3);
+ ext4_fsblk_t goal_full, goal_partial;
+
+ clear_opt(sb, DELALLOC);
+ ctx->sbi.s_log_groups_per_flex = 0;
+ inode->i_mode = S_IFREG | 0644;
+
+ /*
+ * Full group 0 (1024 blocks -> step = 64) vs partial last group 3 with
+ * last_block - g3_start == 320 (321 blocks -> step = 20). Both calls
+ * execute in the same kthread, so (task_pid_nr(current) % 16) is fixed.
+ */
+ ext4_blocks_count_set(&ctx->es, g3_start + 321);
+
+ EXT4_I(inode)->i_block_group = 0;
+ goal_full = ext4_inode_to_goal_block(inode);
+ KUNIT_EXPECT_GE(test, goal_full, g0_start);
+ KUNIT_EXPECT_LE(test, goal_full, g0_start + 15 * 64);
+ KUNIT_EXPECT_EQ(test, (goal_full - g0_start) % 64, 0ULL);
+
+ EXT4_I(inode)->i_block_group = 3;
+ goal_partial = ext4_inode_to_goal_block(inode);
+ KUNIT_EXPECT_GE(test, goal_partial, g3_start);
+ KUNIT_EXPECT_LE(test, goal_partial, g3_start + 15 * 20);
+ KUNIT_EXPECT_EQ(test, (goal_partial - g3_start) % 20, 0ULL);
+ KUNIT_EXPECT_EQ(test, (goal_full - g0_start) * 20,
+ (goal_partial - g3_start) * 64);
+
+ /* Partial last group with < 16 blocks yields colour step 0 -> g3_start */
+ ext4_blocks_count_set(&ctx->es, g3_start + 16);
+ KUNIT_EXPECT_EQ(test, ext4_inode_to_goal_block(inode), g3_start);
+}
+
+static struct kunit_case balloc_test_cases[] = {
+ KUNIT_CASE_PARAM(test_group_no_and_offset_matrix,
+ group_offset_gen_params),
+ KUNIT_CASE(test_group_no_and_offset_optional_out_ptrs),
+ KUNIT_CASE(test_bg_has_super_dense_all_groups),
+ KUNIT_CASE(test_bg_has_super_sparse_super_powers),
+ KUNIT_CASE(test_bg_has_super_sparse_super2_explicit_backups),
+ KUNIT_CASE(test_bg_num_gdb_and_base_meta_non_metabg),
+ KUNIT_CASE(test_bg_num_gdb_and_base_meta_hybrid_metabg),
+ KUNIT_CASE(test_free_clusters_after_init_standard_and_partial_last),
+ KUNIT_CASE(test_free_clusters_after_init_external_and_spanning_meta),
+ KUNIT_CASE(test_free_clusters_after_init_bigalloc_cluster_sharing),
+ KUNIT_CASE(test_get_group_desc_lookup_and_bounds),
+ KUNIT_CASE(test_get_group_info_lookup_and_null_guards),
+ KUNIT_CASE(test_read_block_bitmap_validates_csum),
+ KUNIT_CASE(test_read_block_bitmap_validates_metadata_bits),
+ KUNIT_CASE(test_read_block_bitmap_validates_padding),
+ KUNIT_CASE(test_read_block_bitmap_validation_short_circuits),
+ KUNIT_CASE(test_read_block_bitmap_disk_and_cache_paths),
+ KUNIT_CASE(test_read_block_bitmap_nowait_prefetch_locked),
+ KUNIT_CASE(test_read_block_bitmap_uninit_group),
+ KUNIT_CASE(test_read_block_bitmap_uninit_errors),
+ KUNIT_CASE(test_read_and_wait_block_bitmap_errors),
+ KUNIT_CASE(test_claim_free_clusters_unprivileged_and_bigalloc),
+ KUNIT_CASE(test_claim_free_clusters_root_and_reserved_pools),
+ KUNIT_CASE(test_should_retry_alloc_no_journal_and_retry_limit),
+ KUNIT_CASE(test_should_retry_alloc_free_space_and_discard_flush),
+ KUNIT_CASE(test_new_meta_blocks_alloc_and_delalloc_quota),
+ KUNIT_CASE(test_count_free_clusters_corrupt_and_64bit_groups),
+ KUNIT_CASE(test_inode_to_goal_block_flex_bg_and_delalloc),
+ KUNIT_CASE(test_inode_to_goal_block_nodelalloc_colour),
+ {}
+};
+
+static struct kunit_suite balloc_test_suite = {
+ .name = "ext4_balloc_test",
+ .init = balloc_test_init,
+ .test_cases = balloc_test_cases,
+};
+
+kunit_test_suites(&balloc_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 block allocation helpers in balloc.c");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/balloc.c b/fs/ext4/balloc.c
index 52f4c5169f91..faf27c972d65 100644
--- a/fs/ext4/balloc.c
+++ b/fs/ext4/balloc.c
@@ -243,6 +243,7 @@ unsigned ext4_free_clusters_after_init(struct super_block *sb,
return num_clusters_in_group(sb, block_group) -
ext4_num_overhead_clusters(sb, block_group, gdp);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_clusters_after_init);
/*
* The free blocks are managed by bitmaps. A file system contains several
@@ -489,7 +490,7 @@ ext4_read_block_bitmap_nowait(struct super_block *sb, ext4_group_t block_group,
EXT4_GROUP_INFO_BBITMAP_CORRUPT);
return ERR_PTR(-EFSCORRUPTED);
}
- bh = sb_getblk(sb, bitmap_blk);
+ bh = ext4_sb_getblk(sb, bitmap_blk);
if (unlikely(!bh)) {
ext4_warning(sb, "Cannot get buffer for block bitmap - "
"block_group = %u, block_bitmap = %llu",
@@ -611,6 +612,7 @@ ext4_read_block_bitmap(struct super_block *sb, ext4_group_t block_group)
}
return bh;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_block_bitmap);
/**
* ext4_has_free_clusters()
@@ -678,6 +680,7 @@ int ext4_claim_free_clusters(struct ext4_sb_info *sbi,
} else
return -ENOSPC;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_claim_free_clusters);
/**
* ext4_should_retry_alloc() - check if a block allocation should be retried
@@ -724,6 +727,7 @@ int ext4_should_retry_alloc(struct super_block *sb, int *retries)
(void) jbd2_journal_force_commit_nested(sbi->s_journal);
return 1;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_should_retry_alloc);
/*
* ext4_new_meta_blocks() -- allocate block for meta data (indexing) blocks
@@ -764,6 +768,7 @@ ext4_fsblk_t ext4_new_meta_blocks(handle_t *handle, struct inode *inode,
}
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_new_meta_blocks);
/**
* ext4_count_free_clusters() -- count filesystem free clusters
@@ -877,6 +882,7 @@ int ext4_bg_has_super(struct super_block *sb, ext4_group_t group)
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_bg_has_super);
static unsigned long ext4_bg_num_gdb_meta(struct super_block *sb,
ext4_group_t group)
@@ -923,6 +929,7 @@ unsigned long ext4_bg_num_gdb(struct super_block *sb, ext4_group_t group)
return ext4_bg_num_gdb_meta(sb,group);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_bg_num_gdb);
/*
* This function returns the number of file system metadata blocks at
@@ -949,6 +956,7 @@ unsigned int ext4_num_base_meta_blocks(struct super_block *sb,
}
return num;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_num_base_meta_blocks);
static unsigned int ext4_num_base_meta_clusters(struct super_block *sb,
ext4_group_t block_group)
@@ -1004,4 +1012,5 @@ ext4_fsblk_t ext4_inode_to_goal_block(struct inode *inode)
((last_block - bg_start) / 16);
return bg_start + colour;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_to_goal_block);
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 13/27] ext4: add KUnit tests for fs/ext4/orphan.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (11 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 12/27] ext4: add KUnit tests for fs/ext4/balloc.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (13 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add the ext4_orphan_test KUnit test suite in fs/ext4/orphan-test.c (22
test cases, 25 subtests total when CONFIG_QUOTA is enabled; 21 test
cases, 24 subtests otherwise) to comprehensively exercise orphan inode
tracking, orphan file management, and mount-time orphan recovery in
fs/ext4/orphan.c using an in-memory VFS superblock, mock block device,
and deterministic buffer pool without requiring a mounted block device:
- Orphan block geometry and empty-state checks
(ext4_inodes_per_orphan_block across 1K, 2K, 4K, and 64K block sizes,
and ext4_orphan_file_empty across disabled feature, zero-block files,
and multi-block occupancy transitions).
- Superblock linked-list orphan insertion (ext4_orphan_add), covering
no-journal and bad-inode early exits, I_NEW unlocked insertion and
deletion, idempotency when already tracked via i_orphan or
EXT4_STATE_ORPHAN_FILE, LIFO chaining across regular, directory, and
symlink inodes with superblock CRC32C updates, NEXT_ORPHAN boundary
checks at s_inodes_count vs. s_inodes_count + 1, and journal/iloc
error rollbacks without leaking buffer references or leaving stray
s_orphan entries.
- Superblock linked-list orphan removal (ext4_orphan_del), covering
untracked inodes, no-journal early exit vs. EXT4_ORPHAN_FS recovery
mode, head/middle/tail unlinking, NULL-handle in-core-only cleanup,
and error propagation across superblock and predecessor/target inode
journal operations.
- Orphan-file insertion and deletion (ext4_orphan_file_add and
ext4_orphan_file_del), covering EXT4_JTR_ORPHAN_FILE trigger wiring,
slot claiming and freeing, circular block-ring wrap-around and final
slot allocation, -ENOSPC fallback to the superblock linked list when
all orphan blocks are full, non-ENOSPC journal write-access and
dirty-metadata error propagation without fallback, NULL-handle state
cleanup, and out-of-range block index warning guards.
- Orphan-file JBD2 frozen-buffer checksum trigger and lifecycle
management (ext4_orphan_file_block_trigger on live and frozen buffers,
ext4_init_orphan_info, ext4_orphan_file_block_csum_verify with and
without metadata_csum, the 512-block EXT4_MAX_ORPHAN_FILE_BLOCKS size
boundary, partial-load error cleanup on iget/bread/hole/bad-magic/
bad-checksum failures, and ext4_release_orphan_info).
- End-to-end mount-time orphan recovery (ext4_orphan_cleanup and
ext4_process_orphan), covering empty-fs, read-only bdev, incompatible
ROCOMPAT features, and EXT4_ERROR_FS skip guards, unlinked file and
fast-symlink eviction, linked-inode truncation, combined linked-list
and orphan-file recovery with corrupt-slot skipping and SB_RDONLY
restoration, truncate failure handling via ext4_orphan_del(NULL, ...)
and mid-loop EXT4_ERROR_FS abort, and quota activation/teardown paths
under CONFIG_QUOTA and CONFIG_QFMT_V2.
Add a KUNIT_STATIC_STUB_REDIRECT hook to ext4_truncate() in
fs/ext4/inode.c for truncate failure injection during orphan recovery,
and export the remaining non-static fs/ext4/orphan.c entry points with
EXPORT_SYMBOL_FOR_EXT4_TEST.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 2 +-
fs/ext4/inode.c | 2 +
fs/ext4/orphan-test.c | 2364 +++++++++++++++++++++++++++++++++++++++++
fs/ext4/orphan.c | 5 +
4 files changed, 2372 insertions(+), 1 deletion(-)
create mode 100644 fs/ext4/orphan-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 7d0ae7fd4448..9bad2bff0de9 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -19,7 +19,7 @@ ext4-test-objs += inode-test.o mballoc-test.o \
extents_status-test.o xattr-test.o \
namei-test.o inline-test.o \
fast_commit-test.o ialloc-test.o \
- balloc-test.o
+ balloc-test.o orphan-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/inode.c b/fs/ext4/inode.c
index 98444d8ddb40..92f20d7606ea 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4686,6 +4686,8 @@ int ext4_truncate(struct inode *inode)
int err = 0, err2;
handle_t *handle;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_truncate, inode);
+
/*
* There is a possibility that we're either freeing the inode
* or it's a completely new inode. In those cases we might not
diff --git a/fs/ext4/orphan-test.c b/fs/ext4/orphan-test.c
new file mode 100644
index 000000000000..f93057df1d31
--- /dev/null
+++ b/fs/ext4/orphan-test.c
@@ -0,0 +1,2364 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/orphan.c
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/crc32c.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/iversion.h>
+#include <linux/jbd2.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/part_stat.h>
+#include <linux/percpu.h>
+#include <linux/quotaops.h>
+#include <linux/rcupdate.h>
+#include <linux/slab.h>
+#include <linux/stat.h>
+#include <linux/timer.h>
+#include <linux/workqueue.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "ext4_extents.h"
+#include "xattr.h"
+
+/*
+ * Mock filesystem geometry:
+ * - 2 block groups, 1024-byte blocks (254 orphan entries per orphan block)
+ * - 64 inodes per group (128 total inodes), 256-byte inodes
+ * - s_first_ino = 11, s_orphan_file_inum = 12
+ */
+#define ORPHAN_TEST_NGROUPS 2U
+#define ORPHAN_TEST_BLK_SIZE 1024U
+#define ORPHAN_TEST_BLK_BITS 10U
+#define ORPHAN_TEST_BLOCKS_PER_GRP 1024U
+#define ORPHAN_TEST_FIRST_DATA_BLK 1U
+#define ORPHAN_TEST_TOTAL_BLKS (ORPHAN_TEST_FIRST_DATA_BLK + \
+ ORPHAN_TEST_NGROUPS * \
+ ORPHAN_TEST_BLOCKS_PER_GRP)
+#define ORPHAN_TEST_INODE_SIZE 256U
+#define ORPHAN_TEST_EXTRA_ISIZE 32U
+#define ORPHAN_TEST_INODES_PER_BLK (ORPHAN_TEST_BLK_SIZE / \
+ ORPHAN_TEST_INODE_SIZE)
+#define ORPHAN_TEST_INODES_PER_GRP 64U
+#define ORPHAN_TEST_ITB_PER_GRP (ORPHAN_TEST_INODES_PER_GRP / \
+ ORPHAN_TEST_INODES_PER_BLK)
+#define ORPHAN_TEST_TOTAL_INODES (ORPHAN_TEST_NGROUPS * \
+ ORPHAN_TEST_INODES_PER_GRP)
+#define ORPHAN_TEST_FIRST_INO 11U
+#define ORPHAN_TEST_ORPHAN_INO 12U
+#define ORPHAN_TEST_CSUM_SEED 0x4d7a19c5U
+#define ORPHAN_TEST_OF_CSUM_SEED 0x91e23b77U
+#define ORPHAN_TEST_INODES_PER_OB 254U
+
+#define ORPHAN_TEST_GDESC_BLK 2ULL
+#define ORPHAN_TEST_OB_BASE_BLK 500ULL
+#define ORPHAN_TEST_MAX_OB_BLKS 4U
+#define ORPHAN_TEST_MAX_SLOTS 16U
+
+struct orphan_test_ibitmap {
+ struct buffer_head bh;
+ char buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+};
+
+struct orphan_test_ob_blk {
+ bool present;
+ int bread_err;
+ bool return_hole;
+ struct buffer_head bh;
+ char buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+};
+
+struct orphan_test_inode_slot {
+ bool in_use;
+ unsigned long ino;
+ bool handoff_to_iget;
+ struct inode *inode;
+ struct buffer_head iloc_bh;
+ char raw_buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+};
+
+struct orphan_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 buffer_head sb_bh;
+ struct buffer_head gdesc_bh;
+ char gdesc_buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+
+ struct ext4_group_desc gdps[ORPHAN_TEST_NGROUPS];
+ struct ext4_group_info grp_infos[ORPHAN_TEST_NGROUPS];
+ struct ext4_group_info *grp_info_ptrs[16];
+ struct ext4_group_info **grp_info_slice[1];
+ struct orphan_test_ibitmap ibitmaps[ORPHAN_TEST_NGROUPS];
+
+ journal_t mock_journal;
+ handle_t mock_handle;
+
+ struct orphan_test_ob_blk ob_pool[ORPHAN_TEST_MAX_OB_BLKS];
+ struct ext4_orphan_block static_binfo[ORPHAN_TEST_MAX_OB_BLKS];
+
+ /* Shared read-only block used for 512-block max-size boundary test */
+ bool use_boundary_bh;
+ struct buffer_head boundary_bh;
+ char boundary_buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+ char frozen_buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+#ifdef CONFIG_QUOTA
+ char qf_name[16];
+#endif
+
+ struct orphan_test_inode_slot slots[ORPHAN_TEST_MAX_SLOTS];
+ bool orphan_info_allocated;
+ bool enable_real_evict;
+
+ /* Fault injection controls */
+ int fail_write_access_err;
+ int fail_write_access_on_call;
+ int fail_dirty_metadata_err;
+ int fail_dirty_metadata_on_call;
+ int fail_get_iloc_err;
+ int fail_get_iloc_on_call;
+ int fail_mark_iloc_err;
+ int fail_mark_iloc_on_call;
+ int fail_mark_inode_dirty_err;
+ int fail_iget_err;
+ unsigned long fail_iget_ino;
+ int stub_truncate_err;
+
+ /* Call tracking & verification state */
+ unsigned int write_access_calls;
+ enum ext4_journal_trigger_type last_trigger_type;
+ unsigned int dirty_metadata_calls;
+ unsigned int get_iloc_calls;
+ unsigned int mark_iloc_calls;
+ unsigned int iget_calls;
+ unsigned int truncate_calls;
+ bool saw_rw_during_cleanup;
+
+ unsigned long freed_inos[ORPHAN_TEST_MAX_SLOTS];
+ unsigned int nr_freed_inos;
+ ext4_fsblk_t last_freed_blk;
+ unsigned long total_freed_blks;
+};
+
+static inline ext4_fsblk_t orphan_test_ibitmap_blk(ext4_group_t g)
+{
+ return ORPHAN_TEST_FIRST_DATA_BLK +
+ (ext4_fsblk_t)g * ORPHAN_TEST_BLOCKS_PER_GRP + 3ULL;
+}
+
+static inline ext4_fsblk_t orphan_test_bbitmap_blk(ext4_group_t g)
+{
+ return ORPHAN_TEST_FIRST_DATA_BLK +
+ (ext4_fsblk_t)g * ORPHAN_TEST_BLOCKS_PER_GRP + 2ULL;
+}
+
+static inline ext4_fsblk_t orphan_test_itable_blk(ext4_group_t g)
+{
+ return ORPHAN_TEST_FIRST_DATA_BLK +
+ (ext4_fsblk_t)g * ORPHAN_TEST_BLOCKS_PER_GRP + 4ULL;
+}
+
+static void orphan_test_sb_upd_work(struct work_struct *work)
+{
+}
+
+static void orphan_test_err_timer(struct timer_list *t)
+{
+}
+
+static void orphan_test_init_ei_fields(struct ext4_inode_info *ei)
+{
+ inode_set_iversion(&ei->vfs_inode, 1);
+ ei->i_flags = 0;
+ ext4_clear_state_flags(ei);
+ spin_lock_init(&ei->i_raw_lock);
+ ei->i_prealloc_node = RB_ROOT;
+ atomic_set(&ei->i_prealloc_active, 0);
+ rwlock_init(&ei->i_prealloc_lock);
+ ext4_es_init_tree(&ei->i_es_tree);
+ rwlock_init(&ei->i_es_lock);
+ INIT_LIST_HEAD(&ei->i_es_list);
+ ei->i_es_all_nr = 0;
+ ei->i_es_shk_nr = 0;
+ ei->i_es_shrink_lblk = 0;
+ ei->i_es_seq = 0;
+ ei->i_reserved_data_blocks = 0;
+ spin_lock_init(&ei->i_block_reservation_lock);
+ ext4_init_pending_tree(&ei->i_pending_tree);
+#ifdef CONFIG_QUOTA
+ ei->i_reserved_quota = 0;
+ memset(&ei->i_dquot, 0, sizeof(ei->i_dquot));
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+ ei->i_crypt_info = NULL;
+#endif
+ ei->jinode = NULL;
+ init_rwsem(&ei->xattr_sem);
+ INIT_LIST_HEAD(&ei->i_rsv_conversion_list);
+ spin_lock_init(&ei->i_completed_io_lock);
+ ei->i_sync_tid = 0;
+ ei->i_datasync_tid = 0;
+ INIT_LIST_HEAD(&ei->i_orphan);
+ ext4_fc_init_inode(&ei->vfs_inode);
+ spin_lock_init(&ei->i_fc_lock);
+ ei->i_metadata_bhs = NULL;
+ ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+ ei->i_extra_isize = ORPHAN_TEST_EXTRA_ISIZE;
+ ei->i_csum_seed = ORPHAN_TEST_OF_CSUM_SEED;
+ ei->i_last_alloc_group = ~0U;
+}
+
+static struct inode *orphan_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);
+ orphan_test_init_ei_fields(ei);
+ return &ei->vfs_inode;
+}
+
+static void orphan_test_evict_inode(struct inode *inode)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (ctx && ctx->enable_real_evict) {
+ ext4_evict_inode(inode);
+ return;
+ }
+ truncate_inode_pages_final(&inode->i_data);
+ ext4_clear_inode(inode);
+}
+
+static void orphan_test_destroy_inode(struct inode *inode)
+{
+ kfree(ext4_i_metadata_bhs(inode));
+ EXT4_I(inode)->i_metadata_bhs = NULL;
+}
+
+static void orphan_test_free_inode_mem(struct inode *inode)
+{
+ kfree(EXT4_I(inode));
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **orphan_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+
+static qsize_t *orphan_test_get_reserved_space(struct inode *inode)
+{
+ return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations orphan_test_qops = {
+ .get_reserved_space = orphan_test_get_reserved_space,
+ .alloc_dquot = dquot_alloc,
+ .destroy_dquot = dquot_destroy,
+ .get_next_id = dquot_get_next_id,
+};
+#endif
+
+static const struct super_operations orphan_test_sops = {
+ .alloc_inode = orphan_test_alloc_inode,
+ .evict_inode = orphan_test_evict_inode,
+ .destroy_inode = orphan_test_destroy_inode,
+ .free_inode = orphan_test_free_inode_mem,
+ .drop_inode = inode_just_drop,
+#ifdef CONFIG_QUOTA
+ .get_dquots = orphan_test_get_dquots,
+#endif
+};
+
+static int orphan_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static struct file_system_type orphan_test_fs_type = {
+ .name = "ext4_orphan_kunit",
+ .init_fs_context = orphan_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+/* --- Slot & Orphan Block Helpers --- */
+
+static struct orphan_test_inode_slot *
+orphan_test_find_slot(struct orphan_test_ctx *ctx, unsigned long ino)
+{
+ unsigned int i;
+
+ for (i = 0; i < ORPHAN_TEST_MAX_SLOTS; i++) {
+ if (ctx->slots[i].in_use && ctx->slots[i].ino == ino)
+ return &ctx->slots[i];
+ }
+ return NULL;
+}
+
+static void orphan_test_set_ibitmap_bit(struct orphan_test_ctx *ctx,
+ unsigned long ino, bool set)
+{
+ ext4_group_t g = (ino - 1) / ORPHAN_TEST_INODES_PER_GRP;
+ unsigned int bit = (ino - 1) % ORPHAN_TEST_INODES_PER_GRP;
+
+ if (g >= ORPHAN_TEST_NGROUPS)
+ return;
+ if (set)
+ ext4_set_bit(bit, ctx->ibitmaps[g].buf);
+ else
+ ext4_clear_bit(bit, ctx->ibitmaps[g].buf);
+}
+
+static struct orphan_test_inode_slot *
+orphan_test_create_inode(struct kunit *test, struct orphan_test_ctx *ctx,
+ unsigned long ino, umode_t mode, unsigned int nlink)
+{
+ struct orphan_test_inode_slot *slot = NULL;
+ struct ext4_extent_header *eh;
+ struct inode *inode;
+ unsigned int i;
+
+ for (i = 0; i < ORPHAN_TEST_MAX_SLOTS; i++) {
+ if (!ctx->slots[i].in_use) {
+ slot = &ctx->slots[i];
+ break;
+ }
+ }
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ inode = new_inode(ctx->sb);
+ KUNIT_ASSERT_NOT_NULL(test, inode);
+
+ memset(slot, 0, sizeof(*slot));
+ slot->in_use = true;
+ slot->ino = ino;
+ slot->inode = inode;
+ slot->iloc_bh.b_data = slot->raw_buf;
+ slot->iloc_bh.b_size = ORPHAN_TEST_BLK_SIZE;
+ slot->iloc_bh.b_bdev = &ctx->bdev;
+ slot->iloc_bh.b_blocknr = orphan_test_itable_blk(0) + i;
+ atomic_set(&slot->iloc_bh.b_count, 1);
+ set_buffer_uptodate(&slot->iloc_bh);
+
+ inode->i_ino = ino;
+ inode->i_mode = mode;
+ inode->i_blkbits = ctx->sb->s_blocksize_bits;
+ inode->i_flags |= S_NOQUOTA;
+ set_nlink(inode, nlink);
+ EXT4_I(inode)->i_block_group = (ino - 1) / ORPHAN_TEST_INODES_PER_GRP;
+
+ if (S_ISREG(mode) || S_ISDIR(mode)) {
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ eh = ext_inode_hdr(inode);
+ memset(eh, 0, sizeof(EXT4_I(inode)->i_data));
+ eh->eh_magic = EXT4_EXT_MAGIC;
+ eh->eh_entries = 0;
+ eh->eh_max = cpu_to_le16((sizeof(EXT4_I(inode)->i_data) -
+ sizeof(*eh)) /
+ sizeof(struct ext4_extent));
+ eh->eh_depth = 0;
+ }
+
+ orphan_test_set_ibitmap_bit(ctx, ino, true);
+ return slot;
+}
+
+static void orphan_test_add_single_extent(struct inode *inode,
+ ext4_lblk_t lblk, ext4_fsblk_t pblk,
+ unsigned short len)
+{
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ struct ext4_extent *ex = EXT_FIRST_EXTENT(eh);
+
+ eh->eh_entries = cpu_to_le16(1);
+ ex->ee_block = cpu_to_le32(lblk);
+ ex->ee_len = cpu_to_le16(len);
+ ext4_ext_store_pblock(ex, pblk);
+ inode->i_blocks = (blkcnt_t)len << (inode->i_blkbits - 9);
+}
+
+static __u32 orphan_test_calc_ob_csum(struct super_block *sb, __u32 seed,
+ ext4_fsblk_t blocknr, const void *data)
+{
+ __le64 dsk_block_nr = cpu_to_le64(blocknr);
+ size_t payload_bytes = sb->s_blocksize -
+ sizeof(struct ext4_orphan_block_tail);
+ __u32 csum;
+
+ csum = crc32c(seed, &dsk_block_nr, sizeof(dsk_block_nr));
+ return crc32c(csum, data, payload_bytes);
+}
+
+static void orphan_test_seal_ob_bh(struct super_block *sb, __u32 seed,
+ struct buffer_head *bh, __u32 magic)
+{
+ struct ext4_orphan_block_tail *ot;
+ __u32 csum;
+
+ ot = (struct ext4_orphan_block_tail *)(bh->b_data + sb->s_blocksize -
+ sizeof(*ot));
+ ot->ob_magic = cpu_to_le32(magic);
+ csum = orphan_test_calc_ob_csum(sb, seed, bh->b_blocknr, bh->b_data);
+ ot->ob_checksum = cpu_to_le32(csum);
+}
+
+static void orphan_test_setup_static_orphan_file(struct orphan_test_ctx *ctx,
+ int num_blocks)
+{
+ struct super_block *sb = ctx->sb;
+ struct ext4_orphan_info *oi = &ctx->sbi.s_orphan_info;
+ int i;
+
+ ext4_set_feature_orphan_file(sb);
+ oi->of_blocks = num_blocks;
+ oi->of_csum_seed = ORPHAN_TEST_OF_CSUM_SEED;
+ oi->of_binfo = ctx->static_binfo;
+ ctx->orphan_info_allocated = false;
+
+ for (i = 0; i < num_blocks; i++) {
+ struct orphan_test_ob_blk *oblk = &ctx->ob_pool[i];
+
+ memset(oblk, 0, sizeof(*oblk));
+ oblk->present = true;
+ oblk->bh.b_data = oblk->buf;
+ oblk->bh.b_size = sb->s_blocksize;
+ oblk->bh.b_bdev = &ctx->bdev;
+ oblk->bh.b_blocknr = ORPHAN_TEST_OB_BASE_BLK + i;
+ atomic_set(&oblk->bh.b_count, 1);
+ set_buffer_uptodate(&oblk->bh);
+ orphan_test_seal_ob_bh(sb, oi->of_csum_seed, &oblk->bh,
+ EXT4_ORPHAN_BLOCK_MAGIC);
+
+ ctx->static_binfo[i].ob_bh = &oblk->bh;
+ atomic_set(&ctx->static_binfo[i].ob_free_entries,
+ ORPHAN_TEST_INODES_PER_OB);
+ }
+}
+
+static void orphan_test_reset_faults(struct orphan_test_ctx *ctx)
+{
+ ctx->fail_write_access_err = 0;
+ ctx->fail_write_access_on_call = 0;
+ ctx->fail_dirty_metadata_err = 0;
+ ctx->fail_dirty_metadata_on_call = 0;
+ ctx->fail_get_iloc_err = 0;
+ ctx->fail_get_iloc_on_call = 0;
+ ctx->fail_mark_iloc_err = 0;
+ ctx->fail_mark_iloc_on_call = 0;
+ ctx->fail_mark_inode_dirty_err = 0;
+ ctx->fail_iget_err = 0;
+ ctx->fail_iget_ino = 0;
+ ctx->stub_truncate_err = 0;
+
+ ctx->write_access_calls = 0;
+ ctx->last_trigger_type = EXT4_JTR_NONE;
+ ctx->dirty_metadata_calls = 0;
+ ctx->get_iloc_calls = 0;
+ ctx->mark_iloc_calls = 0;
+ ctx->iget_calls = 0;
+ ctx->truncate_calls = 0;
+ ctx->saw_rw_during_cleanup = false;
+ ctx->nr_freed_inos = 0;
+ ctx->last_freed_blk = 0;
+ ctx->total_freed_blks = 0;
+ ctx->sbi.s_mount_state &= ~EXT4_ERROR_FS;
+}
+
+static bool orphan_test_was_ino_freed(const struct orphan_test_ctx *ctx,
+ unsigned long ino)
+{
+ unsigned int i;
+
+ for (i = 0; i < ctx->nr_freed_inos; i++) {
+ if (ctx->freed_inos[i] == ino)
+ return true;
+ }
+ return false;
+}
+
+static int orphan_test_call_add(handle_t *handle, struct inode *inode)
+{
+ int err;
+
+ inode_lock(inode);
+ err = ext4_orphan_add(handle, inode);
+ inode_unlock(inode);
+ return err;
+}
+
+static int orphan_test_call_del(handle_t *handle, struct inode *inode)
+{
+ int err;
+
+ inode_lock(inode);
+ err = ext4_orphan_del(handle, inode);
+ inode_unlock(inode);
+ return err;
+}
+
+/* --- KUnit Static Stubs --- */
+
+static int
+orphan_test_get_write_access_stub(const char *where, unsigned int line,
+ handle_t *handle, struct super_block *sb,
+ struct buffer_head *bh,
+ enum ext4_journal_trigger_type trigger_type)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return 0;
+
+ ctx->write_access_calls++;
+ ctx->last_trigger_type = trigger_type;
+ if (ctx->fail_write_access_err &&
+ (!ctx->fail_write_access_on_call ||
+ ctx->write_access_calls == (unsigned int)ctx->fail_write_access_on_call))
+ return ctx->fail_write_access_err;
+ return 0;
+}
+
+static int orphan_test_dirty_metadata_stub(const char *where, unsigned int line,
+ handle_t *handle,
+ struct inode *inode,
+ struct buffer_head *bh)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return 0;
+
+ ctx->dirty_metadata_calls++;
+ if (ctx->fail_dirty_metadata_err &&
+ (!ctx->fail_dirty_metadata_on_call ||
+ ctx->dirty_metadata_calls == (unsigned int)ctx->fail_dirty_metadata_on_call))
+ return ctx->fail_dirty_metadata_err;
+
+ if (bh) {
+ set_buffer_uptodate(bh);
+ clear_buffer_dirty(bh);
+ }
+ return 0;
+}
+
+static int orphan_test_get_inode_loc_stub(struct inode *inode,
+ struct ext4_iloc *iloc)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+ struct orphan_test_inode_slot *slot;
+
+ if (!ctx)
+ return -EIO;
+
+ ctx->get_iloc_calls++;
+ if (ctx->fail_get_iloc_err &&
+ (!ctx->fail_get_iloc_on_call ||
+ ctx->get_iloc_calls == (unsigned int)ctx->fail_get_iloc_on_call))
+ return ctx->fail_get_iloc_err;
+
+ slot = orphan_test_find_slot(ctx, inode->i_ino);
+ if (!slot)
+ return -ENOENT;
+
+ get_bh(&slot->iloc_bh);
+ iloc->bh = &slot->iloc_bh;
+ iloc->offset = 0;
+ iloc->block_group = EXT4_I(inode)->i_block_group;
+ return 0;
+}
+
+static int orphan_test_mark_iloc_dirty_stub(handle_t *handle,
+ struct inode *inode,
+ struct ext4_iloc *iloc)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+ int err = 0;
+
+ if (ctx) {
+ ctx->mark_iloc_calls++;
+ if (ctx->fail_mark_iloc_err &&
+ (!ctx->fail_mark_iloc_on_call ||
+ ctx->mark_iloc_calls == (unsigned int)ctx->fail_mark_iloc_on_call))
+ err = ctx->fail_mark_iloc_err;
+ }
+
+ /* Real ext4_mark_iloc_dirty() always drops iloc->bh */
+ brelse(iloc->bh);
+ iloc->bh = NULL;
+ return err;
+}
+
+static int orphan_test_mark_inode_dirty_stub(handle_t *handle,
+ struct inode *inode,
+ const char *func,
+ unsigned int line)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return 0;
+ return ctx->fail_mark_inode_dirty_err;
+}
+
+static struct inode *orphan_test_iget_stub(struct super_block *sb,
+ unsigned long ino,
+ ext4_iget_flags flags,
+ const char *function,
+ unsigned int line)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+ struct orphan_test_inode_slot *slot;
+ struct inode *inode;
+
+ if (!ctx)
+ return ERR_PTR(-EIO);
+
+ ctx->iget_calls++;
+ if (ctx->fail_iget_err &&
+ (!ctx->fail_iget_ino || ctx->fail_iget_ino == ino))
+ return ERR_PTR(ctx->fail_iget_err);
+
+ slot = orphan_test_find_slot(ctx, ino);
+ if (!slot || !slot->inode)
+ return ERR_PTR(-ENOENT);
+
+ inode = slot->inode;
+ if (slot->handoff_to_iget)
+ slot->inode = NULL;
+ else
+ ihold(inode);
+ return inode;
+}
+
+static struct buffer_head *orphan_test_getblk_stub(handle_t *handle,
+ struct inode *inode,
+ ext4_lblk_t block,
+ int map_flags)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+ struct orphan_test_ob_blk *oblk;
+
+ if (!ctx)
+ return ERR_PTR(-EIO);
+
+ if (ctx->use_boundary_bh) {
+ get_bh(&ctx->boundary_bh);
+ return &ctx->boundary_bh;
+ }
+
+ if (block >= ORPHAN_TEST_MAX_OB_BLKS)
+ return ERR_PTR(-EFSCORRUPTED);
+
+ oblk = &ctx->ob_pool[block];
+ if (oblk->bread_err)
+ return ERR_PTR(oblk->bread_err);
+ if (oblk->return_hole || !oblk->present)
+ return NULL;
+
+ get_bh(&oblk->bh);
+ return &oblk->bh;
+}
+
+static struct ext4_group_desc *
+orphan_test_get_group_desc_stub(struct super_block *sb,
+ ext4_group_t block_group,
+ struct buffer_head **bh)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx || block_group >= ORPHAN_TEST_NGROUPS)
+ return NULL;
+ if (bh)
+ *bh = &ctx->gdesc_bh;
+ return &ctx->gdps[block_group];
+}
+
+static struct buffer_head *orphan_test_sb_getblk_stub(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+ ext4_group_t g;
+
+ if (!ctx)
+ return NULL;
+
+ for (g = 0; g < ORPHAN_TEST_NGROUPS; g++) {
+ if (block == orphan_test_ibitmap_blk(g)) {
+ get_bh(&ctx->ibitmaps[g].bh);
+ return &ctx->ibitmaps[g].bh;
+ }
+ }
+ return NULL;
+}
+
+static void orphan_test_free_inode_stub(handle_t *handle, struct inode *inode)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (ctx) {
+ if (!sb_rdonly(inode->i_sb))
+ ctx->saw_rw_during_cleanup = true;
+ if (ctx->nr_freed_inos < ORPHAN_TEST_MAX_SLOTS)
+ ctx->freed_inos[ctx->nr_freed_inos++] = inode->i_ino;
+ orphan_test_set_ibitmap_bit(ctx, inode->i_ino, false);
+ }
+ ext4_clear_inode(inode);
+}
+
+static void orphan_test_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 orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (bh)
+ brelse(bh);
+ if (!ctx)
+ return;
+ ctx->last_freed_blk = block;
+ ctx->total_freed_blks += count;
+}
+
+static int orphan_test_truncate_stub(struct inode *inode)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return 0;
+
+ ctx->truncate_calls++;
+ if (!sb_rdonly(inode->i_sb))
+ ctx->saw_rw_during_cleanup = true;
+ return ctx->stub_truncate_err;
+}
+
+/* --- Fixture Setup & Teardown --- */
+
+static void orphan_test_cleanup(struct orphan_test_ctx *ctx)
+{
+ handle_t *handle;
+ unsigned int i;
+
+ ctx->enable_real_evict = false;
+ ctx->sbi.s_journal = NULL;
+ ctx->sbi.s_mount_state &= ~EXT4_ORPHAN_FS;
+
+ while ((handle = ext4_journal_current_handle()) != NULL)
+ ext4_journal_stop(handle);
+
+ if (ctx->orphan_info_allocated) {
+ ext4_release_orphan_info(ctx->sb);
+ ctx->orphan_info_allocated = false;
+ }
+ memset(&ctx->sbi.s_orphan_info, 0, sizeof(ctx->sbi.s_orphan_info));
+ INIT_LIST_HEAD(&ctx->sbi.s_orphan);
+
+ for (i = 0; i < ORPHAN_TEST_MAX_SLOTS; i++) {
+ struct inode *inode = ctx->slots[i].inode;
+
+ if (!inode)
+ continue;
+ INIT_LIST_HEAD(&EXT4_I(inode)->i_orphan);
+ ext4_clear_inode_state(inode, EXT4_STATE_ORPHAN_FILE);
+ spin_lock(&inode->i_lock);
+ inode_state_clear(inode, I_DIRTY_TIME | I_NEW);
+ spin_unlock(&inode->i_lock);
+ set_nlink(inode, 1);
+ iput(inode);
+ ctx->slots[i].inode = NULL;
+ }
+
+ deactivate_super(ctx->sb);
+ rcu_barrier();
+
+ cancel_work_sync(&ctx->sbi.s_sb_upd_work);
+ timer_delete_sync(&ctx->sbi.s_err_report);
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(ctx);
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(orphan_test_cleanup_action, orphan_test_cleanup,
+ struct orphan_test_ctx *);
+
+static int orphan_test_init(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ struct inode *root_inode;
+ ext4_group_t g;
+ unsigned int bit;
+ int err;
+
+ ctx = kzalloc_obj(*ctx);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ test->priv = ctx;
+
+ 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");
+ }
+
+ fc = fs_context_for_mount(&orphan_test_fs_type, 0);
+ if (IS_ERR(fc)) {
+ err = PTR_ERR(fc);
+ 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);
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+ ctx->sb = sb;
+ sbi = &ctx->sbi;
+
+ sbi->s_sb = sb;
+ sb->s_fs_info = sbi;
+ sb->s_op = &orphan_test_sops;
+#ifdef CONFIG_QUOTA
+ sb->dq_op = &orphan_test_qops;
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
+#endif
+ sb->s_flags |= SB_ACTIVE;
+ sb->s_blocksize = ORPHAN_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = ORPHAN_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_orphan_kunit", sizeof(sb->s_id));
+
+ inode_init_once(&ctx->bd_inode);
+ ctx->bd_inode.i_sb = sb;
+ ctx->bd_inode.i_blkbits = ORPHAN_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;
+ /* Writable bdev by default so ext4_orphan_cleanup can proceed */
+ bdev_clear_flag(&ctx->bdev, BD_READ_ONLY);
+ sb->s_bdev = &ctx->bdev;
+
+ bgl_lock_init(&ctx->bgl);
+ sbi->s_blockgroup_lock = &ctx->bgl;
+
+ sbi->s_es = &ctx->es;
+ ctx->es.s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+ ctx->es.s_first_data_block = cpu_to_le32(ORPHAN_TEST_FIRST_DATA_BLK);
+ ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_LINUX);
+ ctx->es.s_inodes_count = cpu_to_le32(ORPHAN_TEST_TOTAL_INODES);
+ ctx->es.s_inodes_per_group = cpu_to_le32(ORPHAN_TEST_INODES_PER_GRP);
+ ctx->es.s_blocks_per_group = cpu_to_le32(ORPHAN_TEST_BLOCKS_PER_GRP);
+ ctx->es.s_clusters_per_group = cpu_to_le32(ORPHAN_TEST_BLOCKS_PER_GRP);
+ ctx->es.s_first_ino = cpu_to_le32(ORPHAN_TEST_FIRST_INO);
+ ctx->es.s_orphan_file_inum = cpu_to_le32(ORPHAN_TEST_ORPHAN_INO);
+ ctx->es.s_inode_size = cpu_to_le16(ORPHAN_TEST_INODE_SIZE);
+ ctx->es.s_min_extra_isize = cpu_to_le16(ORPHAN_TEST_EXTRA_ISIZE);
+ ctx->es.s_want_extra_isize = cpu_to_le16(ORPHAN_TEST_EXTRA_ISIZE);
+ ctx->es.s_checksum_type = EXT4_CRC32C_CHKSUM;
+ ext4_blocks_count_set(&ctx->es, ORPHAN_TEST_TOTAL_BLKS);
+
+ ext4_set_feature_sparse_super(sb);
+ ext4_set_feature_extents(sb);
+ ext4_set_feature_extra_isize(sb);
+ ext4_set_feature_metadata_csum(sb);
+ ext4_set_feature_64bit(sb);
+
+ /* Keep sb_bh unmapped so ext4_commit_super() skips bh_submit I/O */
+ 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;
+ ext4_superblock_csum_set(sb);
+
+ ctx->gdesc_bh.b_data = ctx->gdesc_buf;
+ ctx->gdesc_bh.b_size = ORPHAN_TEST_BLK_SIZE;
+ ctx->gdesc_bh.b_blocknr = ORPHAN_TEST_GDESC_BLK;
+ ctx->gdesc_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->gdesc_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->gdesc_bh);
+
+ sbi->s_inode_size = ORPHAN_TEST_INODE_SIZE;
+ sbi->s_inodes_per_block = ORPHAN_TEST_INODES_PER_BLK;
+ sbi->s_inodes_per_group = ORPHAN_TEST_INODES_PER_GRP;
+ sbi->s_itb_per_group = ORPHAN_TEST_ITB_PER_GRP;
+ sbi->s_first_ino = ORPHAN_TEST_FIRST_INO;
+ sbi->s_want_extra_isize = ORPHAN_TEST_EXTRA_ISIZE;
+ sbi->s_blocks_per_group = ORPHAN_TEST_BLOCKS_PER_GRP;
+ sbi->s_clusters_per_group = ORPHAN_TEST_BLOCKS_PER_GRP;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_cluster_bits = 0;
+ sbi->s_groups_count = ORPHAN_TEST_NGROUPS;
+ sbi->s_gdb_count = 1;
+ sbi->s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_desc_per_block = ORPHAN_TEST_BLK_SIZE / EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_desc_per_block_bits = ilog2(sbi->s_desc_per_block);
+ sbi->s_bitmap_maxbytes = 0x1ff7ffff000ULL;
+ sbi->s_csum_seed = ORPHAN_TEST_CSUM_SEED;
+
+ mutex_init(&sbi->s_orphan_lock);
+ INIT_LIST_HEAD(&sbi->s_orphan);
+ sbi->s_journal = &ctx->mock_journal;
+ sbi->s_journal_triggers[EXT4_JTR_ORPHAN_FILE].sb = sb;
+ sbi->s_journal_triggers[EXT4_JTR_ORPHAN_FILE].tr_triggers.t_frozen =
+ ext4_orphan_file_block_trigger;
+
+ set_opt(sb, ERRORS_CONT);
+ set_opt(sb, ORDERED_DATA);
+ spin_lock_init(&sbi->s_bdev_wb_lock);
+ spin_lock_init(&sbi->s_error_lock);
+ spin_lock_init(&sbi->s_md_lock);
+ INIT_WORK(&sbi->s_sb_upd_work, orphan_test_sb_upd_work);
+ timer_setup(&sbi->s_err_report, orphan_test_err_timer, 0);
+ 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);
+
+ for (g = 0; g < ORPHAN_TEST_NGROUPS; g++) {
+ struct orphan_test_ibitmap *ibm = &ctx->ibitmaps[g];
+
+ init_rwsem(&ctx->grp_infos[g].alloc_sem);
+ ctx->grp_infos[g].bb_state = 0;
+ ctx->grp_info_ptrs[g] = &ctx->grp_infos[g];
+
+ ext4_block_bitmap_set(sb, &ctx->gdps[g],
+ orphan_test_bbitmap_blk(g));
+ ext4_inode_bitmap_set(sb, &ctx->gdps[g],
+ orphan_test_ibitmap_blk(g));
+ ext4_inode_table_set(sb, &ctx->gdps[g],
+ orphan_test_itable_blk(g));
+
+ ibm->bh.b_data = ibm->buf;
+ ibm->bh.b_size = ORPHAN_TEST_BLK_SIZE;
+ ibm->bh.b_bdev = &ctx->bdev;
+ ibm->bh.b_blocknr = orphan_test_ibitmap_blk(g);
+ atomic_set(&ibm->bh.b_count, 1);
+ if (g == 0) {
+ for (bit = 0; bit < ORPHAN_TEST_FIRST_INO - 1; bit++)
+ ext4_set_bit(bit, ibm->buf);
+ }
+ ext4_mark_bitmap_end(ORPHAN_TEST_INODES_PER_GRP,
+ ORPHAN_TEST_BLK_SIZE * 8, ibm->buf);
+ set_buffer_uptodate(&ibm->bh);
+ set_bitmap_uptodate(&ibm->bh);
+ set_buffer_verified(&ibm->bh);
+ }
+ ctx->grp_info_slice[0] = ctx->grp_info_ptrs;
+ rcu_assign_pointer(sbi->s_group_info, ctx->grp_info_slice);
+
+ root_inode = new_inode(sb);
+ if (!root_inode) {
+ deactivate_locked_super(sb);
+ rcu_barrier();
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(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_op = &simple_dir_inode_operations;
+ set_nlink(root_inode, 2);
+ sb->s_root = d_make_root(root_inode);
+ if (!sb->s_root) {
+ deactivate_locked_super(sb);
+ rcu_barrier();
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(ctx);
+ KUNIT_FAIL_AND_ABORT(test, "failed to create s_root");
+ }
+
+ up_write(&sb->s_umount);
+
+ kunit_activate_static_stub(test, __ext4_journal_get_write_access,
+ orphan_test_get_write_access_stub);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ orphan_test_dirty_metadata_stub);
+ kunit_activate_static_stub(test, ext4_get_inode_loc,
+ orphan_test_get_inode_loc_stub);
+ kunit_activate_static_stub(test, ext4_mark_iloc_dirty,
+ orphan_test_mark_iloc_dirty_stub);
+ kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+ orphan_test_mark_inode_dirty_stub);
+ kunit_activate_static_stub(test, __ext4_iget,
+ orphan_test_iget_stub);
+ kunit_activate_static_stub(test, ext4_getblk,
+ orphan_test_getblk_stub);
+ kunit_activate_static_stub(test, ext4_get_group_desc,
+ orphan_test_get_group_desc_stub);
+ kunit_activate_static_stub(test, ext4_sb_getblk,
+ orphan_test_sb_getblk_stub);
+ kunit_activate_static_stub(test, ext4_free_inode,
+ orphan_test_free_inode_stub);
+ kunit_activate_static_stub(test, ext4_free_blocks,
+ orphan_test_free_blocks_stub);
+
+ err = kunit_add_action_or_reset(test, orphan_test_cleanup_action, ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ ctx->enable_real_evict = true;
+ return 0;
+}
+
+/* =========================================================================
+ * 1. Geometry & Empty-State Helpers
+ * =========================================================================
+ */
+
+struct orphan_blksize_param {
+ const char *desc;
+ unsigned long blocksize;
+ unsigned char blocksize_bits;
+ int expected_inodes_per_ob;
+};
+
+static const struct orphan_blksize_param orphan_blksize_params[] = {
+ { "1K_blocksize", 1024, 10, 254 },
+ { "2K_blocksize", 2048, 11, 510 },
+ { "4K_blocksize", 4096, 12, 1022 },
+ { "64K_blocksize", 65536, 16, 16382 },
+};
+
+KUNIT_ARRAY_PARAM_DESC(orphan_blksize, orphan_blksize_params, desc);
+
+static void test_inodes_per_orphan_block_param(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ const struct orphan_blksize_param *p = test->param_value;
+
+ ctx->sb->s_blocksize = p->blocksize;
+ ctx->sb->s_blocksize_bits = p->blocksize_bits;
+
+ KUNIT_EXPECT_EQ(test, ext4_inodes_per_orphan_block(ctx->sb),
+ p->expected_inodes_per_ob);
+}
+
+static void test_orphan_file_empty(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+
+ /* 1. Feature disabled returns 1 regardless of s_orphan_info */
+ ext4_clear_feature_orphan_file(sb);
+ KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+
+ /* 2. Feature enabled with 0 blocks returns 1 */
+ ext4_set_feature_orphan_file(sb);
+ ctx->sbi.s_orphan_info.of_blocks = 0;
+ KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+
+ /* 3. Multi-block orphan file with all slots free returns 1 */
+ orphan_test_setup_static_orphan_file(ctx, 3);
+ KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+
+ /* 4. Occupied entry in a non-first block makes file non-empty (0) */
+ atomic_set(&ctx->static_binfo[2].ob_free_entries,
+ ORPHAN_TEST_INODES_PER_OB - 1);
+ KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 0);
+
+ /* 5. Restoring free count returns 1 again */
+ atomic_set(&ctx->static_binfo[2].ob_free_entries,
+ ORPHAN_TEST_INODES_PER_OB);
+ KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+}
+
+/* =========================================================================
+ * 2. Superblock Linked-List ext4_orphan_add()
+ * =========================================================================
+ */
+
+static void test_orphan_add_early_exits_and_idempotency(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct orphan_test_inode_slot *s1, *s2, *s3, *s4;
+ int err;
+
+ s1 = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+ s2 = orphan_test_create_inode(test, ctx, 16, S_IFREG | 0644, 0);
+ s3 = orphan_test_create_inode(test, ctx, 17, S_IFREG | 0644, 0);
+ s4 = orphan_test_create_inode(test, ctx, 18, S_IFREG | 0644, 0);
+
+ /* 1. !sbi->s_journal -> no-op returning 0 */
+ ctx->sbi.s_journal = NULL;
+ err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+ KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+ ctx->sbi.s_journal = &ctx->mock_journal;
+
+ /* 2. is_bad_inode(inode) -> no-op returning 0 */
+ make_bad_inode(s2->inode);
+ err = orphan_test_call_add(&ctx->mock_handle, s2->inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s2->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+
+ /* 3. Idempotent when already on sbi->s_orphan list */
+ err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+
+ orphan_test_reset_faults(ctx);
+ err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+ KUNIT_EXPECT_EQ(test, ctx->dirty_metadata_calls, 0U);
+
+ /* 4. Idempotent when already tracked via EXT4_STATE_ORPHAN_FILE */
+ ext4_set_inode_state(s3->inode, EXT4_STATE_ORPHAN_FILE);
+ EXT4_I(s3->inode)->i_orphan_idx = 0;
+ err = orphan_test_call_add(&ctx->mock_handle, s3->inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+
+ /* 5. I_NEW inode can be added & deleted without holding i_rwsem */
+ spin_lock(&s4->inode->i_lock);
+ inode_state_set(s4->inode, I_NEW);
+ spin_unlock(&s4->inode->i_lock);
+
+ err = ext4_orphan_add(&ctx->mock_handle, s4->inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 18U);
+
+ err = ext4_orphan_del(&ctx->mock_handle, s4->inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+
+ spin_lock(&s4->inode->i_lock);
+ inode_state_clear(s4->inode, I_NEW);
+ spin_unlock(&s4->inode->i_lock);
+}
+
+static void test_orphan_add_list_chaining_and_csum(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct orphan_test_inode_slot *s1, *s2, *s3;
+ __le32 csum_before;
+ int err;
+
+ s1 = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+ s2 = orphan_test_create_inode(test, ctx, 20, S_IFDIR | 0755, 0);
+ s3 = orphan_test_create_inode(test, ctx, 25, S_IFLNK | 0777, 0);
+
+ /* Corrupt s_checksum first so we verify ext4_superblock_csum_set() */
+ ctx->es.s_checksum = 0;
+ csum_before = ctx->es.s_checksum;
+
+ err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s1->inode), 0U);
+ KUNIT_EXPECT_NE(test, ctx->es.s_checksum, csum_before);
+ KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+ err = orphan_test_call_add(&ctx->mock_handle, s2->inode);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 20U);
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s2->inode), 15U);
+
+ err = orphan_test_call_add(&ctx->mock_handle, s3->inode);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 25U);
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s3->inode), 20U);
+
+ /* In-memory list head order must match LIFO insertion: s3 -> s2 -> s1 */
+ KUNIT_EXPECT_PTR_EQ(test, ctx->sbi.s_orphan.next,
+ &EXT4_I(s3->inode)->i_orphan);
+ KUNIT_EXPECT_PTR_EQ(test, EXT4_I(s3->inode)->i_orphan.next,
+ &EXT4_I(s2->inode)->i_orphan);
+ KUNIT_EXPECT_PTR_EQ(test, EXT4_I(s2->inode)->i_orphan.next,
+ &EXT4_I(s1->inode)->i_orphan);
+}
+
+static void test_orphan_add_next_orphan_bounds(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct orphan_test_inode_slot *s_valid, *s_oob;
+ int err;
+
+ s_valid = orphan_test_create_inode(test, ctx, 18, S_IFREG | 0644, 0);
+ s_oob = orphan_test_create_inode(test, ctx, 19, S_IFREG | 0644, 0);
+
+ /*
+ * 1. NEXT_ORPHAN == s_inodes_count (valid upper boundary):
+ * inode is treated as already on-disk; skip on-disk modification,
+ * link into in-memory s_orphan, and brelse(iloc.bh).
+ */
+ NEXT_ORPHAN(s_valid->inode) = ORPHAN_TEST_TOTAL_INODES;
+ ctx->es.s_last_orphan = cpu_to_le32(15);
+ orphan_test_reset_faults(ctx);
+
+ err = orphan_test_call_add(&ctx->mock_handle, s_valid->inode);
+
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test, list_empty(&EXT4_I(s_valid->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s_valid->inode),
+ ORPHAN_TEST_TOTAL_INODES);
+ KUNIT_EXPECT_EQ(test, ctx->dirty_metadata_calls, 0U);
+ KUNIT_EXPECT_EQ(test, ctx->mark_iloc_calls, 0U);
+ KUNIT_EXPECT_EQ(test, atomic_read(&s_valid->iloc_bh.b_count), 1);
+
+ /*
+ * 2. NEXT_ORPHAN == s_inodes_count + 1 (invalid / garbage):
+ * must overwrite NEXT_ORPHAN with old s_last_orphan (15), update
+ * s_last_orphan to 19, and dirty both sb and iloc.
+ */
+ NEXT_ORPHAN(s_oob->inode) = ORPHAN_TEST_TOTAL_INODES + 1;
+ orphan_test_reset_faults(ctx);
+
+ err = orphan_test_call_add(&ctx->mock_handle, s_oob->inode);
+
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test, list_empty(&EXT4_I(s_oob->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 19U);
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s_oob->inode), 15U);
+ KUNIT_EXPECT_EQ(test, ctx->dirty_metadata_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->mark_iloc_calls, 1U);
+ KUNIT_EXPECT_EQ(test, atomic_read(&s_oob->iloc_bh.b_count), 1);
+}
+
+static void test_orphan_add_list_error_paths(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct orphan_test_inode_slot *s1, *s2;
+ int err;
+
+ s1 = orphan_test_create_inode(test, ctx, 21, S_IFREG | 0644, 0);
+ s2 = orphan_test_create_inode(test, ctx, 22, S_IFREG | 0644, 0);
+
+ /* 1. ext4_journal_get_write_access(s_sbh) fails */
+ orphan_test_reset_faults(ctx);
+ ctx->fail_write_access_err = -EIO;
+ ctx->fail_write_access_on_call = 1;
+ err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+ KUNIT_EXPECT_NE(test, ctx->sbi.s_mount_state & EXT4_ERROR_FS, 0);
+
+ /* 2. ext4_reserve_inode_write (ext4_get_inode_loc) fails */
+ orphan_test_reset_faults(ctx);
+ ctx->fail_get_iloc_err = -EIO;
+ err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+ /*
+ * 3. ext4_handle_dirty_metadata(s_sbh) fails while ext4_mark_iloc_dirty
+ * succeeds: must still consume iloc.bh and unlink from s_orphan!
+ */
+ orphan_test_reset_faults(ctx);
+ ctx->fail_dirty_metadata_err = -EIO;
+ err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_EQ(test, ctx->mark_iloc_calls, 1U);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+ /*
+ * 4. ext4_handle_dirty_metadata(s_sbh) succeeds while
+ * ext4_mark_iloc_dirty fails: err = rc and rollback from s_orphan.
+ */
+ orphan_test_reset_faults(ctx);
+ ctx->es.s_last_orphan = 0;
+ ctx->fail_mark_iloc_err = -ENOSPC;
+ err = orphan_test_call_add(&ctx->mock_handle, s2->inode);
+ KUNIT_EXPECT_EQ(test, err, -ENOSPC);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s2->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, atomic_read(&s2->iloc_bh.b_count), 1);
+}
+
+/* =========================================================================
+ * 3. Superblock Linked-List ext4_orphan_del()
+ * =========================================================================
+ */
+
+static void test_orphan_del_early_exits(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct orphan_test_inode_slot *s1, *s_untracked;
+ int err;
+
+ s1 = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+ s_untracked = orphan_test_create_inode(test, ctx, 16, S_IFREG | 0644, 0);
+
+ /* 1. Untracked inode is a fast no-op */
+ orphan_test_reset_faults(ctx);
+ err = orphan_test_call_del(&ctx->mock_handle, s_untracked->inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->get_iloc_calls, 0U);
+
+ /* Add s1 to orphan list */
+ err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ /* 2. !s_journal && !(s_mount_state & EXT4_ORPHAN_FS) -> early return 0 */
+ ctx->sbi.s_journal = NULL;
+ ctx->sbi.s_mount_state &= ~EXT4_ORPHAN_FS;
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+
+ /* 3. !s_journal WITH EXT4_ORPHAN_FS proceeds with full deletion */
+ ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS;
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+}
+
+static void test_orphan_del_list_positions_and_null_handle(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct orphan_test_inode_slot *s1, *s2, *s3;
+ int err;
+
+ s1 = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+ s2 = orphan_test_create_inode(test, ctx, 20, S_IFREG | 0644, 0);
+ s3 = orphan_test_create_inode(test, ctx, 25, S_IFREG | 0644, 0);
+
+ /* Build chain: s_last_orphan -> 25 (s3) -> 20 (s2) -> 15 (s1) -> 0 */
+ KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+ 0);
+ KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s2->inode),
+ 0);
+ KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s3->inode),
+ 0);
+
+ /* 1. Delete middle entry (s2 = 20): s3->NEXT_ORPHAN becomes 15 */
+ err = orphan_test_call_del(&ctx->mock_handle, s2->inode);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s2->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s2->inode), 0U);
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s3->inode), 15U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 25U);
+
+ /* 2. Delete tail entry (s1 = 15): s3->NEXT_ORPHAN becomes 0 */
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s1->inode), 0U);
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s3->inode), 0U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 25U);
+
+ /* 3. Delete head entry (s3 = 25): s_last_orphan becomes 0 */
+ err = orphan_test_call_del(&ctx->mock_handle, s3->inode);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->sbi.s_orphan));
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s3->inode), 0U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+
+ /* 4. handle == NULL removes in-core entry without on-disk updates */
+ KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+ 0);
+ orphan_test_reset_faults(ctx);
+
+ err = orphan_test_call_del(NULL, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+ KUNIT_EXPECT_EQ(test, ctx->get_iloc_calls, 0U);
+ KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+}
+
+static void test_orphan_del_list_error_paths(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct orphan_test_inode_slot *s1, *s2;
+ int err;
+
+ s1 = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+ s2 = orphan_test_create_inode(test, ctx, 20, S_IFREG | 0644, 0);
+
+ /* 1. Initial ext4_reserve_inode_write fails -> still unlinks in-core */
+ KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+ 0);
+ orphan_test_reset_faults(ctx);
+ ctx->fail_get_iloc_err = -EIO;
+ ctx->fail_get_iloc_on_call = 1;
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+ /* 2. Head delete: ext4_journal_get_write_access(s_sbh) fails */
+ ctx->es.s_last_orphan = 0;
+ NEXT_ORPHAN(s1->inode) = 0;
+ orphan_test_reset_faults(ctx);
+ KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+ 0);
+ orphan_test_reset_faults(ctx);
+ /* Call 1 is reserve_inode_write(s1); call 2 is get_write_access(s_sbh) */
+ ctx->fail_write_access_err = -EIO;
+ ctx->fail_write_access_on_call = 2;
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+ KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+ /* 3. Head delete: ext4_handle_dirty_metadata(s_sbh) fails */
+ ctx->es.s_last_orphan = 0;
+ NEXT_ORPHAN(s1->inode) = 0;
+ orphan_test_reset_faults(ctx);
+ KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+ 0);
+ orphan_test_reset_faults(ctx);
+ ctx->fail_dirty_metadata_err = -EIO;
+ ctx->fail_dirty_metadata_on_call = 1;
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+ /* 4. Non-head delete: ext4_reserve_inode_write(i_prev) fails */
+ ctx->es.s_last_orphan = 0;
+ NEXT_ORPHAN(s1->inode) = 0;
+ NEXT_ORPHAN(s2->inode) = 0;
+ orphan_test_reset_faults(ctx);
+ KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+ 0);
+ KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s2->inode),
+ 0);
+
+ orphan_test_reset_faults(ctx);
+ /* Call 1 is s1 iloc; call 2 is i_prev (s2) iloc */
+ ctx->fail_get_iloc_err = -EIO;
+ ctx->fail_get_iloc_on_call = 2;
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s2->inode), 15U);
+ KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+ KUNIT_EXPECT_EQ(test, atomic_read(&s2->iloc_bh.b_count), 1);
+
+ /* 5. Non-head delete: ext4_mark_iloc_dirty(i_prev) fails */
+ list_add(&EXT4_I(s1->inode)->i_orphan, &EXT4_I(s2->inode)->i_orphan);
+ orphan_test_reset_faults(ctx);
+ ctx->fail_mark_iloc_err = -EIO;
+ ctx->fail_mark_iloc_on_call = 1;
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+ KUNIT_EXPECT_EQ(test, atomic_read(&s2->iloc_bh.b_count), 1);
+
+ /* 6. Final ext4_mark_iloc_dirty(inode) fails on head delete */
+ orphan_test_reset_faults(ctx);
+ ctx->fail_mark_iloc_err = -ENOSPC;
+ ctx->fail_mark_iloc_on_call = 1;
+ err = orphan_test_call_del(&ctx->mock_handle, s2->inode);
+ KUNIT_EXPECT_EQ(test, err, -ENOSPC);
+ KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s2->inode), 0U);
+ KUNIT_EXPECT_EQ(test, atomic_read(&s2->iloc_bh.b_count), 1);
+}
+
+/* =========================================================================
+ * 4. Orphan-File ext4_orphan_add() & ext4_orphan_del()
+ * =========================================================================
+ */
+
+static void test_orphan_file_add_and_del_basic(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct orphan_test_inode_slot *s1;
+ unsigned int blk, off;
+ __le32 *bdata;
+ int err;
+
+ orphan_test_setup_static_orphan_file(ctx, 2);
+ s1 = orphan_test_create_inode(test, ctx, 24, S_IFREG | 0644, 0);
+
+ err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+ KUNIT_EXPECT_TRUE(test, ext4_inode_orphan_tracked(s1->inode));
+ KUNIT_EXPECT_EQ(test, ctx->last_trigger_type, EXT4_JTR_ORPHAN_FILE);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+ KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 0);
+
+ blk = EXT4_I(s1->inode)->i_orphan_idx / ORPHAN_TEST_INODES_PER_OB;
+ off = EXT4_I(s1->inode)->i_orphan_idx % ORPHAN_TEST_INODES_PER_OB;
+ KUNIT_ASSERT_LT(test, blk, 2U);
+ KUNIT_EXPECT_EQ(test, off, 0U);
+ bdata = (__le32 *)ctx->static_binfo[blk].ob_bh->b_data;
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata[off]), 24U);
+ KUNIT_EXPECT_EQ(test,
+ atomic_read(&ctx->static_binfo[blk].ob_free_entries),
+ (int)(ORPHAN_TEST_INODES_PER_OB - 1));
+
+ /* Delete from orphan file and verify slot & state restoration */
+ orphan_test_reset_faults(ctx);
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+ KUNIT_EXPECT_FALSE(test, ext4_inode_orphan_tracked(s1->inode));
+ KUNIT_EXPECT_EQ(test, ctx->last_trigger_type, EXT4_JTR_ORPHAN_FILE);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata[off]), 0U);
+ KUNIT_EXPECT_EQ(test,
+ atomic_read(&ctx->static_binfo[blk].ob_free_entries),
+ (int)ORPHAN_TEST_INODES_PER_OB);
+ KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+}
+
+static void test_orphan_file_add_block_wraparound_and_last_slot(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct orphan_test_inode_slot *s1, *s2;
+ __le32 *bdata;
+ unsigned int j;
+ int b, err;
+
+ orphan_test_setup_static_orphan_file(ctx, 3);
+ s1 = orphan_test_create_inode(test, ctx, 30, S_IFREG | 0644, 0);
+ s2 = orphan_test_create_inode(test, ctx, 31, S_IFREG | 0644, 0);
+
+ /*
+ * Mark blocks 0 and 1 full (0 free), and leave ONLY the very last slot
+ * (ORPHAN_TEST_INODES_PER_OB - 1 = 253) free in block 2. Then test a
+ * second add when only block 0 has 1 slot free, guaranteeing
+ * wrap-around across the circular block ring regardless of CPU id.
+ */
+ atomic_set(&ctx->static_binfo[0].ob_free_entries, 0);
+ atomic_set(&ctx->static_binfo[1].ob_free_entries, 0);
+ atomic_set(&ctx->static_binfo[2].ob_free_entries, 1);
+ bdata = (__le32 *)ctx->static_binfo[2].ob_bh->b_data;
+ for (j = 0; j < ORPHAN_TEST_INODES_PER_OB - 1; j++)
+ bdata[j] = cpu_to_le32(100 + j);
+ bdata[ORPHAN_TEST_INODES_PER_OB - 1] = 0;
+
+ err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, EXT4_I(s1->inode)->i_orphan_idx,
+ 2 * ORPHAN_TEST_INODES_PER_OB +
+ (ORPHAN_TEST_INODES_PER_OB - 1));
+ KUNIT_EXPECT_EQ(test,
+ le32_to_cpu(bdata[ORPHAN_TEST_INODES_PER_OB - 1]), 30U);
+ KUNIT_EXPECT_EQ(test,
+ atomic_read(&ctx->static_binfo[2].ob_free_entries), 0);
+
+ /* Now make only block 0 slot (ORPHAN_TEST_INODES_PER_OB - 1) free */
+ for (b = 0; b < 3; b++)
+ atomic_set(&ctx->static_binfo[b].ob_free_entries, 0);
+ atomic_set(&ctx->static_binfo[0].ob_free_entries, 1);
+ bdata = (__le32 *)ctx->static_binfo[0].ob_bh->b_data;
+ for (j = 0; j < ORPHAN_TEST_INODES_PER_OB - 1; j++)
+ bdata[j] = cpu_to_le32(100 + j);
+ bdata[ORPHAN_TEST_INODES_PER_OB - 1] = 0;
+
+ err = orphan_test_call_add(&ctx->mock_handle, s2->inode);
+
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, EXT4_I(s2->inode)->i_orphan_idx,
+ ORPHAN_TEST_INODES_PER_OB - 1);
+ KUNIT_EXPECT_EQ(test,
+ le32_to_cpu(bdata[ORPHAN_TEST_INODES_PER_OB - 1]), 31U);
+}
+
+static void test_orphan_file_add_enospc_fallback_and_write_err(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct orphan_test_inode_slot *s_fb, *s_err, *s_dirty_err;
+ int err;
+
+ orphan_test_setup_static_orphan_file(ctx, 2);
+ s_fb = orphan_test_create_inode(test, ctx, 32, S_IFREG | 0644, 0);
+ s_err = orphan_test_create_inode(test, ctx, 33, S_IFREG | 0644, 0);
+ s_dirty_err = orphan_test_create_inode(test, ctx, 34, S_IFREG | 0644, 0);
+
+ /*
+ * 1. Full orphan file (all ob_free_entries == 0):
+ * ext4_orphan_file_add() returns -ENOSPC, and ext4_orphan_add()
+ * falls back to the superblock linked list!
+ */
+ atomic_set(&ctx->static_binfo[0].ob_free_entries, 0);
+ atomic_set(&ctx->static_binfo[1].ob_free_entries, 0);
+
+ err = orphan_test_call_add(&ctx->mock_handle, s_fb->inode);
+
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(s_fb->inode,
+ EXT4_STATE_ORPHAN_FILE));
+ KUNIT_EXPECT_FALSE(test, list_empty(&EXT4_I(s_fb->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 32U);
+
+ /*
+ * 2. Non-ENOSPC error (-EIO from ext4_journal_get_write_access):
+ * must restore ob_free_entries via atomic_inc and return -EIO
+ * directly WITHOUT falling back to the superblock linked list!
+ */
+ atomic_set(&ctx->static_binfo[0].ob_free_entries, 5);
+ atomic_set(&ctx->static_binfo[1].ob_free_entries, 0);
+ orphan_test_reset_faults(ctx);
+ ctx->fail_write_access_err = -EIO;
+ ctx->fail_write_access_on_call = 1;
+
+ err = orphan_test_call_add(&ctx->mock_handle, s_err->inode);
+
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_EQ(test,
+ atomic_read(&ctx->static_binfo[0].ob_free_entries), 5);
+ KUNIT_EXPECT_FALSE(test, ext4_inode_orphan_tracked(s_err->inode));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 32U);
+
+ /*
+ * 3. Non-ENOSPC error (-EIO from ext4_handle_dirty_metadata):
+ * propagates -EIO directly without falling back to s_last_orphan.
+ */
+ orphan_test_reset_faults(ctx);
+ ctx->fail_dirty_metadata_err = -EIO;
+ ctx->fail_dirty_metadata_on_call = 1;
+
+ err = orphan_test_call_add(&ctx->mock_handle, s_dirty_err->inode);
+
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_state(s_dirty_err->inode,
+ EXT4_STATE_ORPHAN_FILE));
+ KUNIT_EXPECT_PTR_EQ(test, ctx->sbi.s_orphan.next,
+ &EXT4_I(s_fb->inode)->i_orphan);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 32U);
+}
+
+static void test_orphan_file_del_edge_and_error_paths(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct orphan_test_inode_slot *s1;
+ __le32 *bdata;
+ int err;
+
+ orphan_test_setup_static_orphan_file(ctx, 2);
+ s1 = orphan_test_create_inode(test, ctx, 35, S_IFREG | 0644, 0);
+ bdata = (__le32 *)ctx->static_binfo[1].ob_bh->b_data;
+
+ /* 1. handle == NULL clears in-core state without touching orphan block */
+ bdata[4] = cpu_to_le32(35);
+ atomic_set(&ctx->static_binfo[1].ob_free_entries,
+ ORPHAN_TEST_INODES_PER_OB - 1);
+ EXT4_I(s1->inode)->i_orphan_idx = 1 * ORPHAN_TEST_INODES_PER_OB + 4;
+ ext4_set_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE);
+
+ err = orphan_test_call_del(NULL, s1->inode);
+
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+ KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata[4]), 35U);
+ KUNIT_EXPECT_EQ(test,
+ atomic_read(&ctx->static_binfo[1].ob_free_entries),
+ (int)(ORPHAN_TEST_INODES_PER_OB - 1));
+
+ /* 2. Out-of-range blk >= oi->of_blocks hits WARN_ON_ONCE guard safely */
+ EXT4_I(s1->inode)->i_orphan_idx = 2 * ORPHAN_TEST_INODES_PER_OB;
+ ext4_set_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE);
+ orphan_test_reset_faults(ctx);
+
+ kunit_warning_suppress(test) {
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+ }
+
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+ KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+
+ /* 3. ext4_journal_get_write_access error leaves bdata[off] intact */
+ EXT4_I(s1->inode)->i_orphan_idx = 1 * ORPHAN_TEST_INODES_PER_OB + 4;
+ ext4_set_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE);
+ orphan_test_reset_faults(ctx);
+ ctx->fail_write_access_err = -EIO;
+
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata[4]), 35U);
+ KUNIT_EXPECT_EQ(test,
+ atomic_read(&ctx->static_binfo[1].ob_free_entries),
+ (int)(ORPHAN_TEST_INODES_PER_OB - 1));
+
+ /* 4. ext4_handle_dirty_metadata error propagates after clearing slot */
+ EXT4_I(s1->inode)->i_orphan_idx = 1 * ORPHAN_TEST_INODES_PER_OB + 4;
+ ext4_set_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE);
+ orphan_test_reset_faults(ctx);
+ ctx->fail_dirty_metadata_err = -ENOSPC;
+
+ err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+
+ KUNIT_EXPECT_EQ(test, err, -ENOSPC);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata[4]), 0U);
+ KUNIT_EXPECT_EQ(test,
+ atomic_read(&ctx->static_binfo[1].ob_free_entries),
+ (int)ORPHAN_TEST_INODES_PER_OB);
+}
+
+/* =========================================================================
+ * 5. Trigger, Init & Release of Orphan File Info
+ * =========================================================================
+ */
+
+static void test_orphan_file_block_trigger_and_frozen_data(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct jbd2_buffer_trigger_type *triggers =
+ &ctx->sbi.s_journal_triggers[EXT4_JTR_ORPHAN_FILE].tr_triggers;
+ struct buffer_head *bh;
+ struct ext4_orphan_block_tail *ot;
+ __le32 *bdata, *fdata;
+ __u32 expected_csum;
+
+ orphan_test_setup_static_orphan_file(ctx, 1);
+ bh = ctx->static_binfo[0].ob_bh;
+ ot = (struct ext4_orphan_block_tail *)(bh->b_data + sb->s_blocksize -
+ sizeof(*ot));
+ bdata = (__le32 *)bh->b_data;
+ bdata[0] = cpu_to_le32(42);
+ ot->ob_checksum = 0;
+
+ /* 1. Trigger with data == bh->b_data computes valid block checksum */
+ ext4_orphan_file_block_trigger(triggers, bh, bh->b_data, bh->b_size);
+ expected_csum = orphan_test_calc_ob_csum(sb, ORPHAN_TEST_OF_CSUM_SEED,
+ bh->b_blocknr, bh->b_data);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ot->ob_checksum), expected_csum);
+
+ /* 2. Trigger with separate JBD2 frozen buffer hashes @data, not bh */
+ memset(ctx->frozen_buf, 0, sizeof(ctx->frozen_buf));
+ fdata = (__le32 *)ctx->frozen_buf;
+ fdata[0] = cpu_to_le32(99);
+ fdata[10] = cpu_to_le32(105);
+
+ ext4_orphan_file_block_trigger(triggers, bh, ctx->frozen_buf,
+ bh->b_size);
+ expected_csum = orphan_test_calc_ob_csum(sb, ORPHAN_TEST_OF_CSUM_SEED,
+ bh->b_blocknr,
+ ctx->frozen_buf);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ot->ob_checksum), expected_csum);
+}
+
+static void test_init_and_release_orphan_info_lifecycle(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_orphan_info *oi = &ctx->sbi.s_orphan_info;
+ struct orphan_test_inode_slot *of_slot;
+ struct ext4_orphan_block_tail *ot;
+ __le32 *bdata;
+ int i, err;
+
+ /* 1. Feature disabled: init and release are clean no-ops */
+ ext4_clear_feature_orphan_file(sb);
+ err = ext4_init_orphan_info(sb);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, oi->of_blocks, 0);
+ KUNIT_EXPECT_NULL(test, oi->of_binfo);
+ ext4_release_orphan_info(sb);
+
+ /* 2. Valid 2-block orphan file load and release */
+ ext4_set_feature_orphan_file(sb);
+ of_slot = orphan_test_create_inode(test, ctx, ORPHAN_TEST_ORPHAN_INO,
+ S_IFREG | 0600, 1);
+ of_slot->inode->i_size = 2 * ORPHAN_TEST_BLK_SIZE;
+ EXT4_I(of_slot->inode)->i_csum_seed = ORPHAN_TEST_OF_CSUM_SEED;
+
+ for (i = 0; i < 2; i++) {
+ struct orphan_test_ob_blk *oblk = &ctx->ob_pool[i];
+
+ memset(oblk, 0, sizeof(*oblk));
+ oblk->present = true;
+ oblk->bh.b_data = oblk->buf;
+ oblk->bh.b_size = sb->s_blocksize;
+ oblk->bh.b_bdev = &ctx->bdev;
+ oblk->bh.b_blocknr = ORPHAN_TEST_OB_BASE_BLK + i;
+ atomic_set(&oblk->bh.b_count, 1);
+ set_buffer_uptodate(&oblk->bh);
+ }
+ /* Populate 3 active entries in block 0, 0 entries in block 1 */
+ bdata = (__le32 *)ctx->ob_pool[0].buf;
+ bdata[0] = cpu_to_le32(20);
+ bdata[5] = cpu_to_le32(21);
+ bdata[253] = cpu_to_le32(22);
+ orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED,
+ &ctx->ob_pool[0].bh, EXT4_ORPHAN_BLOCK_MAGIC);
+ orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED,
+ &ctx->ob_pool[1].bh, EXT4_ORPHAN_BLOCK_MAGIC);
+
+ err = ext4_init_orphan_info(sb);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ ctx->orphan_info_allocated = true;
+
+ KUNIT_EXPECT_EQ(test, oi->of_blocks, 2);
+ KUNIT_EXPECT_EQ(test, oi->of_csum_seed, ORPHAN_TEST_OF_CSUM_SEED);
+ KUNIT_ASSERT_NOT_NULL(test, oi->of_binfo);
+ KUNIT_EXPECT_EQ(test, atomic_read(&oi->of_binfo[0].ob_free_entries),
+ (int)(ORPHAN_TEST_INODES_PER_OB - 3));
+ KUNIT_EXPECT_EQ(test, atomic_read(&oi->of_binfo[1].ob_free_entries),
+ (int)ORPHAN_TEST_INODES_PER_OB);
+ KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 0);
+ KUNIT_EXPECT_EQ(test, atomic_read(&of_slot->inode->i_count), 1);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 2);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[1].bh.b_count), 2);
+
+ /* Release drops every block bh reference and frees of_binfo */
+ ext4_release_orphan_info(sb);
+ ctx->orphan_info_allocated = false;
+ memset(oi, 0, sizeof(*oi));
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 1);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[1].bh.b_count), 1);
+
+ /*
+ * 3. With metadata_csum disabled, ext4_orphan_file_block_csum_verify()
+ * returns 1 even if ob_checksum does not match.
+ */
+ ext4_clear_feature_metadata_csum(sb);
+ of_slot->inode->i_size = ORPHAN_TEST_BLK_SIZE;
+ ot = (struct ext4_orphan_block_tail *)(ctx->ob_pool[0].buf +
+ sb->s_blocksize - sizeof(*ot));
+ ot->ob_checksum = cpu_to_le32(0xdeadbeefU);
+
+ err = ext4_init_orphan_info(sb);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ ctx->orphan_info_allocated = true;
+
+ ext4_release_orphan_info(sb);
+ ctx->orphan_info_allocated = false;
+ memset(oi, 0, sizeof(*oi));
+}
+
+static void test_init_orphan_info_max_blocks_boundary(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_orphan_info *oi = &ctx->sbi.s_orphan_info;
+ struct orphan_test_inode_slot *of_slot;
+ int err;
+
+ ext4_set_feature_orphan_file(sb);
+ of_slot = orphan_test_create_inode(test, ctx, ORPHAN_TEST_ORPHAN_INO,
+ S_IFREG | 0600, 1);
+ EXT4_I(of_slot->inode)->i_csum_seed = ORPHAN_TEST_OF_CSUM_SEED;
+
+ /* 1. Size 1 byte over 512 blocks (EXT4_MAX_ORPHAN_FILE_BLOCKS) fails */
+ of_slot->inode->i_size = (512LL << ORPHAN_TEST_BLK_BITS) + 1;
+ err = ext4_init_orphan_info(sb);
+ KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, atomic_read(&of_slot->inode->i_count), 1);
+
+ /* 2. Exact 512-block limit succeeds and releases all 512 bh refs */
+ memset(ctx->boundary_buf, 0, sizeof(ctx->boundary_buf));
+ ctx->boundary_bh.b_data = ctx->boundary_buf;
+ ctx->boundary_bh.b_size = sb->s_blocksize;
+ ctx->boundary_bh.b_bdev = &ctx->bdev;
+ ctx->boundary_bh.b_blocknr = ORPHAN_TEST_OB_BASE_BLK;
+ atomic_set(&ctx->boundary_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->boundary_bh);
+ orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED,
+ &ctx->boundary_bh, EXT4_ORPHAN_BLOCK_MAGIC);
+
+ ctx->use_boundary_bh = true;
+ of_slot->inode->i_size = 512LL << ORPHAN_TEST_BLK_BITS;
+
+ err = ext4_init_orphan_info(sb);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ ctx->orphan_info_allocated = true;
+ KUNIT_EXPECT_EQ(test, oi->of_blocks, 512);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->boundary_bh.b_count), 513);
+
+ ext4_release_orphan_info(sb);
+ ctx->orphan_info_allocated = false;
+ memset(oi, 0, sizeof(*oi));
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->boundary_bh.b_count), 1);
+}
+
+static void test_init_orphan_info_error_paths_and_cleanup(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_orphan_info *oi = &ctx->sbi.s_orphan_info;
+ struct orphan_test_inode_slot *of_slot;
+ struct ext4_orphan_block_tail *ot;
+ int i, err;
+
+ ext4_set_feature_orphan_file(sb);
+
+ /* 1. ext4_iget failure returns error immediately */
+ ctx->fail_iget_err = -ENOENT;
+ err = ext4_init_orphan_info(sb);
+ KUNIT_EXPECT_EQ(test, err, -ENOENT);
+ orphan_test_reset_faults(ctx);
+
+ /* Prepare 2-block orphan inode where block 0 loads cleanly */
+ of_slot = orphan_test_create_inode(test, ctx, ORPHAN_TEST_ORPHAN_INO,
+ S_IFREG | 0600, 1);
+ of_slot->inode->i_size = 2 * ORPHAN_TEST_BLK_SIZE;
+ EXT4_I(of_slot->inode)->i_csum_seed = ORPHAN_TEST_OF_CSUM_SEED;
+
+ for (i = 0; i < 2; i++) {
+ struct orphan_test_ob_blk *oblk = &ctx->ob_pool[i];
+
+ memset(oblk, 0, sizeof(*oblk));
+ oblk->present = true;
+ oblk->bh.b_data = oblk->buf;
+ oblk->bh.b_size = sb->s_blocksize;
+ oblk->bh.b_bdev = &ctx->bdev;
+ oblk->bh.b_blocknr = ORPHAN_TEST_OB_BASE_BLK + i;
+ atomic_set(&oblk->bh.b_count, 1);
+ set_buffer_uptodate(&oblk->bh);
+ orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED, &oblk->bh,
+ EXT4_ORPHAN_BLOCK_MAGIC);
+ }
+
+ /* 2. Block 1 ext4_bread returns ERR_PTR(-EIO) -> block 0 brelse'd */
+ ctx->ob_pool[1].bread_err = -EIO;
+ err = ext4_init_orphan_info(sb);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 1);
+ KUNIT_EXPECT_EQ(test, atomic_read(&of_slot->inode->i_count), 1);
+ memset(oi, 0, sizeof(*oi));
+ ctx->ob_pool[1].bread_err = 0;
+
+ /* 3. Block 1 ext4_bread returns NULL (unmapped hole) -> -EIO */
+ ctx->ob_pool[1].return_hole = true;
+ err = ext4_init_orphan_info(sb);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 1);
+ memset(oi, 0, sizeof(*oi));
+ ctx->ob_pool[1].return_hole = false;
+
+ /* 4. Block 1 has invalid ob_magic -> ext4_error + -EIO + brelse */
+ orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED,
+ &ctx->ob_pool[1].bh, 0xbad0bad0U);
+ orphan_test_reset_faults(ctx);
+ err = ext4_init_orphan_info(sb);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_NE(test, ctx->sbi.s_mount_state & EXT4_ERROR_FS, 0);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 1);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[1].bh.b_count), 1);
+ memset(oi, 0, sizeof(*oi));
+
+ /* 5. Block 1 has valid magic but corrupted ob_checksum -> -EIO */
+ orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED,
+ &ctx->ob_pool[1].bh, EXT4_ORPHAN_BLOCK_MAGIC);
+ ot = (struct ext4_orphan_block_tail *)(ctx->ob_pool[1].buf +
+ sb->s_blocksize - sizeof(*ot));
+ ot->ob_checksum = cpu_to_le32(le32_to_cpu(ot->ob_checksum) ^ 0x1U);
+ orphan_test_reset_faults(ctx);
+
+ err = ext4_init_orphan_info(sb);
+ KUNIT_EXPECT_EQ(test, err, -EIO);
+ KUNIT_EXPECT_NE(test, ctx->sbi.s_mount_state & EXT4_ERROR_FS, 0);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 1);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[1].bh.b_count), 1);
+ memset(oi, 0, sizeof(*oi));
+}
+
+/* =========================================================================
+ * 6. End-to-End Orphan Recovery: ext4_orphan_cleanup() & ext4_process_orphan()
+ * =========================================================================
+ */
+
+static void test_orphan_cleanup_skip_guards(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ __le32 saved_ro_compat;
+
+ /* 1. Empty s_last_orphan and empty orphan file -> fast return */
+ ctx->es.s_last_orphan = 0;
+ ext4_orphan_cleanup(sb, &ctx->es);
+ KUNIT_EXPECT_EQ(test, ctx->iget_calls, 0U);
+
+ /* 2. Read-only block device skips orphan cleanup */
+ ctx->es.s_last_orphan = cpu_to_le32(15);
+ bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+ ext4_orphan_cleanup(sb, &ctx->es);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+ KUNIT_EXPECT_EQ(test, ctx->iget_calls, 0U);
+ bdev_clear_flag(&ctx->bdev, BD_READ_ONLY);
+
+ /* 3. Unknown ROCOMPAT feature fails ext4_feature_set_ok(sb, 0) */
+ saved_ro_compat = ctx->es.s_feature_ro_compat;
+ ctx->es.s_feature_ro_compat |= cpu_to_le32(0x80000000U);
+ ext4_orphan_cleanup(sb, &ctx->es);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+ KUNIT_EXPECT_EQ(test, ctx->iget_calls, 0U);
+ ctx->es.s_feature_ro_compat = saved_ro_compat;
+
+ /* 4a. EXT4_ERROR_FS on SB_RDONLY mount preserves s_last_orphan */
+ ctx->sbi.s_mount_state |= EXT4_ERROR_FS;
+ sb->s_flags |= SB_RDONLY;
+ ext4_orphan_cleanup(sb, &ctx->es);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+ sb->s_flags &= ~SB_RDONLY;
+
+ /* 4b. EXT4_ERROR_FS on R/W mount clears s_last_orphan to 0 */
+ ext4_orphan_cleanup(sb, &ctx->es);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+ ctx->sbi.s_mount_state &= ~EXT4_ERROR_FS;
+}
+
+static void
+test_orphan_cleanup_linked_list_unlinked_and_truncated(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct orphan_test_inode_slot *s_unlinked, *s_trunc, *s_symlink;
+
+ /*
+ * Match ext4_fill_super() no-journal recovery state:
+ * s_journal = NULL, s_mount_state |= EXT4_ORPHAN_FS.
+ */
+ ctx->sbi.s_journal = NULL;
+ ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS;
+ set_opt(sb, DEBUG);
+
+ /*
+ * Build on-disk orphan chain:
+ * s_last_orphan -> 15 (unlinked reg w/ extent) -> 16 (linked truncate
+ * w/ extent) -> 17 (unlinked fast symlink) -> 0
+ */
+ s_unlinked = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+ s_unlinked->handoff_to_iget = true;
+ orphan_test_add_single_extent(s_unlinked->inode, 0, 200ULL, 2);
+ NEXT_ORPHAN(s_unlinked->inode) = 16;
+
+ s_trunc = orphan_test_create_inode(test, ctx, 16, S_IFREG | 0644, 1);
+ s_trunc->handoff_to_iget = true;
+ s_trunc->inode->i_size = 0;
+ EXT4_I(s_trunc->inode)->i_disksize = 2 * ORPHAN_TEST_BLK_SIZE;
+ orphan_test_add_single_extent(s_trunc->inode, 0, 300ULL, 2);
+ NEXT_ORPHAN(s_trunc->inode) = 17;
+
+ s_symlink = orphan_test_create_inode(test, ctx, 17, S_IFLNK | 0777, 0);
+ s_symlink->handoff_to_iget = true;
+ s_symlink->inode->i_size = 8;
+ memcpy(EXT4_I(s_symlink->inode)->i_data, "target12", 8);
+ NEXT_ORPHAN(s_symlink->inode) = 0;
+
+ ctx->es.s_last_orphan = cpu_to_le32(15);
+
+ ext4_orphan_cleanup(sb, &ctx->es);
+
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->sbi.s_orphan));
+ KUNIT_EXPECT_EQ(test, ctx->nr_freed_inos, 2U);
+ KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 15));
+ KUNIT_EXPECT_FALSE(test, orphan_test_was_ino_freed(ctx, 16));
+ KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 17));
+ /* Both s_unlinked (2 blks) and s_trunc (2 blks) freed their extents */
+ KUNIT_EXPECT_EQ(test, ctx->total_freed_blks, 4UL);
+}
+
+static void
+test_orphan_cleanup_orphan_file_and_rdonly_restore(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct orphan_test_inode_slot *s_list, *s_of_del, *s_of_trunc, *s_of_only;
+ __le32 *bdata0, *bdata1;
+
+ ctx->sbi.s_journal = NULL;
+ ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS;
+ sb->s_flags |= SB_RDONLY;
+
+ orphan_test_setup_static_orphan_file(ctx, 2);
+ bdata0 = (__le32 *)ctx->static_binfo[0].ob_bh->b_data;
+ bdata1 = (__le32 *)ctx->static_binfo[1].ob_bh->b_data;
+
+ /* 1. Linked-list unlinked orphan (ino 15) */
+ s_list = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+ s_list->handoff_to_iget = true;
+ NEXT_ORPHAN(s_list->inode) = 0;
+ ctx->es.s_last_orphan = cpu_to_le32(15);
+
+ /*
+ * 2. Orphan file entries:
+ * - Block 0 slot 2: valid unlinked orphan (ino 16)
+ * - Block 0 slot 10: unallocated ino 99 -> ext4_orphan_get() fails,
+ * loop must continue to remaining entries!
+ * - Block 1 slot 5: valid linked truncate orphan (ino 18)
+ */
+ s_of_del = orphan_test_create_inode(test, ctx, 16, S_IFREG | 0644, 0);
+ s_of_del->handoff_to_iget = true;
+ bdata0[2] = cpu_to_le32(16);
+ bdata0[10] = cpu_to_le32(99);
+ atomic_set(&ctx->static_binfo[0].ob_free_entries,
+ ORPHAN_TEST_INODES_PER_OB - 2);
+
+ s_of_trunc = orphan_test_create_inode(test, ctx, 18, S_IFREG | 0644, 1);
+ s_of_trunc->handoff_to_iget = true;
+ s_of_trunc->inode->i_size = 0;
+ orphan_test_add_single_extent(s_of_trunc->inode, 0, 400ULL, 1);
+ bdata1[5] = cpu_to_le32(18);
+ atomic_set(&ctx->static_binfo[1].ob_free_entries,
+ ORPHAN_TEST_INODES_PER_OB - 1);
+
+ ext4_orphan_cleanup(sb, &ctx->es);
+
+ /* SB_RDONLY was cleared during cleanup and restored upon return */
+ KUNIT_EXPECT_TRUE(test, ctx->saw_rw_during_cleanup);
+ KUNIT_EXPECT_NE(test, sb->s_flags & SB_RDONLY, 0UL);
+
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+ KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 15));
+ KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 16));
+ KUNIT_EXPECT_FALSE(test, orphan_test_was_ino_freed(ctx, 18));
+ KUNIT_EXPECT_EQ(test, ctx->total_freed_blks, 1UL);
+
+ /* Valid orphan-file slots were cleared; bad slot 10 was skipped */
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata0[2]), 0U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata0[10]), 99U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata1[5]), 0U);
+ KUNIT_EXPECT_EQ(test,
+ atomic_read(&ctx->static_binfo[0].ob_free_entries),
+ (int)(ORPHAN_TEST_INODES_PER_OB - 1));
+ KUNIT_EXPECT_EQ(test,
+ atomic_read(&ctx->static_binfo[1].ob_free_entries),
+ (int)ORPHAN_TEST_INODES_PER_OB);
+
+ /*
+ * 3. s_last_orphan == 0 with non-empty orphan file:
+ * must NOT early-return at (!es->s_last_orphan && orphan_file_empty).
+ */
+ bdata0[10] = 0;
+ atomic_set(&ctx->static_binfo[0].ob_free_entries,
+ ORPHAN_TEST_INODES_PER_OB);
+ s_of_only = orphan_test_create_inode(test, ctx, 19, S_IFREG | 0644, 0);
+ s_of_only->handoff_to_iget = true;
+ bdata1[8] = cpu_to_le32(19);
+ atomic_set(&ctx->static_binfo[1].ob_free_entries,
+ ORPHAN_TEST_INODES_PER_OB - 1);
+ orphan_test_reset_faults(ctx);
+
+ ext4_orphan_cleanup(sb, &ctx->es);
+
+ KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 19));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata1[8]), 0U);
+ KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+}
+
+static void
+test_orphan_cleanup_list_errors_and_truncate_fail(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct orphan_test_inode_slot *s1, *s2;
+
+ ctx->sbi.s_journal = NULL;
+ ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS;
+
+ /*
+ * 1. Reserved / invalid orphan ino (< EXT4_FIRST_INO) on s_last_orphan:
+ * ext4_orphan_get() returns -EFSCORRUPTED, loop clears s_last_orphan
+ * and breaks cleanly.
+ */
+ ctx->es.s_last_orphan = cpu_to_le32(5);
+ ext4_orphan_cleanup(sb, &ctx->es);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+
+ /*
+ * 2. ext4_truncate() fails (-EIO) on linked orphan s1 (20), which
+ * points to s2 (21):
+ * - ext4_process_orphan() removes s1 from in-core s_orphan via
+ * ext4_orphan_del(NULL, s1) and sets EXT4_ERROR_FS via
+ * ext4_std_error().
+ * - Next loop iteration detects EXT4_ERROR_FS, clears s_last_orphan,
+ * and aborts recovery before touching s2 (21).
+ */
+ orphan_test_reset_faults(ctx);
+ kunit_activate_static_stub(test, ext4_truncate,
+ orphan_test_truncate_stub);
+ ctx->stub_truncate_err = -EIO;
+
+ s1 = orphan_test_create_inode(test, ctx, 20, S_IFREG | 0644, 1);
+ s1->handoff_to_iget = true;
+ NEXT_ORPHAN(s1->inode) = 21;
+
+ s2 = orphan_test_create_inode(test, ctx, 21, S_IFREG | 0644, 0);
+ s2->handoff_to_iget = true;
+ NEXT_ORPHAN(s2->inode) = 0;
+
+ ctx->es.s_last_orphan = cpu_to_le32(20);
+
+ ext4_orphan_cleanup(sb, &ctx->es);
+ kunit_deactivate_static_stub(test, ext4_truncate);
+
+ KUNIT_EXPECT_EQ(test, ctx->truncate_calls, 1U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->sbi.s_orphan));
+ KUNIT_EXPECT_FALSE(test, orphan_test_was_ino_freed(ctx, 21));
+ KUNIT_EXPECT_NOT_NULL(test, s2->inode);
+}
+
+#ifdef CONFIG_QUOTA
+static void test_orphan_cleanup_quota_paths(struct kunit *test)
+{
+ struct orphan_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct orphan_test_inode_slot *s1, *s2;
+
+ ctx->sbi.s_journal = NULL;
+ ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS;
+ sb->s_flags |= SB_RDONLY;
+ strscpy(ctx->qf_name, "aquota.user", sizeof(ctx->qf_name));
+
+ /*
+ * 1. Exercise journaled quota mount hook (ext4_quota_on_mount) and,
+ * when CONFIG_QFMT_V2 is also enabled, ext4_enable_quotas() success
+ * (s_usr_quota_inum == 0 -> quota_update = 1) on SB_RDONLY orphan
+ * recovery. Hold s_umount to satisfy lockdep in s_qf_names RCU
+ * dereference (matching mount-time ext4_fill_super context).
+ */
+ rcu_assign_pointer(ctx->sbi.s_qf_names[USRQUOTA], ctx->qf_name);
+#if IS_ENABLED(CONFIG_QFMT_V2)
+ ext4_set_feature_quota(sb);
+ ctx->es.s_usr_quota_inum = 0;
+#endif
+
+ s1 = orphan_test_create_inode(test, ctx, 28, S_IFREG | 0644, 0);
+ s1->handoff_to_iget = true;
+ NEXT_ORPHAN(s1->inode) = 0;
+ ctx->es.s_last_orphan = cpu_to_le32(28);
+
+ down_write(&sb->s_umount);
+ ext4_orphan_cleanup(sb, &ctx->es);
+ rcu_assign_pointer(ctx->sbi.s_qf_names[USRQUOTA], NULL);
+ up_write(&sb->s_umount);
+
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+ KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 28));
+ KUNIT_EXPECT_NE(test, sb->s_flags & SB_RDONLY, 0UL);
+
+#if IS_ENABLED(CONFIG_QFMT_V2)
+ /*
+ * 2. Exercise ext4_enable_quotas() failure path: missing quota inode 3
+ * triggers ext4_error() (setting EXT4_ERROR_FS), logs the quota
+ * turn-on error, aborts s_last_orphan recovery while clearing
+ * s_last_orphan to 0, and still restores SB_RDONLY.
+ */
+ orphan_test_reset_faults(ctx);
+ ctx->es.s_usr_quota_inum = cpu_to_le32(EXT4_USR_QUOTA_INO);
+ s2 = orphan_test_create_inode(test, ctx, 29, S_IFREG | 0644, 0);
+ s2->handoff_to_iget = true;
+ NEXT_ORPHAN(s2->inode) = 0;
+ ctx->es.s_last_orphan = cpu_to_le32(29);
+
+ down_write(&sb->s_umount);
+ ext4_orphan_cleanup(sb, &ctx->es);
+ up_write(&sb->s_umount);
+
+ KUNIT_EXPECT_NE(test, ctx->sbi.s_mount_state & EXT4_ERROR_FS, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+ KUNIT_EXPECT_FALSE(test, orphan_test_was_ino_freed(ctx, 29));
+ KUNIT_EXPECT_NE(test, sb->s_flags & SB_RDONLY, 0UL);
+#else
+ (void)s2;
+#endif
+}
+#endif
+
+static struct kunit_case orphan_test_cases[] = {
+ KUNIT_CASE_PARAM(test_inodes_per_orphan_block_param,
+ orphan_blksize_gen_params),
+ KUNIT_CASE(test_orphan_file_empty),
+ KUNIT_CASE(test_orphan_add_early_exits_and_idempotency),
+ KUNIT_CASE(test_orphan_add_list_chaining_and_csum),
+ KUNIT_CASE(test_orphan_add_next_orphan_bounds),
+ KUNIT_CASE(test_orphan_add_list_error_paths),
+ KUNIT_CASE(test_orphan_del_early_exits),
+ KUNIT_CASE(test_orphan_del_list_positions_and_null_handle),
+ KUNIT_CASE(test_orphan_del_list_error_paths),
+ KUNIT_CASE(test_orphan_file_add_and_del_basic),
+ KUNIT_CASE(test_orphan_file_add_block_wraparound_and_last_slot),
+ KUNIT_CASE(test_orphan_file_add_enospc_fallback_and_write_err),
+ KUNIT_CASE(test_orphan_file_del_edge_and_error_paths),
+ KUNIT_CASE(test_orphan_file_block_trigger_and_frozen_data),
+ KUNIT_CASE(test_init_and_release_orphan_info_lifecycle),
+ KUNIT_CASE(test_init_orphan_info_max_blocks_boundary),
+ KUNIT_CASE(test_init_orphan_info_error_paths_and_cleanup),
+ KUNIT_CASE(test_orphan_cleanup_skip_guards),
+ KUNIT_CASE(test_orphan_cleanup_linked_list_unlinked_and_truncated),
+ KUNIT_CASE(test_orphan_cleanup_orphan_file_and_rdonly_restore),
+ KUNIT_CASE(test_orphan_cleanup_list_errors_and_truncate_fail),
+#ifdef CONFIG_QUOTA
+ KUNIT_CASE(test_orphan_cleanup_quota_paths),
+#endif
+ {}
+};
+
+static struct kunit_suite orphan_test_suite = {
+ .name = "ext4_orphan_test",
+ .init = orphan_test_init,
+ .test_cases = orphan_test_cases,
+};
+
+kunit_test_suites(&orphan_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 orphan inode handling");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/orphan.c b/fs/ext4/orphan.c
index 03538d498811..a12022913ec6 100644
--- a/fs/ext4/orphan.c
+++ b/fs/ext4/orphan.c
@@ -511,6 +511,7 @@ void ext4_orphan_cleanup(struct super_block *sb, struct ext4_super_block *es)
#endif
sb->s_flags = s_flags; /* Restore SB_RDONLY status */
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_orphan_cleanup);
void ext4_release_orphan_info(struct super_block *sb)
{
@@ -523,6 +524,7 @@ void ext4_release_orphan_info(struct super_block *sb)
brelse(oi->of_binfo[i].ob_bh);
kvfree(oi->of_binfo);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_release_orphan_info);
static struct ext4_orphan_block_tail *ext4_orphan_block_tail(
struct super_block *sb,
@@ -570,6 +572,7 @@ void ext4_orphan_file_block_trigger(struct jbd2_buffer_trigger_type *triggers,
ot = ext4_orphan_block_tail(sb, bh);
ot->ob_checksum = cpu_to_le32(csum);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_orphan_file_block_trigger);
int ext4_init_orphan_info(struct super_block *sb)
{
@@ -652,6 +655,7 @@ int ext4_init_orphan_info(struct super_block *sb)
iput(inode);
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_orphan_info);
int ext4_orphan_file_empty(struct super_block *sb)
{
@@ -667,3 +671,4 @@ int ext4_orphan_file_empty(struct super_block *sb)
return 0;
return 1;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_orphan_file_empty);
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 14/27] ext4: add KUnit tests for fs/ext4/block_validity.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (12 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 13/27] ext4: add KUnit tests for fs/ext4/orphan.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (12 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add the ext4_block_validity_test KUnit test suite in
fs/ext4/block_validity-test.c (16 test cases, 40 subtests total) to
comprehensively exercise system-zone rbtree construction, RCU release,
block range validation, reserved journal inode protection, and indirect
block reference checking in fs/ext4/block_validity.c using an in-memory
VFS superblock and mock block device without requiring a mounted disk:
- Bounds and 64-bit overflow checks (ext4_sb_block_valid and
ext4_inode_block_valid) across 1KiB (s_first_data_block = 1) and 4KiB
(s_first_data_block = 0) geometries, exact end-of-filesystem single
and multi-block spans, 64-bit u64 addition wrap-around, >32-bit block
counts under EXT4_FEATURE_INCOMPAT_64BIT, and high-32-bit masking when
64bit is cleared.
- System-zone lifecycle management (ext4_setup_system_zone and
ext4_release_system_zone), covering pre-setup NULL behavior, RCU
publication and teardown, and idempotent double-release handling.
- Multi-group sparse_super and flex_bg layouts (including groups with
zero base metadata blocks and relocated bitmaps/inode tables) as well
as DEBUG mount-option tree printing (debug_print_tree).
- System-zone rbtree merging (add_system_zone and can_merge) across
left-neighbor merges, right-neighbor merges, 3-way bridge merges, and
64-bit (>2^32) group descriptor metadata zones, plus parameterized
interval-overlap geometries (adjacent, single-block, boundary
straddling, interior, exact, and enclosing spans).
- Corrupt overlapping group metadata detection (-EFSCORRUPTED) across
block bitmap, inode bitmap, inode table, and cross-group collisions,
verifying partial rbtree cleanup without publishing s_system_blks.
- Reserved journal inode protection (ext4_protect_reserved_inode),
verifying non-block-aligned i_size rounding, multi-extent and sparse
hole mapping, journal-inode ownership vs. regular/NULL inode
rejection, can_merge() isolation between adjacent ino=0 and journal
zones, disabled-feature/zero-inum/empty-inode skips, and -EINVAL,
ext4_iget, ext4_map_blocks, and metadata-overlap (-EFSCORRUPTED +
EXT4_ERROR_FS) failure paths.
- Block reference array validation (ext4_check_blockref,
ext4_ind_check_inode, and ext4_check_indirect_blockref) across empty
and sparse arrays, metadata and out-of-bounds corruption reporting,
active journal inode (s_journal->j_inode) bypass, and integration with
ext4_map_blocks() block validity checks.
Export the five runtime entry points in fs/ext4/block_validity.c with
EXPORT_SYMBOL_FOR_EXT4_TEST for the ext4-test KUnit module.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 3 +-
fs/ext4/block_validity-test.c | 1517 +++++++++++++++++++++++++++++++++
fs/ext4/block_validity.c | 6 +
3 files changed, 1525 insertions(+), 1 deletion(-)
create mode 100644 fs/ext4/block_validity-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 9bad2bff0de9..5c6afff5f12a 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -19,7 +19,8 @@ ext4-test-objs += inode-test.o mballoc-test.o \
extents_status-test.o xattr-test.o \
namei-test.o inline-test.o \
fast_commit-test.o ialloc-test.o \
- balloc-test.o orphan-test.o
+ balloc-test.o orphan-test.o \
+ block_validity-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/block_validity-test.c b/fs/ext4/block_validity-test.c
new file mode 100644
index 000000000000..5d3dadba1328
--- /dev/null
+++ b/fs/ext4/block_validity-test.c
@@ -0,0 +1,1517 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/block_validity.c
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/iversion.h>
+#include <linux/jbd2.h>
+#include <linux/kernel.h>
+#include <linux/limits.h>
+#include <linux/module.h>
+#include <linux/part_stat.h>
+#include <linux/percpu.h>
+#include <linux/rcupdate.h>
+#include <linux/shrinker.h>
+#include <linux/slab.h>
+#include <linux/stat.h>
+#include <linux/timer.h>
+#include <linux/workqueue.h>
+
+#include "ext4.h"
+#include "ext4_extents.h"
+#include "ext4_jbd2.h"
+#include "xattr.h"
+
+/*
+ * Default mock filesystem geometry:
+ * - 4 block groups, 1024-byte blocks, s_first_data_block = 1
+ * - 1024 blocks per group -> total blocks = 4097 (valid blocks 0..4096)
+ * - sparse_super enabled, s_gdb_count = 1, s_reserved_gdt_blocks = 2:
+ * group 0 (blocks 1..1024): 4 base meta blocks (1..4)
+ * group 1 (blocks 1025..2048): 4 base meta blocks (1025..1028)
+ * group 2 (blocks 2049..3072): 0 base meta blocks (sparse_super)
+ * group 3 (blocks 3073..4096): 4 base meta blocks (3073..3076)
+ * - 32 inodes per group (128 total), 256-byte inodes -> 8 inode table
+ * blocks per group
+ */
+#define BV_TEST_NGROUPS 4U
+#define BV_TEST_BLK_SIZE 1024U
+#define BV_TEST_BLK_BITS 10U
+#define BV_TEST_BLOCKS_PER_GRP 1024U
+#define BV_TEST_FIRST_DATA_BLK 1U
+#define BV_TEST_TOTAL_BLKS (BV_TEST_FIRST_DATA_BLK + \
+ BV_TEST_NGROUPS * \
+ BV_TEST_BLOCKS_PER_GRP)
+#define BV_TEST_INODE_SIZE 256U
+#define BV_TEST_EXTRA_ISIZE 32U
+#define BV_TEST_INODES_PER_BLK (BV_TEST_BLK_SIZE / BV_TEST_INODE_SIZE)
+#define BV_TEST_INODES_PER_GRP 32U
+#define BV_TEST_ITB_PER_GRP (BV_TEST_INODES_PER_GRP / \
+ BV_TEST_INODES_PER_BLK)
+#define BV_TEST_TOTAL_INODES (BV_TEST_NGROUPS * \
+ BV_TEST_INODES_PER_GRP)
+#define BV_TEST_FIRST_INO 11U
+#define BV_TEST_GDB_COUNT 1U
+#define BV_TEST_RSV_GDT_BLKS 2U
+#define BV_TEST_GDESC_BLK 2ULL
+#define BV_TEST_MAX_SLOTS 8U
+
+struct bv_test_inode_slot {
+ bool in_use;
+ unsigned long ino;
+ struct inode *inode;
+};
+
+struct bv_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 buffer_head sb_bh;
+ struct buffer_head gdesc_bh;
+ char gdesc_buf[BV_TEST_BLK_SIZE] __aligned(8);
+ char ind_buf[BV_TEST_BLK_SIZE] __aligned(8);
+
+ struct ext4_group_desc gdps[BV_TEST_NGROUPS];
+ journal_t mock_journal;
+
+ struct bv_test_inode_slot slots[BV_TEST_MAX_SLOTS];
+
+ /* Fault injection and call tracking */
+ int fail_iget_err;
+ unsigned int iget_calls;
+ ext4_iget_flags last_iget_flags;
+
+ int fail_map_err;
+ unsigned int fail_map_on_call;
+ unsigned int map_calls;
+};
+
+static inline ext4_fsblk_t bv_group_first_blk(ext4_group_t g)
+{
+ return BV_TEST_FIRST_DATA_BLK +
+ (ext4_fsblk_t)g * BV_TEST_BLOCKS_PER_GRP;
+}
+
+/*
+ * Number of base metadata blocks in group @g under the default 4-group
+ * sparse_super configuration (groups 0, 1, 3 have super + 1 gdb + 2 rsv_gdt;
+ * group 2 has 0).
+ */
+static inline unsigned int bv_default_base_meta(ext4_group_t g)
+{
+ if (g == 2)
+ return 0;
+ return 1U + BV_TEST_GDB_COUNT + BV_TEST_RSV_GDT_BLKS;
+}
+
+static inline ext4_fsblk_t bv_default_bbitmap_blk(ext4_group_t g)
+{
+ return bv_group_first_blk(g) + bv_default_base_meta(g);
+}
+
+static inline ext4_fsblk_t bv_default_ibitmap_blk(ext4_group_t g)
+{
+ return bv_default_bbitmap_blk(g) + 1ULL;
+}
+
+static inline ext4_fsblk_t bv_default_itable_blk(ext4_group_t g)
+{
+ return bv_default_ibitmap_blk(g) + 1ULL;
+}
+
+static inline ext4_fsblk_t bv_default_first_free_blk(ext4_group_t g)
+{
+ return bv_default_itable_blk(g) + BV_TEST_ITB_PER_GRP;
+}
+
+static void bv_test_sb_upd_work(struct work_struct *work)
+{
+}
+
+static void bv_test_err_timer(struct timer_list *t)
+{
+}
+
+static unsigned long bv_dummy_shrinker_count(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return 0;
+}
+
+static unsigned long bv_dummy_shrinker_scan(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return SHRINK_STOP;
+}
+
+static void bv_test_init_ei_fields(struct ext4_inode_info *ei)
+{
+ inode_set_iversion(&ei->vfs_inode, 1);
+ ei->i_flags = 0;
+ ext4_clear_state_flags(ei);
+ spin_lock_init(&ei->i_raw_lock);
+ ei->i_prealloc_node = RB_ROOT;
+ atomic_set(&ei->i_prealloc_active, 0);
+ rwlock_init(&ei->i_prealloc_lock);
+ ext4_es_init_tree(&ei->i_es_tree);
+ rwlock_init(&ei->i_es_lock);
+ INIT_LIST_HEAD(&ei->i_es_list);
+ ei->i_es_all_nr = 0;
+ ei->i_es_shk_nr = 0;
+ ei->i_es_shrink_lblk = 0;
+ ei->i_es_seq = 0;
+ ei->i_reserved_data_blocks = 0;
+ spin_lock_init(&ei->i_block_reservation_lock);
+ ext4_init_pending_tree(&ei->i_pending_tree);
+#ifdef CONFIG_QUOTA
+ ei->i_reserved_quota = 0;
+ memset(&ei->i_dquot, 0, sizeof(ei->i_dquot));
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+ ei->i_crypt_info = NULL;
+#endif
+ ei->jinode = NULL;
+ init_rwsem(&ei->xattr_sem);
+ INIT_LIST_HEAD(&ei->i_rsv_conversion_list);
+ spin_lock_init(&ei->i_completed_io_lock);
+ ei->i_sync_tid = 0;
+ ei->i_datasync_tid = 0;
+ INIT_LIST_HEAD(&ei->i_orphan);
+ ext4_fc_init_inode(&ei->vfs_inode);
+ spin_lock_init(&ei->i_fc_lock);
+ ei->i_metadata_bhs = NULL;
+ ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+ ei->i_extra_isize = BV_TEST_EXTRA_ISIZE;
+ ei->i_last_alloc_group = ~0U;
+}
+
+static struct inode *bv_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);
+ bv_test_init_ei_fields(ei);
+ return &ei->vfs_inode;
+}
+
+static void bv_test_evict_inode(struct inode *inode)
+{
+ truncate_inode_pages_final(&inode->i_data);
+ ext4_clear_inode(inode);
+}
+
+static void bv_test_destroy_inode(struct inode *inode)
+{
+ kfree(ext4_i_metadata_bhs(inode));
+ EXT4_I(inode)->i_metadata_bhs = NULL;
+}
+
+static void bv_test_free_inode_mem(struct inode *inode)
+{
+ kfree(EXT4_I(inode));
+}
+
+static const struct super_operations bv_test_sops = {
+ .alloc_inode = bv_test_alloc_inode,
+ .evict_inode = bv_test_evict_inode,
+ .destroy_inode = bv_test_destroy_inode,
+ .free_inode = bv_test_free_inode_mem,
+ .drop_inode = inode_just_drop,
+};
+
+static int bv_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static struct file_system_type bv_test_fs_type = {
+ .name = "ext4_bv_kunit",
+ .init_fs_context = bv_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+/* --- Helpers & Static Stubs --- */
+
+static void bv_release_system_zone_locked(struct super_block *sb)
+{
+ down_write(&sb->s_umount);
+ ext4_release_system_zone(sb);
+ up_write(&sb->s_umount);
+}
+
+static struct bv_test_inode_slot *
+bv_find_slot(struct bv_test_ctx *ctx, unsigned long ino)
+{
+ unsigned int i;
+
+ for (i = 0; i < BV_TEST_MAX_SLOTS; i++) {
+ if (ctx->slots[i].in_use && ctx->slots[i].ino == ino)
+ return &ctx->slots[i];
+ }
+ return NULL;
+}
+
+static struct inode *bv_create_inode(struct kunit *test,
+ struct bv_test_ctx *ctx,
+ unsigned long ino, umode_t mode,
+ bool use_extents)
+{
+ struct bv_test_inode_slot *slot = NULL;
+ struct ext4_extent_header *eh;
+ struct inode *inode;
+ unsigned int i;
+
+ for (i = 0; i < BV_TEST_MAX_SLOTS; i++) {
+ if (!ctx->slots[i].in_use) {
+ slot = &ctx->slots[i];
+ break;
+ }
+ }
+ KUNIT_ASSERT_NOT_NULL(test, slot);
+
+ inode = new_inode(ctx->sb);
+ KUNIT_ASSERT_NOT_NULL(test, inode);
+
+ slot->in_use = true;
+ slot->ino = ino;
+ slot->inode = inode;
+
+ inode->i_ino = ino;
+ inode->i_mode = mode;
+ inode->i_blkbits = ctx->sb->s_blocksize_bits;
+ inode->i_flags |= S_NOQUOTA;
+ set_nlink(inode, 1);
+ EXT4_I(inode)->i_block_group = (ino - 1) / BV_TEST_INODES_PER_GRP;
+ memset(EXT4_I(inode)->i_data, 0, sizeof(EXT4_I(inode)->i_data));
+
+ if (use_extents) {
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ eh = ext_inode_hdr(inode);
+ eh->eh_magic = EXT4_EXT_MAGIC;
+ eh->eh_entries = 0;
+ eh->eh_max = cpu_to_le16((sizeof(EXT4_I(inode)->i_data) -
+ sizeof(*eh)) /
+ sizeof(struct ext4_extent));
+ eh->eh_depth = 0;
+ }
+
+ return inode;
+}
+
+static void bv_append_extent(struct kunit *test, struct inode *inode,
+ ext4_lblk_t lblk, ext4_fsblk_t pblk,
+ unsigned short len)
+{
+ struct ext4_extent_header *eh = ext_inode_hdr(inode);
+ u16 entries = le16_to_cpu(eh->eh_entries);
+ struct ext4_extent *ex;
+
+ KUNIT_ASSERT_LT(test, entries, le16_to_cpu(eh->eh_max));
+ ex = EXT_FIRST_EXTENT(eh) + entries;
+ ex->ee_block = cpu_to_le32(lblk);
+ ex->ee_len = cpu_to_le16(len);
+ ext4_ext_store_pblock(ex, pblk);
+ eh->eh_entries = cpu_to_le16(entries + 1);
+}
+
+static void bv_set_group_meta(struct super_block *sb,
+ struct ext4_group_desc *gdp,
+ ext4_fsblk_t bbitmap, ext4_fsblk_t ibitmap,
+ ext4_fsblk_t itable)
+{
+ ext4_block_bitmap_set(sb, gdp, bbitmap);
+ ext4_inode_bitmap_set(sb, gdp, ibitmap);
+ ext4_inode_table_set(sb, gdp, itable);
+}
+
+static void bv_clear_fs_error(struct bv_test_ctx *ctx)
+{
+ ctx->sbi.s_mount_state &= ~EXT4_ERROR_FS;
+ ctx->es.s_last_error_block = 0;
+ ctx->es.s_last_error_ino = 0;
+ ctx->es.s_last_error_errcode = 0;
+}
+
+static struct ext4_group_desc *
+bv_get_group_desc_stub(struct super_block *sb, ext4_group_t block_group,
+ struct buffer_head **bh)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct bv_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx || block_group >= BV_TEST_NGROUPS)
+ return NULL;
+ if (bh)
+ *bh = &ctx->gdesc_bh;
+ return &ctx->gdps[block_group];
+}
+
+static struct inode *bv_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 bv_test_ctx *ctx = test ? test->priv : NULL;
+ struct bv_test_inode_slot *slot;
+
+ if (!ctx)
+ return ERR_PTR(-EIO);
+
+ ctx->iget_calls++;
+ ctx->last_iget_flags = flags;
+ if (ctx->fail_iget_err)
+ return ERR_PTR(ctx->fail_iget_err);
+
+ slot = bv_find_slot(ctx, ino);
+ if (!slot || !slot->inode)
+ return ERR_PTR(-ENOENT);
+
+ ihold(slot->inode);
+ return slot->inode;
+}
+
+static int bv_map_blocks_fault_stub(handle_t *handle, struct inode *inode,
+ struct ext4_map_blocks *map, int flags)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct bv_test_ctx *ctx = test ? test->priv : NULL;
+
+ if (!ctx)
+ return -EIO;
+
+ ctx->map_calls++;
+ if (ctx->fail_map_err &&
+ (!ctx->fail_map_on_call ||
+ ctx->map_calls == ctx->fail_map_on_call))
+ return ctx->fail_map_err;
+
+ map->m_pblk = bv_default_first_free_blk(0) + map->m_lblk;
+ map->m_len = 1;
+ map->m_flags = EXT4_MAP_MAPPED;
+ return 1;
+}
+
+/* --- Fixture Setup & Teardown --- */
+
+static void bv_test_cleanup(struct bv_test_ctx *ctx)
+{
+ unsigned int i;
+
+ ctx->sbi.s_journal = NULL;
+ bv_release_system_zone_locked(ctx->sb);
+
+ for (i = 0; i < BV_TEST_MAX_SLOTS; i++) {
+ struct inode *inode = ctx->slots[i].inode;
+
+ if (!inode)
+ continue;
+ spin_lock(&inode->i_lock);
+ inode_state_clear(inode, I_DIRTY_TIME | I_NEW);
+ spin_unlock(&inode->i_lock);
+ set_nlink(inode, 1);
+ iput(inode);
+ ctx->slots[i].inode = NULL;
+ ctx->slots[i].in_use = false;
+ }
+
+ deactivate_super(ctx->sb);
+ ext4_es_unregister_shrinker(&ctx->sbi);
+ rcu_barrier();
+
+ cancel_work_sync(&ctx->sbi.s_sb_upd_work);
+ timer_delete_sync(&ctx->sbi.s_err_report);
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(ctx);
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(bv_test_cleanup_action, bv_test_cleanup,
+ struct bv_test_ctx *);
+
+static int bv_test_init(struct kunit *test)
+{
+ struct bv_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;
+
+ 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");
+ }
+
+ fc = fs_context_for_mount(&bv_test_fs_type, 0);
+ if (IS_ERR(fc)) {
+ err = PTR_ERR(fc);
+ 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);
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+ ctx->sb = sb;
+ sbi = &ctx->sbi;
+
+ sbi->s_sb = sb;
+ sb->s_fs_info = sbi;
+ sb->s_op = &bv_test_sops;
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
+#endif
+ sb->s_flags |= SB_ACTIVE;
+ sb->s_blocksize = BV_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = BV_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_bv_kunit", sizeof(sb->s_id));
+
+ inode_init_once(&ctx->bd_inode);
+ ctx->bd_inode.i_sb = sb;
+ ctx->bd_inode.i_blkbits = BV_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;
+
+ sbi->s_es = &ctx->es;
+ ctx->es.s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+ ctx->es.s_first_data_block = cpu_to_le32(BV_TEST_FIRST_DATA_BLK);
+ ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_LINUX);
+ ctx->es.s_inodes_count = cpu_to_le32(BV_TEST_TOTAL_INODES);
+ ctx->es.s_inodes_per_group = cpu_to_le32(BV_TEST_INODES_PER_GRP);
+ ctx->es.s_blocks_per_group = cpu_to_le32(BV_TEST_BLOCKS_PER_GRP);
+ ctx->es.s_clusters_per_group = cpu_to_le32(BV_TEST_BLOCKS_PER_GRP);
+ ctx->es.s_first_ino = cpu_to_le32(BV_TEST_FIRST_INO);
+ ctx->es.s_inode_size = cpu_to_le16(BV_TEST_INODE_SIZE);
+ ctx->es.s_min_extra_isize = cpu_to_le16(BV_TEST_EXTRA_ISIZE);
+ ctx->es.s_want_extra_isize = cpu_to_le16(BV_TEST_EXTRA_ISIZE);
+ ctx->es.s_reserved_gdt_blocks = cpu_to_le16(BV_TEST_RSV_GDT_BLKS);
+ ctx->es.s_checksum_type = EXT4_CRC32C_CHKSUM;
+ ext4_blocks_count_set(&ctx->es, BV_TEST_TOTAL_BLKS);
+
+ ext4_set_feature_sparse_super(sb);
+ ext4_set_feature_extents(sb);
+ ext4_set_feature_extra_isize(sb);
+ ext4_set_feature_metadata_csum(sb);
+ ext4_set_feature_64bit(sb);
+
+ ctx->sb_bh.b_data = (char *)&ctx->es;
+ ctx->sb_bh.b_size = sizeof(ctx->es);
+ ctx->sb_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->sb_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->sb_bh);
+ sbi->s_sbh = &ctx->sb_bh;
+
+ ctx->gdesc_bh.b_data = ctx->gdesc_buf;
+ ctx->gdesc_bh.b_size = BV_TEST_BLK_SIZE;
+ ctx->gdesc_bh.b_blocknr = BV_TEST_GDESC_BLK;
+ ctx->gdesc_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->gdesc_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->gdesc_bh);
+
+ sbi->s_inode_size = BV_TEST_INODE_SIZE;
+ sbi->s_inodes_per_block = BV_TEST_INODES_PER_BLK;
+ sbi->s_inodes_per_group = BV_TEST_INODES_PER_GRP;
+ sbi->s_itb_per_group = BV_TEST_ITB_PER_GRP;
+ sbi->s_first_ino = BV_TEST_FIRST_INO;
+ sbi->s_want_extra_isize = BV_TEST_EXTRA_ISIZE;
+ sbi->s_blocks_per_group = BV_TEST_BLOCKS_PER_GRP;
+ sbi->s_clusters_per_group = BV_TEST_BLOCKS_PER_GRP;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_cluster_bits = 0;
+ sbi->s_groups_count = BV_TEST_NGROUPS;
+ sbi->s_gdb_count = BV_TEST_GDB_COUNT;
+ sbi->s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_desc_per_block = BV_TEST_BLK_SIZE / EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_desc_per_block_bits = ilog2(sbi->s_desc_per_block);
+ sbi->s_bitmap_maxbytes = 0x1ff7ffff000ULL;
+
+ mutex_init(&sbi->s_orphan_lock);
+ INIT_LIST_HEAD(&sbi->s_orphan);
+ set_opt(sb, ERRORS_CONT);
+ spin_lock_init(&sbi->s_bdev_wb_lock);
+ spin_lock_init(&sbi->s_error_lock);
+ spin_lock_init(&sbi->s_md_lock);
+ INIT_WORK(&sbi->s_sb_upd_work, bv_test_sb_upd_work);
+ timer_setup(&sbi->s_err_report, bv_test_err_timer, 0);
+ 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);
+
+ for (g = 0; g < BV_TEST_NGROUPS; g++) {
+ bv_set_group_meta(sb, &ctx->gdps[g],
+ bv_default_bbitmap_blk(g),
+ bv_default_ibitmap_blk(g),
+ bv_default_itable_blk(g));
+ }
+
+ err = ext4_es_register_shrinker(sbi);
+ if (err) {
+ deactivate_locked_super(sb);
+ rcu_barrier();
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ }
+ WRITE_ONCE(sbi->s_es_shrinker->count_objects, bv_dummy_shrinker_count);
+ WRITE_ONCE(sbi->s_es_shrinker->scan_objects, bv_dummy_shrinker_scan);
+
+ root_inode = new_inode(sb);
+ if (!root_inode) {
+ deactivate_locked_super(sb);
+ ext4_es_unregister_shrinker(sbi);
+ rcu_barrier();
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(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_op = &simple_dir_inode_operations;
+ set_nlink(root_inode, 2);
+ sb->s_root = d_make_root(root_inode);
+ if (!sb->s_root) {
+ deactivate_locked_super(sb);
+ ext4_es_unregister_shrinker(sbi);
+ rcu_barrier();
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(ctx);
+ KUNIT_FAIL_AND_ABORT(test, "failed to create s_root");
+ }
+
+ up_write(&sb->s_umount);
+
+ kunit_activate_static_stub(test, ext4_get_group_desc,
+ bv_get_group_desc_stub);
+ kunit_activate_static_stub(test, __ext4_iget, bv_iget_stub);
+
+ err = kunit_add_action_or_reset(test, bv_test_cleanup_action, ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ return 0;
+}
+
+/* --- Parameterized Test 1: Bounds & 64-bit Overflow Checks --- */
+
+struct bv_bounds_case {
+ const char *desc;
+ u32 first_data_blk;
+ ext4_fsblk_t total_blks;
+ bool enable_64bit;
+ ext4_fsblk_t start_blk;
+ unsigned int count;
+ int expected;
+};
+
+static const struct bv_bounds_case bv_bounds_cases[] = {
+ {
+ .desc = "1k_block_0_below_first_data",
+ .first_data_blk = 1,
+ .total_blks = 4097,
+ .enable_64bit = true,
+ .start_blk = 0,
+ .count = 1,
+ .expected = 0,
+ },
+ {
+ .desc = "1k_block_1_equals_first_data",
+ .first_data_blk = 1,
+ .total_blks = 4097,
+ .enable_64bit = true,
+ .start_blk = 1,
+ .count = 1,
+ .expected = 0,
+ },
+ {
+ .desc = "1k_block_2_just_above_first_data",
+ .first_data_blk = 1,
+ .total_blks = 4097,
+ .enable_64bit = true,
+ .start_blk = 2,
+ .count = 1,
+ .expected = 1,
+ },
+ {
+ .desc = "4k_block_0_equals_first_data",
+ .first_data_blk = 0,
+ .total_blks = 4096,
+ .enable_64bit = true,
+ .start_blk = 0,
+ .count = 1,
+ .expected = 0,
+ },
+ {
+ .desc = "4k_block_1_just_above_first_data",
+ .first_data_blk = 0,
+ .total_blks = 4096,
+ .enable_64bit = true,
+ .start_blk = 1,
+ .count = 1,
+ .expected = 1,
+ },
+ {
+ .desc = "exact_last_single_block",
+ .first_data_blk = 1,
+ .total_blks = 4097,
+ .enable_64bit = true,
+ .start_blk = 4096,
+ .count = 1,
+ .expected = 1,
+ },
+ {
+ .desc = "multiblock_ending_at_last_block",
+ .first_data_blk = 1,
+ .total_blks = 4097,
+ .enable_64bit = true,
+ .start_blk = 4093,
+ .count = 4,
+ .expected = 1,
+ },
+ {
+ .desc = "multiblock_exceeding_end_by_one",
+ .first_data_blk = 1,
+ .total_blks = 4097,
+ .enable_64bit = true,
+ .start_blk = 4093,
+ .count = 5,
+ .expected = 0,
+ },
+ {
+ .desc = "start_at_blocks_count",
+ .first_data_blk = 1,
+ .total_blks = 4097,
+ .enable_64bit = true,
+ .start_blk = 4097,
+ .count = 1,
+ .expected = 0,
+ },
+ {
+ .desc = "u64_wrap_around_into_in_bounds_value",
+ .first_data_blk = 1,
+ .total_blks = 4097,
+ .enable_64bit = true,
+ .start_blk = U64_MAX - 3ULL,
+ .count = 10,
+ .expected = 0,
+ },
+ {
+ .desc = "u64_max_single_block_overflow",
+ .first_data_blk = 1,
+ .total_blks = 4097,
+ .enable_64bit = true,
+ .start_blk = U64_MAX,
+ .count = 1,
+ .expected = 0,
+ },
+ {
+ .desc = "64bit_high_block_at_exact_end",
+ .first_data_blk = 0,
+ .total_blks = (1ULL << 33) + 64ULL,
+ .enable_64bit = true,
+ .start_blk = (1ULL << 33) + 60ULL,
+ .count = 4,
+ .expected = 1,
+ },
+ {
+ .desc = "32bit_fs_ignores_high_blocks_count",
+ .first_data_blk = 0,
+ .total_blks = (1ULL << 33) + 4096ULL,
+ .enable_64bit = false,
+ .start_blk = (1ULL << 33) + 10ULL,
+ .count = 1,
+ .expected = 0,
+ },
+};
+
+KUNIT_ARRAY_PARAM_DESC(bv_bounds, bv_bounds_cases, desc);
+
+static void test_sb_block_valid_bounds(struct kunit *test)
+{
+ const struct bv_bounds_case *tc = test->param_value;
+ struct bv_test_ctx *ctx = test->priv;
+ struct inode *inode;
+
+ ctx->es.s_first_data_block = cpu_to_le32(tc->first_data_blk);
+ ext4_blocks_count_set(&ctx->es, tc->total_blks);
+ if (tc->enable_64bit)
+ ext4_set_feature_64bit(ctx->sb);
+ else
+ ext4_clear_feature_64bit(ctx->sb);
+
+ inode = bv_create_inode(test, ctx, 12, S_IFREG | 0644, true);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL, tc->start_blk,
+ tc->count),
+ tc->expected);
+ KUNIT_EXPECT_EQ(test,
+ ext4_inode_block_valid(inode, tc->start_blk, tc->count),
+ tc->expected);
+}
+
+/* --- Test 2: Setup and Release Lifecycle (noblock_validity / RCU) --- */
+
+static void test_release_system_zone_lifecycle(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ ext4_fsblk_t meta_blk = bv_default_bbitmap_blk(0);
+ ext4_fsblk_t data_blk = bv_default_first_free_blk(0);
+
+ /*
+ * Before ext4_setup_system_zone(), s_system_blks is NULL: releasing is
+ * a safe no-op and metadata block numbers pass validity as long as
+ * they are within (s_first_data_block, blocks_count].
+ */
+ KUNIT_EXPECT_NULL(test, rcu_access_pointer(ctx->sbi.s_system_blks));
+ bv_release_system_zone_locked(ctx->sb);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, meta_blk, 1),
+ 1);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL,
+ BV_TEST_FIRST_DATA_BLK, 1),
+ 0);
+
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+ KUNIT_EXPECT_NOT_NULL(test,
+ rcu_access_pointer(ctx->sbi.s_system_blks));
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, meta_blk, 1),
+ 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, data_blk, 1),
+ 1);
+
+ /*
+ * Releasing clears s_system_blks immediately and frees the rbtree via
+ * RCU; repeating release when already NULL remains a safe no-op.
+ */
+ bv_release_system_zone_locked(ctx->sb);
+ rcu_barrier();
+ KUNIT_EXPECT_NULL(test, rcu_access_pointer(ctx->sbi.s_system_blks));
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, meta_blk, 1),
+ 1);
+
+ bv_release_system_zone_locked(ctx->sb);
+}
+
+/* --- Test 3: Multi-group sparse_super, flex_bg, and DEBUG Tree Print --- */
+
+static void test_setup_multigroup_sparse_super_and_flex_bg(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ ext4_fsblk_t g2_bbitmap = 20ULL;
+ ext4_fsblk_t g2_ibitmap = 22ULL;
+ ext4_fsblk_t g2_itable = 30ULL;
+
+ /*
+ * Relocate group 2's bitmaps and inode table into group 0 (flex_bg
+ * layout). Since group 2 also has 0 base metadata blocks under
+ * sparse_super, every block in group 2 (2049..3072) becomes valid for
+ * data, while group 0 gains three separated system zones.
+ */
+ bv_set_group_meta(ctx->sb, &ctx->gdps[2], g2_bbitmap, g2_ibitmap,
+ g2_itable);
+ set_opt(ctx->sb, DEBUG);
+
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+
+ /* Group 0 contiguous metadata [1..14] rejected; gap [15..19] valid */
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 2, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 14, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 15, 5), 1);
+
+ /* Relocated group 2 metadata inside group 0 and surrounding gaps */
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL, g2_bbitmap, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 21, 1), 1);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL, g2_ibitmap, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 23, 7), 1);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL, g2_itable, 1), 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL,
+ g2_itable + BV_TEST_ITB_PER_GRP - 1,
+ 1),
+ 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL,
+ g2_itable + BV_TEST_ITB_PER_GRP,
+ 10),
+ 1);
+
+ /* Group 1 [1025..1038] protected, 1024 and 1039 valid */
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 1024, 1), 1);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 1025, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 1038, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 1039, 1), 1);
+
+ /* Group 2 [2049..3072] has zero metadata blocks at all */
+ KUNIT_EXPECT_EQ(test, ext4_num_base_meta_blocks(ctx->sb, 2), 0U);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL,
+ bv_group_first_blk(2),
+ BV_TEST_BLOCKS_PER_GRP),
+ 1);
+
+ /* Group 3 (backup superblock 3^1): [3073..3086] protected, 3087+ ok */
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 3073, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 3086, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 3087, 10), 1);
+}
+
+/* --- Test 4: Left, Right, and 3-Way Bridge Merging (+ 64-bit Zones) --- */
+
+static void test_rbtree_left_right_and_bridge_merges(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ ext4_fsblk_t hi_base = (1ULL << 33) + 100ULL;
+
+ ext4_blocks_count_set(&ctx->es, hi_base + 512ULL);
+
+ /*
+ * Group 0: base_meta = [1..4], bbitmap = 5 (merges left -> [1..5]),
+ * ibitmap = 14 (standalone [14..14]), itable = [6..13] (bridges
+ * [1..5] on the left AND [14..14] on the right into [1..14]).
+ */
+ bv_set_group_meta(ctx->sb, &ctx->gdps[0], 5ULL, 14ULL, 6ULL);
+
+ /*
+ * Group 2 (0 base meta): bbitmap = 2100, ibitmap = 2099 (inserted left
+ * of 2100 and merges right into [2099..2100]), itable = [2110..2117].
+ */
+ bv_set_group_meta(ctx->sb, &ctx->gdps[2], 2100ULL, 2099ULL, 2110ULL);
+
+ /*
+ * Group 3: relocate bbitmap/ibitmap/itable above 2^32 to exercise
+ * 64-bit group descriptor blocks and 3-way merge above 4G blocks:
+ * bbitmap = hi_base, ibitmap = hi_base + 9, itable = hi_base + 1..8
+ * -> merged 10-block 64-bit zone [hi_base .. hi_base + 9].
+ */
+ bv_set_group_meta(ctx->sb, &ctx->gdps[3], hi_base, hi_base + 9ULL,
+ hi_base + 1ULL);
+
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+
+ /* Group 0 bridged zone [1..14] */
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 5, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 6, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 13, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 14, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 15, 1), 1);
+
+ /* Group 2 right-merged zone [2099..2100] and separate [2110..2117] */
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 2098, 1), 1);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 2099, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 2100, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 2101, 9), 1);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 2110, 8), 0);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 2118, 1), 1);
+
+ /* Group 3 64-bit bridged zone [hi_base .. hi_base + 9] */
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL, hi_base - 1ULL, 1),
+ 1);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, hi_base, 1),
+ 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL, hi_base + 5ULL, 1),
+ 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL, hi_base + 9ULL, 1),
+ 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL, hi_base + 10ULL, 4),
+ 1);
+}
+
+/* --- Parameterized Test 5: Interval Overlap Geometries --- */
+
+/*
+ * Tests against an isolated 8-block system zone [50, 58) (blocks 50..57)
+ * surrounded by valid data blocks on both sides.
+ */
+struct bv_overlap_case {
+ const char *desc;
+ ext4_fsblk_t start_blk;
+ unsigned int count;
+ int expected;
+};
+
+static const struct bv_overlap_case bv_overlap_cases[] = {
+ {
+ .desc = "strictly_before_adjacent",
+ .start_blk = 46,
+ .count = 4,
+ .expected = 1,
+ },
+ {
+ .desc = "strictly_after_adjacent",
+ .start_blk = 58,
+ .count = 4,
+ .expected = 1,
+ },
+ {
+ .desc = "single_first_block_of_zone",
+ .start_blk = 50,
+ .count = 1,
+ .expected = 0,
+ },
+ {
+ .desc = "single_interior_block_of_zone",
+ .start_blk = 54,
+ .count = 1,
+ .expected = 0,
+ },
+ {
+ .desc = "single_last_block_of_zone",
+ .start_blk = 57,
+ .count = 1,
+ .expected = 0,
+ },
+ {
+ .desc = "straddle_left_boundary_by_one",
+ .start_blk = 47,
+ .count = 4,
+ .expected = 0,
+ },
+ {
+ .desc = "straddle_right_boundary_by_one",
+ .start_blk = 57,
+ .count = 4,
+ .expected = 0,
+ },
+ {
+ .desc = "strictly_inside_zone",
+ .start_blk = 52,
+ .count = 3,
+ .expected = 0,
+ },
+ {
+ .desc = "exact_zone_span",
+ .start_blk = 50,
+ .count = 8,
+ .expected = 0,
+ },
+ {
+ .desc = "strictly_enclosing_entire_zone",
+ .start_blk = 45,
+ .count = 20,
+ .expected = 0,
+ },
+};
+
+KUNIT_ARRAY_PARAM_DESC(bv_overlap, bv_overlap_cases, desc);
+
+static void test_sb_block_valid_interval_overlaps(struct kunit *test)
+{
+ const struct bv_overlap_case *tc = test->param_value;
+ struct bv_test_ctx *ctx = test->priv;
+ struct inode *inode;
+
+ /* Place group 2's 8-block inode table at [50..57] */
+ bv_set_group_meta(ctx->sb, &ctx->gdps[2], 2049ULL, 2050ULL, 50ULL);
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+
+ inode = bv_create_inode(test, ctx, 13, S_IFREG | 0644, true);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_sb_block_valid(ctx->sb, NULL, tc->start_blk,
+ tc->count),
+ tc->expected);
+ KUNIT_EXPECT_EQ(test,
+ ext4_inode_block_valid(inode, tc->start_blk, tc->count),
+ tc->expected);
+}
+
+/* --- Parameterized Test 6: Corrupt Overlapping Group Metadata --- */
+
+struct bv_corrupt_meta_case {
+ const char *desc;
+ ext4_group_t grp;
+ ext4_fsblk_t bbitmap;
+ ext4_fsblk_t ibitmap;
+ ext4_fsblk_t itable;
+};
+
+static const struct bv_corrupt_meta_case bv_corrupt_meta_cases[] = {
+ {
+ .desc = "bbitmap_overlaps_group0_base_meta",
+ .grp = 0,
+ .bbitmap = 2,
+ .ibitmap = 6,
+ .itable = 7,
+ },
+ {
+ .desc = "ibitmap_collides_with_bbitmap",
+ .grp = 0,
+ .bbitmap = 20,
+ .ibitmap = 20,
+ .itable = 30,
+ },
+ {
+ .desc = "itable_collides_with_ibitmap",
+ .grp = 0,
+ .bbitmap = 20,
+ .ibitmap = 25,
+ .itable = 25,
+ },
+ {
+ .desc = "group2_bbitmap_collides_with_group0_itable",
+ .grp = 2,
+ .bbitmap = 10,
+ .ibitmap = 2050,
+ .itable = 2051,
+ },
+};
+
+KUNIT_ARRAY_PARAM_DESC(bv_corrupt_meta, bv_corrupt_meta_cases, desc);
+
+static void test_setup_corrupt_group_metadata_overlaps(struct kunit *test)
+{
+ const struct bv_corrupt_meta_case *tc = test->param_value;
+ struct bv_test_ctx *ctx = test->priv;
+
+ bv_set_group_meta(ctx->sb, &ctx->gdps[tc->grp], tc->bbitmap,
+ tc->ibitmap, tc->itable);
+
+ KUNIT_EXPECT_EQ(test, ext4_setup_system_zone(ctx->sb), -EFSCORRUPTED);
+ KUNIT_EXPECT_NULL(test, rcu_access_pointer(ctx->sbi.s_system_blks));
+}
+
+/* --- Test 7: Journal Reserved Inode Ownership & can_merge Isolation --- */
+
+static void test_journal_inode_protection_and_ino_isolation(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ struct inode *j_inode;
+ struct inode *reg_inode;
+
+ /*
+ * Place group 2's block bitmap at block 20 (ino = 0) while group 0's
+ * metadata occupies [1..14] (ino = 0).
+ */
+ bv_set_group_meta(ctx->sb, &ctx->gdps[2], 20ULL, 2050ULL, 2051ULL);
+
+ ext4_set_feature_journal(ctx->sb);
+ ctx->es.s_journal_inum = cpu_to_le32(EXT4_JOURNAL_INO);
+
+ j_inode = bv_create_inode(test, ctx, EXT4_JOURNAL_INO,
+ S_IFREG | 0600, true);
+ reg_inode = bv_create_inode(test, ctx, 14, S_IFREG | 0644, true);
+
+ /*
+ * Non-block-aligned i_size (7 blocks + 1 byte -> rounds up to 8 logical
+ * blocks 0..7) with 4 real extents, an interior sparse hole at lblk 2,
+ * and a trailing sparse hole at lblk 7:
+ * - lblk [0..1] -> pblk [15..16] (right-adjacent to [1..14] ino=0)
+ * - lblk 2 -> sparse hole (n == 0)
+ * - lblk [3..4] -> pblk [18..19] (left-adjacent to [20..20] ino=0)
+ * - lblk 5 -> pblk [201..201] (ino=8)
+ * - lblk 6 -> pblk [200..200] (merges right with 201 -> [200..201])
+ * - lblk 7 -> sparse hole (n == 0)
+ */
+ i_size_write(j_inode, 7 * BV_TEST_BLK_SIZE + 1);
+ bv_append_extent(test, j_inode, 0, 15ULL, 2);
+ bv_append_extent(test, j_inode, 3, 18ULL, 2);
+ bv_append_extent(test, j_inode, 5, 201ULL, 1);
+ bv_append_extent(test, j_inode, 6, 200ULL, 1);
+
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+ KUNIT_EXPECT_EQ(test, ctx->iget_calls, 1U);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)(ctx->last_iget_flags & EXT4_IGET_SPECIAL));
+ KUNIT_EXPECT_EQ(test, atomic_read(&j_inode->i_count), 1);
+
+ /* Journal blocks are rejected for NULL and regular inodes */
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 15, 2), 0);
+ KUNIT_EXPECT_EQ(test, ext4_inode_block_valid(reg_inode, 15, 2), 0);
+ KUNIT_EXPECT_EQ(test, ext4_inode_block_valid(reg_inode, 18, 2), 0);
+ KUNIT_EXPECT_EQ(test, ext4_inode_block_valid(reg_inode, 200, 2), 0);
+
+ /* Journal inode is permitted on all of its own extents */
+ KUNIT_EXPECT_EQ(test, ext4_inode_block_valid(j_inode, 15, 2), 1);
+ KUNIT_EXPECT_EQ(test, ext4_inode_block_valid(j_inode, 18, 2), 1);
+ KUNIT_EXPECT_EQ(test, ext4_inode_block_valid(j_inode, 200, 2), 1);
+
+ /*
+ * Verify can_merge() kept ino=0 and ino=8 zones separate on both the
+ * left ([1..14] vs [15..16]) and right ([18..19] vs [20..20]): the
+ * journal inode must still be rejected on adjacent group metadata.
+ */
+ KUNIT_EXPECT_EQ(test, ext4_inode_block_valid(j_inode, 14, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_inode_block_valid(j_inode, 20, 1), 0);
+
+ /* Unmapped physical gap block 17 remains valid for regular files */
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 17, 1), 1);
+ KUNIT_EXPECT_EQ(test, ext4_inode_block_valid(reg_inode, 17, 1), 1);
+}
+
+/* --- Test 8: Disabled Journal Feature, Zero inum, and Empty Journal --- */
+
+static void test_journal_disabled_or_empty_skips_zones(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ struct inode *j_inode;
+
+ j_inode = bv_create_inode(test, ctx, EXT4_JOURNAL_INO,
+ S_IFREG | 0600, true);
+ i_size_write(j_inode, 2 * BV_TEST_BLK_SIZE);
+ bv_append_extent(test, j_inode, 0, 100ULL, 2);
+
+ /* Case 1: s_journal_inum set, but HAS_JOURNAL feature cleared */
+ ext4_clear_feature_journal(ctx->sb);
+ ctx->es.s_journal_inum = cpu_to_le32(EXT4_JOURNAL_INO);
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+ KUNIT_EXPECT_EQ(test, ctx->iget_calls, 0U);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 100, 2), 1);
+ bv_release_system_zone_locked(ctx->sb);
+
+ /* Case 2: HAS_JOURNAL set, but s_journal_inum == 0 */
+ ext4_set_feature_journal(ctx->sb);
+ ctx->es.s_journal_inum = 0;
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+ KUNIT_EXPECT_EQ(test, ctx->iget_calls, 0U);
+ bv_release_system_zone_locked(ctx->sb);
+
+ /* Case 3: HAS_JOURNAL + valid s_journal_inum, but i_size == 0 */
+ ctx->es.s_journal_inum = cpu_to_le32(EXT4_JOURNAL_INO);
+ i_size_write(j_inode, 0);
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+ KUNIT_EXPECT_EQ(test, ctx->iget_calls, 1U);
+ KUNIT_EXPECT_EQ(test, atomic_read(&j_inode->i_count), 1);
+ KUNIT_EXPECT_EQ(test, ext4_sb_block_valid(ctx->sb, NULL, 100, 2), 1);
+}
+
+/* --- Test 9: Journal Reserved Inode Error Paths --- */
+
+static void test_journal_inode_invalid_inum(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+
+ ext4_set_feature_journal(ctx->sb);
+
+ /* Below EXT4_ROOT_INO (e.g. EXT4_BAD_INO = 1) */
+ ctx->es.s_journal_inum = cpu_to_le32(EXT4_BAD_INO);
+ KUNIT_EXPECT_EQ(test, ext4_setup_system_zone(ctx->sb), -EINVAL);
+ KUNIT_EXPECT_EQ(test, ctx->iget_calls, 0U);
+ KUNIT_EXPECT_NULL(test, rcu_access_pointer(ctx->sbi.s_system_blks));
+
+ /* Above s_inodes_count */
+ ctx->es.s_journal_inum = cpu_to_le32(BV_TEST_TOTAL_INODES + 1);
+ KUNIT_EXPECT_EQ(test, ext4_setup_system_zone(ctx->sb), -EINVAL);
+ KUNIT_EXPECT_EQ(test, ctx->iget_calls, 0U);
+ KUNIT_EXPECT_NULL(test, rcu_access_pointer(ctx->sbi.s_system_blks));
+}
+
+static void test_journal_inode_iget_error(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+
+ ext4_set_feature_journal(ctx->sb);
+ ctx->es.s_journal_inum = cpu_to_le32(EXT4_JOURNAL_INO);
+ ctx->fail_iget_err = -EIO;
+
+ KUNIT_EXPECT_EQ(test, ext4_setup_system_zone(ctx->sb), -EIO);
+ KUNIT_EXPECT_EQ(test, ctx->iget_calls, 1U);
+ KUNIT_EXPECT_NULL(test, rcu_access_pointer(ctx->sbi.s_system_blks));
+}
+
+static void test_journal_inode_map_blocks_error(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ struct inode *j_inode;
+
+ ext4_set_feature_journal(ctx->sb);
+ ctx->es.s_journal_inum = cpu_to_le32(EXT4_JOURNAL_INO);
+ j_inode = bv_create_inode(test, ctx, EXT4_JOURNAL_INO,
+ S_IFREG | 0600, true);
+ i_size_write(j_inode, 3 * BV_TEST_BLK_SIZE);
+
+ /* Fail on 2nd ext4_map_blocks call after 1st extent was inserted */
+ ctx->fail_map_err = -EIO;
+ ctx->fail_map_on_call = 2;
+ kunit_activate_static_stub(test, ext4_map_blocks,
+ bv_map_blocks_fault_stub);
+
+ KUNIT_EXPECT_EQ(test, ext4_setup_system_zone(ctx->sb), -EIO);
+ KUNIT_EXPECT_EQ(test, ctx->map_calls, 2U);
+ KUNIT_EXPECT_EQ(test, atomic_read(&j_inode->i_count), 1);
+ KUNIT_EXPECT_FALSE(test,
+ (bool)(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+ KUNIT_EXPECT_NULL(test, rcu_access_pointer(ctx->sbi.s_system_blks));
+}
+
+static void test_journal_inode_overlap_sets_fs_error(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ struct inode *j_inode;
+
+ ext4_set_feature_journal(ctx->sb);
+ ctx->es.s_journal_inum = cpu_to_le32(EXT4_JOURNAL_INO);
+ j_inode = bv_create_inode(test, ctx, EXT4_JOURNAL_INO,
+ S_IFREG | 0600, true);
+ i_size_write(j_inode, 2 * BV_TEST_BLK_SIZE);
+
+ /* Extent at block 10 collides with group 0's inode table [7..14] */
+ bv_append_extent(test, j_inode, 0, 10ULL, 2);
+
+ KUNIT_EXPECT_EQ(test, ext4_setup_system_zone(ctx->sb), -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_error_ino),
+ (u32)EXT4_JOURNAL_INO);
+ KUNIT_EXPECT_EQ(test, ctx->es.s_last_error_errcode,
+ EXT4_ERR_EFSCORRUPTED);
+ KUNIT_EXPECT_NULL(test, rcu_access_pointer(ctx->sbi.s_system_blks));
+}
+
+/* --- Test 10: ext4_check_blockref and Indirect Macros --- */
+
+static void test_check_blockref_valid_and_sparse(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ struct inode *inode;
+ __le32 zero_refs[4] = { 0, 0, 0, 0 };
+ __le32 mixed_refs[5] = {
+ 0,
+ cpu_to_le32(15),
+ 0,
+ cpu_to_le32(1039),
+ cpu_to_le32(4096),
+ };
+ __le32 bad_first = cpu_to_le32(7);
+
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+ inode = bv_create_inode(test, ctx, 15, S_IFREG | 0644, false);
+
+ /* max == 0 does not inspect entries */
+ KUNIT_EXPECT_EQ(test,
+ ext4_check_blockref(__func__, __LINE__, inode,
+ &bad_first, 0),
+ 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_check_blockref(__func__, __LINE__, inode,
+ zero_refs, ARRAY_SIZE(zero_refs)),
+ 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_check_blockref(__func__, __LINE__, inode,
+ mixed_refs, ARRAY_SIZE(mixed_refs)),
+ 0);
+ KUNIT_EXPECT_FALSE(test,
+ (bool)(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+}
+
+static void
+test_check_blockref_corrupt_entries_and_journal_bypass(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ struct inode *reg_inode;
+ struct inode *j_inode;
+ __le32 meta_refs[4] = {
+ cpu_to_le32(15),
+ 0,
+ cpu_to_le32(7),
+ cpu_to_le32(9),
+ };
+ __le32 oob_refs[2] = {
+ cpu_to_le32(16),
+ cpu_to_le32(BV_TEST_TOTAL_BLKS),
+ };
+
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+ reg_inode = bv_create_inode(test, ctx, 16, S_IFREG | 0644, false);
+ j_inode = bv_create_inode(test, ctx, EXT4_JOURNAL_INO,
+ S_IFREG | 0600, false);
+
+ /* Stops at the first metadata block (7) and records error details */
+ KUNIT_EXPECT_EQ(test,
+ ext4_check_blockref(__func__, __LINE__, reg_inode,
+ meta_refs, ARRAY_SIZE(meta_refs)),
+ -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+ KUNIT_EXPECT_EQ(test, le64_to_cpu(ctx->es.s_last_error_block), 7ULL);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_error_ino), 16U);
+
+ /* Out-of-bounds block reference also triggers -EFSCORRUPTED */
+ bv_clear_fs_error(ctx);
+ KUNIT_EXPECT_EQ(test,
+ ext4_check_blockref(__func__, __LINE__, reg_inode,
+ oob_refs, ARRAY_SIZE(oob_refs)),
+ -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+ KUNIT_EXPECT_EQ(test, le64_to_cpu(ctx->es.s_last_error_block),
+ (u64)BV_TEST_TOTAL_BLKS);
+
+ /* Active journal inode (s_journal->j_inode) bypasses blockref checks */
+ bv_clear_fs_error(ctx);
+ ctx->mock_journal.j_inode = j_inode;
+ ctx->sbi.s_journal = &ctx->mock_journal;
+ KUNIT_EXPECT_EQ(test,
+ ext4_check_blockref(__func__, __LINE__, j_inode,
+ meta_refs, ARRAY_SIZE(meta_refs)),
+ 0);
+ KUNIT_EXPECT_FALSE(test,
+ (bool)(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+}
+
+static void test_ind_check_inode_and_indirect_blockref(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ unsigned int max_addrs = EXT4_ADDR_PER_BLOCK(ctx->sb);
+ struct buffer_head ind_bh;
+ struct inode *inode;
+ __le32 *addrs;
+
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+ inode = bv_create_inode(test, ctx, 17, S_IFREG | 0644, false);
+
+ /* ext4_ind_check_inode() checks direct slots i_data[0..11] */
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(15);
+ EXT4_I(inode)->i_data[EXT4_NDIR_BLOCKS - 1] = cpu_to_le32(20);
+ KUNIT_EXPECT_EQ(test, ext4_ind_check_inode(inode), 0);
+
+ EXT4_I(inode)->i_data[EXT4_NDIR_BLOCKS - 1] = cpu_to_le32(6);
+ KUNIT_EXPECT_EQ(test, ext4_ind_check_inode(inode), -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, le64_to_cpu(ctx->es.s_last_error_block), 6ULL);
+
+ /*
+ * ext4_check_indirect_blockref() checks all EXT4_ADDR_PER_BLOCK(sb)
+ * entries (256 for 1KiB blocks), including the final slot.
+ */
+ bv_clear_fs_error(ctx);
+ memset(&ind_bh, 0, sizeof(ind_bh));
+ memset(ctx->ind_buf, 0, sizeof(ctx->ind_buf));
+ ind_bh.b_data = ctx->ind_buf;
+ ind_bh.b_size = BV_TEST_BLK_SIZE;
+ addrs = (__le32 *)ind_bh.b_data;
+
+ addrs[0] = cpu_to_le32(15);
+ addrs[max_addrs / 2] = cpu_to_le32(1039);
+ addrs[max_addrs - 1] = cpu_to_le32(4096);
+ KUNIT_EXPECT_EQ(test, ext4_check_indirect_blockref(inode, &ind_bh), 0);
+ KUNIT_EXPECT_FALSE(test,
+ (bool)(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+
+ addrs[max_addrs - 1] = cpu_to_le32(1029);
+ KUNIT_EXPECT_EQ(test, ext4_check_indirect_blockref(inode, &ind_bh),
+ -EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, le64_to_cpu(ctx->es.s_last_error_block), 1029ULL);
+}
+
+/* --- Test 11: Integration with ext4_map_blocks() Validity Check --- */
+
+static void test_map_blocks_rejects_extent_in_system_zone(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ struct ext4_map_blocks map;
+ struct inode *bad_inode;
+ struct inode *ok_inode;
+
+ KUNIT_ASSERT_EQ(test, ext4_setup_system_zone(ctx->sb), 0);
+
+ ok_inode = bv_create_inode(test, ctx, 18, S_IFREG | 0644, true);
+ bv_append_extent(test, ok_inode, 0, 25ULL, 3);
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = 0;
+ map.m_len = 3;
+ KUNIT_EXPECT_EQ(test, ext4_map_blocks(NULL, ok_inode, &map, 0), 3);
+ KUNIT_EXPECT_FALSE(test,
+ (bool)(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+
+ /* Corrupt extent mapped onto group 0's inode table [7..14] */
+ bad_inode = bv_create_inode(test, ctx, 19, S_IFREG | 0644, true);
+ bv_append_extent(test, bad_inode, 0, 8ULL, 2);
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = 0;
+ map.m_len = 2;
+ KUNIT_EXPECT_EQ(test, ext4_map_blocks(NULL, bad_inode, &map, 0),
+ -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+ KUNIT_EXPECT_EQ(test, le64_to_cpu(ctx->es.s_last_error_block), 8ULL);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_error_ino), 19U);
+}
+
+static struct kunit_case ext4_block_validity_test_cases[] = {
+ KUNIT_CASE_PARAM(test_sb_block_valid_bounds, bv_bounds_gen_params),
+ KUNIT_CASE(test_release_system_zone_lifecycle),
+ KUNIT_CASE(test_setup_multigroup_sparse_super_and_flex_bg),
+ KUNIT_CASE(test_rbtree_left_right_and_bridge_merges),
+ KUNIT_CASE_PARAM(test_sb_block_valid_interval_overlaps,
+ bv_overlap_gen_params),
+ KUNIT_CASE_PARAM(test_setup_corrupt_group_metadata_overlaps,
+ bv_corrupt_meta_gen_params),
+ KUNIT_CASE(test_journal_inode_protection_and_ino_isolation),
+ KUNIT_CASE(test_journal_disabled_or_empty_skips_zones),
+ KUNIT_CASE(test_journal_inode_invalid_inum),
+ KUNIT_CASE(test_journal_inode_iget_error),
+ KUNIT_CASE(test_journal_inode_map_blocks_error),
+ KUNIT_CASE(test_journal_inode_overlap_sets_fs_error),
+ KUNIT_CASE(test_check_blockref_valid_and_sparse),
+ KUNIT_CASE(test_check_blockref_corrupt_entries_and_journal_bypass),
+ KUNIT_CASE(test_ind_check_inode_and_indirect_blockref),
+ KUNIT_CASE(test_map_blocks_rejects_extent_in_system_zone),
+ {}
+};
+
+static struct kunit_suite ext4_block_validity_test_suite = {
+ .name = "ext4_block_validity_test",
+ .init = bv_test_init,
+ .test_cases = ext4_block_validity_test_cases,
+};
+
+kunit_test_suites(&ext4_block_validity_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 block validity (system zone) tracking");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/block_validity.c b/fs/ext4/block_validity.c
index 3a31e3d718c9..53b042f36a69 100644
--- a/fs/ext4/block_validity.c
+++ b/fs/ext4/block_validity.c
@@ -17,6 +17,7 @@
#include <linux/swap.h>
#include <linux/pagemap.h>
#include <linux/blkdev.h>
+#include <linux/export.h>
#include <linux/slab.h>
#include "ext4.h"
@@ -269,6 +270,7 @@ int ext4_setup_system_zone(struct super_block *sb)
kfree(system_blks);
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_setup_system_zone);
/*
* Called when the filesystem is unmounted or when remounting it with
@@ -291,6 +293,7 @@ void ext4_release_system_zone(struct super_block *sb)
if (system_blks)
call_rcu(&system_blks->rcu, ext4_destroy_system_zone);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_release_system_zone);
int ext4_sb_block_valid(struct super_block *sb, struct inode *inode,
ext4_fsblk_t start_blk, unsigned int count)
@@ -334,6 +337,7 @@ int ext4_sb_block_valid(struct super_block *sb, struct inode *inode,
rcu_read_unlock();
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_block_valid);
/*
* Returns 1 if the passed-in block region (start_blk,
@@ -345,6 +349,7 @@ int ext4_inode_block_valid(struct inode *inode, ext4_fsblk_t start_blk,
{
return ext4_sb_block_valid(inode->i_sb, inode, start_blk, count);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_block_valid);
int ext4_check_blockref(const char *function, unsigned int line,
struct inode *inode, __le32 *p, unsigned int max)
@@ -367,4 +372,5 @@ int ext4_check_blockref(const char *function, unsigned int line,
}
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_check_blockref);
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 15/27] ext4: check multi-block overlap on left traversal in add_system_zone()
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (13 preceding siblings ...)
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:06 ` 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
` (11 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
When walking system_blks->root to insert [start_blk, start_blk + count),
add_system_zone() takes the left branch whenever start_blk <
entry->start_blk instead of checking that the entire range ends at or
before entry->start_blk (unlike ext4_sb_block_valid(), which checks
start_blk + count - 1 < entry->start_blk).
As a result, a corrupt multi-block metadata range (such as a group's
inode table or a reserved journal inode extent mapped in
ext4_protect_reserved_inode() before sbi->s_system_blks is published)
that starts strictly before an existing system zone and extends into or
across it traverses rb_left instead of returning -EFSCORRUPTED. This
inserts overlapping intervals into system_blks->root and breaks the
disjoint-interval invariant relied upon by ext4_sb_block_valid() (for
example, a lookup on blocks past entry->start_blk + entry->count
branches right and misses the overlapping zone stored in rb_left). A
corrupt 64-bit start_blk whose start_blk + count wraps around u64 can
similarly corrupt the rbtree ordering.
Reject zero-length or u64-wrapping ranges up front and require
start_blk + count <= entry->start_blk before taking rb_left in
add_system_zone(). Add KUnit regression tests in
fs/ext4/block_validity-test.c covering group metadata and journal
extents that partially overlap from the left, completely enclose an
existing system zone, or wrap 64-bit block numbers.
Fixes: bf9a379d0980 ("ext4: don't allow overlapping system zones")
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/block_validity-test.c | 68 +++++++++++++++++++++++++++++++++++
fs/ext4/block_validity.c | 5 ++-
2 files changed, 72 insertions(+), 1 deletion(-)
diff --git a/fs/ext4/block_validity-test.c b/fs/ext4/block_validity-test.c
index 5d3dadba1328..5b50f56b3453 100644
--- a/fs/ext4/block_validity-test.c
+++ b/fs/ext4/block_validity-test.c
@@ -1111,6 +1111,27 @@ static const struct bv_corrupt_meta_case bv_corrupt_meta_cases[] = {
.ibitmap = 2050,
.itable = 2051,
},
+ {
+ .desc = "itable_starts_before_and_overlaps_bbitmap",
+ .grp = 0,
+ .bbitmap = 20,
+ .ibitmap = 40,
+ .itable = 15,
+ },
+ {
+ .desc = "itable_starts_before_and_encloses_ibitmap",
+ .grp = 2,
+ .bbitmap = 50,
+ .ibitmap = 55,
+ .itable = 52,
+ },
+ {
+ .desc = "itable_u64_wrap_around_overflow",
+ .grp = 2,
+ .bbitmap = 2049,
+ .ibitmap = 2050,
+ .itable = U64_MAX - 2ULL,
+ },
};
KUNIT_ARRAY_PARAM_DESC(bv_corrupt_meta, bv_corrupt_meta_cases, desc);
@@ -1314,6 +1335,52 @@ static void test_journal_inode_overlap_sets_fs_error(struct kunit *test)
KUNIT_EXPECT_NULL(test, rcu_access_pointer(ctx->sbi.s_system_blks));
}
+static void test_journal_extent_left_overlap_rejected(struct kunit *test)
+{
+ struct bv_test_ctx *ctx = test->priv;
+ struct inode *j_inode;
+
+ /*
+ * Place group 2's block bitmap at block 20 (ino = 0) and map a
+ * multi-block journal extent at [18..21] that starts strictly before
+ * block 20 and extends across it. During ext4_setup_system_zone(),
+ * s_system_blks is still NULL when ext4_map_blocks() runs, so
+ * add_system_zone() must catch this left-traversal overlap.
+ */
+ bv_set_group_meta(ctx->sb, &ctx->gdps[2], 20ULL, 2050ULL, 2051ULL);
+ ext4_set_feature_journal(ctx->sb);
+ ctx->es.s_journal_inum = cpu_to_le32(EXT4_JOURNAL_INO);
+
+ j_inode = bv_create_inode(test, ctx, EXT4_JOURNAL_INO,
+ S_IFREG | 0600, true);
+ i_size_write(j_inode, 4 * BV_TEST_BLK_SIZE);
+ bv_append_extent(test, j_inode, 0, 18ULL, 4);
+
+ KUNIT_EXPECT_EQ(test, ext4_setup_system_zone(ctx->sb), -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_error_ino),
+ (u32)EXT4_JOURNAL_INO);
+ KUNIT_EXPECT_EQ(test, ctx->es.s_last_error_errcode,
+ EXT4_ERR_EFSCORRUPTED);
+ KUNIT_EXPECT_NULL(test, rcu_access_pointer(ctx->sbi.s_system_blks));
+
+ /*
+ * Also verify a journal extent [1020..1040] that completely encloses
+ * group 1's entire metadata zone [1025..1038].
+ */
+ bv_clear_fs_error(ctx);
+ ext4_es_remove_extent(j_inode, 0, EXT_MAX_BLOCKS);
+ ext_inode_hdr(j_inode)->eh_entries = 0;
+ i_size_write(j_inode, 21 * BV_TEST_BLK_SIZE);
+ bv_append_extent(test, j_inode, 0, 1020ULL, 21);
+
+ KUNIT_EXPECT_EQ(test, ext4_setup_system_zone(ctx->sb), -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test,
+ (bool)(ctx->sbi.s_mount_state & EXT4_ERROR_FS));
+ KUNIT_EXPECT_NULL(test, rcu_access_pointer(ctx->sbi.s_system_blks));
+}
+
/* --- Test 10: ext4_check_blockref and Indirect Macros --- */
static void test_check_blockref_valid_and_sparse(struct kunit *test)
@@ -1498,6 +1565,7 @@ static struct kunit_case ext4_block_validity_test_cases[] = {
KUNIT_CASE(test_journal_inode_iget_error),
KUNIT_CASE(test_journal_inode_map_blocks_error),
KUNIT_CASE(test_journal_inode_overlap_sets_fs_error),
+ KUNIT_CASE(test_journal_extent_left_overlap_rejected),
KUNIT_CASE(test_check_blockref_valid_and_sparse),
KUNIT_CASE(test_check_blockref_corrupt_entries_and_journal_bypass),
KUNIT_CASE(test_ind_check_inode_and_indirect_blockref),
diff --git a/fs/ext4/block_validity.c b/fs/ext4/block_validity.c
index 53b042f36a69..34468932e461 100644
--- a/fs/ext4/block_validity.c
+++ b/fs/ext4/block_validity.c
@@ -75,10 +75,13 @@ static int add_system_zone(struct ext4_system_blocks *system_blks,
struct rb_node **n = &system_blks->root.rb_node, *node;
struct rb_node *parent = NULL, *new_node;
+ if (!count || (start_blk + count < start_blk))
+ return -EFSCORRUPTED;
+
while (*n) {
parent = *n;
entry = rb_entry(parent, struct ext4_system_zone, node);
- if (start_blk < entry->start_blk)
+ if (start_blk + count <= entry->start_blk)
n = &(*n)->rb_left;
else if (start_blk >= (entry->start_blk + entry->count))
n = &(*n)->rb_right;
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 16/27] ext4: enable full feature coverage in fs/ext4/.kunitconfig
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (14 preceding siblings ...)
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:06 ` 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
` (10 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Enable CONFIG_EXT4_FS_POSIX_ACL, CONFIG_EXT4_FS_SECURITY,
CONFIG_FS_VERITY, CONFIG_FS_ENCRYPTION, CONFIG_QUOTA, CONFIG_QFMT_V2,
and CONFIG_UNICODE in fs/ext4/.kunitconfig so conditional ext4 source
files and feature-gated code paths (POSIX ACLs, security xattrs,
fs-verity, fscrypt, quota accounting, and UTF-8 casefolding) are
compiled and exercised during KUnit runs.
Fix mock superblock and test setup bugs in existing KUnit suites exposed
by enabling these Kconfig options:
- Provide .get_dquots callbacks in mock super_operations under
CONFIG_QUOTA in block_validity-test.c, extents-test.c,
fast_commit-test.c, inline-test.c, inode-test.c, mballoc-test.c,
namei-test.c, and xattr-test.c so dquot_drop() does not dereference a
NULL callback during ext4_clear_inode().
- Populate sb->s_cop = &ext4_cryptops under CONFIG_FS_ENCRYPTION in
balloc-test.c, extents-test.c, fast_commit-test.c, inline-test.c,
inode-test.c, mballoc-test.c, and xattr-test.c so
fscrypt_put_encryption_info() can resolve inode_info_offs safely on
eviction.
- Fix test_namei_casefold_lookup_and_strict() under CONFIG_UNICODE in
namei-test.c to use SB_ENC_STRICT_MODE_FL (instead of undeclared
SB_STRICT_ENCODING) and propagate EXT4_INODE_CASEFOLD to S_CASEFOLD on
the VFS directory inode via ext4_set_inode_flags().
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/.kunitconfig | 7 +++++++
fs/ext4/balloc-test.c | 3 +++
fs/ext4/block_validity-test.c | 10 ++++++++++
fs/ext4/extents-test.c | 16 ++++++++++++++--
fs/ext4/fast_commit-test.c | 16 ++++++++++++++--
fs/ext4/inline-test.c | 16 +++++++++++++++-
fs/ext4/inode-test.c | 27 +++++++++++++++++++--------
fs/ext4/mballoc-test.c | 13 +++++++++++++
fs/ext4/namei-test.c | 18 ++++++++++++++----
fs/ext4/xattr-test.c | 16 ++++++++++++++--
10 files changed, 123 insertions(+), 19 deletions(-)
diff --git a/fs/ext4/.kunitconfig b/fs/ext4/.kunitconfig
index bf51da7cd9fc..17103c583fdb 100644
--- a/fs/ext4/.kunitconfig
+++ b/fs/ext4/.kunitconfig
@@ -1,3 +1,10 @@
CONFIG_KUNIT=y
CONFIG_EXT4_FS=y
+CONFIG_EXT4_FS_POSIX_ACL=y
+CONFIG_EXT4_FS_SECURITY=y
CONFIG_EXT4_KUNIT_TESTS=y
+CONFIG_FS_VERITY=y
+CONFIG_FS_ENCRYPTION=y
+CONFIG_QUOTA=y
+CONFIG_QFMT_V2=y
+CONFIG_UNICODE=y
diff --git a/fs/ext4/balloc-test.c b/fs/ext4/balloc-test.c
index f3f3479d6afb..96e8aafe231d 100644
--- a/fs/ext4/balloc-test.c
+++ b/fs/ext4/balloc-test.c
@@ -516,6 +516,9 @@ static int balloc_test_init(struct kunit *test)
sb->s_op = &balloc_test_sops;
#ifdef CONFIG_QUOTA
sb->dq_op = &balloc_test_qops;
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
#endif
sb->s_flags |= SB_ACTIVE;
sb->s_blocksize = BALLOC_TEST_BLK_SIZE;
diff --git a/fs/ext4/block_validity-test.c b/fs/ext4/block_validity-test.c
index 5b50f56b3453..ce1e8172c66c 100644
--- a/fs/ext4/block_validity-test.c
+++ b/fs/ext4/block_validity-test.c
@@ -228,12 +228,22 @@ static void bv_test_free_inode_mem(struct inode *inode)
kfree(EXT4_I(inode));
}
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **bv_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+#endif
+
static const struct super_operations bv_test_sops = {
.alloc_inode = bv_test_alloc_inode,
.evict_inode = bv_test_evict_inode,
.destroy_inode = bv_test_destroy_inode,
.free_inode = bv_test_free_inode_mem,
.drop_inode = inode_just_drop,
+#ifdef CONFIG_QUOTA
+ .get_dquots = bv_test_get_dquots,
+#endif
};
static int bv_test_init_fs_context(struct fs_context *fc)
diff --git a/fs/ext4/extents-test.c b/fs/ext4/extents-test.c
index 47ddeb94e2bf..7565a8aa4e10 100644
--- a/fs/ext4/extents-test.c
+++ b/fs/ext4/extents-test.c
@@ -153,9 +153,12 @@ static unsigned long ext_dummy_shrinker_scan(struct shrinker *shrink,
return SHRINK_STOP;
}
-static const struct super_operations ext_dummy_sops = {};
-
#ifdef CONFIG_QUOTA
+static struct dquot __rcu **ext_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+
static qsize_t *ext_get_reserved_space(struct inode *inode)
{
return &EXT4_I(inode)->i_reserved_quota;
@@ -166,6 +169,12 @@ static const struct dquot_operations ext_dummy_qops = {
};
#endif
+static const struct super_operations ext_dummy_sops = {
+#ifdef CONFIG_QUOTA
+ .get_dquots = ext_get_dquots,
+#endif
+};
+
static int ext_init_fs_context(struct fs_context *fc)
{
return 0;
@@ -461,6 +470,9 @@ static int extents_kunit_init(struct kunit *test)
sb->s_op = &ext_dummy_sops;
#ifdef CONFIG_QUOTA
sb->dq_op = &ext_dummy_qops;
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
#endif
sb->s_blocksize = 4096;
sb->s_blocksize_bits = 12;
diff --git a/fs/ext4/fast_commit-test.c b/fs/ext4/fast_commit-test.c
index 57745e51bea6..fdc57dc0d9a8 100644
--- a/fs/ext4/fast_commit-test.c
+++ b/fs/ext4/fast_commit-test.c
@@ -134,9 +134,12 @@ struct fc_test_ctx {
int custom_es_ret;
};
-static const struct super_operations fc_test_sops = {};
-
#ifdef CONFIG_QUOTA
+static struct dquot __rcu **fc_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+
static qsize_t *fc_test_get_reserved_space(struct inode *inode)
{
return &EXT4_I(inode)->i_reserved_quota;
@@ -147,6 +150,12 @@ static const struct dquot_operations fc_test_qops = {
};
#endif
+static const struct super_operations fc_test_sops = {
+#ifdef CONFIG_QUOTA
+ .get_dquots = fc_test_get_dquots,
+#endif
+};
+
static unsigned long fc_dummy_shrinker_count(struct shrinker *shrink,
struct shrink_control *sc)
{
@@ -867,6 +876,9 @@ static int fast_commit_test_init(struct kunit *test)
sb->s_op = &fc_test_sops;
#ifdef CONFIG_QUOTA
sb->dq_op = &fc_test_qops;
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
#endif
strscpy(sb->s_id, "ext4-fc-test", sizeof(sb->s_id));
diff --git a/fs/ext4/inline-test.c b/fs/ext4/inline-test.c
index a75f46475c4a..96ea716d0d61 100644
--- a/fs/ext4/inline-test.c
+++ b/fs/ext4/inline-test.c
@@ -66,7 +66,18 @@ struct inline_test_ctx {
static struct inline_test_ctx *t_ctx;
-static const struct super_operations inline_test_sops = {};
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **inline_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+#endif
+
+static const struct super_operations inline_test_sops = {
+#ifdef CONFIG_QUOTA
+ .get_dquots = inline_test_get_dquots,
+#endif
+};
static bool test_release_folio(struct folio *folio, gfp_t wait)
{
@@ -265,6 +276,9 @@ static int inline_test_init(struct kunit *test)
ctx->sb.s_bdi = &noop_backing_dev_info;
ctx->sb.s_fs_info = &ctx->sbi;
ctx->sb.s_op = &inline_test_sops;
+#ifdef CONFIG_FS_ENCRYPTION
+ ctx->sb.s_cop = &ext4_cryptops;
+#endif
snprintf(ctx->sb.s_id, sizeof(ctx->sb.s_id), "ext4-inline-test");
init_rwsem(&ctx->sb.s_umount);
diff --git a/fs/ext4/inode-test.c b/fs/ext4/inode-test.c
index e8d19c1d4270..181023d1ae83 100644
--- a/fs/ext4/inode-test.c
+++ b/fs/ext4/inode-test.c
@@ -462,15 +462,12 @@ static void inode_test_free_inode(struct inode *inode)
kfree(EXT4_I(inode));
}
-static const struct super_operations inode_test_sops = {
- .alloc_inode = inode_test_alloc_inode,
- .evict_inode = ext4_evict_inode,
- .destroy_inode = inode_test_destroy_inode,
- .free_inode = inode_test_free_inode,
- .drop_inode = inode_just_drop,
-};
-
#ifdef CONFIG_QUOTA
+static struct dquot __rcu **inode_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+
static qsize_t *inode_test_get_reserved_space(struct inode *inode)
{
return &EXT4_I(inode)->i_reserved_quota;
@@ -481,6 +478,17 @@ static const struct dquot_operations inode_test_qops = {
};
#endif
+static const struct super_operations inode_test_sops = {
+ .alloc_inode = inode_test_alloc_inode,
+ .evict_inode = ext4_evict_inode,
+ .destroy_inode = inode_test_destroy_inode,
+ .free_inode = inode_test_free_inode,
+ .drop_inode = inode_just_drop,
+#ifdef CONFIG_QUOTA
+ .get_dquots = inode_test_get_dquots,
+#endif
+};
+
static int inode_test_init_fs_context(struct fs_context *fc)
{
return 0;
@@ -950,6 +958,9 @@ static struct inode_test_ctx *inode_test_setup(struct kunit *test)
sb->s_op = &inode_test_sops;
#ifdef CONFIG_QUOTA
sb->dq_op = &inode_test_qops;
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
#endif
sb->s_flags |= SB_ACTIVE;
sb->s_blocksize = INODE_TEST_BLK_SIZE;
diff --git a/fs/ext4/mballoc-test.c b/fs/ext4/mballoc-test.c
index 402f4efdf25a..48048359f255 100644
--- a/fs/ext4/mballoc-test.c
+++ b/fs/ext4/mballoc-test.c
@@ -54,9 +54,19 @@ static void mbt_free_inode(struct inode *inode)
kfree(EXT4_I(inode));
}
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **mbt_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+#endif
+
static const struct super_operations mbt_sops = {
.alloc_inode = mbt_alloc_inode,
.free_inode = mbt_free_inode,
+#ifdef CONFIG_QUOTA
+ .get_dquots = mbt_get_dquots,
+#endif
};
static int mbt_init_fs_context(struct fs_context *fc)
@@ -93,6 +103,9 @@ static int mbt_mb_init(struct super_block *sb)
*/
INIT_LIST_HEAD(&sb->s_inodes);
sb->s_op = &mbt_sops;
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
+#endif
ret = ext4_mb_init(sb);
if (ret != 0)
diff --git a/fs/ext4/namei-test.c b/fs/ext4/namei-test.c
index df5affef1289..2eec9a1102ce 100644
--- a/fs/ext4/namei-test.c
+++ b/fs/ext4/namei-test.c
@@ -84,9 +84,12 @@ struct namei_test_ctx {
unsigned int mark_inode_dirty_calls;
};
-static const struct super_operations namei_test_sops = {};
-
#ifdef CONFIG_QUOTA
+static struct dquot __rcu **namei_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+
static qsize_t *namei_test_get_reserved_space(struct inode *inode)
{
return &EXT4_I(inode)->i_reserved_quota;
@@ -97,6 +100,12 @@ static const struct dquot_operations namei_test_qops = {
};
#endif
+static const struct super_operations namei_test_sops = {
+#ifdef CONFIG_QUOTA
+ .get_dquots = namei_test_get_dquots,
+#endif
+};
+
static void init_mock_inode_slot(struct namei_test_ctx *ctx,
struct mock_inode_slot *slot,
unsigned long ino, umode_t mode)
@@ -2245,6 +2254,7 @@ static void test_namei_casefold_lookup_and_strict(struct kunit *test)
ctx->es.s_feature_incompat |=
cpu_to_le32(EXT4_FEATURE_INCOMPAT_CASEFOLD);
ext4_set_inode_flag(dir, EXT4_INODE_CASEFOLD);
+ ext4_set_inode_flags(dir, true);
KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root_dir, dir), 0);
init_mock_dentry(&parent_dentry, NULL, "cfdir", dir, &ctx->sb);
@@ -2261,8 +2271,8 @@ static void test_namei_casefold_lookup_and_strict(struct kunit *test)
KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), TEST_FILE1_INO);
brelse(bh);
- /* Enable SB_STRICT_ENCODING -> invalid UTF-8 sequence is rejected */
- ctx->sb.s_encoding_flags |= SB_STRICT_ENCODING;
+ /* Enable SB_ENC_STRICT_MODE_FL -> invalid UTF-8 sequence is rejected */
+ ctx->sb.s_encoding_flags |= SB_ENC_STRICT_MODE_FL;
add_dentry.__d_name = bad_utf8;
KUNIT_EXPECT_EQ(test,
ext4_add_entry_test(NULL, &add_dentry, file1),
diff --git a/fs/ext4/xattr-test.c b/fs/ext4/xattr-test.c
index 35be150353a2..8f85fa481fce 100644
--- a/fs/ext4/xattr-test.c
+++ b/fs/ext4/xattr-test.c
@@ -79,9 +79,12 @@ struct xattr_test_ctx {
unsigned int dirty_metadata_calls;
};
-static const struct super_operations xattr_test_sops = {};
-
#ifdef CONFIG_QUOTA
+static struct dquot __rcu **xattr_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+
static qsize_t *xattr_test_get_reserved_space(struct inode *inode)
{
return &EXT4_I(inode)->i_reserved_quota;
@@ -92,6 +95,12 @@ static const struct dquot_operations xattr_test_qops = {
};
#endif
+static const struct super_operations xattr_test_sops = {
+#ifdef CONFIG_QUOTA
+ .get_dquots = xattr_test_get_dquots,
+#endif
+};
+
static void init_mock_inode_slot(struct xattr_test_ctx *ctx,
struct mock_inode_slot *slot,
unsigned long ino)
@@ -476,6 +485,9 @@ static int xattr_test_init(struct kunit *test)
sb->s_op = &xattr_test_sops;
#ifdef CONFIG_QUOTA
sb->dq_op = &xattr_test_qops;
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
#endif
sb->s_blocksize = TEST_BLK_SIZE;
sb->s_blocksize_bits = TEST_BLK_BITS;
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 17/27] ext4: add KUnit tests for fs/ext4/super.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (15 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 16/27] ext4: enable full feature coverage in fs/ext4/.kunitconfig Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (9 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add a KUnit test suite (ext4_super_test) covering superblock validation,
geometry checks, metadata checksums, mount options, overhead/statfs
accounting, and persistent error recording in fs/ext4/super.c:
- Feature flag compatibility in ext4_feature_set_ok() and
ext4_check_feature_compatibility() across RW/RO mounts, bigalloc
constraints, Hurd creator-OS restrictions, DAX, and encryption levels.
- Inode, block-group, cluster-size, and directory hash initialization in
ext4_inode_info_init(), ext4_block_group_meta_init(),
ext4_handle_clustersize(), and ext4_hash_info_init().
- Filesystem geometry validation in ext4_check_geometry(), descriptor
location arithmetic in descriptor_loc() (linear and meta_bg),
superblock and group descriptor CRC16/CRC32C verification, descriptor
overlap checks in ext4_check_descriptors(), and flex_bg counter
aggregation in ext4_fill_flex_info() / ext4_alloc_flex_bg_array().
- Maximum file size helpers ext4_max_size() / ext4_max_bitmap_size(),
stripe selection in ext4_get_stripe_size(), reserved cluster defaults
in ext4_set_resv_clusters(), overhead calculation in count_overhead()
and ext4_calculate_overhead(), and space reporting in ext4_statfs().
- Mount option defaults, parsing, consistency checks, remount guards,
on-disk s_mount_opts parsing, and _ext4_show_options() formatting.
- Error translation, first/last error persistence in save_error_info(),
and superblock state flushing in ext4_update_super().
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 2 +-
fs/ext4/ext4.h | 45 ++
fs/ext4/super-test.c | 1341 ++++++++++++++++++++++++++++++++++++++++++
fs/ext4/super.c | 222 +++++++
4 files changed, 1609 insertions(+), 1 deletion(-)
create mode 100644 fs/ext4/super-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 5c6afff5f12a..ca1a1201cbfe 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -20,7 +20,7 @@ ext4-test-objs += inode-test.o mballoc-test.o \
namei-test.o inline-test.o \
fast_commit-test.o ialloc-test.o \
balloc-test.o orphan-test.o \
- block_validity-test.o
+ block_validity-test.o super-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 9262ff021bda..230b01c3c4f4 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3404,6 +3404,51 @@ extern unsigned int ext4_num_base_meta_blocks(struct super_block *sb,
ext4_group_t block_group);
extern void print_daily_error_info(struct timer_list *t);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+extern int ext4_errno_to_code_test(int err);
+extern void ext4_save_error_info_test(struct super_block *sb, int error,
+ __u32 ino, __u64 block,
+ const char *func, unsigned int line);
+extern void ext4_update_super_test(struct super_block *sb);
+extern int ext4_check_descriptors_test(struct super_block *sb,
+ ext4_fsblk_t sb_block,
+ ext4_group_t *first_not_zeroed);
+extern loff_t ext4_max_size_test(int blkbits, int has_huge_files);
+extern loff_t ext4_max_bitmap_size_test(int bits, int has_huge_files);
+extern ext4_fsblk_t ext4_descriptor_loc_test(struct super_block *sb,
+ ext4_fsblk_t logical_sb_block,
+ int nr);
+extern unsigned long ext4_get_stripe_size_test(struct ext4_sb_info *sbi);
+extern int ext4_count_overhead_test(struct super_block *sb, ext4_group_t grp,
+ char *buf);
+extern void ext4_set_resv_clusters_test(struct super_block *sb);
+extern void ext4_set_def_opts_test(struct super_block *sb,
+ struct ext4_super_block *es);
+extern int ext4_handle_clustersize_test(struct super_block *sb);
+extern int ext4_inode_info_init_test(struct super_block *sb,
+ struct ext4_super_block *es);
+extern int ext4_init_metadata_csum_test(struct super_block *sb,
+ struct ext4_super_block *es);
+extern int ext4_check_feature_compatibility_test(struct super_block *sb,
+ struct ext4_super_block *es,
+ int silent);
+extern int ext4_check_geometry_test(struct super_block *sb,
+ struct ext4_super_block *es);
+extern int ext4_hash_info_init_test(struct super_block *sb);
+extern int ext4_block_group_meta_init_test(struct super_block *sb, int silent);
+extern int ext4_fill_flex_info_test(struct super_block *sb);
+extern void ext4_flex_groups_free_test(struct ext4_sb_info *sbi);
+extern int ext4_show_options_test(struct seq_file *seq, struct super_block *sb,
+ int nodefs);
+extern int ext4_statfs_test(struct dentry *dentry, struct kstatfs *buf);
+extern int ext4_parse_and_apply_options_test(struct super_block *sb,
+ const char *options,
+ bool is_remount);
+extern int ext4_parse_sb_mount_options_test(struct super_block *sb,
+ unsigned long *journal_devnum,
+ unsigned int *journal_ioprio);
+#endif
+
extern __printf(7, 8)
void __ext4_error(struct super_block *, const char *, unsigned int, bool,
int, __u64, const char *, ...);
diff --git a/fs/ext4/super-test.c b/fs/ext4/super-test.c
new file mode 100644
index 000000000000..3fd7a4d86f31
--- /dev/null
+++ b/fs/ext4/super-test.c
@@ -0,0 +1,1341 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/super.c
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/iversion.h>
+#include <linux/jbd2.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/part_stat.h>
+#include <linux/percpu.h>
+#include <linux/percpu_counter.h>
+#include <linux/quotaops.h>
+#include <linux/rcupdate.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+#include <linux/statfs.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 SUPER_TEST_NGROUPS 4U
+#define SUPER_TEST_BLK_SIZE 1024U
+#define SUPER_TEST_BLK_BITS 10U
+#define SUPER_TEST_BLOCKS_PER_GRP 1024U
+#define SUPER_TEST_FIRST_DATA_BLK 1U
+#define SUPER_TEST_TOTAL_BLKS (SUPER_TEST_FIRST_DATA_BLK + \
+ SUPER_TEST_NGROUPS * \
+ SUPER_TEST_BLOCKS_PER_GRP)
+#define SUPER_TEST_INODE_SIZE 256U
+#define SUPER_TEST_EXTRA_ISIZE 32U
+#define SUPER_TEST_INODES_PER_BLK (SUPER_TEST_BLK_SIZE / \
+ SUPER_TEST_INODE_SIZE)
+#define SUPER_TEST_INODES_PER_GRP 32U
+#define SUPER_TEST_ITB_PER_GRP (SUPER_TEST_INODES_PER_GRP / \
+ SUPER_TEST_INODES_PER_BLK)
+#define SUPER_TEST_TOTAL_INODES (SUPER_TEST_NGROUPS * \
+ SUPER_TEST_INODES_PER_GRP)
+#define SUPER_TEST_FIRST_INO 11U
+#define SUPER_TEST_GDB_COUNT 1U
+#define SUPER_TEST_RSV_GDT_BLKS 2U
+
+struct super_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 buffer_head sb_bh;
+ struct ext4_group_desc gdps[8];
+ journal_t mock_journal;
+ struct inode *journal_inode;
+ bool counters_inited;
+};
+
+static struct kunit *active_super_test;
+
+static struct inode *super_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 = SUPER_TEST_EXTRA_ISIZE;
+ ei->i_last_alloc_group = ~0U;
+
+ return &ei->vfs_inode;
+}
+
+static void super_test_free_inode(struct inode *inode)
+{
+ kfree(EXT4_I(inode));
+}
+
+static void super_test_evict_inode(struct inode *inode)
+{
+ truncate_inode_pages_final(&inode->i_data);
+ clear_inode(inode);
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **super_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+#endif
+
+static const struct super_operations super_test_sops = {
+ .alloc_inode = super_test_alloc_inode,
+ .free_inode = super_test_free_inode,
+ .evict_inode = super_test_evict_inode,
+#ifdef CONFIG_QUOTA
+ .get_dquots = super_test_get_dquots,
+#endif
+};
+
+static void super_test_sb_upd_work(struct work_struct *work)
+{
+}
+
+static void super_test_err_timer(struct timer_list *t)
+{
+}
+
+static int super_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static struct file_system_type super_test_fs_type = {
+ .name = "ext4_super_kunit",
+ .init_fs_context = super_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static inline ext4_fsblk_t super_group_first_blk(ext4_group_t g)
+{
+ return SUPER_TEST_FIRST_DATA_BLK +
+ (ext4_fsblk_t)g * SUPER_TEST_BLOCKS_PER_GRP;
+}
+
+static inline unsigned int super_default_base_meta(ext4_group_t g)
+{
+ if (g == 2)
+ return 0;
+ return 1U + SUPER_TEST_GDB_COUNT + SUPER_TEST_RSV_GDT_BLKS;
+}
+
+static void super_setup_default_groups(struct super_test_ctx *ctx)
+{
+ struct super_block *sb = ctx->sb;
+ ext4_group_t g;
+
+ memset(ctx->gdps, 0, sizeof(ctx->gdps));
+ for (g = 0; g < SUPER_TEST_NGROUPS; g++) {
+ ext4_fsblk_t base = super_group_first_blk(g) +
+ super_default_base_meta(g);
+
+ ext4_block_bitmap_set(sb, &ctx->gdps[g], base);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[g], base + 1);
+ ext4_inode_table_set(sb, &ctx->gdps[g], base + 2);
+ ctx->gdps[g].bg_flags = cpu_to_le16(EXT4_BG_INODE_ZEROED);
+ ext4_group_desc_csum_set(sb, g, &ctx->gdps[g]);
+ }
+}
+
+static struct ext4_group_desc *
+super_get_group_desc_stub(struct super_block *sb, ext4_group_t block_group,
+ struct buffer_head **bh)
+{
+ struct kunit *test = active_super_test;
+ struct super_test_ctx *ctx;
+
+ if (!test)
+ return NULL;
+ ctx = test->priv;
+ if (!ctx || sb != ctx->sb || block_group >= ARRAY_SIZE(ctx->gdps))
+ return NULL;
+ if (bh)
+ *bh = NULL;
+ return &ctx->gdps[block_group];
+}
+
+static struct inode *super_iget_stub(struct super_block *sb, unsigned long ino,
+ ext4_iget_flags flags,
+ const char *function, unsigned int line)
+{
+ struct kunit *test = active_super_test;
+ struct super_test_ctx *ctx;
+
+ if (!test)
+ return ERR_PTR(-EIO);
+ ctx = test->priv;
+ if (!ctx || sb != ctx->sb || !ctx->journal_inode ||
+ ctx->journal_inode->i_ino != ino)
+ return ERR_PTR(-ENOENT);
+
+ ihold(ctx->journal_inode);
+ return ctx->journal_inode;
+}
+
+static void super_test_cleanup(struct super_test_ctx *ctx)
+{
+ int i;
+
+ active_super_test = NULL;
+ ctx->sbi.s_journal = NULL;
+
+ if (ctx->journal_inode) {
+ iput(ctx->journal_inode);
+ ctx->journal_inode = NULL;
+ }
+
+ ext4_flex_groups_free_test(&ctx->sbi);
+
+#ifdef CONFIG_QUOTA
+ for (i = 0; i < EXT4_MAXQUOTAS; i++) {
+ kfree(rcu_access_pointer(ctx->sbi.s_qf_names[i]));
+ rcu_assign_pointer(ctx->sbi.s_qf_names[i], NULL);
+ }
+#endif
+ fscrypt_free_dummy_policy(&ctx->sbi.s_dummy_enc_policy);
+
+ 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);
+ }
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(ctx);
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(super_test_cleanup_action, super_test_cleanup,
+ struct super_test_ctx *);
+
+static int super_test_init(struct kunit *test)
+{
+ struct super_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ struct inode *root_inode;
+ int err;
+
+ ctx = kzalloc_obj(*ctx);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ test->priv = ctx;
+ active_super_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");
+ }
+
+ fc = fs_context_for_mount(&super_test_fs_type, 0);
+ if (IS_ERR(fc)) {
+ err = PTR_ERR(fc);
+ 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);
+ 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)
+ goto err_sb;
+ err = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0, GFP_KERNEL);
+ if (err) {
+ percpu_counter_destroy(&sbi->s_freeclusters_counter);
+ goto err_sb;
+ }
+ 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);
+ goto err_sb;
+ }
+ ctx->counters_inited = true;
+
+ sbi->s_sb = sb;
+ sb->s_fs_info = sbi;
+ sb->s_op = &super_test_sops;
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
+#endif
+ sb->s_flags |= SB_ACTIVE;
+ sb->s_blocksize = SUPER_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = SUPER_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_super_kunit", sizeof(sb->s_id));
+
+ inode_init_once(&ctx->bd_inode);
+ ctx->bd_inode.i_sb = sb;
+ ctx->bd_inode.i_blkbits = SUPER_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;
+
+ sbi->s_es = &ctx->es;
+ ctx->es.s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+ ctx->es.s_first_data_block = cpu_to_le32(SUPER_TEST_FIRST_DATA_BLK);
+ ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_LINUX);
+ ctx->es.s_inodes_count = cpu_to_le32(SUPER_TEST_TOTAL_INODES);
+ ctx->es.s_inodes_per_group = cpu_to_le32(SUPER_TEST_INODES_PER_GRP);
+ ctx->es.s_blocks_per_group = cpu_to_le32(SUPER_TEST_BLOCKS_PER_GRP);
+ ctx->es.s_clusters_per_group = cpu_to_le32(SUPER_TEST_BLOCKS_PER_GRP);
+ ctx->es.s_first_ino = cpu_to_le32(SUPER_TEST_FIRST_INO);
+ ctx->es.s_inode_size = cpu_to_le16(SUPER_TEST_INODE_SIZE);
+ ctx->es.s_min_extra_isize = cpu_to_le16(SUPER_TEST_EXTRA_ISIZE);
+ ctx->es.s_want_extra_isize = cpu_to_le16(SUPER_TEST_EXTRA_ISIZE);
+ ctx->es.s_desc_size = cpu_to_le16(EXT4_MIN_DESC_SIZE_64BIT);
+ ctx->es.s_reserved_gdt_blocks = cpu_to_le16(SUPER_TEST_RSV_GDT_BLKS);
+ ctx->es.s_errors = cpu_to_le16(EXT4_ERRORS_RO);
+ ctx->es.s_checksum_type = EXT4_CRC32C_CHKSUM;
+ generate_random_uuid(ctx->es.s_uuid);
+ ext4_blocks_count_set(&ctx->es, SUPER_TEST_TOTAL_BLKS);
+
+ ext4_set_feature_sparse_super(sb);
+ ext4_set_feature_extents(sb);
+ ext4_set_feature_extra_isize(sb);
+ ext4_set_feature_metadata_csum(sb);
+ ext4_set_feature_64bit(sb);
+ sbi->s_csum_seed = ext4_chksum(~0, ctx->es.s_uuid,
+ sizeof(ctx->es.s_uuid));
+ ext4_superblock_csum_set(sb);
+
+ ctx->sb_bh.b_data = (char *)&ctx->es;
+ ctx->sb_bh.b_size = sizeof(ctx->es);
+ ctx->sb_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->sb_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->sb_bh);
+ sbi->s_sbh = &ctx->sb_bh;
+
+ sbi->s_sb_block = 1;
+ sbi->s_inode_size = SUPER_TEST_INODE_SIZE;
+ sbi->s_inodes_per_block = SUPER_TEST_INODES_PER_BLK;
+ sbi->s_inodes_per_group = SUPER_TEST_INODES_PER_GRP;
+ sbi->s_itb_per_group = SUPER_TEST_ITB_PER_GRP;
+ sbi->s_first_ino = SUPER_TEST_FIRST_INO;
+ sbi->s_want_extra_isize = SUPER_TEST_EXTRA_ISIZE;
+ sbi->s_blocks_per_group = SUPER_TEST_BLOCKS_PER_GRP;
+ sbi->s_clusters_per_group = SUPER_TEST_BLOCKS_PER_GRP;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_cluster_bits = 0;
+ sbi->s_groups_count = SUPER_TEST_NGROUPS;
+ sbi->s_gdb_count = SUPER_TEST_GDB_COUNT;
+ sbi->s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_desc_per_block = SUPER_TEST_BLK_SIZE / EXT4_MIN_DESC_SIZE_64BIT;
+ sbi->s_desc_per_block_bits = ilog2(sbi->s_desc_per_block);
+ sbi->s_resuid = make_kuid(&init_user_ns, EXT4_DEF_RESUID);
+ sbi->s_resgid = make_kgid(&init_user_ns, EXT4_DEF_RESGID);
+ sbi->s_commit_interval = JBD2_DEFAULT_MAX_COMMIT_AGE * HZ;
+ sbi->s_min_batch_time = EXT4_DEF_MIN_BATCH_TIME;
+ sbi->s_max_batch_time = EXT4_DEF_MAX_BATCH_TIME;
+ sbi->s_inode_readahead_blks = EXT4_DEF_INODE_READAHEAD_BLKS;
+ sbi->s_li_wait_mult = EXT4_DEF_LI_WAIT_MULT;
+ sbi->s_err_report_sec = 86400;
+
+ spin_lock_init(&sbi->s_bdev_wb_lock);
+ spin_lock_init(&sbi->s_error_lock);
+ spin_lock_init(&sbi->s_md_lock);
+ INIT_WORK(&sbi->s_sb_upd_work, super_test_sb_upd_work);
+ timer_setup(&sbi->s_err_report, super_test_err_timer, 0);
+
+ 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);
+
+ super_setup_default_groups(ctx);
+
+ root_inode = new_inode(sb);
+ if (!root_inode) {
+ err = -ENOMEM;
+ goto err_counters;
+ }
+ 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) {
+ err = -ENOMEM;
+ goto err_counters;
+ }
+
+ up_write(&sb->s_umount);
+
+ kunit_activate_static_stub(test, ext4_get_group_desc,
+ super_get_group_desc_stub);
+ kunit_activate_static_stub(test, __ext4_iget, super_iget_stub);
+
+ err = kunit_add_action_or_reset(test, super_test_cleanup_action, ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ return 0;
+
+err_counters:
+ percpu_counter_destroy(&sbi->s_freeinodes_counter);
+ percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
+ percpu_counter_destroy(&sbi->s_freeclusters_counter);
+err_sb:
+ active_super_test = NULL;
+ deactivate_locked_super(sb);
+ rcu_barrier();
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ return err;
+}
+
+static void test_feature_set_and_compatibility(struct kunit *test)
+{
+ struct super_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_super_block *es = &ctx->es;
+ __le32 orig_incompat = es->s_feature_incompat;
+ __le32 orig_ro_compat = es->s_feature_ro_compat;
+
+ KUNIT_EXPECT_EQ(test, ext4_feature_set_ok(sb, 0), 1);
+ KUNIT_EXPECT_EQ(test, ext4_check_feature_compatibility_test(sb, es, 0),
+ 0);
+
+ /* Unknown incompat flag must fail on both RW and RO */
+ es->s_feature_incompat = orig_incompat | cpu_to_le32(0x80000000U);
+ KUNIT_EXPECT_EQ(test, ext4_feature_set_ok(sb, 0), 0);
+ KUNIT_EXPECT_EQ(test, ext4_feature_set_ok(sb, 1), 0);
+ KUNIT_EXPECT_EQ(test, ext4_check_feature_compatibility_test(sb, es, 0),
+ -EINVAL);
+ es->s_feature_incompat = orig_incompat;
+
+ /* Unknown ro_compat flag fails RW, succeeds RO */
+ es->s_feature_ro_compat = orig_ro_compat | cpu_to_le32(0x80000000U);
+ KUNIT_EXPECT_EQ(test, ext4_feature_set_ok(sb, 0), 0);
+ KUNIT_EXPECT_EQ(test, ext4_feature_set_ok(sb, 1), 1);
+ es->s_feature_ro_compat = orig_ro_compat;
+
+ /* RO_COMPAT_READONLY forces SB_RDONLY on RW mount request */
+ ext4_set_feature_readonly(sb);
+ sb->s_flags &= ~SB_RDONLY;
+ KUNIT_EXPECT_EQ(test, ext4_feature_set_ok(sb, 0), 1);
+ KUNIT_EXPECT_TRUE(test, sb_rdonly(sb));
+ sb->s_flags &= ~SB_RDONLY;
+ es->s_feature_ro_compat = orig_ro_compat;
+
+ /* bigalloc requires extents and s_first_data_block == 0 on RW */
+ ext4_set_feature_bigalloc(sb);
+ ext4_clear_feature_extents(sb);
+ es->s_first_data_block = 0;
+ KUNIT_EXPECT_EQ(test, ext4_feature_set_ok(sb, 0), 0);
+ KUNIT_EXPECT_EQ(test, ext4_feature_set_ok(sb, 1), 1);
+
+ ext4_set_feature_extents(sb);
+ es->s_first_data_block = cpu_to_le32(1);
+ KUNIT_EXPECT_EQ(test, ext4_feature_set_ok(sb, 0), 0);
+ es->s_first_data_block = 0;
+ KUNIT_EXPECT_EQ(test, ext4_feature_set_ok(sb, 0), 1);
+ es->s_first_data_block = cpu_to_le32(SUPER_TEST_FIRST_DATA_BLK);
+ es->s_feature_ro_compat = orig_ro_compat;
+ es->s_feature_incompat = orig_incompat;
+
+ /* Hurd creator OS sets HURD_COMPAT and rejects 64bit and ea_inode */
+ es->s_creator_os = cpu_to_le32(EXT4_OS_HURD);
+ KUNIT_EXPECT_EQ(test, ext4_check_feature_compatibility_test(sb, es, 0),
+ -EINVAL);
+ KUNIT_EXPECT_NE(test, test_opt2(sb, HURD_COMPAT), 0);
+
+ ext4_clear_feature_64bit(sb);
+ ext4_set_feature_ea_inode(sb);
+ KUNIT_EXPECT_EQ(test, ext4_check_feature_compatibility_test(sb, es, 0),
+ -EINVAL);
+
+ ext4_clear_feature_ea_inode(sb);
+ KUNIT_EXPECT_EQ(test, ext4_check_feature_compatibility_test(sb, es, 0),
+ 0);
+ clear_opt2(sb, HURD_COMPAT);
+ es->s_creator_os = cpu_to_le32(EXT4_OS_LINUX);
+ es->s_feature_incompat = orig_incompat;
+
+#ifdef CONFIG_FS_DAX
+ /* DAX_ALWAYS rejects inline_data and non-DAX block devices */
+ set_opt(sb, DAX_ALWAYS);
+ ext4_set_feature_inline_data(sb);
+ set_bit(EXT4_FLAGS_BDEV_IS_DAX, &ctx->sbi.s_ext4_flags);
+ KUNIT_EXPECT_EQ(test, ext4_check_feature_compatibility_test(sb, es, 0),
+ -EINVAL);
+
+ ext4_clear_feature_inline_data(sb);
+ clear_bit(EXT4_FLAGS_BDEV_IS_DAX, &ctx->sbi.s_ext4_flags);
+ KUNIT_EXPECT_EQ(test, ext4_check_feature_compatibility_test(sb, es, 0),
+ -EINVAL);
+
+ set_bit(EXT4_FLAGS_BDEV_IS_DAX, &ctx->sbi.s_ext4_flags);
+ KUNIT_EXPECT_EQ(test, ext4_check_feature_compatibility_test(sb, es, 0),
+ 0);
+ clear_bit(EXT4_FLAGS_BDEV_IS_DAX, &ctx->sbi.s_ext4_flags);
+ clear_opt(sb, DAX_ALWAYS);
+#endif
+
+ /* Non-zero s_encryption_level with encrypt feature is rejected */
+ ext4_set_feature_encrypt(sb);
+ es->s_encryption_level = 1;
+ KUNIT_EXPECT_EQ(test, ext4_check_feature_compatibility_test(sb, es, 0),
+ -EINVAL);
+ es->s_encryption_level = 0;
+ KUNIT_EXPECT_EQ(test, ext4_check_feature_compatibility_test(sb, es, 0),
+ 0);
+ ext4_clear_feature_encrypt(sb);
+}
+
+static void test_inode_info_init(struct kunit *test)
+{
+ struct super_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;
+
+ /* EXT4_GOOD_OLD_REV forces 128-byte inodes and first_ino == 11 */
+ es->s_rev_level = cpu_to_le32(EXT4_GOOD_OLD_REV);
+ es->s_inode_size = cpu_to_le16(512);
+ es->s_first_ino = cpu_to_le32(99);
+ KUNIT_EXPECT_EQ(test, ext4_inode_info_init_test(sb, es), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_inode_size, EXT4_GOOD_OLD_INODE_SIZE);
+ KUNIT_EXPECT_EQ(test, sbi->s_first_ino,
+ (unsigned int)EXT4_GOOD_OLD_FIRST_INO);
+
+ /* EXT4_DYNAMIC_REV validation of s_first_ino and s_inode_size */
+ es->s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ es->s_first_ino = cpu_to_le32(10);
+ es->s_inode_size = cpu_to_le16(256);
+ KUNIT_EXPECT_EQ(test, ext4_inode_info_init_test(sb, es), -EINVAL);
+
+ es->s_first_ino = cpu_to_le32(11);
+ es->s_inode_size = cpu_to_le16(64);
+ KUNIT_EXPECT_EQ(test, ext4_inode_info_init_test(sb, es), -EINVAL);
+
+ es->s_inode_size = cpu_to_le16(192);
+ KUNIT_EXPECT_EQ(test, ext4_inode_info_init_test(sb, es), -EINVAL);
+
+ es->s_inode_size = cpu_to_le16(2048);
+ KUNIT_EXPECT_EQ(test, ext4_inode_info_init_test(sb, es), -EINVAL);
+
+ /* 128-byte inode uses 1-second timestamp granularity */
+ es->s_inode_size = cpu_to_le16(128);
+ KUNIT_EXPECT_EQ(test, ext4_inode_info_init_test(sb, es), 0);
+ KUNIT_EXPECT_EQ(test, sb->s_time_gran, (u32)NSEC_PER_SEC);
+ KUNIT_EXPECT_EQ(test, sb->s_time_max, EXT4_NON_EXTRA_TIMESTAMP_MAX);
+ KUNIT_EXPECT_EQ(test, sb->s_time_min, EXT4_TIMESTAMP_MIN);
+
+ /* 256-byte inode enables nanosecond extra timestamps and extra_isize */
+ es->s_inode_size = cpu_to_le16(256);
+ es->s_want_extra_isize = cpu_to_le16(200);
+ KUNIT_EXPECT_EQ(test, ext4_inode_info_init_test(sb, es), -EINVAL);
+
+ es->s_want_extra_isize = cpu_to_le16(32);
+ es->s_min_extra_isize = cpu_to_le16(200);
+ KUNIT_EXPECT_EQ(test, ext4_inode_info_init_test(sb, es), -EINVAL);
+
+ es->s_min_extra_isize = cpu_to_le16(64);
+ KUNIT_EXPECT_EQ(test, ext4_inode_info_init_test(sb, es), 0);
+ KUNIT_EXPECT_EQ(test, sb->s_time_gran, 1U);
+ KUNIT_EXPECT_EQ(test, sb->s_time_max, EXT4_EXTRA_TIMESTAMP_MAX);
+ KUNIT_EXPECT_EQ(test, sbi->s_want_extra_isize, 64U);
+}
+
+static void test_block_group_meta_and_clustersize(struct kunit *test)
+{
+ struct super_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;
+
+ /* Without 64bit, descriptor size is forced to 32 bytes */
+ ext4_clear_feature_64bit(sb);
+ es->s_desc_size = cpu_to_le16(128);
+ es->s_state = cpu_to_le16(EXT4_VALID_FS | EXT4_FC_REPLAY);
+ KUNIT_EXPECT_EQ(test, ext4_block_group_meta_init_test(sb, 1), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_desc_size,
+ (unsigned long)EXT4_MIN_DESC_SIZE);
+ KUNIT_EXPECT_EQ(test, sbi->s_mount_state,
+ (unsigned short)EXT4_VALID_FS);
+
+ /* With 64bit, s_desc_size must be a power of 2 in [64, 1024] */
+ ext4_set_feature_64bit(sb);
+ es->s_desc_size = cpu_to_le16(32);
+ KUNIT_EXPECT_EQ(test, ext4_block_group_meta_init_test(sb, 1), -EINVAL);
+ es->s_desc_size = cpu_to_le16(96);
+ KUNIT_EXPECT_EQ(test, ext4_block_group_meta_init_test(sb, 1), -EINVAL);
+ es->s_desc_size = cpu_to_le16(2048);
+ KUNIT_EXPECT_EQ(test, ext4_block_group_meta_init_test(sb, 1), -EINVAL);
+
+ es->s_desc_size = cpu_to_le16(64);
+ es->s_blocks_per_group = 0;
+ KUNIT_EXPECT_EQ(test, ext4_block_group_meta_init_test(sb, 1), -EINVAL);
+ es->s_blocks_per_group = cpu_to_le32(SUPER_TEST_BLOCKS_PER_GRP);
+
+ /* Invalid s_inodes_per_group bounds and alignment */
+ es->s_inodes_per_group = cpu_to_le32(2);
+ KUNIT_EXPECT_EQ(test, ext4_block_group_meta_init_test(sb, 1), -EINVAL);
+ es->s_inodes_per_group = cpu_to_le32(SUPER_TEST_BLK_SIZE * 8 + 8);
+ KUNIT_EXPECT_EQ(test, ext4_block_group_meta_init_test(sb, 1), -EINVAL);
+ es->s_inodes_per_group = cpu_to_le32(36);
+ KUNIT_EXPECT_EQ(test, ext4_block_group_meta_init_test(sb, 1), -EINVAL);
+
+ es->s_inodes_per_group = cpu_to_le32(SUPER_TEST_INODES_PER_GRP);
+ KUNIT_EXPECT_EQ(test, ext4_block_group_meta_init_test(sb, 1), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_itb_per_group,
+ (unsigned long)SUPER_TEST_ITB_PER_GRP);
+ KUNIT_EXPECT_EQ(test, sbi->s_desc_per_block, 16UL);
+
+ /* Non-bigalloc clustersize checks */
+ es->s_log_block_size = 0;
+ es->s_log_cluster_size = cpu_to_le32(1);
+ KUNIT_EXPECT_EQ(test, ext4_handle_clustersize_test(sb), -EINVAL);
+
+ es->s_log_cluster_size = 0;
+ sbi->s_blocks_per_group = SUPER_TEST_BLK_SIZE * 8 + 8;
+ KUNIT_EXPECT_EQ(test, ext4_handle_clustersize_test(sb), -EINVAL);
+
+ sbi->s_blocks_per_group = SUPER_TEST_BLK_SIZE * 8;
+ es->s_clusters_per_group = cpu_to_le32(SUPER_TEST_BLK_SIZE * 8 - 4);
+ KUNIT_EXPECT_EQ(test, ext4_handle_clustersize_test(sb), -EINVAL);
+
+ es->s_clusters_per_group = cpu_to_le32(SUPER_TEST_BLK_SIZE * 4);
+ KUNIT_EXPECT_EQ(test, ext4_handle_clustersize_test(sb), -EINVAL);
+
+ clear_opt2(sb, STD_GROUP_SIZE);
+ es->s_clusters_per_group = cpu_to_le32(SUPER_TEST_BLK_SIZE * 8);
+ KUNIT_EXPECT_EQ(test, ext4_handle_clustersize_test(sb), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_cluster_ratio, 1);
+ KUNIT_EXPECT_EQ(test, sbi->s_cluster_bits, 0);
+ KUNIT_EXPECT_NE(test, test_opt2(sb, STD_GROUP_SIZE), 0);
+
+ /* Bigalloc clustersize checks: 4K blocks, 16K clusters (ratio 4) */
+ ext4_set_feature_bigalloc(sb);
+ sb->s_blocksize = 4096;
+ sb->s_blocksize_bits = 12;
+ es->s_log_block_size = cpu_to_le32(2);
+ es->s_log_cluster_size = cpu_to_le32(1);
+ KUNIT_EXPECT_EQ(test, ext4_handle_clustersize_test(sb), -EINVAL);
+
+ es->s_log_cluster_size = cpu_to_le32(4);
+ sbi->s_blocks_per_group = 4096;
+ es->s_clusters_per_group = cpu_to_le32(512);
+ KUNIT_EXPECT_EQ(test, ext4_handle_clustersize_test(sb), -EINVAL);
+
+ es->s_clusters_per_group = cpu_to_le32(1024);
+ KUNIT_EXPECT_EQ(test, ext4_handle_clustersize_test(sb), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_cluster_bits, 2);
+ KUNIT_EXPECT_EQ(test, sbi->s_cluster_ratio, 4);
+}
+
+static void test_hash_info_init(struct kunit *test)
+{
+ struct super_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;
+
+ es->s_def_hash_version = DX_HASH_LAST + 1;
+ KUNIT_EXPECT_EQ(test, ext4_hash_info_init_test(sb), -EINVAL);
+
+ es->s_def_hash_version = DX_HASH_SIPHASH;
+ KUNIT_EXPECT_EQ(test, ext4_hash_info_init_test(sb), -EINVAL);
+
+ es->s_def_hash_version = DX_HASH_HALF_MD4;
+ es->s_hash_seed[0] = cpu_to_le32(0x11223344U);
+ es->s_hash_seed[1] = cpu_to_le32(0x55667788U);
+ es->s_hash_seed[2] = cpu_to_le32(0x99aabbccU);
+ es->s_hash_seed[3] = cpu_to_le32(0xddeeff00U);
+ ext4_set_feature_dir_index(sb);
+ es->s_flags = cpu_to_le32(EXT2_FLAGS_UNSIGNED_HASH);
+ sbi->s_hash_unsigned = 0;
+
+ KUNIT_EXPECT_EQ(test, ext4_hash_info_init_test(sb), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_def_hash_version,
+ (unsigned char)DX_HASH_HALF_MD4);
+ KUNIT_EXPECT_EQ(test, sbi->s_hash_seed[0], 0x11223344U);
+ KUNIT_EXPECT_EQ(test, sbi->s_hash_seed[1], 0x55667788U);
+ KUNIT_EXPECT_EQ(test, sbi->s_hash_seed[2], 0x99aabbccU);
+ KUNIT_EXPECT_EQ(test, sbi->s_hash_seed[3], 0xddeeff00U);
+ KUNIT_EXPECT_EQ(test, sbi->s_hash_unsigned, 3);
+
+ /* Unflagged dir_index superblock gets native hash signedness flag */
+ es->s_flags = 0;
+ sbi->s_hash_unsigned = 0;
+ KUNIT_EXPECT_EQ(test, ext4_hash_info_init_test(sb), 0);
+ KUNIT_EXPECT_NE(test, le32_to_cpu(es->s_flags) &
+ (EXT2_FLAGS_SIGNED_HASH | EXT2_FLAGS_UNSIGNED_HASH),
+ 0U);
+}
+
+static void test_check_geometry(struct kunit *test)
+{
+ struct super_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;
+
+ /* Excessive s_reserved_gdt_blocks (> blocksize / 4) */
+ es->s_reserved_gdt_blocks = cpu_to_le16(SUPER_TEST_BLK_SIZE / 4 + 1);
+ KUNIT_EXPECT_EQ(test, ext4_check_geometry_test(sb, es), -EINVAL);
+ es->s_reserved_gdt_blocks = cpu_to_le16(SUPER_TEST_RSV_GDT_BLKS);
+
+ /* Filesystem block count exceeding device capacity */
+ ctx->bdev.bd_nr_sectors = 1000;
+ KUNIT_EXPECT_EQ(test, ext4_check_geometry_test(sb, es), -EINVAL);
+ ctx->bdev.bd_nr_sectors = 0;
+
+ /* s_first_data_block >= blocks_count */
+ es->s_first_data_block = cpu_to_le32(SUPER_TEST_TOTAL_BLKS);
+ KUNIT_EXPECT_EQ(test, ext4_check_geometry_test(sb, es), -EINVAL);
+
+ /* s_first_data_block == 0 on 1K blocksize without bigalloc */
+ es->s_first_data_block = 0;
+ es->s_log_block_size = 0;
+ sbi->s_cluster_ratio = 1;
+ KUNIT_EXPECT_EQ(test, ext4_check_geometry_test(sb, es), -EINVAL);
+ es->s_first_data_block = cpu_to_le32(SUPER_TEST_FIRST_DATA_BLK);
+
+ /* Inode count mismatch against groups_count * inodes_per_group */
+ es->s_inodes_count = cpu_to_le32(SUPER_TEST_TOTAL_INODES + 1);
+ KUNIT_EXPECT_EQ(test, ext4_check_geometry_test(sb, es), -EINVAL);
+
+ /* Valid 5-group geometry with partial final group */
+ ext4_blocks_count_set(es, SUPER_TEST_FIRST_DATA_BLK +
+ 4 * SUPER_TEST_BLOCKS_PER_GRP + 200);
+ es->s_inodes_count = cpu_to_le32(5 * SUPER_TEST_INODES_PER_GRP);
+ KUNIT_EXPECT_EQ(test, ext4_check_geometry_test(sb, es), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_groups_count, 5U);
+}
+
+static void test_descriptor_loc(struct kunit *test)
+{
+ struct super_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_super_block *es = &ctx->es;
+
+ /* Non-meta_bg places descriptor blocks linearly after superblock */
+ ext4_clear_feature_meta_bg(sb);
+ KUNIT_EXPECT_EQ(test, ext4_descriptor_loc_test(sb, 1, 0), 2ULL);
+ KUNIT_EXPECT_EQ(test, ext4_descriptor_loc_test(sb, 1, 2), 4ULL);
+
+ /*
+ * With meta_bg and s_desc_per_block == 16 on 1K blocks:
+ * - nr == 0 -> bg 0 (has_super): block 2 for both first_data_block
+ * 1 and 0 (compensated)
+ * - nr == 1 -> bg 16 (no backup super under sparse_super): first
+ * block of group 16
+ * - nr == 2 -> bg 32 ( still no backup super; 32 is power of 2):
+ * first block of group 32
+ */
+ ext4_set_feature_meta_bg(sb);
+ es->s_first_meta_bg = 0;
+ es->s_first_data_block = cpu_to_le32(1);
+ KUNIT_EXPECT_EQ(test, ext4_descriptor_loc_test(sb, 1, 0), 2ULL);
+ KUNIT_EXPECT_EQ(test, ext4_descriptor_loc_test(sb, 1, 1),
+ ext4_group_first_block_no(sb, 16));
+
+ es->s_first_data_block = 0;
+ KUNIT_EXPECT_EQ(test, ext4_descriptor_loc_test(sb, 0, 0), 2ULL);
+
+ /* Hybrid s_first_meta_bg == 2 uses linear layout for nr < 2 */
+ es->s_first_data_block = cpu_to_le32(1);
+ es->s_first_meta_bg = cpu_to_le32(2);
+ KUNIT_EXPECT_EQ(test, ext4_descriptor_loc_test(sb, 1, 0), 2ULL);
+ KUNIT_EXPECT_EQ(test, ext4_descriptor_loc_test(sb, 1, 1), 3ULL);
+ KUNIT_EXPECT_EQ(test, ext4_descriptor_loc_test(sb, 1, 2),
+ ext4_group_first_block_no(sb, 32));
+}
+
+static void test_superblock_and_group_desc_csum(struct kunit *test)
+{
+ struct super_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_group_desc gdp = {};
+
+ /* Superblock checksum type, verification, and seed derivation */
+ es->s_checksum_type = 99;
+ KUNIT_EXPECT_EQ(test, ext4_init_metadata_csum_test(sb, es), -EINVAL);
+
+ es->s_checksum_type = EXT4_CRC32C_CHKSUM;
+ ext4_superblock_csum_set(sb);
+ es->s_checksum ^= cpu_to_le32(1);
+ KUNIT_EXPECT_EQ(test, ext4_init_metadata_csum_test(sb, es), -EFSBADCRC);
+
+ ext4_clear_feature_csum_seed(sb);
+ ext4_superblock_csum_set(sb);
+ sbi->s_csum_seed = 0;
+ KUNIT_EXPECT_EQ(test, ext4_init_metadata_csum_test(sb, es), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_csum_seed,
+ ext4_chksum(~0, es->s_uuid, sizeof(es->s_uuid)));
+
+ ext4_set_feature_csum_seed(sb);
+ es->s_checksum_seed = cpu_to_le32(0xdeadbeefU);
+ ext4_superblock_csum_set(sb);
+ KUNIT_EXPECT_EQ(test, ext4_init_metadata_csum_test(sb, es), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_csum_seed, 0xdeadbeefU);
+
+ /* 32-byte vs 64-byte free_group_clusters accessor/mutator */
+ sbi->s_desc_size = EXT4_MIN_DESC_SIZE;
+ ext4_free_group_clusters_set(sb, &gdp, 0x12345678U);
+ KUNIT_EXPECT_EQ(test, ext4_free_group_clusters(sb, &gdp), 0x5678U);
+
+ sbi->s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+ ext4_free_group_clusters_set(sb, &gdp, 0x12345678U);
+ KUNIT_EXPECT_EQ(test, ext4_free_group_clusters(sb, &gdp), 0x12345678U);
+
+ /* metadata_csum (CRC32C) group descriptor verification */
+ ext4_group_desc_csum_set(sb, 3, &gdp);
+ KUNIT_EXPECT_EQ(test, ext4_group_desc_csum_verify(sb, 3, &gdp), 1);
+ KUNIT_EXPECT_EQ(test, ext4_group_desc_csum_verify(sb, 2, &gdp), 0);
+ gdp.bg_block_bitmap_hi = cpu_to_le32(1);
+ KUNIT_EXPECT_EQ(test, ext4_group_desc_csum_verify(sb, 3, &gdp), 0);
+
+ /* Legacy gdt_csum (CRC16) verification across 64-byte descriptor */
+ ext4_clear_feature_metadata_csum(sb);
+ ext4_set_feature_gdt_csum(sb);
+ ext4_group_desc_csum_set(sb, 1, &gdp);
+ KUNIT_EXPECT_EQ(test, ext4_group_desc_csum_verify(sb, 1, &gdp), 1);
+ gdp.bg_free_blocks_count_hi ^= cpu_to_le16(1);
+ KUNIT_EXPECT_EQ(test, ext4_group_desc_csum_verify(sb, 1, &gdp), 0);
+
+ /* No checksum features enabled -> verify always succeeds */
+ ext4_clear_feature_gdt_csum(sb);
+ KUNIT_EXPECT_EQ(test, ext4_group_desc_csum_verify(sb, 1, &gdp), 1);
+}
+
+static void test_check_descriptors_and_flex_info(struct kunit *test)
+{
+ struct super_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ struct flex_groups *fg0, *fg1;
+ ext4_group_t first_not_zeroed = 0;
+
+ ctx->gdps[2].bg_flags = 0;
+ ext4_group_desc_csum_set(sb, 2, &ctx->gdps[2]);
+ KUNIT_EXPECT_EQ(test,
+ ext4_check_descriptors_test(sb, 1, &first_not_zeroed),
+ 1);
+ KUNIT_EXPECT_EQ(test, first_not_zeroed, 2U);
+
+ /* Block bitmap overlapping superblock fails RW, tolerated on RO */
+ ext4_block_bitmap_set(sb, &ctx->gdps[0], 1);
+ ext4_group_desc_csum_set(sb, 0, &ctx->gdps[0]);
+ KUNIT_EXPECT_EQ(test, ext4_check_descriptors_test(sb, 1, NULL), 0);
+ sb->s_flags |= SB_RDONLY;
+ KUNIT_EXPECT_EQ(test, ext4_check_descriptors_test(sb, 1, NULL), 1);
+ sb->s_flags &= ~SB_RDONLY;
+
+ /* Inode table overlapping GDT block (block 2) fails RW */
+ super_setup_default_groups(ctx);
+ ext4_inode_table_set(sb, &ctx->gdps[0], 2);
+ ext4_group_desc_csum_set(sb, 0, &ctx->gdps[0]);
+ KUNIT_EXPECT_EQ(test, ext4_check_descriptors_test(sb, 1, NULL), 0);
+
+ /* Out-of-group bitmap fails without flex_bg, succeeds with flex_bg */
+ super_setup_default_groups(ctx);
+ ext4_block_bitmap_set(sb, &ctx->gdps[1], 20);
+ ext4_group_desc_csum_set(sb, 1, &ctx->gdps[1]);
+ sb->s_flags |= SB_RDONLY;
+ KUNIT_EXPECT_EQ(test, ext4_check_descriptors_test(sb, 1, NULL), 0);
+ sb->s_flags &= ~SB_RDONLY;
+
+ ext4_set_feature_flex_bg(sb);
+ KUNIT_EXPECT_EQ(test, ext4_check_descriptors_test(sb, 1, NULL), 1);
+
+ /* Bad group descriptor checksum fails RW, tolerated on RO */
+ ctx->gdps[1].bg_checksum ^= cpu_to_le16(0xffff);
+ KUNIT_EXPECT_EQ(test, ext4_check_descriptors_test(sb, 1, NULL), 0);
+ sb->s_flags |= SB_RDONLY;
+ KUNIT_EXPECT_EQ(test, ext4_check_descriptors_test(sb, 1, NULL), 1);
+ sb->s_flags &= ~SB_RDONLY;
+
+ /* Flex-bg allocation and per-flex-group counter aggregation */
+ super_setup_default_groups(ctx);
+ ctx->es.s_log_groups_per_flex = 32;
+ KUNIT_EXPECT_EQ(test, ext4_fill_flex_info_test(sb), 1);
+ KUNIT_EXPECT_EQ(test, sbi->s_log_groups_per_flex, 0U);
+
+ ext4_free_group_clusters_set(sb, &ctx->gdps[0], 100);
+ ext4_free_inodes_set(sb, &ctx->gdps[0], 10);
+ ext4_used_dirs_set(sb, &ctx->gdps[0], 2);
+ ext4_free_group_clusters_set(sb, &ctx->gdps[1], 150);
+ ext4_free_inodes_set(sb, &ctx->gdps[1], 12);
+ ext4_used_dirs_set(sb, &ctx->gdps[1], 3);
+ ext4_free_group_clusters_set(sb, &ctx->gdps[2], 200);
+ ext4_free_inodes_set(sb, &ctx->gdps[2], 14);
+ ext4_used_dirs_set(sb, &ctx->gdps[2], 1);
+ ext4_free_group_clusters_set(sb, &ctx->gdps[3], 250);
+ ext4_free_inodes_set(sb, &ctx->gdps[3], 16);
+ ext4_used_dirs_set(sb, &ctx->gdps[3], 4);
+
+ ctx->es.s_log_groups_per_flex = 1;
+ KUNIT_EXPECT_EQ(test, ext4_fill_flex_info_test(sb), 1);
+ KUNIT_EXPECT_EQ(test, sbi->s_flex_groups_allocated, 2);
+
+ fg0 = sbi_array_rcu_deref(sbi, s_flex_groups, 0);
+ fg1 = sbi_array_rcu_deref(sbi, s_flex_groups, 1);
+ KUNIT_EXPECT_EQ(test, atomic64_read(&fg0->free_clusters), 250LL);
+ KUNIT_EXPECT_EQ(test, atomic_read(&fg0->free_inodes), 22);
+ KUNIT_EXPECT_EQ(test, atomic_read(&fg0->used_dirs), 5);
+ KUNIT_EXPECT_EQ(test, atomic64_read(&fg1->free_clusters), 450LL);
+ KUNIT_EXPECT_EQ(test, atomic_read(&fg1->free_inodes), 30);
+ KUNIT_EXPECT_EQ(test, atomic_read(&fg1->used_dirs), 5);
+
+ /* Growing the flex_bg array preserves populated entries */
+ KUNIT_EXPECT_EQ(test, ext4_alloc_flex_bg_array(sb, 8), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_flex_groups_allocated, 4);
+ fg0 = sbi_array_rcu_deref(sbi, s_flex_groups, 0);
+ KUNIT_EXPECT_EQ(test, atomic64_read(&fg0->free_clusters), 250LL);
+}
+
+static void test_max_sizes_stripe_and_resv_clusters(struct kunit *test)
+{
+ struct super_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+
+ /* Without huge_file, 2^32 - 1 512B sectors caps max extent file size */
+ KUNIT_EXPECT_EQ(test, ext4_max_size_test(10, 0),
+ (((1LL << 32) - 1) >> 1) << 10);
+ KUNIT_EXPECT_EQ(test, ext4_max_size_test(12, 0),
+ (((1LL << 32) - 1) >> 3) << 12);
+
+ /* With huge_file, 32-bit ee_block caps max size at (2^32 - 1) blocks */
+ KUNIT_EXPECT_EQ(test, ext4_max_size_test(10, 1),
+ ((1LL << 32) - 1) << 10);
+ KUNIT_EXPECT_EQ(test, ext4_max_size_test(12, 1),
+ min_t(loff_t, ((1LL << 32) - 1) << 12,
+ MAX_LFS_FILESIZE));
+
+ /* Indirect bitmap max size is strictly smaller with !huge_file on 4K */
+ KUNIT_EXPECT_LT(test, ext4_max_bitmap_size_test(12, 0),
+ ext4_max_bitmap_size_test(12, 1));
+ KUNIT_EXPECT_EQ(test, ext4_max_bitmap_size_test(10, 1),
+ (12LL + 256 + 65536 + 16777216) << 10);
+
+ /* Stripe priority: mount s_stripe -> raid_stripe_width -> stride */
+ sbi->s_blocks_per_group = 1024;
+ sbi->s_stripe = 64;
+ ctx->es.s_raid_stripe_width = cpu_to_le32(128);
+ ctx->es.s_raid_stride = cpu_to_le16(16);
+ KUNIT_EXPECT_EQ(test, ext4_get_stripe_size_test(sbi), 64UL);
+
+ sbi->s_stripe = 2048;
+ KUNIT_EXPECT_EQ(test, ext4_get_stripe_size_test(sbi), 128UL);
+
+ ctx->es.s_raid_stripe_width = cpu_to_le32(2048);
+ KUNIT_EXPECT_EQ(test, ext4_get_stripe_size_test(sbi), 16UL);
+
+ ctx->es.s_raid_stride = cpu_to_le16(1);
+ KUNIT_EXPECT_EQ(test, ext4_get_stripe_size_test(sbi), 0UL);
+
+ /* Reserved clusters: skipped without extents, 2% up to 4096 cap */
+ atomic64_set(&sbi->s_resv_clusters, 7);
+ ext4_clear_feature_extents(sb);
+ ext4_blocks_count_set(&ctx->es, 5000);
+ ext4_set_resv_clusters_test(sb);
+ KUNIT_EXPECT_EQ(test, atomic64_read(&sbi->s_resv_clusters), 7LL);
+
+ ext4_set_feature_extents(sb);
+ ext4_set_resv_clusters_test(sb);
+ KUNIT_EXPECT_EQ(test, atomic64_read(&sbi->s_resv_clusters), 100LL);
+
+ ext4_blocks_count_set(&ctx->es, 500000);
+ ext4_set_resv_clusters_test(sb);
+ KUNIT_EXPECT_EQ(test, atomic64_read(&sbi->s_resv_clusters), 4096LL);
+}
+
+static void test_overhead_and_statfs(struct kunit *test)
+{
+ struct super_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ struct kstatfs st = {};
+ char *buf;
+
+ buf = kunit_kzalloc(test, SUPER_TEST_BLK_SIZE, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+
+ /*
+ * Non-bigalloc per-group overhead:
+ * - groups 0, 1, 3: 1 (super) + 1 (gdb) + 2 (rsv_gdt) + 8 (itb) + 2
+ * = 14
+ * - group 2 (sparse_super): 0 + 0 + 0 + 8 (itb) + 2 = 10
+ * Total with s_first_data_block == 1: 1 + 14 + 14 + 10 + 14 = 53
+ */
+ KUNIT_EXPECT_EQ(test, ext4_count_overhead_test(sb, 0, buf), 14);
+ KUNIT_EXPECT_EQ(test, ext4_count_overhead_test(sb, 2, buf), 10);
+ KUNIT_EXPECT_EQ(test, ext4_calculate_overhead(sb), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_overhead, 53UL);
+
+ /* Loaded internal journal adds j_total_len clusters */
+ ctx->mock_journal.j_total_len = 64;
+ sbi->s_journal = &ctx->mock_journal;
+ KUNIT_EXPECT_EQ(test, ext4_calculate_overhead(sb), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_overhead, 117UL);
+ sbi->s_journal = NULL;
+
+ /* Unloaded internal journal falls back to ext4_get_journal_inode() */
+ ctx->journal_inode = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->journal_inode);
+ ctx->journal_inode->i_ino = EXT4_JOURNAL_INO;
+ ctx->journal_inode->i_mode = S_IFREG | 0600;
+ ctx->journal_inode->i_flags |= S_NOQUOTA;
+ set_nlink(ctx->journal_inode, 1);
+ i_size_write(ctx->journal_inode, 32 * SUPER_TEST_BLK_SIZE);
+ ext4_set_feature_journal(sb);
+ ctx->es.s_journal_inum = cpu_to_le32(EXT4_JOURNAL_INO);
+ KUNIT_EXPECT_EQ(test, ext4_calculate_overhead(sb), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_overhead, 85UL);
+ ext4_clear_feature_journal(sb);
+ ctx->es.s_journal_inum = 0;
+
+ /* Bigalloc deduplicates metadata blocks sharing the same cluster */
+ ext4_set_feature_bigalloc(sb);
+ ctx->es.s_first_data_block = 0;
+ sbi->s_cluster_bits = 2;
+ sbi->s_cluster_ratio = 4;
+ sbi->s_clusters_per_group = SUPER_TEST_BLOCKS_PER_GRP / 4;
+ sbi->s_groups_count = 1;
+ ext4_block_bitmap_set(sb, &ctx->gdps[0], 2);
+ ext4_inode_bitmap_set(sb, &ctx->gdps[0], 3);
+ ext4_inode_table_set(sb, &ctx->gdps[0], 4);
+ memset(buf, 0, SUPER_TEST_BLK_SIZE);
+ /* Cluster 0 holds super+gdb+bbitmap+ibitmap; clusters 1..2 hold itb */
+ KUNIT_EXPECT_EQ(test, ext4_count_overhead_test(sb, 0, buf), 3);
+
+ /* Restore 1:1 geometry and verify ext4_statfs accounting & clamping */
+ ext4_clear_feature_bigalloc(sb);
+ ctx->es.s_first_data_block = cpu_to_le32(SUPER_TEST_FIRST_DATA_BLK);
+ sbi->s_cluster_bits = 0;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_clusters_per_group = SUPER_TEST_BLOCKS_PER_GRP;
+ sbi->s_groups_count = SUPER_TEST_NGROUPS;
+ sbi->s_overhead = 53;
+ ext4_r_blocks_count_set(&ctx->es, 100);
+ atomic64_set(&sbi->s_resv_clusters, 20);
+ percpu_counter_set(&sbi->s_freeclusters_counter, 500);
+ percpu_counter_set(&sbi->s_dirtyclusters_counter, 50);
+ percpu_counter_set(&sbi->s_freeinodes_counter, 90);
+
+ KUNIT_EXPECT_EQ(test, ext4_statfs_test(sb->s_root, &st), 0);
+ KUNIT_EXPECT_EQ(test, st.f_type, (long)EXT4_SUPER_MAGIC);
+ KUNIT_EXPECT_EQ(test, st.f_blocks, (u64)(SUPER_TEST_TOTAL_BLKS - 53));
+ KUNIT_EXPECT_EQ(test, st.f_bfree, 450ULL);
+ KUNIT_EXPECT_EQ(test, st.f_bavail, 330ULL);
+ KUNIT_EXPECT_EQ(test, st.f_files, (u64)SUPER_TEST_TOTAL_INODES);
+ KUNIT_EXPECT_EQ(test, st.f_ffree, 90ULL);
+ KUNIT_EXPECT_EQ(test, st.f_namelen, (long)EXT4_NAME_LEN);
+
+ /* MINIX_DF omits s_overhead; dirty > free clamps f_bfree/f_bavail */
+ set_opt(sb, MINIX_DF);
+ percpu_counter_set(&sbi->s_freeclusters_counter, 30);
+ percpu_counter_set(&sbi->s_dirtyclusters_counter, 80);
+ KUNIT_EXPECT_EQ(test, ext4_statfs_test(sb->s_root, &st), 0);
+ KUNIT_EXPECT_EQ(test, st.f_blocks, (u64)SUPER_TEST_TOTAL_BLKS);
+ KUNIT_EXPECT_EQ(test, st.f_bfree, 0ULL);
+ KUNIT_EXPECT_EQ(test, st.f_bavail, 0ULL);
+}
+
+static void test_mount_options_and_show_options(struct kunit *test)
+{
+ struct super_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;
+ unsigned int ioprio = 0;
+ struct seq_file seq = {};
+ char *out;
+
+ out = kunit_kzalloc(test, 1024, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, out);
+
+ /* Superblock default mount options */
+ sbi->s_mount_opt = 0;
+ sbi->s_mount_opt2 = 0;
+ es->s_default_mount_opts = cpu_to_le32(EXT4_DEFM_DISCARD |
+ EXT4_DEFM_NOBARRIER |
+ EXT4_DEFM_JMODE_WBACK);
+ es->s_errors = cpu_to_le16(EXT4_ERRORS_CONTINUE);
+ ext4_set_feature_fast_commit(sb);
+ ext4_set_def_opts_test(sb, es);
+
+ KUNIT_EXPECT_NE(test, test_opt(sb, DISCARD), 0);
+ KUNIT_EXPECT_EQ(test, test_opt(sb, BARRIER), 0);
+ KUNIT_EXPECT_EQ(test, test_opt(sb, DATA_FLAGS),
+ EXT4_MOUNT_WRITEBACK_DATA);
+ KUNIT_EXPECT_NE(test, test_opt(sb, ERRORS_CONT), 0);
+ KUNIT_EXPECT_NE(test, test_opt(sb, JOURNAL_CHECKSUM), 0);
+ KUNIT_EXPECT_NE(test, test_opt2(sb, JOURNAL_FAST_COMMIT), 0);
+ sbi->s_def_mount_opt = sbi->s_mount_opt;
+ sbi->s_def_mount_opt2 = sbi->s_mount_opt2;
+
+ /* Valid option string parsing and application */
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "nodelalloc,minixdf,commit=15,stripe=32,errors=panic",
+ false),
+ 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "resuid=1000,resgid=2000,inode_readahead_blks=64",
+ false),
+ 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "max_dir_size_kb=512", false),
+ 0);
+ KUNIT_EXPECT_EQ(test, test_opt(sb, DELALLOC), 0);
+ KUNIT_EXPECT_NE(test, test_opt(sb, MINIX_DF), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_commit_interval, 15UL * HZ);
+ KUNIT_EXPECT_EQ(test, sbi->s_stripe, 32UL);
+ KUNIT_EXPECT_EQ(test, from_kuid(&init_user_ns, sbi->s_resuid), 1000U);
+ KUNIT_EXPECT_EQ(test, from_kgid(&init_user_ns, sbi->s_resgid), 2000U);
+ KUNIT_EXPECT_EQ(test, sbi->s_inode_readahead_blks, 64U);
+ KUNIT_EXPECT_EQ(test, sbi->s_max_dir_size_kb, 512U);
+ KUNIT_EXPECT_NE(test, test_opt(sb, ERRORS_PANIC), 0);
+
+ /* Invalid parameter values and consistency rejections */
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "inode_readahead_blks=3", false),
+ -EINVAL);
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "mb_optimize_scan=2", false),
+ -EINVAL);
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "debug_want_extra_isize=200", false),
+ -EINVAL);
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "debug_want_extra_isize=5", false),
+ -EINVAL);
+#ifdef CONFIG_QUOTA
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "prjquota", false),
+ -EINVAL);
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "usrjquota=aquota.user", false),
+ -EINVAL);
+#endif
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "test_dummy_encryption", false),
+ -EINVAL);
+
+ /* Remount restrictions on data= mode changes */
+ sbi->s_journal = &ctx->mock_journal;
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "data=ordered", true),
+ -EINVAL);
+#ifdef CONFIG_FS_DAX
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "dax=never", false),
+ 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "dax=always", true),
+ -EINVAL);
+#else
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_and_apply_options_test(sb,
+ "dax=never", false),
+ -EINVAL);
+#endif
+ sbi->s_journal = NULL;
+
+ /* On-disk es->s_mount_opts parsing and malformed fallback */
+ strscpy_pad(es->s_mount_opts, "nodiscard,commit=25,journal_ioprio=5");
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_sb_mount_options_test(sb, NULL, &ioprio),
+ 0);
+ KUNIT_EXPECT_EQ(test, test_opt(sb, DISCARD), 0);
+ KUNIT_EXPECT_EQ(test, sbi->s_commit_interval, 25UL * HZ);
+ KUNIT_EXPECT_EQ(test, IOPRIO_PRIO_DATA(ioprio), 5UL);
+
+ strscpy_pad(es->s_mount_opts, "nonexistent_opt=1");
+ KUNIT_EXPECT_EQ(test,
+ ext4_parse_sb_mount_options_test(sb, NULL, NULL), 0);
+
+ /* _ext4_show_options and ext4_seq_options_show formatting */
+ seq.buf = out;
+ seq.size = 1023;
+ seq.private = sb;
+ KUNIT_EXPECT_EQ(test, ext4_show_options_test(&seq, sb, 0), 0);
+ out[seq.count] = '\0';
+ KUNIT_EXPECT_NOT_NULL(test, strstr(out, ",minixdf"));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(out, ",commit=25"));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(out, ",stripe=32"));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(out, ",errors=panic"));
+
+ memset(out, 0, 1024);
+ seq.count = 0;
+ KUNIT_EXPECT_EQ(test, ext4_seq_options_show(&seq, NULL), 0);
+ out[seq.count] = '\0';
+ KUNIT_EXPECT_TRUE(test, str_has_prefix(out, "rw\n"));
+ KUNIT_EXPECT_NOT_NULL(test, strstr(out, "\nresuid=1000\n"));
+}
+
+static void test_error_recording_and_update_super(struct kunit *test)
+{
+ struct super_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;
+ __le32 saved_wtime;
+
+ KUNIT_EXPECT_EQ(test, ext4_errno_to_code_test(EIO), EXT4_ERR_EIO);
+ KUNIT_EXPECT_EQ(test, ext4_errno_to_code_test(ENOMEM), EXT4_ERR_ENOMEM);
+ KUNIT_EXPECT_EQ(test, ext4_errno_to_code_test(EFSCORRUPTED),
+ EXT4_ERR_EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, ext4_errno_to_code_test(ENOSPC), EXT4_ERR_ENOSPC);
+ KUNIT_EXPECT_EQ(test, ext4_errno_to_code_test(999), EXT4_ERR_UNKNOWN);
+
+ /* First error defaults 0 -> EFSCORRUPTED; second updates last only */
+ ext4_save_error_info_test(sb, 0, 17, 1024, "first_fn", 111);
+ ext4_save_error_info_test(sb, EIO, 29, 2048, "second_fn", 222);
+
+ KUNIT_EXPECT_EQ(test, sbi->s_add_error_count, 2U);
+ KUNIT_EXPECT_EQ(test, sbi->s_first_error_code, EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, sbi->s_last_error_code, EIO);
+
+ sbi->s_kbytes_written = 100;
+ percpu_counter_set(&sbi->s_freeclusters_counter, 321);
+ percpu_counter_set(&sbi->s_freeinodes_counter, 45);
+
+ ext4_update_super_test(sb);
+
+ KUNIT_EXPECT_EQ(test, sbi->s_add_error_count, 0U);
+ KUNIT_EXPECT_NE(test, le16_to_cpu(es->s_state) & EXT4_ERROR_FS, 0U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_error_count), 2U);
+ KUNIT_EXPECT_EQ(test, es->s_first_error_errcode,
+ (u8)EXT4_ERR_EFSCORRUPTED);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_first_error_ino), 17U);
+ KUNIT_EXPECT_EQ(test, le64_to_cpu(es->s_first_error_block), 1024ULL);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_first_error_line), 111U);
+ KUNIT_EXPECT_STREQ(test, (char *)es->s_first_error_func, "first_fn");
+
+ KUNIT_EXPECT_EQ(test, es->s_last_error_errcode, (u8)EXT4_ERR_EIO);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_last_error_ino), 29U);
+ KUNIT_EXPECT_EQ(test, le64_to_cpu(es->s_last_error_block), 2048ULL);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_last_error_line), 222U);
+ KUNIT_EXPECT_STREQ(test, (char *)es->s_last_error_func, "second_fn");
+
+ KUNIT_EXPECT_EQ(test, ext4_free_blocks_count(es), 321ULL);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(es->s_free_inodes_count), 45U);
+ KUNIT_EXPECT_EQ(test, le64_to_cpu(es->s_kbytes_written), 100ULL);
+ KUNIT_EXPECT_EQ(test, es->s_checksum, ext4_superblock_csum(es));
+
+ /* Read-only superblock update must not touch s_wtime */
+ es->s_wtime = cpu_to_le32(123456U);
+ saved_wtime = es->s_wtime;
+ sb->s_flags |= SB_RDONLY;
+ ext4_update_super_test(sb);
+ KUNIT_EXPECT_EQ(test, es->s_wtime, saved_wtime);
+}
+
+static struct kunit_case ext4_super_test_cases[] = {
+ KUNIT_CASE(test_feature_set_and_compatibility),
+ KUNIT_CASE(test_inode_info_init),
+ KUNIT_CASE(test_block_group_meta_and_clustersize),
+ KUNIT_CASE(test_hash_info_init),
+ KUNIT_CASE(test_check_geometry),
+ KUNIT_CASE(test_descriptor_loc),
+ KUNIT_CASE(test_superblock_and_group_desc_csum),
+ KUNIT_CASE(test_check_descriptors_and_flex_info),
+ KUNIT_CASE(test_max_sizes_stripe_and_resv_clusters),
+ KUNIT_CASE(test_overhead_and_statfs),
+ KUNIT_CASE(test_mount_options_and_show_options),
+ KUNIT_CASE(test_error_recording_and_update_super),
+ {}
+};
+
+static struct kunit_suite ext4_super_test_suite = {
+ .name = "ext4_super_test",
+ .init = super_test_init,
+ .test_cases = ext4_super_test_cases,
+};
+
+kunit_test_suites(&ext4_super_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 superblock validation and options");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/super.c b/fs/ext4/super.c
index 88bd28cedd99..838dabda35c5 100644
--- a/fs/ext4/super.c
+++ b/fs/ext4/super.c
@@ -325,6 +325,7 @@ __le32 ext4_superblock_csum(struct ext4_super_block *es)
return cpu_to_le32(csum);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_superblock_csum);
static int ext4_superblock_csum_verify(struct super_block *sb,
struct ext4_super_block *es)
@@ -344,6 +345,7 @@ void ext4_superblock_csum_set(struct super_block *sb)
es->s_checksum = ext4_superblock_csum(es);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_superblock_csum_set);
ext4_fsblk_t ext4_block_bitmap(struct super_block *sb,
struct ext4_group_desc *bg)
@@ -379,6 +381,7 @@ __u32 ext4_free_group_clusters(struct super_block *sb,
(EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
(__u32)le16_to_cpu(bg->bg_free_blocks_count_hi) << 16 : 0);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_group_clusters);
__u32 ext4_free_inodes_count(struct super_block *sb,
struct ext4_group_desc *bg)
@@ -441,6 +444,7 @@ void ext4_free_group_clusters_set(struct super_block *sb,
if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
bg->bg_free_blocks_count_hi = cpu_to_le16(count >> 16);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_group_clusters_set);
void ext4_free_inodes_set(struct super_block *sb,
struct ext4_group_desc *bg, __u32 count)
@@ -3154,6 +3158,7 @@ int ext4_seq_options_show(struct seq_file *seq, void *offset)
seq_putc(seq, '\n');
return rc;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_seq_options_show);
static int ext4_setup_super(struct super_block *sb, struct ext4_super_block *es,
int read_only)
@@ -3259,6 +3264,7 @@ int ext4_alloc_flex_bg_array(struct super_block *sb, ext4_group_t ngroup)
ext4_kvfree_array_rcu(old_groups);
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_alloc_flex_bg_array);
static int ext4_fill_flex_info(struct super_block *sb)
{
@@ -3732,6 +3738,7 @@ int ext4_feature_set_ok(struct super_block *sb, int readonly)
#endif /* CONFIG_QUOTA */
return 1;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_feature_set_ok);
/*
* This function is called once a day by default if we have errors logged
@@ -4342,6 +4349,7 @@ int ext4_calculate_overhead(struct super_block *sb)
kvfree(buf);
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_calculate_overhead);
static void ext4_set_resv_clusters(struct super_block *sb)
{
@@ -7679,4 +7687,218 @@ module_exit(ext4_exit_fs)
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_bread);
+
+int ext4_errno_to_code_test(int err)
+{
+ return ext4_errno_to_code(err);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_errno_to_code_test);
+
+void ext4_save_error_info_test(struct super_block *sb, int error,
+ __u32 ino, __u64 block,
+ const char *func, unsigned int line)
+{
+ save_error_info(sb, error, ino, block, func, line);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_save_error_info_test);
+
+void ext4_update_super_test(struct super_block *sb)
+{
+ ext4_update_super(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_update_super_test);
+
+int ext4_check_descriptors_test(struct super_block *sb,
+ ext4_fsblk_t sb_block,
+ ext4_group_t *first_not_zeroed)
+{
+ return ext4_check_descriptors(sb, sb_block, first_not_zeroed);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_check_descriptors_test);
+
+loff_t ext4_max_size_test(int blkbits, int has_huge_files)
+{
+ return ext4_max_size(blkbits, has_huge_files);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_max_size_test);
+
+loff_t ext4_max_bitmap_size_test(int bits, int has_huge_files)
+{
+ return ext4_max_bitmap_size(bits, has_huge_files);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_max_bitmap_size_test);
+
+ext4_fsblk_t ext4_descriptor_loc_test(struct super_block *sb,
+ ext4_fsblk_t logical_sb_block, int nr)
+{
+ return descriptor_loc(sb, logical_sb_block, nr);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_descriptor_loc_test);
+
+unsigned long ext4_get_stripe_size_test(struct ext4_sb_info *sbi)
+{
+ return ext4_get_stripe_size(sbi);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_stripe_size_test);
+
+int ext4_count_overhead_test(struct super_block *sb, ext4_group_t grp,
+ char *buf)
+{
+ return count_overhead(sb, grp, buf);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_count_overhead_test);
+
+void ext4_set_resv_clusters_test(struct super_block *sb)
+{
+ ext4_set_resv_clusters(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_set_resv_clusters_test);
+
+void ext4_set_def_opts_test(struct super_block *sb,
+ struct ext4_super_block *es)
+{
+ ext4_set_def_opts(sb, es);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_set_def_opts_test);
+
+int ext4_handle_clustersize_test(struct super_block *sb)
+{
+ return ext4_handle_clustersize(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_handle_clustersize_test);
+
+int ext4_inode_info_init_test(struct super_block *sb,
+ struct ext4_super_block *es)
+{
+ return ext4_inode_info_init(sb, es);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_info_init_test);
+
+int ext4_init_metadata_csum_test(struct super_block *sb,
+ struct ext4_super_block *es)
+{
+ return ext4_init_metadata_csum(sb, es);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_metadata_csum_test);
+
+int ext4_check_feature_compatibility_test(struct super_block *sb,
+ struct ext4_super_block *es,
+ int silent)
+{
+ return ext4_check_feature_compatibility(sb, es, silent);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_check_feature_compatibility_test);
+
+int ext4_check_geometry_test(struct super_block *sb,
+ struct ext4_super_block *es)
+{
+ return ext4_check_geometry(sb, es);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_check_geometry_test);
+
+int ext4_hash_info_init_test(struct super_block *sb)
+{
+ return ext4_hash_info_init(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_hash_info_init_test);
+
+int ext4_block_group_meta_init_test(struct super_block *sb, int silent)
+{
+ return ext4_block_group_meta_init(sb, silent);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_group_meta_init_test);
+
+int ext4_fill_flex_info_test(struct super_block *sb)
+{
+ return ext4_fill_flex_info(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fill_flex_info_test);
+
+void ext4_flex_groups_free_test(struct ext4_sb_info *sbi)
+{
+ ext4_flex_groups_free(sbi);
+ rcu_assign_pointer(sbi->s_flex_groups, NULL);
+ sbi->s_flex_groups_allocated = 0;
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_flex_groups_free_test);
+
+int ext4_show_options_test(struct seq_file *seq, struct super_block *sb,
+ int nodefs)
+{
+ return _ext4_show_options(seq, sb, nodefs);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_show_options_test);
+
+int ext4_statfs_test(struct dentry *dentry, struct kstatfs *buf)
+{
+ return ext4_statfs(dentry, buf);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_statfs_test);
+
+int ext4_parse_and_apply_options_test(struct super_block *sb,
+ const char *options, bool is_remount)
+{
+ struct ext4_fs_context *ctx;
+ struct fs_context *fc;
+ char *opts_copy = NULL;
+ int ret;
+
+ if (options) {
+ opts_copy = kstrdup(options, GFP_KERNEL);
+ if (!opts_copy)
+ return -ENOMEM;
+ }
+
+ fc = kzalloc_obj(*fc);
+ if (!fc) {
+ kfree(opts_copy);
+ return -ENOMEM;
+ }
+
+ ctx = kzalloc_obj(*ctx);
+ if (!ctx) {
+ kfree(fc);
+ kfree(opts_copy);
+ return -ENOMEM;
+ }
+
+ fc->fs_private = ctx;
+ fc->s_fs_info = EXT4_SB(sb);
+ fc->purpose = is_remount ? FS_CONTEXT_FOR_RECONFIGURE :
+ FS_CONTEXT_FOR_MOUNT;
+
+ ret = parse_options(fc, opts_copy);
+ if (ret == 0) {
+ down_write(&sb->s_umount);
+ ret = ext4_check_opt_consistency(fc, sb);
+ if (ret == 0)
+ ext4_apply_options(fc, sb);
+ up_write(&sb->s_umount);
+ }
+
+ kfree(opts_copy);
+ ext4_fc_free(fc);
+ kfree(fc);
+ return ret;
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_parse_and_apply_options_test);
+
+int ext4_parse_sb_mount_options_test(struct super_block *sb,
+ unsigned long *journal_devnum,
+ unsigned int *journal_ioprio)
+{
+ struct ext4_fs_context m_ctx = {};
+ int ret;
+
+ down_write(&sb->s_umount);
+ ret = parse_apply_sb_mount_options(sb, &m_ctx);
+ up_write(&sb->s_umount);
+
+ if (journal_devnum)
+ *journal_devnum = m_ctx.journal_devnum;
+ if (journal_ioprio)
+ *journal_ioprio = m_ctx.journal_ioprio;
+ return ret;
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_parse_sb_mount_options_test);
#endif
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 18/27] ext4: add KUnit tests for fs/ext4/sysfs.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (16 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 17/27] ext4: add KUnit tests for fs/ext4/super.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (8 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add a KUnit test suite (ext4_sysfs_test) covering ext4 sysfs attribute
show/store handlers, feature capability attributes, and sysfs/procfs
registration in fs/ext4/sysfs.c:
- Generic pointer attributes across sbi, superblock, and explicit
targets (attr_pointer_ui, attr_pointer_ul, attr_pointer_pi negative
value rejection, attr_pointer_u8, attr_pointer_u64 LE64 decoding,
attr_pointer_string 32-byte bounding, and attr_pointer_atomic).
- Range-checked read-write attributes: inode_readahead_blks power-of-2
and upper-bound enforcement, reserved_clusters bounds against total
clusters, mb_best_avail_max_trim_order <= 64, mb_group_prealloc <=
s_clusters_per_group, and err_report_sec <= 1 year.
- Computed read-only attributes: delayed_allocation_blocks cluster-to-
block scaling, session_write_kbytes and lifetime_write_kbytes sector
accounting, sra_exceeded_retry_limit percpu summation, 40-bit error
timestamps (first_error_time / last_error_time), and journal_task.
- Feature capability attributes under /sys/fs/ext4/features, error
injection via trigger_fs_error, and lifecycle coverage of
ext4_register_sysfs(), ext4_notify_error_sysfs(), and
ext4_unregister_sysfs().
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 3 +-
fs/ext4/ext4.h | 8 +
fs/ext4/sysfs-test.c | 653 +++++++++++++++++++++++++++++++++++++++++++
fs/ext4/sysfs.c | 55 ++++
4 files changed, 718 insertions(+), 1 deletion(-)
create mode 100644 fs/ext4/sysfs-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index ca1a1201cbfe..ff489dff825d 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -20,7 +20,8 @@ ext4-test-objs += inode-test.o mballoc-test.o \
namei-test.o inline-test.o \
fast_commit-test.o ialloc-test.o \
balloc-test.o orphan-test.o \
- block_validity-test.o super-test.o
+ block_validity-test.o super-test.o \
+ sysfs-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 230b01c3c4f4..599b595befd8 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -4008,6 +4008,14 @@ extern int ext4_register_sysfs(struct super_block *sb);
extern void ext4_unregister_sysfs(struct super_block *sb);
extern int __init ext4_init_sysfs(void);
extern void ext4_exit_sysfs(void);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+extern ssize_t ext4_sysfs_attr_show_test(struct ext4_sb_info *sbi,
+ const char *name, char *buf);
+extern ssize_t ext4_sysfs_attr_store_test(struct ext4_sb_info *sbi,
+ const char *name, const char *buf,
+ size_t len);
+extern ssize_t ext4_sysfs_feat_show_test(const char *name, char *buf);
+#endif
/* block_validity */
extern void ext4_release_system_zone(struct super_block *sb);
diff --git a/fs/ext4/sysfs-test.c b/fs/ext4/sysfs-test.c
new file mode 100644
index 000000000000..c13785420aa1
--- /dev/null
+++ b/fs/ext4/sysfs-test.c
@@ -0,0 +1,653 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/sysfs.c
+ */
+
+#include <kunit/test.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/completion.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/iversion.h>
+#include <linux/jbd2.h>
+#include <linux/kernel.h>
+#include <linux/kobject.h>
+#include <linux/module.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/sched.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/timer.h>
+#include <linux/workqueue.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "xattr.h"
+
+#define SYSFS_TEST_BLK_SIZE 4096U
+#define SYSFS_TEST_BLK_BITS 12U
+#define SYSFS_TEST_TOTAL_BLKS 4096U
+#define SYSFS_TEST_CLUSTERS_PER_GRP 256U
+
+struct sysfs_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 inode buddy_inode;
+ struct buffer_head sb_bh;
+ journal_t mock_journal;
+ bool counters_inited;
+};
+
+static struct inode *sysfs_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 = 32;
+ ei->i_last_alloc_group = ~0U;
+
+ return &ei->vfs_inode;
+}
+
+static void sysfs_test_free_inode(struct inode *inode)
+{
+ kfree(EXT4_I(inode));
+}
+
+static void sysfs_test_evict_inode(struct inode *inode)
+{
+ truncate_inode_pages_final(&inode->i_data);
+ clear_inode(inode);
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **sysfs_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+#endif
+
+static const struct super_operations sysfs_test_sops = {
+ .alloc_inode = sysfs_test_alloc_inode,
+ .free_inode = sysfs_test_free_inode,
+ .evict_inode = sysfs_test_evict_inode,
+#ifdef CONFIG_QUOTA
+ .get_dquots = sysfs_test_get_dquots,
+#endif
+};
+
+static void sysfs_test_sb_upd_work(struct work_struct *work)
+{
+}
+
+static void sysfs_test_err_timer(struct timer_list *t)
+{
+}
+
+static int sysfs_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static struct file_system_type sysfs_test_fs_type = {
+ .name = "ext4_sysfs_kunit",
+ .init_fs_context = sysfs_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static void sysfs_test_cleanup(struct sysfs_test_ctx *ctx)
+{
+ ctx->sbi.s_journal = NULL;
+
+ 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_dirtyclusters_counter);
+ percpu_counter_destroy(&ctx->sbi.s_sra_exceeded_retry_limit);
+ }
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(ctx);
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(sysfs_test_cleanup_action, sysfs_test_cleanup,
+ struct sysfs_test_ctx *);
+
+static int sysfs_test_init(struct kunit *test)
+{
+ struct sysfs_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ int err;
+
+ ctx = kzalloc_obj(*ctx);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ test->priv = ctx;
+
+ 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");
+ }
+
+ fc = fs_context_for_mount(&sysfs_test_fs_type, 0);
+ if (IS_ERR(fc)) {
+ err = PTR_ERR(fc);
+ 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);
+ 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_dirtyclusters_counter, 0, GFP_KERNEL);
+ if (err)
+ goto err_sb;
+ err = percpu_counter_init(&sbi->s_sra_exceeded_retry_limit, 0,
+ GFP_KERNEL);
+ if (err) {
+ percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
+ goto err_sb;
+ }
+ ctx->counters_inited = true;
+
+ sbi->s_sb = sb;
+ sb->s_fs_info = sbi;
+ sb->s_op = &sysfs_test_sops;
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
+#endif
+ sb->s_flags |= SB_ACTIVE;
+ sb->s_blocksize = SYSFS_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = SYSFS_TEST_BLK_BITS;
+ strscpy(sb->s_id, "ext4_sysfs_kunit", sizeof(sb->s_id));
+
+ inode_init_once(&ctx->bd_inode);
+ ctx->bd_inode.i_sb = sb;
+ ctx->bd_inode.i_blkbits = SYSFS_TEST_BLK_BITS;
+ ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data;
+ ctx->bd_inode.i_data.host = &ctx->bd_inode;
+ 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;
+
+ inode_init_once(&ctx->buddy_inode);
+ ctx->buddy_inode.i_sb = sb;
+ sbi->s_buddy_cache = &ctx->buddy_inode;
+
+ bgl_lock_init(&ctx->bgl);
+ sbi->s_blockgroup_lock = &ctx->bgl;
+
+ sbi->s_es = &ctx->es;
+ ctx->es.s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+ ext4_blocks_count_set(&ctx->es, SYSFS_TEST_TOTAL_BLKS);
+
+ 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_cluster_bits = 2;
+ sbi->s_cluster_ratio = 4;
+ sbi->s_clusters_per_group = SYSFS_TEST_CLUSTERS_PER_GRP;
+ set_opt(sb, ERRORS_CONT);
+
+ mutex_init(&sbi->s_error_notify_mutex);
+ spin_lock_init(&sbi->s_error_lock);
+ spin_lock_init(&sbi->s_md_lock);
+ INIT_WORK(&sbi->s_sb_upd_work, sysfs_test_sb_upd_work);
+ timer_setup(&sbi->s_err_report, sysfs_test_err_timer, 0);
+
+ 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);
+
+ up_write(&sb->s_umount);
+
+ err = kunit_add_action_or_reset(test, sysfs_test_cleanup_action, ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ return 0;
+
+err_sb:
+ deactivate_locked_super(sb);
+ rcu_barrier();
+ free_percpu(ctx->bdev.bd_stats);
+ kfree(ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ return err;
+}
+
+static void test_generic_pointer_attrs(struct kunit *test)
+{
+ struct sysfs_test_ctx *ctx = test->priv;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ struct ext4_super_block *es = &ctx->es;
+ char *buf;
+
+ buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_show_test(sbi, "no_such_attr", buf),
+ (ssize_t)-ENOENT);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "no_such_attr",
+ "1", 1),
+ (ssize_t)-ENOENT);
+
+ /* Explicit pointer RO attr: max_writeback_mb_bump == 128 */
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi,
+ "max_writeback_mb_bump", buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "128\n");
+
+ /* RW sbi unsigned int (attr_pointer_ui) and unsigned long (ul) */
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "inode_goal",
+ " 42\n", 5),
+ (ssize_t)5);
+ KUNIT_EXPECT_EQ(test, sbi->s_inode_goal, 42U);
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "inode_goal", buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "42\n");
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "last_trim_minblks",
+ "1048576", 7),
+ (ssize_t)7);
+ KUNIT_EXPECT_EQ(test, sbi->s_last_trim_minblks, 1048576UL);
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "last_trim_minblks",
+ buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "1048576\n");
+
+ /* RW positive int (attr_pointer_pi) rejects negative values */
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "err_ratelimit_burst",
+ "-1", 2),
+ (ssize_t)-EINVAL);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "err_ratelimit_burst",
+ "0x80000000", 10),
+ (ssize_t)-EINVAL);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "err_ratelimit_burst",
+ "15", 2),
+ (ssize_t)2);
+ KUNIT_EXPECT_EQ(test, sbi->s_err_ratelimit_state.burst, 15);
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "err_ratelimit_burst",
+ buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "15\n");
+
+ /* RO atomic_t on sbi */
+ atomic_set(&sbi->s_warning_count, 9);
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "warning_count", buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "9\n");
+
+ /* RO superblock LE32, U8, LE64, and fixed-size string attrs */
+ es->s_error_count = cpu_to_le32(77);
+ es->s_first_error_errcode = EXT4_ERR_EIO;
+ es->s_first_error_block = cpu_to_le64(0x123456789abcULL);
+ memset(es->s_first_error_func, 'A', sizeof(es->s_first_error_func));
+
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "errors_count", buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "77\n");
+
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "first_error_errcode",
+ buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "2\n");
+
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "first_error_block",
+ buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "20015998343868\n");
+
+ /* Non-NUL-terminated 32-byte s_first_error_func is bounded to 32 */
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_show_test(sbi, "first_error_func",
+ buf),
+ (ssize_t)33);
+ KUNIT_EXPECT_EQ(test, buf[32], '\n');
+}
+
+static void test_validated_rw_attrs(struct kunit *test)
+{
+ struct sysfs_test_ctx *ctx = test->priv;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ char *buf;
+
+ buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+
+ /* inode_readahead_blks: 0 or power-of-2 <= 0x40000000 */
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "inode_readahead_blks",
+ "0", 1),
+ (ssize_t)1);
+ KUNIT_EXPECT_EQ(test, sbi->s_inode_readahead_blks, 0U);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "inode_readahead_blks",
+ "0x40000000", 10),
+ (ssize_t)10);
+ KUNIT_EXPECT_EQ(test, sbi->s_inode_readahead_blks, 0x40000000U);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "inode_readahead_blks",
+ "3", 1),
+ (ssize_t)-EINVAL);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "inode_readahead_blks",
+ "0x80000000", 10),
+ (ssize_t)-EINVAL);
+
+ /*
+ * reserved_clusters: 4096 blocks >> 2 cluster_bits == 1024 clusters;
+ * valid range is [0, 1023].
+ */
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "reserved_clusters",
+ "1023", 4),
+ (ssize_t)4);
+ KUNIT_EXPECT_EQ(test, atomic64_read(&sbi->s_resv_clusters), 1023LL);
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "reserved_clusters",
+ buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "1023\n");
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "reserved_clusters",
+ "1024", 4),
+ (ssize_t)-EINVAL);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "reserved_clusters",
+ "0xffffffffffffffff", 18),
+ (ssize_t)-EINVAL);
+
+ /* mb_best_avail_max_trim_order: [0, 64] */
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi,
+ "mb_best_avail_max_trim_order", "64", 2),
+ (ssize_t)2);
+ KUNIT_EXPECT_EQ(test, sbi->s_mb_best_avail_max_trim_order, 64U);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi,
+ "mb_best_avail_max_trim_order", "65", 2),
+ (ssize_t)-EINVAL);
+
+ /* mb_group_prealloc: [0, s_clusters_per_group == 256] */
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "mb_group_prealloc",
+ "256", 3),
+ (ssize_t)3);
+ KUNIT_EXPECT_EQ(test, sbi->s_mb_group_prealloc, 256U);
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "mb_group_prealloc",
+ "257", 3),
+ (ssize_t)-EINVAL);
+
+ /* err_report_sec: <= 365 * 24 * 60 * 60 (31536000) */
+ sbi->s_err_report_sec = 0;
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "err_report_sec",
+ "3600", 4),
+ (ssize_t)4);
+ KUNIT_EXPECT_EQ(test, sbi->s_err_report_sec, 3600UL);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "err_report_sec",
+ "31536000", 8),
+ (ssize_t)8);
+ KUNIT_EXPECT_EQ(test, sbi->s_err_report_sec, 31536000UL);
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "err_report_sec", buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "31536000\n");
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "err_report_sec",
+ "31536001", 8),
+ (ssize_t)-EINVAL);
+}
+
+static void test_computed_ro_and_error_time_attrs(struct kunit *test)
+{
+ struct sysfs_test_ctx *ctx = test->priv;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ struct ext4_super_block *es = &ctx->es;
+ char expected_pid[32];
+ char *buf;
+
+ buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+
+ /* 7 dirty clusters * 4 blocks/cluster == 28 delayed allocation blks */
+ percpu_counter_set(&sbi->s_dirtyclusters_counter, 7);
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi,
+ "delayed_allocation_blocks", buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "28\n");
+
+ /* (50 - 10) sectors >> 1 == 20 KB session, + 100 == 120 KB lifetime */
+ sbi->s_sectors_written_start = 10;
+ sbi->s_kbytes_written = 100;
+ part_stat_add(&ctx->bdev, sectors[STAT_WRITE], 50);
+
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "session_write_kbytes",
+ buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "20\n");
+
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "lifetime_write_kbytes",
+ buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "120\n");
+
+ /* sra_exceeded_retry_limit sums percpu counter */
+ percpu_counter_set(&sbi->s_sra_exceeded_retry_limit, 13);
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi,
+ "sra_exceeded_retry_limit", buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "13\n");
+
+ /* 40-bit timestamp formatting: (2 << 32) + 100 == 8589934692 */
+ es->s_first_error_time = cpu_to_le32(100);
+ es->s_first_error_time_hi = 2;
+ es->s_last_error_time = cpu_to_le32(250);
+ es->s_last_error_time_hi = 0;
+
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "first_error_time",
+ buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "8589934692\n");
+
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "last_error_time", buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "250\n");
+
+ /* journal_task emits "<none>\n" without journal, or j_task PID */
+ sbi->s_journal = NULL;
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "journal_task", buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, "<none>\n");
+
+ ctx->mock_journal.j_task = current;
+ sbi->s_journal = &ctx->mock_journal;
+ snprintf(expected_pid, sizeof(expected_pid), "%d\n",
+ task_pid_vnr(current));
+ KUNIT_EXPECT_GT(test,
+ ext4_sysfs_attr_show_test(sbi, "journal_task", buf),
+ (ssize_t)0);
+ KUNIT_EXPECT_STREQ(test, buf, expected_pid);
+ sbi->s_journal = NULL;
+}
+
+static void test_feature_attrs_and_trigger_error(struct kunit *test)
+{
+ struct sysfs_test_ctx *ctx = test->priv;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ static const char * const feat_names[] = {
+ "lazy_itable_init",
+ "batched_discard",
+ "meta_bg_resize",
+ "metadata_csum_seed",
+ "fast_commit",
+#ifdef CONFIG_FS_ENCRYPTION
+ "encryption",
+ "test_dummy_encryption_v2",
+#endif
+#if IS_ENABLED(CONFIG_UNICODE)
+ "casefold",
+#endif
+#ifdef CONFIG_FS_VERITY
+ "verity",
+#endif
+#if IS_ENABLED(CONFIG_UNICODE) && defined(CONFIG_FS_ENCRYPTION)
+ "encrypted_casefold",
+#endif
+ };
+ char *buf;
+ size_t i;
+
+ buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+
+ for (i = 0; i < ARRAY_SIZE(feat_names); i++) {
+ KUNIT_EXPECT_GT_MSG(test,
+ ext4_sysfs_feat_show_test(feat_names[i],
+ buf),
+ (ssize_t)0, "feat=%s", feat_names[i]);
+ KUNIT_EXPECT_STREQ_MSG(test, buf, "supported\n",
+ "feat=%s", feat_names[i]);
+ }
+ KUNIT_EXPECT_EQ(test, ext4_sysfs_feat_show_test("unknown_feat", buf),
+ (ssize_t)-ENOENT);
+
+ /* Empty newline write to trigger_fs_error is a no-op */
+ sbi->s_journal = &ctx->mock_journal;
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "trigger_fs_error",
+ "\n", 1),
+ (ssize_t)1);
+ KUNIT_EXPECT_EQ(test, sbi->s_mount_state & EXT4_ERROR_FS, 0);
+
+ /* Non-empty message records error state via ext4_error() */
+ KUNIT_EXPECT_EQ(test,
+ ext4_sysfs_attr_store_test(sbi, "trigger_fs_error",
+ "kunit injected\n", 15),
+ (ssize_t)15);
+ KUNIT_EXPECT_NE(test, sbi->s_mount_state & EXT4_ERROR_FS, 0);
+ KUNIT_EXPECT_STREQ(test, sbi->s_last_error_func, "trigger_test_error");
+ sbi->s_journal = NULL;
+}
+
+static void test_sysfs_register_notify_and_unregister(struct kunit *test)
+{
+ struct sysfs_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+
+ KUNIT_ASSERT_EQ(test, ext4_register_sysfs(sb), 0);
+ KUNIT_EXPECT_EQ(test, (int)sbi->s_kobj.state_in_sysfs, 1);
+
+ ext4_notify_error_sysfs(sbi);
+
+ ext4_unregister_sysfs(sb);
+ KUNIT_EXPECT_EQ(test, (int)sbi->s_kobj.state_in_sysfs, 0);
+ kobject_put(&sbi->s_kobj);
+ wait_for_completion(&sbi->s_kobj_unregister);
+}
+
+static struct kunit_case ext4_sysfs_test_cases[] = {
+ KUNIT_CASE(test_generic_pointer_attrs),
+ KUNIT_CASE(test_validated_rw_attrs),
+ KUNIT_CASE(test_computed_ro_and_error_time_attrs),
+ KUNIT_CASE(test_feature_attrs_and_trigger_error),
+ KUNIT_CASE(test_sysfs_register_notify_and_unregister),
+ {}
+};
+
+static struct kunit_suite ext4_sysfs_test_suite = {
+ .name = "ext4_sysfs_test",
+ .init = sysfs_test_init,
+ .test_cases = ext4_sysfs_test_cases,
+};
+
+kunit_test_suites(&ext4_sysfs_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 sysfs and procfs interfaces");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/sysfs.c b/fs/ext4/sysfs.c
index 923b375e017f..faa76f021099 100644
--- a/fs/ext4/sysfs.c
+++ b/fs/ext4/sysfs.c
@@ -602,6 +602,7 @@ void ext4_notify_error_sysfs(struct ext4_sb_info *sbi)
sysfs_notify(&sbi->s_kobj, NULL, "errors_count");
mutex_unlock(&sbi->s_error_notify_mutex);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_notify_error_sysfs);
static struct kobject *ext4_root;
@@ -642,6 +643,7 @@ int ext4_register_sysfs(struct super_block *sb)
}
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_register_sysfs);
void ext4_unregister_sysfs(struct super_block *sb)
{
@@ -654,6 +656,7 @@ void ext4_unregister_sysfs(struct super_block *sb)
kobject_del(&sbi->s_kobj);
mutex_unlock(&sbi->s_error_notify_mutex);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_unregister_sysfs);
int __init ext4_init_sysfs(void)
{
@@ -696,3 +699,55 @@ void ext4_exit_sysfs(void)
ext4_proc_root = NULL;
}
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+static struct attribute *ext4_find_attr_test(struct attribute **list,
+ const char *name)
+{
+ for (; *list; list++) {
+ if (strcmp((*list)->name, name) == 0)
+ return *list;
+ }
+ return NULL;
+}
+
+ssize_t ext4_sysfs_attr_show_test(struct ext4_sb_info *sbi, const char *name,
+ char *buf)
+{
+ struct attribute *attr = ext4_find_attr_test(ext4_attrs, name);
+
+ if (!attr)
+ return -ENOENT;
+ return ext4_attr_show(&sbi->s_kobj, attr, buf);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sysfs_attr_show_test);
+
+ssize_t ext4_sysfs_attr_store_test(struct ext4_sb_info *sbi, const char *name,
+ const char *buf, size_t len)
+{
+ struct attribute *attr = ext4_find_attr_test(ext4_attrs, name);
+
+ if (!attr)
+ return -ENOENT;
+ return ext4_attr_store(&sbi->s_kobj, attr, buf, len);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sysfs_attr_store_test);
+
+ssize_t ext4_sysfs_feat_show_test(const char *name, char *buf)
+{
+ struct attribute *attr = ext4_find_attr_test(ext4_feat_attrs, name);
+ struct ext4_sb_info *dummy_sbi;
+ ssize_t ret;
+
+ if (!attr)
+ return -ENOENT;
+ /* struct ext4_sb_info is too large to place on the stack */
+ dummy_sbi = kzalloc_obj(*dummy_sbi);
+ if (!dummy_sbi)
+ return -ENOMEM;
+ ret = ext4_attr_show(&dummy_sbi->s_kobj, attr, buf);
+ kfree(dummy_sbi);
+ return ret;
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sysfs_feat_show_test);
+#endif
+
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 19/27] ext4: add KUnit tests for fs/ext4/ioctl.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (17 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 18/27] ext4: add KUnit tests for fs/ext4/sysfs.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (7 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
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(¶ms, 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, ¶ms);
+
+ 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(¶ms, 0, sizeof(params));
+ memset(params.mount_opts, 'a', sizeof(params.mount_opts));
+ KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, ¶ms),
+ -E2BIG);
+
+ /* Unknown set_flags bit -> -EOPNOTSUPP */
+ memset(¶ms, 0, sizeof(params));
+ params.set_flags = 0x80000000U;
+ KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, ¶ms),
+ -EOPNOTSUPP);
+
+ /* Invalid errors_behavior -> -EINVAL */
+ memset(¶ms, 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, ¶ms),
+ -EINVAL);
+
+ /* Reserved blocks > blocks_count / 2 -> -EINVAL, <= half -> 0 */
+ memset(¶ms, 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, ¶ms),
+ -EINVAL);
+ params.reserved_blocks = ext4_blocks_count(es) / 2;
+ KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, ¶ms),
+ 0);
+
+ /* Hash algorithm validation: SIPHASH and > DX_HASH_LAST rejected */
+ memset(¶ms, 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, ¶ms),
+ -EINVAL);
+ params.def_hash_alg = DX_HASH_LAST + 1;
+ KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, ¶ms),
+ -EINVAL);
+ params.def_hash_alg = DX_HASH_HALF_MD4;
+ KUNIT_EXPECT_EQ(test, ext4_validate_tune_sb_params_test(sb, ¶ms),
+ 0);
+
+ /* Mutual exclusion of FEATURES and EDIT_FEATURES */
+ memset(¶ms, 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, ¶ms),
+ -EINVAL);
+
+ /* Clearing an existing feature via FEATURES -> -EOPNOTSUPP */
+ memset(¶ms, 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, ¶ms),
+ -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(¶ms, 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, ¶ms),
+ 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(¶ms, 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, ¶ms),
+ -EINVAL);
+
+ memset(¶ms, 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, ¶ms),
+ 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(¶ms, 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(¶ms, in, sizeof(params)))
+ return -EFAULT;
+
+ ret = ext4_validate_tune_sb_params(sb, ¶ms);
+ 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
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 20/27] ext4: add KUnit tests for fs/ext4/resize.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (18 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 19/27] ext4: add KUnit tests for fs/ext4/ioctl.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (6 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add a KUnit test suite (ext4_resize_test) covering online filesystem
resizing, group validation, flex_bg table packing, backup descriptor
reservation, and meta_bg conversion in fs/ext4/resize.c:
- Precondition checks in ext4_resize_begin() and EXT4_FLAGS_RESIZING
mutual exclusion with ext4_resize_end().
- Backup group iteration in ext4_list_backups() (sparse_super powers of
3/5/7, dense non-sparse sequence, and sparse_super2 s_backup_bgs) and
reserved GDT validation in verify_reserved_gdb().
- Group geometry and overlap validation in verify_group_input() across
partial prior groups, reserved-block ratios, out-of-bounds bitmaps and
inode tables, bitmap collisions, and superblock/GDT overhead overlaps.
- Flex group batch sizing in alloc_flex_gd(), descriptor flag and
partial-group initialization in ext4_setup_next_flex_gd(), and packed
metadata table placement and -ENOSPC handling in
ext4_alloc_group_tables().
- Online partial-last-group extension in ext4_group_extend() updating
superblock counts, block bitmaps, buddy caches, and backup
superblocks.
- Group addition in ext4_group_add() including backup reserved GDT
updates via reserve_backup_gdb(), resize_inode validation and meta_bg
conversion in ext4_convert_meta_bg(), and end-to-end ext4_resize_fs()
growth across reserved GDT consumption into meta_bg.
Route raw sb_getblk() calls in resize.c through ext4_sb_getblk() and
wrap sb_issue_zeroout() in ext4_resize_issue_zeroout() with
KUNIT_STATIC_STUB_REDIRECT so resize metadata I/O can be exercised
deterministically in memory.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 3 +-
fs/ext4/ext4.h | 32 +
fs/ext4/resize-test.c | 1341 +++++++++++++++++++++++++++++++++++++++++
fs/ext4/resize.c | 102 +++-
4 files changed, 1453 insertions(+), 25 deletions(-)
create mode 100644 fs/ext4/resize-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index c18530508863..6163d47f3627 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -21,7 +21,8 @@ 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 ioctl-test.o
+ sysfs-test.o ioctl-test.o \
+ resize-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 842fba63f404..3a735c69e2e3 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -688,6 +688,17 @@ enum {
GROUP_TABLE_COUNT,
};
+/*
+ * ext4_new_flex_group_data is used by 64bit-resize interface to add a flex
+ * group each time.
+ */
+struct ext4_new_flex_group_data {
+ struct ext4_new_group_data *groups;
+ __u16 *bg_flags;
+ ext4_group_t resize_bg;
+ ext4_group_t count;
+};
+
/*
* Flags used by ext4_map_blocks()
*/
@@ -3399,6 +3410,8 @@ extern bool ext4_empty_dir(struct inode *inode);
/* resize.c */
extern void ext4_kvfree_array_rcu(void *to_free);
+extern int ext4_resize_issue_zeroout(struct super_block *sb, sector_t blk,
+ sector_t nr_blks, gfp_t gfp_mask);
extern int ext4_group_add(struct super_block *sb,
struct ext4_new_group_data *input);
extern int ext4_group_extend(struct super_block *sb,
@@ -3408,6 +3421,25 @@ extern int ext4_resize_fs(struct super_block *sb, ext4_fsblk_t n_blocks_count);
extern unsigned int ext4_list_backups(struct super_block *sb,
unsigned int *three, unsigned int *five,
unsigned int *seven);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+extern int ext4_verify_group_input_test(struct super_block *sb,
+ struct ext4_new_group_data *input);
+extern struct ext4_new_flex_group_data *
+ext4_alloc_flex_gd_test(unsigned int flexbg_size, ext4_group_t o_group,
+ ext4_group_t n_group);
+extern void ext4_free_flex_gd_test(struct ext4_new_flex_group_data *flex_gd);
+extern int ext4_alloc_group_tables_test(struct super_block *sb,
+ struct ext4_new_flex_group_data *flex_gd,
+ unsigned int flexbg_size);
+extern int ext4_verify_reserved_gdb_test(struct super_block *sb,
+ ext4_group_t end,
+ struct buffer_head *primary);
+extern int ext4_setup_next_flex_gd_test(struct super_block *sb,
+ struct ext4_new_flex_group_data *flex_gd,
+ ext4_fsblk_t n_blocks_count);
+extern int ext4_convert_meta_bg_test(struct super_block *sb,
+ struct inode *inode);
+#endif
/* super.c */
extern struct buffer_head *ext4_sb_bread(struct super_block *sb,
diff --git a/fs/ext4/resize-test.c b/fs/ext4/resize-test.c
new file mode 100644
index 000000000000..081a5d6c6fd9
--- /dev/null
+++ b/fs/ext4/resize-test.c
@@ -0,0 +1,1341 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/resize.c
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/fscrypt.h>
+#include <linux/iversion.h>
+#include <linux/kernel.h>
+#include <linux/list.h>
+#include <linux/mm.h>
+#include <linux/module.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 "mballoc.h"
+
+#define RESIZE_TEST_BLK_SIZE 1024U
+#define RESIZE_TEST_BLK_BITS 10U
+#define RESIZE_TEST_BLOCKS_PER_GRP 1024U
+#define RESIZE_TEST_FIRST_DATA_BLK 1U
+#define RESIZE_TEST_INODE_SIZE 256U
+#define RESIZE_TEST_EXTRA_ISIZE 32U
+#define RESIZE_TEST_INODES_PER_GRP 32U
+#define RESIZE_TEST_ITB_PER_GRP 8U
+#define RESIZE_TEST_DESC_SIZE 64U
+#define RESIZE_TEST_DESC_PER_BLK 16U
+#define RESIZE_TEST_DESC_PER_BLK_BITS 4U
+
+struct resize_mock_blk {
+ struct list_head node;
+ ext4_fsblk_t blocknr;
+ struct buffer_head bh;
+ u8 *data;
+};
+
+struct resize_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 list_head blks;
+ struct buffer_head iloc_bh;
+ u8 iloc_buf[RESIZE_TEST_BLK_SIZE];
+ struct inode *resize_inode;
+ ext4_fsblk_t device_blocks;
+ ext4_fsblk_t last_freed_blk;
+ unsigned long last_freed_count;
+ int zeroout_calls;
+ sector_t zeroout_blks;
+ int zeroout_err;
+ bool sb_getblk_fail;
+ bool mb_inited;
+ bool counters_inited;
+};
+
+static struct kunit *active_resize_test;
+
+static struct inode *resize_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 = RESIZE_TEST_EXTRA_ISIZE;
+ ei->i_last_alloc_group = ~0U;
+
+ return &ei->vfs_inode;
+}
+
+static void resize_test_free_inode(struct inode *inode)
+{
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ kfree(EXT4_I(inode));
+}
+
+static void resize_test_evict_inode(struct inode *inode)
+{
+ truncate_inode_pages_final(&inode->i_data);
+ clear_inode(inode);
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **resize_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+#endif
+
+static const struct super_operations resize_test_sops = {
+ .alloc_inode = resize_test_alloc_inode,
+ .free_inode = resize_test_free_inode,
+ .evict_inode = resize_test_evict_inode,
+#ifdef CONFIG_QUOTA
+ .get_dquots = resize_test_get_dquots,
+#endif
+};
+
+static int resize_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static void resize_test_sb_upd_work(struct work_struct *work)
+{
+}
+
+static void resize_test_err_timer(struct timer_list *t)
+{
+}
+
+static struct file_system_type resize_test_fs_type = {
+ .name = "ext4_resize_kunit",
+ .init_fs_context = resize_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static void resize_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 struct buffer_head *resize_lookup_bh(struct resize_test_ctx *ctx,
+ ext4_fsblk_t blocknr)
+{
+ struct resize_mock_blk *blk;
+
+ list_for_each_entry(blk, &ctx->blks, node) {
+ if (blk->blocknr == blocknr)
+ return &blk->bh;
+ }
+ return NULL;
+}
+
+static struct buffer_head *resize_get_or_create_bh(struct resize_test_ctx *ctx,
+ ext4_fsblk_t blocknr)
+{
+ struct buffer_head *bh;
+ struct resize_mock_blk *blk;
+
+ bh = resize_lookup_bh(ctx, blocknr);
+ if (bh) {
+ get_bh(bh);
+ return bh;
+ }
+
+ blk = kzalloc_obj(*blk);
+ if (!blk)
+ return NULL;
+ blk->data = kzalloc(RESIZE_TEST_BLK_SIZE, GFP_KERNEL);
+ if (!blk->data) {
+ kfree(blk);
+ return NULL;
+ }
+ blk->blocknr = blocknr;
+ resize_init_mock_bh(&blk->bh, ctx->bh_folio, blk->data,
+ RESIZE_TEST_BLK_SIZE, blocknr);
+ list_add_tail(&blk->node, &ctx->blks);
+ get_bh(&blk->bh);
+ return &blk->bh;
+}
+
+static struct buffer_head *resize_sb_getblk_stub(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ struct kunit *test = active_resize_test;
+ struct resize_test_ctx *ctx;
+
+ if (!test)
+ return NULL;
+ ctx = test->priv;
+ if (!ctx || sb != ctx->sb || ctx->sb_getblk_fail)
+ return NULL;
+
+ return resize_get_or_create_bh(ctx, block);
+}
+
+static struct buffer_head *resize_sb_bread_stub(struct super_block *sb,
+ sector_t block,
+ blk_opf_t op_flags)
+{
+ struct kunit *test = active_resize_test;
+ struct resize_test_ctx *ctx;
+ struct buffer_head *bh;
+
+ if (!test)
+ return ERR_PTR(-EIO);
+ ctx = test->priv;
+ if (!ctx || sb != ctx->sb)
+ return ERR_PTR(-EIO);
+ if ((ext4_fsblk_t)block >= ctx->device_blocks)
+ return ERR_PTR(-EIO);
+
+ bh = resize_get_or_create_bh(ctx, block);
+ if (!bh)
+ return ERR_PTR(-ENOMEM);
+ return bh;
+}
+
+static int resize_zeroout_stub(struct super_block *sb, sector_t blk,
+ sector_t nr_blks, gfp_t gfp_mask)
+{
+ struct kunit *test = active_resize_test;
+ struct resize_test_ctx *ctx;
+ sector_t i;
+
+ if (!test)
+ return 0;
+ ctx = test->priv;
+ if (!ctx)
+ return 0;
+ if (ctx->zeroout_err)
+ return ctx->zeroout_err;
+
+ ctx->zeroout_calls++;
+ ctx->zeroout_blks += nr_blks;
+ for (i = 0; i < nr_blks; i++) {
+ struct buffer_head *bh = resize_lookup_bh(ctx, blk + i);
+
+ if (bh)
+ memset(bh->b_data, 0, bh->b_size);
+ }
+ return 0;
+}
+
+static struct inode *resize_iget_stub(struct super_block *sb, unsigned long ino,
+ ext4_iget_flags flags,
+ const char *function, unsigned int line)
+{
+ struct kunit *test = active_resize_test;
+ struct resize_test_ctx *ctx;
+
+ if (!test)
+ return ERR_PTR(-ENOENT);
+ ctx = test->priv;
+ if (!ctx || sb != ctx->sb)
+ return ERR_PTR(-ENOENT);
+ if (ino == EXT4_RESIZE_INO && ctx->resize_inode) {
+ ihold(ctx->resize_inode);
+ return ctx->resize_inode;
+ }
+ return ERR_PTR(-ENOENT);
+}
+
+static int resize_get_inode_loc_stub(struct inode *inode,
+ struct ext4_iloc *iloc)
+{
+ struct kunit *test = active_resize_test;
+ struct resize_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 resize_mark_iloc_dirty_stub(handle_t *handle, struct inode *inode,
+ struct ext4_iloc *iloc)
+{
+ brelse(iloc->bh);
+ iloc->bh = NULL;
+ return 0;
+}
+
+static int resize_mark_inode_dirty_stub(handle_t *handle, struct inode *inode,
+ const char *func, unsigned int line)
+{
+ return 0;
+}
+
+static void resize_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 = active_resize_test;
+ struct resize_test_ctx *ctx;
+
+ if (!test)
+ return;
+ ctx = test->priv;
+ if (!ctx)
+ return;
+
+ ctx->last_freed_blk = block;
+ ctx->last_freed_count = count;
+ if (bh)
+ brelse(bh);
+}
+
+static void resize_test_cleanup(struct resize_test_ctx *ctx)
+{
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ struct flex_groups **flex_groups;
+ struct resize_mock_blk *blk, *tmp;
+ unsigned int i;
+
+ if (sb) {
+ cancel_work_sync(&sbi->s_sb_upd_work);
+ timer_delete_sync(&sbi->s_err_report);
+ }
+
+ if (ctx->resize_inode) {
+ iput(ctx->resize_inode);
+ ctx->resize_inode = NULL;
+ }
+
+ if (ctx->mb_inited && sb) {
+ ext4_mb_release(sb);
+ ctx->mb_inited = false;
+ }
+
+ if (ctx->counters_inited) {
+ percpu_counter_destroy(&sbi->s_freeclusters_counter);
+ percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
+ percpu_counter_destroy(&sbi->s_freeinodes_counter);
+ ctx->counters_inited = false;
+ }
+
+ flex_groups = rcu_access_pointer(sbi->s_flex_groups);
+ if (flex_groups) {
+ for (i = 0; i < sbi->s_flex_groups_allocated; i++)
+ kvfree(flex_groups[i]);
+ kvfree(flex_groups);
+ rcu_assign_pointer(sbi->s_flex_groups, NULL);
+ sbi->s_flex_groups_allocated = 0;
+ }
+
+ rcu_barrier();
+ kvfree(rcu_access_pointer(sbi->s_group_desc));
+ rcu_assign_pointer(sbi->s_group_desc, NULL);
+
+ if (sb) {
+ deactivate_super(sb);
+ ctx->sb = NULL;
+ }
+
+ list_for_each_entry_safe(blk, tmp, &ctx->blks, node) {
+ list_del(&blk->node);
+ kfree(blk->data);
+ kfree(blk);
+ }
+
+ if (ctx->bh_folio) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ }
+
+ if (ctx->bdev.bd_stats) {
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ }
+
+ active_resize_test = NULL;
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(resize_test_cleanup_action, resize_test_cleanup,
+ struct resize_test_ctx *);
+
+static int resize_test_set_super(struct super_block *sb, struct fs_context *fc)
+{
+ return set_anon_super_fc(sb, fc);
+}
+
+/*
+ * Configure an in-memory ext4 filesystem with @ngroups initial groups,
+ * optional @reserved_gdb reserved GDT blocks (enabling RESIZE_INODE and
+ * populating the resize inode's DIND + reserved GDT blocks), optional
+ * flex_bg (@log_flex > 0), and @last_grp_blks blocks in the final group.
+ */
+static void resize_configure_fs(struct kunit *test, ext4_group_t ngroups,
+ u16 reserved_gdb, u8 log_flex,
+ u32 last_grp_blks)
+{
+ struct resize_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 buffer_head **group_desc;
+ unsigned long gdb_count;
+ ext4_fsblk_t total_blks = RESIZE_TEST_FIRST_DATA_BLK;
+ ext4_fsblk_t total_free_blks = 0;
+ u32 total_inodes = ngroups * RESIZE_TEST_INODES_PER_GRP;
+ ext4_group_t g;
+ int ret;
+
+ KUNIT_ASSERT_GT(test, ngroups, 0U);
+ KUNIT_ASSERT_LE(test, last_grp_blks, RESIZE_TEST_BLOCKS_PER_GRP);
+
+ es->s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+ es->s_log_block_size = cpu_to_le32(0);
+ es->s_log_cluster_size = cpu_to_le32(0);
+ es->s_first_data_block = cpu_to_le32(RESIZE_TEST_FIRST_DATA_BLK);
+ es->s_blocks_per_group = cpu_to_le32(RESIZE_TEST_BLOCKS_PER_GRP);
+ es->s_clusters_per_group = cpu_to_le32(RESIZE_TEST_BLOCKS_PER_GRP);
+ es->s_inodes_per_group = cpu_to_le32(RESIZE_TEST_INODES_PER_GRP);
+ es->s_inodes_count = cpu_to_le32(total_inodes);
+ es->s_free_inodes_count = cpu_to_le32(total_inodes - 10);
+ es->s_first_ino = cpu_to_le32(11);
+ es->s_inode_size = cpu_to_le16(RESIZE_TEST_INODE_SIZE);
+ es->s_desc_size = cpu_to_le16(RESIZE_TEST_DESC_SIZE);
+ es->s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ es->s_state = cpu_to_le16(EXT4_VALID_FS);
+ es->s_errors = cpu_to_le16(EXT4_ERRORS_CONTINUE);
+ es->s_checksum_type = EXT4_CRC32C_CHKSUM;
+ es->s_reserved_gdt_blocks = cpu_to_le16(reserved_gdb);
+ generate_random_uuid(es->s_uuid);
+
+ ext4_set_feature_extents(sb);
+ ext4_set_feature_64bit(sb);
+ ext4_set_feature_filetype(sb);
+ ext4_set_feature_sparse_super(sb);
+ ext4_set_feature_metadata_csum(sb);
+ if (reserved_gdb > 0)
+ ext4_set_feature_resize_inode(sb);
+ if (log_flex > 0) {
+ ext4_set_feature_flex_bg(sb);
+ es->s_log_groups_per_flex = log_flex;
+ sbi->s_log_groups_per_flex = log_flex;
+ }
+
+ sbi->s_csum_seed = ext4_chksum(~0, es->s_uuid, sizeof(es->s_uuid));
+ sbi->s_groups_count = ngroups;
+ gdb_count = DIV_ROUND_UP(ngroups, RESIZE_TEST_DESC_PER_BLK);
+ sbi->s_gdb_count = gdb_count;
+
+ group_desc = kvmalloc_array(gdb_count, sizeof(*group_desc), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, group_desc);
+ for (g = 0; g < gdb_count; g++) {
+ struct buffer_head *bh;
+
+ bh = resize_get_or_create_bh(ctx,
+ RESIZE_TEST_FIRST_DATA_BLK + 1 + g);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ brelse(bh);
+ group_desc[g] = bh;
+ }
+ rcu_assign_pointer(sbi->s_group_desc, group_desc);
+
+ for (g = 0; g < ngroups; g++) {
+ u32 grp_blks = (g == ngroups - 1) ?
+ last_grp_blks : RESIZE_TEST_BLOCKS_PER_GRP;
+ ext4_fsblk_t start = ext4_group_first_block_no(sb, g);
+ unsigned int overhead = ext4_bg_num_gdb(sb, g);
+ unsigned int used_meta;
+ ext4_fsblk_t bb_blk, ib_blk, it_blk;
+ struct buffer_head *bb_bh, *ib_bh;
+ struct ext4_group_desc *gdp;
+ u32 free_blks;
+
+ if (ext4_bg_has_super(sb, g))
+ overhead += 1 + reserved_gdb;
+
+ bb_blk = start + overhead;
+ ib_blk = bb_blk + 1;
+ it_blk = ib_blk + 1;
+ used_meta = overhead + 2 + RESIZE_TEST_ITB_PER_GRP;
+ if (g == 0 && reserved_gdb > 0)
+ used_meta++;
+
+ bb_bh = resize_get_or_create_bh(ctx, bb_blk);
+ ib_bh = resize_get_or_create_bh(ctx, ib_blk);
+ KUNIT_ASSERT_NOT_NULL(test, bb_bh);
+ KUNIT_ASSERT_NOT_NULL(test, ib_bh);
+
+ mb_set_bits(bb_bh->b_data, 0, used_meta);
+ ext4_mark_bitmap_end(grp_blks, sb->s_blocksize * 8,
+ bb_bh->b_data);
+ ext4_mark_bitmap_end(RESIZE_TEST_INODES_PER_GRP,
+ sb->s_blocksize * 8, ib_bh->b_data);
+
+ gdp = ext4_get_group_desc(sb, g, NULL);
+ KUNIT_ASSERT_NOT_NULL(test, gdp);
+ memset(gdp, 0, RESIZE_TEST_DESC_SIZE);
+ ext4_block_bitmap_set(sb, gdp, bb_blk);
+ ext4_inode_bitmap_set(sb, gdp, ib_blk);
+ ext4_inode_table_set(sb, gdp, it_blk);
+ free_blks = grp_blks - used_meta;
+ ext4_free_group_clusters_set(sb, gdp, free_blks);
+ ext4_free_inodes_set(sb, gdp, RESIZE_TEST_INODES_PER_GRP);
+ ext4_itable_unused_set(sb, gdp, RESIZE_TEST_INODES_PER_GRP);
+ gdp->bg_flags = cpu_to_le16(EXT4_BG_INODE_ZEROED);
+ ext4_block_bitmap_csum_set(sb, gdp, bb_bh);
+ ext4_inode_bitmap_csum_set(sb, gdp, ib_bh);
+ ext4_group_desc_csum_set(sb, g, gdp);
+
+ brelse(bb_bh);
+ brelse(ib_bh);
+
+ total_blks += grp_blks;
+ total_free_blks += free_blks;
+ }
+
+ ext4_blocks_count_set(es, total_blks);
+ ext4_free_blocks_count_set(es, total_free_blks);
+ ext4_r_blocks_count_set(es, total_blks / 20);
+ sbi->s_blockfile_groups = ngroups;
+
+ if (reserved_gdb > 0) {
+ ext4_fsblk_t dind_blk;
+ struct buffer_head *dind_bh;
+ __le32 *dind_data;
+ unsigned int res;
+ unsigned int total_rsv_blks = 1;
+
+ dind_blk = ext4_group_first_block_no(sb, 0) + 1 + gdb_count +
+ reserved_gdb + 2 + RESIZE_TEST_ITB_PER_GRP;
+ dind_bh = resize_get_or_create_bh(ctx, dind_blk);
+ KUNIT_ASSERT_NOT_NULL(test, dind_bh);
+ dind_data = (__le32 *)dind_bh->b_data;
+
+ for (res = 0; res < reserved_gdb; res++) {
+ ext4_fsblk_t pri_blk = RESIZE_TEST_FIRST_DATA_BLK + 1 +
+ gdb_count + res;
+ struct buffer_head *pri_bh;
+ __le32 *pri_data;
+ unsigned int three = 1, five = 5, seven = 7;
+ unsigned int bgrp, idx = 0;
+
+ dind_data[(gdb_count + res) %
+ EXT4_ADDR_PER_BLOCK(sb)] =
+ cpu_to_le32(pri_blk);
+ pri_bh = resize_get_or_create_bh(ctx, pri_blk);
+ KUNIT_ASSERT_NOT_NULL(test, pri_bh);
+ pri_data = (__le32 *)pri_bh->b_data;
+
+ while ((bgrp = ext4_list_backups(sb, &three, &five,
+ &seven)) < ngroups) {
+ pri_data[idx++] = cpu_to_le32(
+ bgrp * RESIZE_TEST_BLOCKS_PER_GRP +
+ pri_blk);
+ }
+ total_rsv_blks += 1 + idx;
+ brelse(pri_bh);
+ }
+ brelse(dind_bh);
+
+ ctx->resize_inode = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->resize_inode);
+ ctx->resize_inode->i_ino = EXT4_RESIZE_INO;
+ ctx->resize_inode->i_mode = S_IFREG | 0600;
+ ctx->resize_inode->i_blkbits = RESIZE_TEST_BLK_BITS;
+ ctx->resize_inode->i_blocks = total_rsv_blks *
+ (RESIZE_TEST_BLK_SIZE >> 9);
+ EXT4_I(ctx->resize_inode)->i_data[EXT4_DIND_BLOCK] =
+ cpu_to_le32(dind_blk);
+ }
+
+ if (log_flex > 0) {
+ ret = ext4_fill_flex_info_test(sb);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ }
+
+ ret = ext4_mb_init(sb);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->mb_inited = true;
+
+ ret = percpu_counter_init(&sbi->s_freeclusters_counter,
+ total_free_blks, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = percpu_counter_init(&sbi->s_freeinodes_counter,
+ total_inodes - 10, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->counters_inited = true;
+
+ ext4_calculate_overhead(sb);
+ es->s_overhead_clusters = cpu_to_le32(sbi->s_overhead);
+ ext4_superblock_csum_set(sb);
+}
+
+static int resize_test_init(struct kunit *test)
+{
+ struct resize_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ struct buffer_head *sbh;
+ int ret;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ INIT_LIST_HEAD(&ctx->blks);
+ ctx->device_blocks = 1 + 64 * RESIZE_TEST_BLOCKS_PER_GRP;
+ test->priv = ctx;
+
+ ctx->bdev.bd_stats = alloc_percpu(struct disk_stats);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->bdev.bd_stats);
+
+ ctx->bh_folio = folio_alloc(GFP_KERNEL, 0);
+ if (!ctx->bh_folio) {
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "folio_alloc failed");
+ }
+
+ fc = fs_context_for_mount(&resize_test_fs_type, 0);
+ if (IS_ERR(fc)) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "fs_context_for_mount failed: %ld",
+ PTR_ERR(fc));
+ }
+ sb = sget_fc(fc, NULL, resize_test_set_super);
+ put_fs_context(fc);
+ if (IS_ERR(sb)) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "sget_fc failed: %ld", PTR_ERR(sb));
+ }
+ ctx->sb = sb;
+ up_write(&sb->s_umount);
+
+ ret = kunit_add_action_or_reset(test, resize_test_cleanup_action, ctx);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ sbi = &ctx->sbi;
+ sb->s_fs_info = sbi;
+ sb->s_op = &resize_test_sops;
+ sb->s_blocksize = RESIZE_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = RESIZE_TEST_BLK_BITS;
+ sb->s_magic = EXT4_SUPER_MAGIC;
+ sb->s_maxbytes = MAX_LFS_FILESIZE;
+ INIT_LIST_HEAD(&sb->s_inodes);
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
+#endif
+
+ strscpy(sb->s_id, "ext4_resize_kunit", sizeof(sb->s_id));
+ inode_init_once(&ctx->bd_inode);
+ ctx->bd_inode.i_sb = sb;
+ ctx->bd_inode.i_blkbits = RESIZE_TEST_BLK_BITS;
+ ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data;
+ ctx->bd_inode.i_data.host = &ctx->bd_inode;
+ 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;
+
+ sbi->s_sb = sb;
+ sbi->s_blockgroup_lock = &ctx->bgl;
+ bgl_lock_init(&ctx->bgl);
+ INIT_WORK(&sbi->s_sb_upd_work, resize_test_sb_upd_work);
+ timer_setup(&sbi->s_err_report, resize_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, 5 * HZ, 10);
+ ratelimit_state_init(&sbi->s_warning_ratelimit_state, 5 * HZ, 10);
+ ratelimit_state_init(&sbi->s_msg_ratelimit_state, 5 * HZ, 10);
+
+ sbi->s_blocks_per_group = RESIZE_TEST_BLOCKS_PER_GRP;
+ sbi->s_clusters_per_group = RESIZE_TEST_BLOCKS_PER_GRP;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_cluster_bits = 0;
+ sbi->s_inodes_per_group = RESIZE_TEST_INODES_PER_GRP;
+ sbi->s_inode_size = RESIZE_TEST_INODE_SIZE;
+ sbi->s_inodes_per_block = RESIZE_TEST_BLK_SIZE / RESIZE_TEST_INODE_SIZE;
+ sbi->s_itb_per_group = RESIZE_TEST_ITB_PER_GRP;
+ sbi->s_desc_size = RESIZE_TEST_DESC_SIZE;
+ sbi->s_desc_per_block = RESIZE_TEST_DESC_PER_BLK;
+ sbi->s_desc_per_block_bits = RESIZE_TEST_DESC_PER_BLK_BITS;
+ sbi->s_first_ino = 11;
+ sbi->s_mount_state = EXT4_VALID_FS;
+ set_opt(sb, ERRORS_CONT);
+
+ sbh = resize_get_or_create_bh(ctx, RESIZE_TEST_FIRST_DATA_BLK);
+ KUNIT_ASSERT_NOT_NULL(test, sbh);
+ brelse(sbh);
+ sbi->s_sbh = sbh;
+ ctx->es = (struct ext4_super_block *)sbh->b_data;
+ sbi->s_es = ctx->es;
+
+ resize_init_mock_bh(&ctx->iloc_bh, ctx->bh_folio, ctx->iloc_buf,
+ RESIZE_TEST_BLK_SIZE, 99);
+
+ active_resize_test = test;
+ kunit_activate_static_stub(test, ext4_sb_getblk, resize_sb_getblk_stub);
+ kunit_activate_static_stub(test, ext4_sb_bread, resize_sb_bread_stub);
+ kunit_activate_static_stub(test, ext4_resize_issue_zeroout,
+ resize_zeroout_stub);
+ kunit_activate_static_stub(test, __ext4_iget, resize_iget_stub);
+ kunit_activate_static_stub(test, ext4_get_inode_loc,
+ resize_get_inode_loc_stub);
+ kunit_activate_static_stub(test, ext4_mark_iloc_dirty,
+ resize_mark_iloc_dirty_stub);
+ kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+ resize_mark_inode_dirty_stub);
+ kunit_activate_static_stub(test, ext4_free_blocks,
+ resize_free_blocks_stub);
+
+ return 0;
+}
+
+/*
+ * Test 1: ext4_resize_begin() precondition checks and EXT4_FLAGS_RESIZING
+ * mutual exclusion with ext4_resize_end().
+ */
+static void test_resize_begin_and_end_guards(struct kunit *test)
+{
+ struct resize_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+
+ resize_configure_fs(test, 2, 0, 0, RESIZE_TEST_BLOCKS_PER_GRP);
+
+ /* Non-zero s_reserved_gdt_blocks without resize_inode -> -EFSCORRUPTED */
+ ctx->es->s_reserved_gdt_blocks = cpu_to_le16(2);
+ KUNIT_EXPECT_EQ(test, ext4_resize_begin(sb), -EFSCORRUPTED);
+ ctx->es->s_reserved_gdt_blocks = 0;
+ ctx->sbi.s_mount_state &= ~EXT4_ERROR_FS;
+
+ /* Mounted from backup superblock -> -EPERM */
+ ctx->sbi.s_sbh->b_blocknr = 1025;
+ KUNIT_EXPECT_EQ(test, ext4_resize_begin(sb), -EPERM);
+ ctx->sbi.s_sbh->b_blocknr = RESIZE_TEST_FIRST_DATA_BLK;
+
+ /* Filesystem has recorded errors -> -EPERM */
+ ctx->sbi.s_mount_state |= EXT4_ERROR_FS;
+ KUNIT_EXPECT_EQ(test, ext4_resize_begin(sb), -EPERM);
+ ctx->sbi.s_mount_state &= ~EXT4_ERROR_FS;
+
+ /* sparse_super2 online resize unsupported -> -EOPNOTSUPP */
+ ext4_set_feature_sparse_super2(sb);
+ KUNIT_EXPECT_EQ(test, ext4_resize_begin(sb), -EOPNOTSUPP);
+ ext4_clear_feature_sparse_super2(sb);
+
+ /* Valid begin acquires EXT4_FLAGS_RESIZING; second call gets -EBUSY */
+ KUNIT_EXPECT_EQ(test, ext4_resize_begin(sb), 0);
+ KUNIT_EXPECT_TRUE(test, test_bit(EXT4_FLAGS_RESIZING,
+ &ctx->sbi.s_ext4_flags));
+ KUNIT_EXPECT_EQ(test, ext4_resize_begin(sb), -EBUSY);
+
+ /* ext4_resize_end releases lock so subsequent begin succeeds */
+ KUNIT_EXPECT_EQ(test, ext4_resize_end(sb, false), 0);
+ KUNIT_EXPECT_FALSE(test, test_bit(EXT4_FLAGS_RESIZING,
+ &ctx->sbi.s_ext4_flags));
+ KUNIT_EXPECT_EQ(test, ext4_resize_begin(sb), 0);
+ KUNIT_EXPECT_EQ(test, ext4_resize_end(sb, false), 0);
+}
+
+/*
+ * Test 2: ext4_list_backups() sequence generation (sparse_super, dense, and
+ * sparse_super2) and verify_reserved_gdb() validation.
+ */
+static void test_list_backups_and_verify_reserved_gdb(struct kunit *test)
+{
+ struct resize_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ static const unsigned int expected_sparse[] = {
+ 1, 3, 5, 7, 9, 25, 27, 49, 81
+ };
+ unsigned int three, five, seven;
+ struct buffer_head *pri_bh;
+ __le32 *pri_data;
+ unsigned int i;
+
+ resize_configure_fs(test, 4, 0, 0, RESIZE_TEST_BLOCKS_PER_GRP);
+
+ /* 1. sparse_super yields sorted powers of 3, 5, and 7 */
+ three = 1;
+ five = 5;
+ seven = 7;
+ for (i = 0; i < ARRAY_SIZE(expected_sparse); i++) {
+ KUNIT_EXPECT_EQ_MSG(test,
+ ext4_list_backups(sb, &three, &five, &seven),
+ expected_sparse[i], "sparse index %u", i);
+ }
+
+ /* 2. Without sparse_super: every consecutive group 1, 2, 3, 4 */
+ ext4_clear_feature_sparse_super(sb);
+ three = 1;
+ five = 5;
+ seven = 7;
+ for (i = 1; i <= 4; i++)
+ KUNIT_EXPECT_EQ(test,
+ ext4_list_backups(sb, &three, &five, &seven),
+ i);
+ ext4_set_feature_sparse_super(sb);
+
+ /* 3. sparse_super2: reads s_backup_bgs[0..1], skipping 0, then UINT_MAX */
+ ext4_set_feature_sparse_super2(sb);
+ ctx->es->s_backup_bgs[0] = 0;
+ ctx->es->s_backup_bgs[1] = cpu_to_le32(12);
+ three = 1;
+ five = 5;
+ seven = 7;
+ KUNIT_EXPECT_EQ(test, ext4_list_backups(sb, &three, &five, &seven), 12U);
+ KUNIT_EXPECT_EQ(test, ext4_list_backups(sb, &three, &five, &seven),
+ UINT_MAX);
+ ext4_clear_feature_sparse_super2(sb);
+
+ /* 4. verify_reserved_gdb: valid backup list for groups < 10 (5 backups) */
+ pri_bh = resize_get_or_create_bh(ctx, 3);
+ KUNIT_ASSERT_NOT_NULL(test, pri_bh);
+ pri_data = (__le32 *)pri_bh->b_data;
+ for (i = 0; i < 5; i++)
+ pri_data[i] = cpu_to_le32(expected_sparse[i] *
+ RESIZE_TEST_BLOCKS_PER_GRP + 3);
+ KUNIT_EXPECT_EQ(test, ext4_verify_reserved_gdb_test(sb, 10, pri_bh), 5);
+
+ /* Corrupted backup block entry -> -EINVAL */
+ pri_data[2] = cpu_to_le32(99999);
+ KUNIT_EXPECT_EQ(test, ext4_verify_reserved_gdb_test(sb, 10, pri_bh),
+ -EINVAL);
+
+ /* Exceeding EXT4_ADDR_PER_BLOCK (256 on 1K blocks) -> -EFBIG */
+ ext4_clear_feature_sparse_super(sb);
+ for (i = 0; i <= 257 && i < RESIZE_TEST_BLK_SIZE / sizeof(__le32); i++)
+ pri_data[i] = cpu_to_le32((i + 1) * RESIZE_TEST_BLOCKS_PER_GRP +
+ 3);
+ /*
+ * Allocate an oversized buffer temporarily so 257 entries can match
+ * before hitting ++gdbackups > EXT4_ADDR_PER_BLOCK(sb) (256).
+ */
+ {
+ __le32 *big_buf = kunit_kcalloc(test, 260, sizeof(__le32),
+ GFP_KERNEL);
+ u8 *orig_data = (u8 *)pri_bh->b_data;
+
+ KUNIT_ASSERT_NOT_NULL(test, big_buf);
+ for (i = 0; i < 258; i++)
+ big_buf[i] = cpu_to_le32((i + 1) *
+ RESIZE_TEST_BLOCKS_PER_GRP +
+ 3);
+ pri_bh->b_data = (char *)big_buf;
+ KUNIT_EXPECT_EQ(test,
+ ext4_verify_reserved_gdb_test(sb, 259, pri_bh),
+ -EFBIG);
+ pri_bh->b_data = (char *)orig_data;
+ }
+ brelse(pri_bh);
+}
+
+/*
+ * Test 3: verify_group_input() boundary and overlap checks when adding sparse
+ * backup group 3 (which carries superblock + GDT overhead at group start).
+ */
+static void test_verify_group_input_rejections_and_acceptance(struct kunit *test)
+{
+ struct resize_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_new_group_data input;
+ ext4_fsblk_t start;
+ unsigned int overhead;
+
+ /* Configure 3 full groups (0..2); next group to add is sparse group 3 */
+ resize_configure_fs(test, 3, 1, 0, RESIZE_TEST_BLOCKS_PER_GRP);
+ start = ext4_blocks_count(ctx->es);
+ /* Group 3 has backup super (1) + 1 GDT + 1 reserved GDT = 3 blocks */
+ overhead = 3;
+
+ memset(&input, 0, sizeof(input));
+ input.group = 3;
+ input.blocks_count = RESIZE_TEST_BLOCKS_PER_GRP;
+ input.reserved_blocks = 50;
+ input.block_bitmap = start + overhead;
+ input.inode_bitmap = start + overhead + 1;
+ input.inode_table = start + overhead + 2;
+
+ /* 1. Wrong group number */
+ input.group = 4;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.group = 3;
+
+ /* 2. Previous last group not full */
+ ext4_blocks_count_set(ctx->es, start - 10);
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ ext4_blocks_count_set(ctx->es, start);
+
+ /* 3. reserved_blocks > blocks_count / 5 */
+ input.reserved_blocks = 205;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.reserved_blocks = 50;
+
+ /* 4. blocks_count smaller than metadata (free_blocks_count < 0) */
+ input.blocks_count = 10;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.blocks_count = RESIZE_TEST_BLOCKS_PER_GRP;
+
+ /* 5. Last block beyond underlying device -> -EIO */
+ ctx->device_blocks = start + 100;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input), -EIO);
+ ctx->device_blocks = 1 + 64 * RESIZE_TEST_BLOCKS_PER_GRP;
+
+ /* 6. Bitmaps or inode table outside [start, end) */
+ input.block_bitmap = start - 1;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.block_bitmap = start + overhead;
+
+ input.inode_bitmap = start + RESIZE_TEST_BLOCKS_PER_GRP;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.inode_bitmap = start + overhead + 1;
+
+ input.inode_table = start + RESIZE_TEST_BLOCKS_PER_GRP -
+ RESIZE_TEST_ITB_PER_GRP + 1;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.inode_table = start + overhead + 2;
+
+ /* 7. Bitmap collision and bitmap inside inode table */
+ input.inode_bitmap = input.block_bitmap;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.inode_bitmap = input.inode_table + 3;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.inode_bitmap = start + overhead + 1;
+
+ input.block_bitmap = input.inode_table;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.block_bitmap = start + overhead;
+
+ /* 8. Bitmaps or inode table overlapping superblock/GDT overhead */
+ input.block_bitmap = start + overhead - 1;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.block_bitmap = start + overhead;
+
+ input.inode_bitmap = start;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.inode_bitmap = start + overhead + 1;
+
+ input.inode_table = start + 1;
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input),
+ -EINVAL);
+ input.inode_table = start + overhead + 2;
+
+ /* 9. Valid group input accepted with exact free_clusters_count */
+ KUNIT_EXPECT_EQ(test, ext4_verify_group_input_test(sb, &input), 0);
+ /* 1024 - 2 (bitmaps) - 3 (overhead) - 8 (itable) = 1011 */
+ KUNIT_EXPECT_EQ(test, input.free_clusters_count, 1011U);
+}
+
+/*
+ * Test 4: alloc_flex_gd(), ext4_setup_next_flex_gd(), and
+ * ext4_alloc_group_tables() packed flex_bg layout and -ENOSPC handling.
+ */
+static void test_alloc_setup_flex_gd_and_group_tables(struct kunit *test)
+{
+ struct resize_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_new_flex_group_data *flex_gd;
+ ext4_fsblk_t g4_start;
+
+ /* 4 initial groups (1 flex_bg of size 4, log_flex = 2), meta_bg mode */
+ resize_configure_fs(test, 4, 0, 2, RESIZE_TEST_BLOCKS_PER_GRP);
+ ext4_set_feature_meta_bg(sb);
+ ctx->es->s_first_meta_bg = 0;
+
+ /* 1. alloc_flex_gd shrinks allocation when adding only 1 group (4..4) */
+ flex_gd = ext4_alloc_flex_gd_test(16, 4, 4);
+ KUNIT_ASSERT_NOT_NULL(test, flex_gd);
+ KUNIT_EXPECT_EQ(test, flex_gd->resize_bg, 1U);
+ ext4_free_flex_gd_test(flex_gd);
+
+ /* 2. Full flex_bg batch (groups 4..7, with group 7 partial = 500 blks) */
+ flex_gd = ext4_alloc_flex_gd_test(4, 3, 7);
+ KUNIT_ASSERT_NOT_NULL(test, flex_gd);
+ KUNIT_EXPECT_EQ(test, flex_gd->resize_bg, 4U);
+
+ /* No-op when target equals current blocks_count */
+ KUNIT_EXPECT_EQ(test,
+ ext4_setup_next_flex_gd_test(sb, flex_gd,
+ ext4_blocks_count(ctx->es)),
+ 0);
+
+ /* Setup groups 4, 5, 6 (1024 blks each) + group 7 (500 blks) */
+ KUNIT_EXPECT_EQ(test,
+ ext4_setup_next_flex_gd_test(sb, flex_gd,
+ 1 + 7 * 1024 + 500),
+ 1);
+ KUNIT_EXPECT_EQ(test, flex_gd->count, 4U);
+ KUNIT_EXPECT_EQ(test, flex_gd->groups[0].group, 4U);
+ KUNIT_EXPECT_EQ(test, flex_gd->groups[3].group, 7U);
+ KUNIT_EXPECT_EQ(test, flex_gd->groups[3].blocks_count, 500U);
+ /* Final group clears BG_BLOCK_UNINIT under group_desc_csum */
+ KUNIT_EXPECT_NE(test, flex_gd->bg_flags[0] & EXT4_BG_BLOCK_UNINIT, 0);
+ KUNIT_EXPECT_EQ(test, flex_gd->bg_flags[3] & EXT4_BG_BLOCK_UNINIT, 0);
+
+ /*
+ * 3. ext4_alloc_group_tables: group 4 has no super/meta_bg overhead
+ * (group 5 has sparse backup super -> overhead=1, stopping contiguous
+ * span at end of group 4). All 4 block bitmaps, 4 inode bitmaps, and
+ * 4*8=32 inode table blocks (40 blocks total) pack into group 4!
+ */
+ g4_start = ext4_group_first_block_no(sb, 4);
+ KUNIT_EXPECT_EQ(test, ext4_alloc_group_tables_test(sb, flex_gd, 4), 0);
+ KUNIT_EXPECT_EQ(test, flex_gd->groups[0].block_bitmap, g4_start);
+ KUNIT_EXPECT_EQ(test, flex_gd->groups[3].block_bitmap, g4_start + 3);
+ KUNIT_EXPECT_EQ(test, flex_gd->groups[0].inode_bitmap, g4_start + 4);
+ KUNIT_EXPECT_EQ(test, flex_gd->groups[3].inode_bitmap, g4_start + 7);
+ KUNIT_EXPECT_EQ(test, flex_gd->groups[0].inode_table, g4_start + 8);
+ KUNIT_EXPECT_EQ(test, flex_gd->groups[3].inode_table, g4_start + 32);
+ /* Group 4 hosts all 40 metadata blocks -> BG_BLOCK_UNINIT cleared */
+ KUNIT_EXPECT_EQ(test, flex_gd->bg_flags[0] & EXT4_BG_BLOCK_UNINIT, 0);
+ KUNIT_EXPECT_EQ(test, flex_gd->groups[0].free_clusters_count,
+ 1024U - 40U);
+
+ /* 4. Insufficient space in flex_gd returns -ENOSPC */
+ flex_gd->count = 1;
+ flex_gd->groups[0].group = 4;
+ flex_gd->groups[0].blocks_count = 5;
+ flex_gd->groups[0].mdata_blocks = 0;
+ KUNIT_EXPECT_EQ(test, ext4_alloc_group_tables_test(sb, flex_gd, 4),
+ -ENOSPC);
+
+ ext4_free_flex_gd_test(flex_gd);
+}
+
+/*
+ * Test 5: ext4_group_extend() validation and partial-last-group online
+ * extension updating superblock, block bitmap, buddy cache, and backup SB.
+ */
+static void test_group_extend_partial_last_group(struct kunit *test)
+{
+ struct resize_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_super_block *es = ctx->es;
+ struct ext4_super_block *backup_es;
+ struct buffer_head *b1_bh;
+ ext4_fsblk_t o_blks, o_free;
+ s64 free_before;
+
+ /* 2 groups: group 0 full (1024 blks), group 1 partial (600 blks) */
+ resize_configure_fs(test, 2, 0, 0, 600);
+ o_blks = ext4_blocks_count(es);
+ o_free = ext4_free_blocks_count(es);
+ KUNIT_ASSERT_EQ(test, o_blks, 1ULL + 1024ULL + 600ULL);
+
+ /* No-op for 0 and o_blocks_count */
+ KUNIT_EXPECT_EQ(test, ext4_group_extend(sb, es, 0), 0);
+ KUNIT_EXPECT_EQ(test, ext4_group_extend(sb, es, o_blks), 0);
+
+ /* Shrink attempt -> -EINVAL */
+ KUNIT_EXPECT_EQ(test, ext4_group_extend(sb, es, o_blks - 10), -EINVAL);
+
+ /* Target beyond underlying device -> -ENOSPC */
+ ctx->device_blocks = o_blks + 100;
+ KUNIT_EXPECT_EQ(test, ext4_group_extend(sb, es, o_blks + 200), -ENOSPC);
+ ctx->device_blocks = 1 + 64 * RESIZE_TEST_BLOCKS_PER_GRP;
+
+ /* Extend group 1 by 300 blocks (from 600 to 900 blocks) */
+ free_before = percpu_counter_sum(&ctx->sbi.s_freeclusters_counter);
+ KUNIT_EXPECT_EQ(test, ext4_group_extend(sb, es, o_blks + 300), 0);
+ KUNIT_EXPECT_EQ(test, ext4_blocks_count(es), o_blks + 300);
+ KUNIT_EXPECT_EQ(test, ext4_free_blocks_count(es), o_free + 300);
+ KUNIT_EXPECT_EQ(test,
+ percpu_counter_sum(&ctx->sbi.s_freeclusters_counter),
+ free_before + 300);
+
+ /* Extend past group 1 boundary clamps to end of group 1 (+124 blocks) */
+ KUNIT_EXPECT_EQ(test, ext4_group_extend(sb, es, o_blks + 800), 0);
+ KUNIT_EXPECT_EQ(test, ext4_blocks_count(es), 1ULL + 2048ULL);
+
+ /* Once group 1 is full, further ext4_group_extend returns -EPERM */
+ KUNIT_EXPECT_EQ(test, ext4_group_extend(sb, es, 1ULL + 2200ULL),
+ -EPERM);
+
+ /* Backup superblock in group 1 (block 1025) reflects new size */
+ b1_bh = resize_lookup_bh(ctx, ext4_group_first_block_no(sb, 1));
+ KUNIT_ASSERT_NOT_NULL(test, b1_bh);
+ backup_es = (struct ext4_super_block *)b1_bh->b_data;
+ KUNIT_EXPECT_EQ(test, ext4_blocks_count(backup_es), 1ULL + 2048ULL);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(backup_es->s_block_group_nr), 1U);
+}
+
+/*
+ * Test 6: ext4_group_add() error guards and adding non-sparse group 2 plus
+ * sparse backup group 3 (exercising reserve_backup_gdb() on resize_inode).
+ */
+static void test_group_add_with_resize_inode_and_backup_gdb(struct kunit *test)
+{
+ struct resize_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct ext4_new_group_data input;
+ struct buffer_head *pri_bh;
+ __le32 *pri_data;
+ blkcnt_t rsv_iblocks_before;
+ ext4_fsblk_t start;
+
+ /* 2 initial groups, 1 reserved GDT block (at block 3) */
+ resize_configure_fs(test, 2, 1, 0, RESIZE_TEST_BLOCKS_PER_GRP);
+
+ /* 1. Overflow guards on blocks_count and s_inodes_count */
+ memset(&input, 0, sizeof(input));
+ input.group = 2;
+ input.blocks_count = 1024;
+ ext4_blocks_count_set(ctx->es, ~0ULL - 100);
+ KUNIT_EXPECT_EQ(test, ext4_group_add(sb, &input), -EINVAL);
+ ext4_blocks_count_set(ctx->es, 1 + 2 * RESIZE_TEST_BLOCKS_PER_GRP);
+
+ ctx->es->s_inodes_count = cpu_to_le32(~0U - 10);
+ KUNIT_EXPECT_EQ(test, ext4_group_add(sb, &input), -EINVAL);
+ ctx->es->s_inodes_count = cpu_to_le32(2 * RESIZE_TEST_INODES_PER_GRP);
+
+ /* 2. Add group 2 (no backup super, overhead = 0) */
+ start = ext4_blocks_count(ctx->es);
+ memset(&input, 0, sizeof(input));
+ input.group = 2;
+ input.blocks_count = RESIZE_TEST_BLOCKS_PER_GRP;
+ input.block_bitmap = start;
+ input.inode_bitmap = start + 1;
+ input.inode_table = start + 2;
+
+ KUNIT_EXPECT_EQ(test, ext4_group_add(sb, &input), 0);
+ KUNIT_EXPECT_EQ(test, ctx->sbi.s_groups_count, 3U);
+
+ /* 3. Add group 3 (sparse backup super -> overhead = 3, reserves backup) */
+ start = ext4_blocks_count(ctx->es);
+ memset(&input, 0, sizeof(input));
+ input.group = 3;
+ input.blocks_count = RESIZE_TEST_BLOCKS_PER_GRP;
+ input.block_bitmap = start + 3;
+ input.inode_bitmap = start + 4;
+ input.inode_table = start + 5;
+
+ /* Without resize_inode feature -> -EPERM */
+ ext4_clear_feature_resize_inode(sb);
+ KUNIT_EXPECT_EQ(test, ext4_group_add(sb, &input), -EPERM);
+ ext4_set_feature_resize_inode(sb);
+
+ rsv_iblocks_before = ctx->resize_inode->i_blocks;
+ KUNIT_EXPECT_EQ(test, ext4_group_add(sb, &input), 0);
+ KUNIT_EXPECT_EQ(test, ctx->sbi.s_groups_count, 4U);
+
+ /*
+ * reserve_backup_gdb() must append group 3's reserved GDT block
+ * (3 * 1024 + 3 = 3075) at index 1 (after group 1's backup) in
+ * primary reserved GDT block 3 and increment resize_inode->i_blocks.
+ */
+ pri_bh = resize_lookup_bh(ctx, 3);
+ KUNIT_ASSERT_NOT_NULL(test, pri_bh);
+ pri_data = (__le32 *)pri_bh->b_data;
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(pri_data[1]), 3U * 1024U + 3U);
+ KUNIT_EXPECT_EQ(test, ctx->resize_inode->i_blocks,
+ rsv_iblocks_before + (RESIZE_TEST_BLK_SIZE >> 9));
+}
+
+/*
+ * Test 7: ext4_convert_meta_bg() sanity validation of resize_inode and
+ * conversion from resize_inode to meta_bg.
+ */
+static void test_convert_meta_bg_validation_and_conversion(struct kunit *test)
+{
+ struct resize_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *rinode;
+ ext4_fsblk_t dind_blk = 42;
+
+ resize_configure_fs(test, 16, 0, 0, RESIZE_TEST_BLOCKS_PER_GRP);
+ ext4_set_feature_resize_inode(sb);
+
+ rinode = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, rinode);
+ rinode->i_blkbits = RESIZE_TEST_BLK_BITS;
+ rinode->i_blocks = RESIZE_TEST_BLK_SIZE >> 9;
+ EXT4_I(rinode)->i_data[EXT4_DIND_BLOCK] = cpu_to_le32(dind_blk);
+
+ /* 1. Non-zero s_reserved_gdt_blocks -> -EPERM */
+ ctx->es->s_reserved_gdt_blocks = cpu_to_le16(1);
+ KUNIT_EXPECT_EQ(test, ext4_convert_meta_bg_test(sb, rinode), -EPERM);
+ ctx->es->s_reserved_gdt_blocks = 0;
+
+ /* 2. Wrong i_blocks on resize_inode -> -EINVAL */
+ rinode->i_blocks = 8;
+ KUNIT_EXPECT_EQ(test, ext4_convert_meta_bg_test(sb, rinode), -EINVAL);
+ rinode->i_blocks = RESIZE_TEST_BLK_SIZE >> 9;
+
+ /* 3. Unexpected direct block pointer or missing DIND -> -EINVAL */
+ EXT4_I(rinode)->i_data[0] = cpu_to_le32(10);
+ KUNIT_EXPECT_EQ(test, ext4_convert_meta_bg_test(sb, rinode), -EINVAL);
+ EXT4_I(rinode)->i_data[0] = 0;
+
+ EXT4_I(rinode)->i_data[EXT4_DIND_BLOCK] = 0;
+ KUNIT_EXPECT_EQ(test, ext4_convert_meta_bg_test(sb, rinode), -EINVAL);
+ EXT4_I(rinode)->i_data[EXT4_DIND_BLOCK] = cpu_to_le32(dind_blk);
+ ctx->sbi.s_mount_state &= ~EXT4_ERROR_FS;
+
+ /* 4. Valid conversion clears RESIZE_INODE, sets META_BG, frees DIND */
+ KUNIT_EXPECT_EQ(test, ext4_convert_meta_bg_test(sb, rinode), 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_feature_resize_inode(sb));
+ KUNIT_EXPECT_TRUE(test, ext4_has_feature_meta_bg(sb));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es->s_first_meta_bg), 1U);
+ KUNIT_EXPECT_EQ(test, ctx->last_freed_blk, dind_blk);
+ KUNIT_EXPECT_EQ(test, ctx->last_freed_count, 1UL);
+ KUNIT_EXPECT_EQ(test, rinode->i_blocks, 0ULL);
+ KUNIT_EXPECT_EQ(test, EXT4_I(rinode)->i_data[EXT4_DIND_BLOCK], 0);
+
+ iput(rinode);
+}
+
+/*
+ * Test 8: End-to-end ext4_resize_fs() across error guards, bigalloc cluster
+ * rounding, consuming reserved GDT blocks (16 -> 32 groups via add_new_gdb),
+ * converting resize_inode -> meta_bg, and growing into meta_bg (32 -> 34
+ * groups via add_new_gdb_meta_bg).
+ */
+static void test_resize_fs_across_resize_inode_to_meta_bg(struct kunit *test)
+{
+ struct resize_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ ext4_fsblk_t o_blks, target_blks, dind_blk;
+
+ /*
+ * Start with 16 groups (1 full 64-byte GDT block), 1 reserved GDT
+ * block (allowing growth to 32 groups before meta_bg conversion), and
+ * flex_bg of 4 groups (log_flex = 2).
+ */
+ resize_configure_fs(test, 16, 1, 2, RESIZE_TEST_BLOCKS_PER_GRP);
+ o_blks = ext4_blocks_count(ctx->es);
+ dind_blk = le32_to_cpu(
+ EXT4_I(ctx->resize_inode)->i_data[EXT4_DIND_BLOCK]);
+
+ /* 1. Beyond device -> -ENOSPC; shrink -> -EINVAL; same size -> 0 */
+ KUNIT_EXPECT_EQ(test, ext4_resize_fs(sb, ctx->device_blocks + 100),
+ -ENOSPC);
+ KUNIT_EXPECT_EQ(test, ext4_resize_fs(sb, o_blks - 100), -EINVAL);
+ KUNIT_EXPECT_EQ(test, ext4_resize_fs(sb, o_blks), 0);
+
+ /* 2. Simultaneous resize_inode + meta_bg -> -EINVAL */
+ ext4_set_feature_meta_bg(sb);
+ KUNIT_EXPECT_EQ(test, ext4_resize_fs(sb, o_blks + 1024), -EINVAL);
+ ext4_clear_feature_meta_bg(sb);
+ ctx->sbi.s_mount_state &= ~EXT4_ERROR_FS;
+
+ /* 3. Grow from 16 groups to 34 groups across GDT & meta_bg boundary */
+ target_blks = RESIZE_TEST_FIRST_DATA_BLK +
+ 34ULL * RESIZE_TEST_BLOCKS_PER_GRP;
+ KUNIT_EXPECT_EQ(test, ext4_resize_fs(sb, target_blks), 0);
+
+ KUNIT_EXPECT_EQ(test, ctx->sbi.s_groups_count, 34U);
+ KUNIT_EXPECT_EQ(test, ctx->sbi.s_gdb_count, 3UL);
+ KUNIT_EXPECT_EQ(test, ext4_blocks_count(ctx->es), target_blks);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ctx->es->s_reserved_gdt_blocks), 0U);
+ KUNIT_EXPECT_FALSE(test, ext4_has_feature_resize_inode(sb));
+ KUNIT_EXPECT_TRUE(test, ext4_has_feature_meta_bg(sb));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es->s_first_meta_bg), 2U);
+ KUNIT_EXPECT_EQ(test, ctx->last_freed_blk, dind_blk);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es->s_inodes_count),
+ 34U * RESIZE_TEST_INODES_PER_GRP);
+}
+
+static struct kunit_case ext4_resize_test_cases[] = {
+ KUNIT_CASE(test_resize_begin_and_end_guards),
+ KUNIT_CASE(test_list_backups_and_verify_reserved_gdb),
+ KUNIT_CASE(test_verify_group_input_rejections_and_acceptance),
+ KUNIT_CASE(test_alloc_setup_flex_gd_and_group_tables),
+ KUNIT_CASE(test_group_extend_partial_last_group),
+ KUNIT_CASE(test_group_add_with_resize_inode_and_backup_gdb),
+ KUNIT_CASE(test_convert_meta_bg_validation_and_conversion),
+ KUNIT_CASE(test_resize_fs_across_resize_inode_to_meta_bg),
+ {}
+};
+
+static struct kunit_suite ext4_resize_test_suite = {
+ .name = "ext4_resize_test",
+ .init = resize_test_init,
+ .test_cases = ext4_resize_test_cases,
+};
+
+kunit_test_suites(&ext4_resize_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 online filesystem resizing");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/resize.c b/fs/ext4/resize.c
index 2c5b851c552a..e873e09e609a 100644
--- a/fs/ext4/resize.c
+++ b/fs/ext4/resize.c
@@ -13,6 +13,7 @@
#include <linux/errno.h>
#include <linux/slab.h>
#include <linux/jiffies.h>
+#include <kunit/static_stub.h>
#include "ext4_jbd2.h"
@@ -42,6 +43,16 @@ void ext4_kvfree_array_rcu(void *to_free)
synchronize_rcu();
kvfree(to_free);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_kvfree_array_rcu);
+
+int ext4_resize_issue_zeroout(struct super_block *sb, sector_t blk,
+ sector_t nr_blks, gfp_t gfp_mask)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_resize_issue_zeroout, sb, blk,
+ nr_blks, gfp_mask);
+ return sb_issue_zeroout(sb, blk, nr_blks, gfp_mask);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_resize_issue_zeroout);
int ext4_resize_begin(struct super_block *sb)
{
@@ -94,6 +105,7 @@ int ext4_resize_begin(struct super_block *sb)
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_resize_begin);
int ext4_resize_end(struct super_block *sb, bool update_backups)
{
@@ -103,6 +115,7 @@ int ext4_resize_end(struct super_block *sb, bool update_backups)
return ext4_update_overhead(sb, true);
return 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_resize_end);
static ext4_grpblk_t ext4_group_overhead_blocks(struct super_block *sb,
ext4_group_t group) {
@@ -209,21 +222,6 @@ static int verify_group_input(struct super_block *sb,
return err;
}
-/*
- * ext4_new_flex_group_data is used by 64bit-resize interface to add a flex
- * group each time.
- */
-struct ext4_new_flex_group_data {
- struct ext4_new_group_data *groups; /* new_group_data for groups
- in the flex group */
- __u16 *bg_flags; /* block group flags of groups
- in @groups */
- ext4_group_t resize_bg; /* number of allocated
- new_group_data */
- ext4_group_t count; /* number of groups in @groups
- */
-};
-
/*
* Avoiding memory allocation failures due to too many groups added each time.
*/
@@ -425,7 +423,7 @@ static struct buffer_head *bclean(handle_t *handle, struct super_block *sb,
struct buffer_head *bh;
int err;
- bh = sb_getblk(sb, blk);
+ bh = ext4_sb_getblk(sb, blk);
if (unlikely(!bh))
return ERR_PTR(-ENOMEM);
BUFFER_TRACE(bh, "get_write_access");
@@ -489,7 +487,7 @@ static int set_flexbg_block_bitmap(struct super_block *sb, handle_t *handle,
if (err < 0)
return err;
- bh = sb_getblk(sb, flex_gd->groups[group].block_bitmap);
+ bh = ext4_sb_getblk(sb, flex_gd->groups[group].block_bitmap);
if (unlikely(!bh))
return -ENOMEM;
@@ -578,7 +576,7 @@ static int setup_new_flex_group_blocks(struct super_block *sb,
if (err < 0)
goto out;
- gdb = sb_getblk(sb, block);
+ gdb = ext4_sb_getblk(sb, block);
if (unlikely(!gdb)) {
err = -ENOMEM;
goto out;
@@ -607,8 +605,8 @@ static int setup_new_flex_group_blocks(struct super_block *sb,
* table blocks
*/
if (ext4_bg_has_super(sb, group)) {
- err = sb_issue_zeroout(sb, gdblocks + start + 1,
- reserved_gdb, GFP_NOFS);
+ err = ext4_resize_issue_zeroout(sb, gdblocks + start + 1,
+ reserved_gdb, GFP_NOFS);
if (err)
goto out;
}
@@ -622,8 +620,8 @@ static int setup_new_flex_group_blocks(struct super_block *sb,
block = group_data[i].inode_table;
ext4_debug("clear inode table blocks %#04llx -> %#04lx\n",
block, sbi->s_itb_per_group);
- err = sb_issue_zeroout(sb, block, sbi->s_itb_per_group,
- GFP_NOFS);
+ err = ext4_resize_issue_zeroout(sb, block, sbi->s_itb_per_group,
+ GFP_NOFS);
if (err)
goto out;
@@ -767,6 +765,7 @@ unsigned int ext4_list_backups(struct super_block *sb, unsigned int *three,
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_list_backups);
/*
* Check that all of the backup GDT blocks are held in the primary GDT block.
@@ -1182,7 +1181,7 @@ static void update_backups(struct super_block *sb, sector_t blk_off, char *data,
else
backup_block = first_block + has_super;
- bh = sb_getblk(sb, backup_block);
+ bh = ext4_sb_getblk(sb, backup_block);
if (unlikely(!bh)) {
err = -ENOMEM;
break;
@@ -1295,7 +1294,7 @@ static int ext4_add_new_descs(handle_t *handle, struct super_block *sb,
static struct buffer_head *ext4_get_bitmap(struct super_block *sb, __u64 block)
{
- struct buffer_head *bh = sb_getblk(sb, block);
+ struct buffer_head *bh = ext4_sb_getblk(sb, block);
if (unlikely(!bh))
return NULL;
if (!bh_uptodate_or_lock(bh)) {
@@ -1763,6 +1762,7 @@ int ext4_group_add(struct super_block *sb, struct ext4_new_group_data *input)
iput(inode);
return err;
} /* ext4_group_add */
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_group_add);
/*
* extend a group without checking assuming that checking has been done.
@@ -1894,6 +1894,7 @@ int ext4_group_extend(struct super_block *sb, struct ext4_super_block *es,
return ext4_group_extend_no_check(sb, o_blocks_count, add);
} /* ext4_group_extend */
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_group_extend);
static int num_desc_blocks(struct super_block *sb, ext4_group_t groups)
@@ -2190,3 +2191,56 @@ int ext4_resize_fs(struct super_block *sb, ext4_fsblk_t n_blocks_count)
ext4_blocks_count(es));
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_resize_fs);
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+int ext4_verify_group_input_test(struct super_block *sb,
+ struct ext4_new_group_data *input)
+{
+ return verify_group_input(sb, input);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_verify_group_input_test);
+
+struct ext4_new_flex_group_data *
+ext4_alloc_flex_gd_test(unsigned int flexbg_size, ext4_group_t o_group,
+ ext4_group_t n_group)
+{
+ return alloc_flex_gd(flexbg_size, o_group, n_group);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_alloc_flex_gd_test);
+
+void ext4_free_flex_gd_test(struct ext4_new_flex_group_data *flex_gd)
+{
+ free_flex_gd(flex_gd);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_flex_gd_test);
+
+int ext4_alloc_group_tables_test(struct super_block *sb,
+ struct ext4_new_flex_group_data *flex_gd,
+ unsigned int flexbg_size)
+{
+ return ext4_alloc_group_tables(sb, flex_gd, flexbg_size);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_alloc_group_tables_test);
+
+int ext4_verify_reserved_gdb_test(struct super_block *sb, ext4_group_t end,
+ struct buffer_head *primary)
+{
+ return verify_reserved_gdb(sb, end, primary);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_verify_reserved_gdb_test);
+
+int ext4_setup_next_flex_gd_test(struct super_block *sb,
+ struct ext4_new_flex_group_data *flex_gd,
+ ext4_fsblk_t n_blocks_count)
+{
+ return ext4_setup_next_flex_gd(sb, flex_gd, n_blocks_count);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_setup_next_flex_gd_test);
+
+int ext4_convert_meta_bg_test(struct super_block *sb, struct inode *inode)
+{
+ return ext4_convert_meta_bg(sb, inode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_convert_meta_bg_test);
+#endif
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 21/27] ext4: add KUnit tests for fs/ext4/migrate.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (19 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 20/27] ext4: add KUnit tests for fs/ext4/resize.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (5 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add the ext4_migrate_test KUnit suite in fs/ext4/migrate-test.c to test
online inode format migration between indirect block maps and extent
trees in fs/ext4/migrate.c:
- ext4_ext_migrate() precondition guards, fast symlink no-op handling,
and temporary inode allocation error propagation
- direct and single-indirect physically contiguous block coalescing,
sparse logical hole preservation, and indirect metadata freeing
- double-indirect and triple-indirect tree traversal and multi-level
extent tree construction with proper i_csum_seed propagation
- error rollback freeing allocated temporary extent tree blocks while
preserving the original indirect inode layout
- ext4_ext_swap_inode_data() concurrent block allocation detection via
EXT4_STATE_EXT_MIGRATE (-EAGAIN)
- ext4_ind_migrate() conversion of empty and single-extent inodes back
to direct block maps and rejection of unsupported layouts
Export ext4_ext_migrate(), ext4_ind_migrate(), and a KUnit wrapper for
ext4_ext_swap_inode_data() via EXPORT_SYMBOL_FOR_EXT4_TEST.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 2 +-
fs/ext4/ext4.h | 4 +
fs/ext4/migrate-test.c | 1064 ++++++++++++++++++++++++++++++++++++++++
fs/ext4/migrate.c | 11 +
4 files changed, 1080 insertions(+), 1 deletion(-)
create mode 100644 fs/ext4/migrate-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 6163d47f3627..fafd9600cacb 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -22,7 +22,7 @@ ext4-test-objs += inode-test.o mballoc-test.o \
balloc-test.o orphan-test.o \
block_validity-test.o super-test.o \
sysfs-test.o ioctl-test.o \
- resize-test.o
+ resize-test.o migrate-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 3a735c69e2e3..91cc0e00aaa5 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3385,6 +3385,10 @@ bool ext4_dax_compatible_test(struct inode *inode, unsigned int oldflags,
/* migrate.c */
extern int ext4_ext_migrate(struct inode *);
extern int ext4_ind_migrate(struct inode *inode);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+extern int ext4_ext_swap_inode_data_test(handle_t *handle, struct inode *inode,
+ struct inode *tmp_inode);
+#endif
/* namei.c */
extern int ext4_init_new_dir(handle_t *handle, struct inode *dir,
diff --git a/fs/ext4/migrate-test.c b/fs/ext4/migrate-test.c
new file mode 100644
index 000000000000..e83d208a5117
--- /dev/null
+++ b/fs/ext4/migrate-test.c
@@ -0,0 +1,1064 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/migrate.c
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/fscrypt.h>
+#include <linux/iversion.h>
+#include <linux/kernel.h>
+#include <linux/list.h>
+#include <linux/mm.h>
+#include <linux/module.h>
+#include <linux/part_stat.h>
+#include <linux/percpu.h>
+#include <linux/percpu-rwsem.h>
+#include <linux/percpu_counter.h>
+#include <linux/quotaops.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_extents.h"
+#include "ext4_jbd2.h"
+
+#define MIGRATE_TEST_BLK_SIZE 1024U
+#define MIGRATE_TEST_BLK_BITS 10U
+#define MIGRATE_TEST_BLOCKS_PER_GRP 1024U
+#define MIGRATE_TEST_NGROUPS 8U
+#define MIGRATE_TEST_FIRST_DATA_BLK 1U
+#define MIGRATE_TEST_TOTAL_BLKS (MIGRATE_TEST_FIRST_DATA_BLK + \
+ MIGRATE_TEST_NGROUPS * \
+ MIGRATE_TEST_BLOCKS_PER_GRP)
+#define MIGRATE_TEST_INODE_SIZE 256U
+#define MIGRATE_TEST_EXTRA_ISIZE 32U
+#define MIGRATE_TEST_INODES_PER_GRP 32U
+#define MIGRATE_MAX_FREED_REC 32U
+
+struct migrate_mock_blk {
+ struct list_head node;
+ ext4_fsblk_t blocknr;
+ struct buffer_head bh;
+ u8 *data;
+};
+
+struct migrate_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 list_head blks;
+ ext4_fsblk_t device_blocks;
+ ext4_fsblk_t next_alloc_pblk;
+ int new_inode_err;
+ ext4_fsblk_t freed_blks[MIGRATE_MAX_FREED_REC];
+ unsigned int freed_cnt;
+ bool rwsem_inited;
+ bool es_shrinker_inited;
+ bool counters_inited;
+};
+
+static struct kunit *active_migrate_test;
+
+static struct inode *migrate_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 = MIGRATE_TEST_EXTRA_ISIZE;
+ ei->i_last_alloc_group = ~0U;
+
+ return &ei->vfs_inode;
+}
+
+static void migrate_test_free_inode(struct inode *inode)
+{
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ kfree(EXT4_I(inode));
+}
+
+static void migrate_test_evict_inode(struct inode *inode)
+{
+ truncate_inode_pages_final(&inode->i_data);
+ clear_inode(inode);
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **migrate_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+#endif
+
+static const struct super_operations migrate_test_sops = {
+ .alloc_inode = migrate_test_alloc_inode,
+ .free_inode = migrate_test_free_inode,
+ .evict_inode = migrate_test_evict_inode,
+#ifdef CONFIG_QUOTA
+ .get_dquots = migrate_test_get_dquots,
+#endif
+};
+
+static int migrate_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static void migrate_test_sb_upd_work(struct work_struct *work)
+{
+}
+
+static void migrate_test_err_timer(struct timer_list *t)
+{
+}
+
+static struct file_system_type migrate_test_fs_type = {
+ .name = "ext4_migrate_kunit",
+ .init_fs_context = migrate_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static void migrate_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);
+ set_buffer_mapped(bh);
+}
+
+static struct buffer_head *migrate_lookup_bh(struct migrate_test_ctx *ctx,
+ ext4_fsblk_t blocknr)
+{
+ struct migrate_mock_blk *blk;
+
+ list_for_each_entry(blk, &ctx->blks, node) {
+ if (blk->blocknr == blocknr)
+ return &blk->bh;
+ }
+ return NULL;
+}
+
+static struct buffer_head *migrate_get_or_create_bh(struct migrate_test_ctx *ctx,
+ ext4_fsblk_t blocknr)
+{
+ struct buffer_head *bh;
+ struct migrate_mock_blk *blk;
+
+ bh = migrate_lookup_bh(ctx, blocknr);
+ if (bh) {
+ get_bh(bh);
+ return bh;
+ }
+
+ blk = kzalloc_obj(*blk);
+ if (!blk)
+ return NULL;
+ blk->data = kzalloc(MIGRATE_TEST_BLK_SIZE, GFP_KERNEL);
+ if (!blk->data) {
+ kfree(blk);
+ return NULL;
+ }
+ blk->blocknr = blocknr;
+ migrate_init_mock_bh(&blk->bh, ctx->bh_folio, blk->data,
+ MIGRATE_TEST_BLK_SIZE, blocknr);
+ list_add_tail(&blk->node, &ctx->blks);
+ get_bh(&blk->bh);
+ return &blk->bh;
+}
+
+static struct buffer_head *migrate_sb_bread_stub(struct super_block *sb,
+ sector_t block,
+ blk_opf_t op_flags)
+{
+ struct kunit *test = active_migrate_test;
+ struct migrate_test_ctx *ctx;
+ struct buffer_head *bh;
+
+ if (!test)
+ return ERR_PTR(-EIO);
+ ctx = test->priv;
+ if (!ctx || sb != ctx->sb)
+ return ERR_PTR(-EIO);
+ if ((ext4_fsblk_t)block >= ctx->device_blocks)
+ return ERR_PTR(-EIO);
+
+ bh = migrate_get_or_create_bh(ctx, block);
+ if (!bh)
+ return ERR_PTR(-ENOMEM);
+ return bh;
+}
+
+static struct buffer_head *migrate_ext_getblk_stub(struct inode *inode,
+ ext4_fsblk_t pblk,
+ gfp_t gfp_flags)
+{
+ struct kunit *test = active_migrate_test;
+ struct migrate_test_ctx *ctx;
+
+ if (!test)
+ return NULL;
+ ctx = test->priv;
+ if (!ctx)
+ return NULL;
+
+ return migrate_get_or_create_bh(ctx, pblk);
+}
+
+static int migrate_ext_dirty_stub(const char *where, unsigned int line,
+ handle_t *handle, struct inode *inode,
+ struct ext4_ext_path *path)
+{
+ if (path && path->p_bh) {
+ ext4_extent_block_csum_set_test(inode,
+ ext_block_hdr(path->p_bh));
+ set_buffer_verified(path->p_bh);
+ }
+ return 0;
+}
+
+static int migrate_handle_dirty_metadata_stub(const char *where,
+ unsigned int line,
+ handle_t *handle,
+ struct inode *inode,
+ struct buffer_head *bh)
+{
+ if (bh)
+ set_buffer_uptodate(bh);
+ return 0;
+}
+
+static int migrate_mark_inode_dirty_stub(handle_t *handle, struct inode *inode,
+ const char *func, unsigned int line)
+{
+ return 0;
+}
+
+static struct inode *migrate_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 = active_migrate_test;
+ struct migrate_test_ctx *ctx;
+ struct inode *inode;
+
+ if (!test)
+ return ERR_PTR(-EIO);
+ ctx = test->priv;
+ if (!ctx)
+ return ERR_PTR(-EIO);
+ if (ctx->new_inode_err)
+ return ERR_PTR(ctx->new_inode_err);
+
+ inode = new_inode(dir->i_sb);
+ if (!inode)
+ return ERR_PTR(-ENOMEM);
+
+ inode->i_ino = 99;
+ inode->i_mode = mode;
+ inode->i_blkbits = dir->i_sb->s_blocksize_bits;
+ inode->i_flags |= S_NOQUOTA;
+ EXT4_I(inode)->i_flags = i_flags;
+ EXT4_I(inode)->i_csum_seed = 0xabcdef01U;
+ if (owner) {
+ i_uid_write(inode, owner[0]);
+ i_gid_write(inode, owner[1]);
+ }
+ inode_state_set_raw(inode, I_NEW);
+ return inode;
+}
+
+static ext4_fsblk_t migrate_mb_new_blocks_stub(handle_t *handle,
+ struct ext4_allocation_request *ar,
+ int *errp)
+{
+ struct kunit *test = active_migrate_test;
+ struct migrate_test_ctx *ctx;
+ ext4_fsblk_t pblk;
+
+ if (!test) {
+ *errp = -EIO;
+ return 0;
+ }
+ ctx = test->priv;
+ pblk = ctx->next_alloc_pblk;
+ ctx->next_alloc_pblk += ar->len;
+ ar->inode->i_blocks += (blkcnt_t)ar->len <<
+ (ar->inode->i_blkbits - 9);
+ *errp = 0;
+ return pblk;
+}
+
+static void migrate_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 = active_migrate_test;
+ struct migrate_test_ctx *ctx;
+ blkcnt_t sectors;
+ unsigned long i;
+
+ if (!test)
+ return;
+ ctx = test->priv;
+ for (i = 0; i < count && ctx->freed_cnt < MIGRATE_MAX_FREED_REC; i++)
+ ctx->freed_blks[ctx->freed_cnt++] = block + i;
+
+ sectors = (blkcnt_t)count << (inode->i_blkbits - 9);
+ if (inode->i_blocks >= sectors)
+ inode->i_blocks -= sectors;
+ if (bh)
+ brelse(bh);
+}
+
+static bool migrate_was_freed(struct migrate_test_ctx *ctx, ext4_fsblk_t blk)
+{
+ unsigned int i;
+
+ for (i = 0; i < ctx->freed_cnt; i++) {
+ if (ctx->freed_blks[i] == blk)
+ return true;
+ }
+ return false;
+}
+
+static struct inode *migrate_make_test_inode(struct kunit *test, unsigned long ino,
+ umode_t mode)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct inode *inode;
+
+ inode = new_inode(ctx->sb);
+ KUNIT_ASSERT_NOT_NULL(test, inode);
+ inode->i_ino = ino;
+ inode->i_mode = mode;
+ inode->i_blkbits = MIGRATE_TEST_BLK_BITS;
+ inode->i_flags |= S_NOQUOTA;
+ EXT4_I(inode)->i_csum_seed = 0x55aa55aaU;
+ return inode;
+}
+
+static void migrate_test_cleanup(struct migrate_test_ctx *ctx)
+{
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ struct migrate_mock_blk *blk, *tmp;
+
+ if (sb) {
+ cancel_work_sync(&sbi->s_sb_upd_work);
+ timer_delete_sync(&sbi->s_err_report);
+ }
+
+ if (ctx->es_shrinker_inited) {
+ ext4_es_unregister_shrinker(sbi);
+ ctx->es_shrinker_inited = false;
+ }
+
+ if (ctx->counters_inited) {
+ percpu_counter_destroy(&sbi->s_freeclusters_counter);
+ percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
+ percpu_counter_destroy(&sbi->s_freeinodes_counter);
+ ctx->counters_inited = false;
+ }
+
+ if (ctx->rwsem_inited) {
+ percpu_free_rwsem(&sbi->s_writepages_rwsem);
+ ctx->rwsem_inited = false;
+ }
+
+ if (sb) {
+ deactivate_super(sb);
+ ctx->sb = NULL;
+ }
+
+ list_for_each_entry_safe(blk, tmp, &ctx->blks, node) {
+ list_del(&blk->node);
+ kfree(blk->data);
+ kfree(blk);
+ }
+
+ if (ctx->bh_folio) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ }
+
+ if (ctx->bdev.bd_stats) {
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ }
+
+ active_migrate_test = NULL;
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(migrate_test_cleanup_action, migrate_test_cleanup,
+ struct migrate_test_ctx *);
+
+static int migrate_test_set_super(struct super_block *sb, struct fs_context *fc)
+{
+ return set_anon_super_fc(sb, fc);
+}
+
+static int migrate_test_init(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ struct buffer_head *sbh;
+ struct inode *root_inode;
+ int ret;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ INIT_LIST_HEAD(&ctx->blks);
+ ctx->device_blocks = MIGRATE_TEST_TOTAL_BLKS;
+ ctx->next_alloc_pblk = 900;
+ test->priv = ctx;
+
+ ctx->bdev.bd_stats = alloc_percpu(struct disk_stats);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->bdev.bd_stats);
+
+ ctx->bh_folio = folio_alloc(GFP_KERNEL, 0);
+ if (!ctx->bh_folio) {
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "folio_alloc failed");
+ }
+
+ fc = fs_context_for_mount(&migrate_test_fs_type, 0);
+ if (IS_ERR(fc)) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "fs_context_for_mount failed: %ld",
+ PTR_ERR(fc));
+ }
+ sb = sget_fc(fc, NULL, migrate_test_set_super);
+ put_fs_context(fc);
+ if (IS_ERR(sb)) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "sget_fc failed: %ld", PTR_ERR(sb));
+ }
+ ctx->sb = sb;
+ up_write(&sb->s_umount);
+
+ ret = kunit_add_action_or_reset(test, migrate_test_cleanup_action, ctx);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ sbi = &ctx->sbi;
+ sb->s_fs_info = sbi;
+ sb->s_op = &migrate_test_sops;
+ sb->s_flags |= SB_ACTIVE;
+ sb->s_blocksize = MIGRATE_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = MIGRATE_TEST_BLK_BITS;
+ sb->s_magic = EXT4_SUPER_MAGIC;
+ sb->s_maxbytes = MAX_LFS_FILESIZE;
+ INIT_LIST_HEAD(&sb->s_inodes);
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
+#endif
+
+ strscpy(sb->s_id, "ext4_migrate_kunit", sizeof(sb->s_id));
+ inode_init_once(&ctx->bd_inode);
+ ctx->bd_inode.i_sb = sb;
+ ctx->bd_inode.i_blkbits = MIGRATE_TEST_BLK_BITS;
+ ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data;
+ ctx->bd_inode.i_data.host = &ctx->bd_inode;
+ 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;
+
+ sbi->s_sb = sb;
+ sbi->s_blockgroup_lock = &ctx->bgl;
+ bgl_lock_init(&ctx->bgl);
+ INIT_WORK(&sbi->s_sb_upd_work, migrate_test_sb_upd_work);
+ timer_setup(&sbi->s_err_report, migrate_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, 5 * HZ, 10);
+ ratelimit_state_init(&sbi->s_warning_ratelimit_state, 5 * HZ, 10);
+ ratelimit_state_init(&sbi->s_msg_ratelimit_state, 5 * HZ, 10);
+
+ ret = percpu_init_rwsem(&sbi->s_writepages_rwsem);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->rwsem_inited = true;
+
+ ret = percpu_counter_init(&sbi->s_freeclusters_counter, 4096,
+ GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = percpu_counter_init(&sbi->s_freeinodes_counter, 200, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->counters_inited = true;
+
+ ret = ext4_es_register_shrinker(sbi);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->es_shrinker_inited = true;
+
+ sbi->s_blocks_per_group = MIGRATE_TEST_BLOCKS_PER_GRP;
+ sbi->s_clusters_per_group = MIGRATE_TEST_BLOCKS_PER_GRP;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_cluster_bits = 0;
+ sbi->s_groups_count = MIGRATE_TEST_NGROUPS;
+ sbi->s_inodes_per_group = MIGRATE_TEST_INODES_PER_GRP;
+ sbi->s_inode_size = MIGRATE_TEST_INODE_SIZE;
+ sbi->s_inodes_per_block = MIGRATE_TEST_BLK_SIZE /
+ MIGRATE_TEST_INODE_SIZE;
+ sbi->s_first_ino = 11;
+ sbi->s_mount_state = EXT4_VALID_FS;
+ set_opt(sb, ERRORS_CONT);
+
+ sbh = migrate_get_or_create_bh(ctx, MIGRATE_TEST_FIRST_DATA_BLK);
+ KUNIT_ASSERT_NOT_NULL(test, sbh);
+ brelse(sbh);
+ sbi->s_sbh = sbh;
+ ctx->es = (struct ext4_super_block *)sbh->b_data;
+ sbi->s_es = ctx->es;
+
+ 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_data_block = cpu_to_le32(MIGRATE_TEST_FIRST_DATA_BLK);
+ ctx->es->s_blocks_per_group = cpu_to_le32(MIGRATE_TEST_BLOCKS_PER_GRP);
+ ctx->es->s_clusters_per_group =
+ cpu_to_le32(MIGRATE_TEST_BLOCKS_PER_GRP);
+ ctx->es->s_inodes_per_group = cpu_to_le32(MIGRATE_TEST_INODES_PER_GRP);
+ ctx->es->s_inodes_count = cpu_to_le32(MIGRATE_TEST_NGROUPS *
+ MIGRATE_TEST_INODES_PER_GRP);
+ ctx->es->s_inode_size = cpu_to_le16(MIGRATE_TEST_INODE_SIZE);
+ ctx->es->s_checksum_type = EXT4_CRC32C_CHKSUM;
+ generate_random_uuid(ctx->es->s_uuid);
+ ext4_blocks_count_set(ctx->es, MIGRATE_TEST_TOTAL_BLKS);
+ ext4_set_feature_extents(sb);
+ ext4_set_feature_64bit(sb);
+ ext4_set_feature_metadata_csum(sb);
+ sbi->s_csum_seed = ext4_chksum(~0, ctx->es->s_uuid,
+ sizeof(ctx->es->s_uuid));
+ ext4_superblock_csum_set(sb);
+
+ root_inode = migrate_make_test_inode(test, EXT4_ROOT_INO,
+ S_IFDIR | 0755);
+ sb->s_root = d_make_root(root_inode);
+ KUNIT_ASSERT_NOT_NULL(test, sb->s_root);
+
+ active_migrate_test = test;
+ kunit_activate_static_stub(test, ext4_sb_bread, migrate_sb_bread_stub);
+ kunit_activate_static_stub(test, ext4_ext_getblk,
+ migrate_ext_getblk_stub);
+ kunit_activate_static_stub(test, __ext4_ext_dirty,
+ migrate_ext_dirty_stub);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ migrate_handle_dirty_metadata_stub);
+ kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+ migrate_mark_inode_dirty_stub);
+ kunit_activate_static_stub(test, __ext4_new_inode,
+ migrate_new_inode_stub);
+ kunit_activate_static_stub(test, ext4_mb_new_blocks,
+ migrate_mb_new_blocks_stub);
+ kunit_activate_static_stub(test, ext4_free_blocks,
+ migrate_free_blocks_stub);
+
+ return 0;
+}
+
+/*
+ * Test 1: ext4_ext_migrate() precondition guards, fast symlink no-op, and
+ * temporary inode allocation failure handling.
+ */
+static void test_ext_migrate_precondition_checks_and_fast_symlink(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *inode, *symlink;
+
+ inode = migrate_make_test_inode(test, 12, S_IFREG | 0644);
+
+ /* 1. Filesystem lacks extents feature -> -EINVAL */
+ ext4_clear_feature_extents(sb);
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), -EINVAL);
+ ext4_set_feature_extents(sb);
+
+ /* 2. Inode already uses extents -> -EINVAL */
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), -EINVAL);
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+
+ /* 3. Inline-data inode -> -EINVAL */
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ EXT4_I(inode)->i_inline_off = 164;
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), -EINVAL);
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ EXT4_I(inode)->i_inline_off = 0;
+
+ /* 4. Fast symlink (S_IFLNK && i_blocks == 0) is a clean no-op */
+ symlink = migrate_make_test_inode(test, 13, S_IFLNK | 0777);
+ symlink->i_blocks = 0;
+ memcpy(EXT4_I(symlink)->i_data, "target/path", 11);
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(symlink), 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(symlink, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_MEMEQ(test, EXT4_I(symlink)->i_data, "target/path", 11);
+ iput(symlink);
+
+ /* 5. Temporary inode allocation failure (-ENOSPC) propagates */
+ ctx->new_inode_err = -ENOSPC;
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), -ENOSPC);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ ctx->new_inode_err = 0;
+
+ iput(inode);
+}
+
+/*
+ * Test 2: ext4_ext_migrate() coalesces contiguous direct + single-indirect
+ * runs across the EXT4_NDIR_BLOCKS boundary, preserves sparse holes, and
+ * frees the indirect metadata block.
+ */
+static void test_ext_migrate_direct_and_ind_coalescing_and_holes(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct inode *inode;
+ struct buffer_head *ind_bh;
+ __le32 *ind_data;
+ struct ext4_extent_header *eh;
+ struct ext4_extent *ex;
+
+ inode = migrate_make_test_inode(test, 14, S_IFREG | 0644);
+ i_size_write(inode, 25 * MIGRATE_TEST_BLK_SIZE);
+ inode->i_blocks = 8 * (MIGRATE_TEST_BLK_SIZE >> 9);
+
+ /* Direct [0..2] -> 100..102; hole [3..10]; Direct [11] -> 200 */
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(100);
+ EXT4_I(inode)->i_data[1] = cpu_to_le32(101);
+ EXT4_I(inode)->i_data[2] = cpu_to_le32(102);
+ EXT4_I(inode)->i_data[11] = cpu_to_le32(200);
+
+ /* IND block 500: [0..1] (lblks 12..13) -> 201..202; [10] (lblk 22) -> 350 */
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] = cpu_to_le32(500);
+ ind_bh = migrate_get_or_create_bh(ctx, 500);
+ KUNIT_ASSERT_NOT_NULL(test, ind_bh);
+ ind_data = (__le32 *)ind_bh->b_data;
+ ind_data[0] = cpu_to_le32(201);
+ ind_data[1] = cpu_to_le32(202);
+ ind_data[10] = cpu_to_le32(350);
+ brelse(ind_bh);
+
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), 0);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+
+ eh = ext_inode_hdr(inode);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_magic), EXT4_EXT_MAGIC);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_depth), 0U);
+ KUNIT_ASSERT_EQ(test, le16_to_cpu(eh->eh_entries), 3U);
+
+ ex = EXT_FIRST_EXTENT(eh);
+ /* Extent 0: lblk [0..2] -> pblk 100 */
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 0U);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ex[0].ee_len), 3U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[0]), 100ULL);
+
+ /* Extent 1: lblk [11..13] coalesced across DIRECT/IND -> pblk 200 */
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[1].ee_block), 11U);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ex[1].ee_len), 3U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[1]), 200ULL);
+
+ /* Extent 2: lblk [22..22] -> pblk 350 */
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[2].ee_block), 22U);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ex[2].ee_len), 1U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[2]), 350ULL);
+
+ /* Indirect metadata block 500 was freed */
+ KUNIT_EXPECT_TRUE(test, migrate_was_freed(ctx, 500));
+
+ iput(inode);
+}
+
+/*
+ * Test 3: ext4_ext_migrate() across DIRECT, IND, DIND, and TIND sparse blocks
+ * (> 4 discontiguous extents, growing a depth-1 extent tree) and freeing all
+ * indirect metadata blocks at every level.
+ */
+static void test_ext_migrate_dind_tind_and_multilevel_tree(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct inode *inode;
+ struct buffer_head *bh;
+ struct ext4_ext_path *path;
+ static const struct {
+ ext4_lblk_t lblk;
+ ext4_fsblk_t pblk;
+ } expected[] = {
+ { 0, 100 },
+ { 5, 110 },
+ { 15, 120 },
+ { 780, 130 },
+ { 131341, 140 },
+ { 131345, 150 },
+ };
+ static const ext4_fsblk_t expected_freed_meta[] = {
+ 510, 520, 521, 530, 531, 532
+ };
+ unsigned int i;
+
+ inode = migrate_make_test_inode(test, 15, S_IFREG | 0644);
+ i_size_write(inode, 140000ULL * MIGRATE_TEST_BLK_SIZE);
+ inode->i_blocks = 12 * (MIGRATE_TEST_BLK_SIZE >> 9);
+
+ /* Direct: lblk 0 -> 100, lblk 5 -> 110 */
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(100);
+ EXT4_I(inode)->i_data[5] = cpu_to_le32(110);
+
+ /* IND (510): entry 3 (lblk 12 + 3 = 15) -> 120 */
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] = cpu_to_le32(510);
+ bh = migrate_get_or_create_bh(ctx, 510);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[3] = cpu_to_le32(120);
+ brelse(bh);
+
+ /* DIND (520) -> [2] = 521 -> [0] (lblk 12 + 256 + 512 = 780) -> 130 */
+ EXT4_I(inode)->i_data[EXT4_DIND_BLOCK] = cpu_to_le32(520);
+ bh = migrate_get_or_create_bh(ctx, 520);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[2] = cpu_to_le32(521);
+ brelse(bh);
+
+ bh = migrate_get_or_create_bh(ctx, 521);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[0] = cpu_to_le32(130);
+ brelse(bh);
+
+ /*
+ * TIND (530) -> [1] = 531 -> [0] = 532 -> [1] (lblk 12 + 256 + 65536 +
+ * 65536 + 1 = 131341 -> 140) and [5] (lblk 131345 -> 150).
+ */
+ EXT4_I(inode)->i_data[EXT4_TIND_BLOCK] = cpu_to_le32(530);
+ bh = migrate_get_or_create_bh(ctx, 530);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[1] = cpu_to_le32(531);
+ brelse(bh);
+
+ bh = migrate_get_or_create_bh(ctx, 531);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[0] = cpu_to_le32(532);
+ brelse(bh);
+
+ bh = migrate_get_or_create_bh(ctx, 532);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[1] = cpu_to_le32(140);
+ ((__le32 *)bh->b_data)[5] = cpu_to_le32(150);
+ brelse(bh);
+
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), 0);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ext_inode_hdr(inode)->eh_depth), 1U);
+
+ for (i = 0; i < ARRAY_SIZE(expected); i++) {
+ path = ext4_find_extent(inode, expected[i].lblk, NULL, 0);
+ KUNIT_ASSERT_FALSE_MSG(test, IS_ERR(path),
+ "find_extent failed at idx %u", i);
+ KUNIT_ASSERT_NOT_NULL(test, path[path->p_depth].p_ext);
+ KUNIT_EXPECT_EQ_MSG(test,
+ le32_to_cpu(path[path->p_depth].p_ext->ee_block),
+ expected[i].lblk, "lblk idx %u", i);
+ KUNIT_EXPECT_EQ_MSG(test,
+ ext4_ext_pblock(path[path->p_depth].p_ext),
+ expected[i].pblk, "pblk idx %u", i);
+ ext4_free_ext_path(path);
+ }
+
+ for (i = 0; i < ARRAY_SIZE(expected_freed_meta); i++) {
+ KUNIT_EXPECT_TRUE_MSG(test,
+ migrate_was_freed(ctx,
+ expected_freed_meta[i]),
+ "expected meta block %llu freed",
+ expected_freed_meta[i]);
+ }
+
+ iput(inode);
+}
+
+/*
+ * Test 4: When reading an indirect block fails midway after tmp_inode has
+ * already grown a depth-1 extent tree, free_ext_block() / free_ext_idx()
+ * frees the allocated temporary extent metadata block and leaves the original
+ * inode untouched.
+ */
+static void test_ext_migrate_error_rollback_frees_tmp_extent_blocks(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct inode *inode;
+ ext4_fsblk_t expected_leaf_blk = ctx->next_alloc_pblk;
+
+ inode = migrate_make_test_inode(test, 16, S_IFREG | 0644);
+ inode->i_blocks = 12;
+
+ /*
+ * 6 discontiguous direct blocks flush 5 extents into tmp_inode
+ * (the 6th stays buffered in lb), growing tmp_inode past eh_max=4
+ * to a depth-1 extent leaf block before EXT4_IND_BLOCK is read.
+ */
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(100);
+ EXT4_I(inode)->i_data[2] = cpu_to_le32(110);
+ EXT4_I(inode)->i_data[4] = cpu_to_le32(120);
+ EXT4_I(inode)->i_data[6] = cpu_to_le32(130);
+ EXT4_I(inode)->i_data[8] = cpu_to_le32(140);
+ EXT4_I(inode)->i_data[10] = cpu_to_le32(150);
+ /* Unreadable IND block beyond device_blocks triggers -EIO rollback */
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] =
+ cpu_to_le32(MIGRATE_TEST_TOTAL_BLKS + 50);
+
+ (void)ext4_ext_migrate(inode);
+
+ /* Temporary extent leaf block allocated during build must be freed */
+ KUNIT_EXPECT_TRUE(test, migrate_was_freed(ctx, expected_leaf_blk));
+ /* Original inode remains indirect with intact direct pointers */
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[0]), 100U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[8]), 140U);
+
+ iput(inode);
+}
+
+/*
+ * Test 5: ext4_ext_swap_inode_data() returns -EAGAIN without modifying the
+ * target inode or freeing its indirect blocks if EXT4_STATE_EXT_MIGRATE was
+ * cleared by a concurrent allocation.
+ */
+static void test_ext_swap_inode_data_detects_concurrent_allocation(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct inode *inode, *tmp_inode;
+ handle_t *handle;
+ int ret;
+
+ inode = migrate_make_test_inode(test, 17, S_IFREG | 0644);
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(210);
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] = cpu_to_le32(550);
+
+ tmp_inode = migrate_make_test_inode(test, 18, S_IFREG | 0644);
+ handle = ext4_journal_start(inode, EXT4_HT_MIGRATE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ext4_ext_tree_init(handle, tmp_inode);
+
+ /* EXT4_STATE_EXT_MIGRATE intentionally NOT set on inode */
+ ret = ext4_ext_swap_inode_data_test(handle, inode, tmp_inode);
+ ext4_journal_stop(handle);
+
+ KUNIT_EXPECT_EQ(test, ret, -EAGAIN);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[0]), 210U);
+ KUNIT_EXPECT_EQ(test,
+ le32_to_cpu(EXT4_I(inode)->i_data[EXT4_IND_BLOCK]),
+ 550U);
+ KUNIT_EXPECT_FALSE(test, migrate_was_freed(ctx, 550));
+
+ iput(tmp_inode);
+ iput(inode);
+}
+
+/*
+ * Test 6: ext4_ind_migrate() converts an empty 0-extent file and a 1-extent
+ * file with a leading sparse hole ([3..7] -> 200..204) to direct block mapping.
+ */
+static void test_ind_migrate_empty_and_single_direct_extent(struct kunit *test)
+{
+ struct inode *inode;
+ struct ext4_extent_header *eh;
+ struct ext4_extent *ex;
+ handle_t *handle;
+ unsigned int i;
+
+ /* 1. Empty 0-extent file */
+ inode = migrate_make_test_inode(test, 19, S_IFREG | 0644);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ handle = ext4_journal_start(inode, EXT4_HT_MIGRATE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ext4_ext_tree_init(handle, inode);
+ ext4_journal_stop(handle);
+
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ for (i = 0; i < EXT4_N_BLOCKS; i++)
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[i], 0);
+ iput(inode);
+
+ /* 2. Single extent covering logical blocks [3..7] -> physical 200..204 */
+ inode = migrate_make_test_inode(test, 20, S_IFREG | 0644);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ handle = ext4_journal_start(inode, EXT4_HT_MIGRATE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ext4_ext_tree_init(handle, inode);
+ ext4_journal_stop(handle);
+
+ eh = ext_inode_hdr(inode);
+ eh->eh_entries = cpu_to_le16(1);
+ ex = EXT_FIRST_EXTENT(eh);
+ ex->ee_block = cpu_to_le32(3);
+ ex->ee_len = cpu_to_le16(5);
+ ext4_ext_store_pblock(ex, 200);
+
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ for (i = 0; i < 3; i++)
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[i], 0);
+ for (i = 3; i <= 7; i++)
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[i]),
+ 200U + (i - 3));
+ for (i = 8; i < EXT4_N_BLOCKS; i++)
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[i], 0);
+
+ iput(inode);
+}
+
+/*
+ * Test 7: ext4_ind_migrate() rejects non-extents, bigalloc, multi-extent,
+ * non-zero depth, >= EXT4_NDIR_BLOCKS logical range, and >32-bit filesystems.
+ */
+static void test_ind_migrate_rejections(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *inode;
+ struct ext4_extent_header *eh;
+ struct ext4_extent *ex;
+ handle_t *handle;
+
+ inode = migrate_make_test_inode(test, 21, S_IFREG | 0644);
+
+ /* 1. Inode without EXT4_INODE_EXTENTS -> -EINVAL */
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EINVAL);
+
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ handle = ext4_journal_start(inode, EXT4_HT_MIGRATE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ext4_ext_tree_init(handle, inode);
+ ext4_journal_stop(handle);
+
+ /* 2. Filesystem without extents feature -> -EINVAL */
+ ext4_clear_feature_extents(sb);
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EINVAL);
+ ext4_set_feature_extents(sb);
+
+ /* 3. bigalloc enabled -> -EOPNOTSUPP */
+ ext4_set_feature_bigalloc(sb);
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EOPNOTSUPP);
+ ext4_clear_feature_bigalloc(sb);
+
+ /* 4. Extent reaching EXT4_NDIR_BLOCKS (end = 10 + 3 - 1 = 12) */
+ eh = ext_inode_hdr(inode);
+ eh->eh_entries = cpu_to_le16(1);
+ ex = EXT_FIRST_EXTENT(eh);
+ ex[0].ee_block = cpu_to_le32(10);
+ ex[0].ee_len = cpu_to_le16(3);
+ ext4_ext_store_pblock(&ex[0], 200);
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EOPNOTSUPP);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+
+ /* 5. More than 1 extent (eh_entries == 2) -> -EOPNOTSUPP */
+ ex[0].ee_block = cpu_to_le32(0);
+ ex[0].ee_len = cpu_to_le16(2);
+ ext4_ext_store_pblock(&ex[0], 200);
+ ex[1].ee_block = cpu_to_le32(4);
+ ex[1].ee_len = cpu_to_le16(2);
+ ext4_ext_store_pblock(&ex[1], 210);
+ eh->eh_entries = cpu_to_le16(2);
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EOPNOTSUPP);
+
+ /* 6. Filesystem > EXT4_MAX_BLOCK_FILE_PHYS -> -EOPNOTSUPP */
+ eh->eh_entries = cpu_to_le16(1);
+ ext4_blocks_count_set(ctx->es, (ext4_fsblk_t)EXT4_MAX_BLOCK_FILE_PHYS +
+ 100ULL);
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EOPNOTSUPP);
+ ext4_blocks_count_set(ctx->es, MIGRATE_TEST_TOTAL_BLKS);
+
+ iput(inode);
+}
+
+static struct kunit_case ext4_migrate_test_cases[] = {
+ KUNIT_CASE(test_ext_migrate_precondition_checks_and_fast_symlink),
+ KUNIT_CASE(test_ext_migrate_direct_and_ind_coalescing_and_holes),
+ KUNIT_CASE(test_ext_migrate_dind_tind_and_multilevel_tree),
+ KUNIT_CASE(test_ext_migrate_error_rollback_frees_tmp_extent_blocks),
+ KUNIT_CASE(test_ext_swap_inode_data_detects_concurrent_allocation),
+ KUNIT_CASE(test_ind_migrate_empty_and_single_direct_extent),
+ KUNIT_CASE(test_ind_migrate_rejections),
+ {}
+};
+
+static struct kunit_suite ext4_migrate_test_suite = {
+ .name = "ext4_migrate_test",
+ .init = migrate_test_init,
+ .test_cases = ext4_migrate_test_cases,
+};
+
+kunit_test_suites(&ext4_migrate_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 indirect/extent inode migration");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/migrate.c b/fs/ext4/migrate.c
index 5d60ef10fe11..8d76042f0cae 100644
--- a/fs/ext4/migrate.c
+++ b/fs/ext4/migrate.c
@@ -597,6 +597,7 @@ int ext4_ext_migrate(struct inode *inode)
iput(tmp_inode);
return retval;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_migrate);
/*
* Migrate a simple extent-based inode to use the i_blocks[] array
@@ -683,3 +684,13 @@ int ext4_ind_migrate(struct inode *inode)
ext4_writepages_up_write(inode->i_sb, alloc_ctx);
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ind_migrate);
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+int ext4_ext_swap_inode_data_test(handle_t *handle, struct inode *inode,
+ struct inode *tmp_inode)
+{
+ return ext4_ext_swap_inode_data(handle, inode, tmp_inode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_swap_inode_data_test);
+#endif
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 22/27] ext4: add KUnit tests for fs/ext4/indirect.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (20 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 21/27] ext4: add KUnit tests for fs/ext4/migrate.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (4 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add the ext4_indirect_test KUnit suite in fs/ext4/indirect-test.c to
test legacy direct, single-, double-, and triple-indirect block mapping,
truncation, and hole punching in fs/ext4/indirect.c:
- ext4_block_to_path() offset decomposition, boundary distance, and
out-of-range block rejection, plus ext4_ind_trans_blocks() credits
- ext4_ind_map_blocks() read-only lookup across contiguous runs,
EXT4_MAP_BOUNDARY detection, and multi-level hole span calculation
- ext4_ind_map_blocks() allocation and branch splicing across direct,
IND, DIND, and TIND depths
- error handling (out-of-range logical/physical blocks, bigalloc
rejection, indirect read I/O failure, corrupt indirect blockref) and
partial branch allocation unwind freeing intermediate metadata blocks
- ext4_ind_truncate() partial shared-branch truncation, indirect root
detachment, and full truncation to zero
- ext4_ind_remove_space() hole punching within direct blocks, across
mixed levels, and converging on shared IND and DIND blocks
- corrupt block pointer rejection in ext4_clear_blocks() and
ext4_free_branches()
Route indirect block buffer lookups through ext4_sb_getblk(), add a
KUnit static stub redirect to ext4_sb_bread_nofail(), and export the
indirect mapping and truncation entry points for KUnit testing.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 3 +-
fs/ext4/ext4.h | 4 +
fs/ext4/indirect-test.c | 1270 +++++++++++++++++++++++++++++++++++++++
fs/ext4/indirect.c | 18 +-
fs/ext4/super.c | 7 +-
5 files changed, 1298 insertions(+), 4 deletions(-)
create mode 100644 fs/ext4/indirect-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index fafd9600cacb..503acc946409 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -22,7 +22,8 @@ ext4-test-objs += inode-test.o mballoc-test.o \
balloc-test.o orphan-test.o \
block_validity-test.o super-test.o \
sysfs-test.o ioctl-test.o \
- resize-test.o migrate-test.o
+ resize-test.o migrate-test.o \
+ indirect-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 91cc0e00aaa5..4db096244a81 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3338,6 +3338,10 @@ extern int ext4_ind_trans_blocks(struct inode *inode, int nrblocks);
extern void ext4_ind_truncate(handle_t *, struct inode *inode);
extern int ext4_ind_remove_space(handle_t *handle, struct inode *inode,
ext4_lblk_t start, ext4_lblk_t end);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+extern int ext4_block_to_path_test(struct inode *inode, ext4_lblk_t i_block,
+ ext4_lblk_t offsets[4], int *boundary);
+#endif
/* ioctl.c */
extern long ext4_ioctl(struct file *, unsigned int, unsigned long);
diff --git a/fs/ext4/indirect-test.c b/fs/ext4/indirect-test.c
new file mode 100644
index 000000000000..add5662adc3b
--- /dev/null
+++ b/fs/ext4/indirect-test.c
@@ -0,0 +1,1270 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/indirect.c
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/fscrypt.h>
+#include <linux/iversion.h>
+#include <linux/kernel.h>
+#include <linux/list.h>
+#include <linux/mm.h>
+#include <linux/module.h>
+#include <linux/part_stat.h>
+#include <linux/percpu.h>
+#include <linux/percpu-rwsem.h>
+#include <linux/percpu_counter.h>
+#include <linux/quotaops.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"
+
+#define IND_TEST_BLK_SIZE 1024U
+#define IND_TEST_BLK_BITS 10U
+#define IND_TEST_PTRS_PER_BLK (IND_TEST_BLK_SIZE / sizeof(__le32))
+#define IND_TEST_BLOCKS_PER_GRP 1024U
+#define IND_TEST_NGROUPS 8U
+#define IND_TEST_FIRST_DATA_BLK 1U
+#define IND_TEST_TOTAL_BLKS (IND_TEST_FIRST_DATA_BLK + \
+ IND_TEST_NGROUPS * \
+ IND_TEST_BLOCKS_PER_GRP)
+#define IND_TEST_INODE_SIZE 256U
+#define IND_TEST_EXTRA_ISIZE 32U
+#define IND_TEST_INODES_PER_GRP 32U
+#define IND_TEST_MAX_FREED_REC 128U
+
+#define IND_TEST_IND_BASE EXT4_NDIR_BLOCKS
+#define IND_TEST_DIND_BASE (IND_TEST_IND_BASE + \
+ IND_TEST_PTRS_PER_BLK)
+#define IND_TEST_TIND_BASE (IND_TEST_DIND_BASE + \
+ IND_TEST_PTRS_PER_BLK * \
+ IND_TEST_PTRS_PER_BLK)
+#define IND_TEST_MAX_LBLK (IND_TEST_TIND_BASE + \
+ IND_TEST_PTRS_PER_BLK * \
+ IND_TEST_PTRS_PER_BLK * \
+ IND_TEST_PTRS_PER_BLK)
+
+struct ind_mock_blk {
+ struct list_head node;
+ ext4_fsblk_t blocknr;
+ struct buffer_head bh;
+ u8 *data;
+};
+
+struct ind_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 list_head blks;
+ ext4_fsblk_t device_blocks;
+ ext4_fsblk_t next_alloc_pblk;
+ unsigned int alloc_calls;
+ unsigned int fail_alloc_on_call;
+ int fail_alloc_err;
+ int read_bh_err;
+ ext4_fsblk_t freed_blks[IND_TEST_MAX_FREED_REC];
+ unsigned int freed_cnt;
+ bool rwsem_inited;
+ bool es_shrinker_inited;
+ bool counters_inited;
+};
+
+static struct kunit *active_ind_test;
+
+static struct inode *ind_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 = IND_TEST_EXTRA_ISIZE;
+ ei->i_last_alloc_group = ~0U;
+
+ return &ei->vfs_inode;
+}
+
+static void ind_test_free_inode(struct inode *inode)
+{
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ kfree(EXT4_I(inode));
+}
+
+static void ind_test_evict_inode(struct inode *inode)
+{
+ truncate_inode_pages_final(&inode->i_data);
+ clear_inode(inode);
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **ind_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+#endif
+
+static const struct super_operations ind_test_sops = {
+ .alloc_inode = ind_test_alloc_inode,
+ .free_inode = ind_test_free_inode,
+ .evict_inode = ind_test_evict_inode,
+#ifdef CONFIG_QUOTA
+ .get_dquots = ind_test_get_dquots,
+#endif
+};
+
+static int ind_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static void ind_test_sb_upd_work(struct work_struct *work)
+{
+}
+
+static void ind_test_err_timer(struct timer_list *t)
+{
+}
+
+static struct file_system_type ind_test_fs_type = {
+ .name = "ext4_indirect_kunit",
+ .init_fs_context = ind_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static void ind_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);
+ set_buffer_mapped(bh);
+}
+
+static struct buffer_head *ind_lookup_bh(struct ind_test_ctx *ctx,
+ ext4_fsblk_t blocknr)
+{
+ struct ind_mock_blk *blk;
+
+ list_for_each_entry(blk, &ctx->blks, node) {
+ if (blk->blocknr == blocknr)
+ return &blk->bh;
+ }
+ return NULL;
+}
+
+static struct buffer_head *ind_get_or_create_bh(struct ind_test_ctx *ctx,
+ ext4_fsblk_t blocknr)
+{
+ struct buffer_head *bh;
+ struct ind_mock_blk *blk;
+
+ bh = ind_lookup_bh(ctx, blocknr);
+ if (bh) {
+ get_bh(bh);
+ return bh;
+ }
+
+ blk = kzalloc_obj(*blk);
+ if (!blk)
+ return NULL;
+ blk->data = kzalloc(IND_TEST_BLK_SIZE, GFP_KERNEL);
+ if (!blk->data) {
+ kfree(blk);
+ return NULL;
+ }
+ blk->blocknr = blocknr;
+ ind_init_mock_bh(&blk->bh, ctx->bh_folio, blk->data,
+ IND_TEST_BLK_SIZE, blocknr);
+ list_add_tail(&blk->node, &ctx->blks);
+ get_bh(&blk->bh);
+ return &blk->bh;
+}
+
+static struct buffer_head *ind_sb_getblk_stub(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ struct kunit *test = active_ind_test;
+ struct ind_test_ctx *ctx;
+
+ if (!test)
+ return NULL;
+ ctx = test->priv;
+ if (!ctx || sb != ctx->sb)
+ return NULL;
+
+ return ind_get_or_create_bh(ctx, block);
+}
+
+static struct buffer_head *ind_sb_bread_nofail_stub(struct super_block *sb,
+ sector_t block)
+{
+ struct kunit *test = active_ind_test;
+ struct ind_test_ctx *ctx;
+ struct buffer_head *bh;
+
+ if (!test)
+ return ERR_PTR(-EIO);
+ ctx = test->priv;
+ if (!ctx || sb != ctx->sb)
+ return ERR_PTR(-EIO);
+ if ((ext4_fsblk_t)block >= ctx->device_blocks)
+ return ERR_PTR(-EIO);
+
+ bh = ind_get_or_create_bh(ctx, block);
+ if (!bh)
+ return ERR_PTR(-ENOMEM);
+ return bh;
+}
+
+static int ind_read_bh_stub(struct buffer_head *bh, blk_opf_t op_flags,
+ bio_end_io_t end_io, bool simu_fail)
+{
+ struct kunit *test = active_ind_test;
+ struct ind_test_ctx *ctx;
+
+ if (!test) {
+ unlock_buffer(bh);
+ return -EIO;
+ }
+ ctx = test->priv;
+ if (ctx->read_bh_err) {
+ unlock_buffer(bh);
+ return ctx->read_bh_err;
+ }
+
+ set_buffer_uptodate(bh);
+ unlock_buffer(bh);
+ return 0;
+}
+
+static int ind_handle_dirty_metadata_stub(const char *where, unsigned int line,
+ handle_t *handle, struct inode *inode,
+ struct buffer_head *bh)
+{
+ if (bh)
+ set_buffer_uptodate(bh);
+ return 0;
+}
+
+static int ind_mark_inode_dirty_stub(handle_t *handle, struct inode *inode,
+ const char *func, unsigned int line)
+{
+ return 0;
+}
+
+static ext4_fsblk_t ind_mb_new_blocks_stub(handle_t *handle,
+ struct ext4_allocation_request *ar,
+ int *errp)
+{
+ struct kunit *test = active_ind_test;
+ struct ind_test_ctx *ctx;
+ ext4_fsblk_t pblk;
+
+ if (!test) {
+ *errp = -EIO;
+ return 0;
+ }
+ ctx = test->priv;
+ ctx->alloc_calls++;
+ if (ctx->fail_alloc_on_call &&
+ ctx->alloc_calls == ctx->fail_alloc_on_call) {
+ *errp = ctx->fail_alloc_err;
+ return 0;
+ }
+
+ pblk = ctx->next_alloc_pblk;
+ ctx->next_alloc_pblk += ar->len;
+ ar->inode->i_blocks += (blkcnt_t)ar->len <<
+ (ar->inode->i_blkbits - 9);
+ *errp = 0;
+ return pblk;
+}
+
+static void ind_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 = active_ind_test;
+ struct ind_test_ctx *ctx;
+ ext4_fsblk_t start_blk = block;
+ blkcnt_t sectors;
+ unsigned long i;
+
+ if (!test)
+ return;
+ ctx = test->priv;
+ if (!start_blk && bh)
+ start_blk = bh->b_blocknr;
+
+ for (i = 0; i < count && ctx->freed_cnt < IND_TEST_MAX_FREED_REC; i++)
+ ctx->freed_blks[ctx->freed_cnt++] = start_blk + i;
+
+ sectors = (blkcnt_t)count << (inode->i_blkbits - 9);
+ if (inode->i_blocks >= sectors)
+ inode->i_blocks -= sectors;
+ if (bh)
+ brelse(bh);
+}
+
+static bool ind_was_freed(struct ind_test_ctx *ctx, ext4_fsblk_t blk)
+{
+ unsigned int i;
+
+ for (i = 0; i < ctx->freed_cnt; i++) {
+ if (ctx->freed_blks[i] == blk)
+ return true;
+ }
+ return false;
+}
+
+static __le32 *ind_blk_ptrs(struct kunit *test, ext4_fsblk_t blk)
+{
+ struct ind_test_ctx *ctx = test->priv;
+ struct buffer_head *bh;
+
+ bh = ind_get_or_create_bh(ctx, blk);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ brelse(bh);
+ return (__le32 *)bh->b_data;
+}
+
+static struct inode *ind_make_test_inode(struct kunit *test, unsigned long ino)
+{
+ struct ind_test_ctx *ctx = test->priv;
+ struct inode *inode;
+
+ inode = new_inode(ctx->sb);
+ KUNIT_ASSERT_NOT_NULL(test, inode);
+ inode->i_ino = ino;
+ inode->i_mode = S_IFREG | 0644;
+ inode->i_blkbits = IND_TEST_BLK_BITS;
+ inode->i_flags |= S_NOQUOTA;
+ EXT4_I(inode)->i_block_group = 0;
+ return inode;
+}
+
+static void ind_test_cleanup(struct ind_test_ctx *ctx)
+{
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ struct ind_mock_blk *blk, *tmp;
+
+ if (sb) {
+ cancel_work_sync(&sbi->s_sb_upd_work);
+ timer_delete_sync(&sbi->s_err_report);
+ }
+
+ if (ctx->es_shrinker_inited) {
+ ext4_es_unregister_shrinker(sbi);
+ ctx->es_shrinker_inited = false;
+ }
+
+ if (ctx->counters_inited) {
+ percpu_counter_destroy(&sbi->s_freeclusters_counter);
+ percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
+ percpu_counter_destroy(&sbi->s_freeinodes_counter);
+ ctx->counters_inited = false;
+ }
+
+ if (ctx->rwsem_inited) {
+ percpu_free_rwsem(&sbi->s_writepages_rwsem);
+ ctx->rwsem_inited = false;
+ }
+
+ if (sb) {
+ deactivate_super(sb);
+ ctx->sb = NULL;
+ }
+
+ list_for_each_entry_safe(blk, tmp, &ctx->blks, node) {
+ list_del(&blk->node);
+ kfree(blk->data);
+ kfree(blk);
+ }
+
+ if (ctx->bh_folio) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ }
+
+ if (ctx->bdev.bd_stats) {
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ }
+
+ active_ind_test = NULL;
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(ind_test_cleanup_action, ind_test_cleanup,
+ struct ind_test_ctx *);
+
+static int ind_test_set_super(struct super_block *sb, struct fs_context *fc)
+{
+ return set_anon_super_fc(sb, fc);
+}
+
+static int ind_test_init(struct kunit *test)
+{
+ struct ind_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ struct buffer_head *sbh;
+ int ret;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ INIT_LIST_HEAD(&ctx->blks);
+ ctx->device_blocks = IND_TEST_TOTAL_BLKS;
+ ctx->next_alloc_pblk = 1000;
+ test->priv = ctx;
+
+ ctx->bdev.bd_stats = alloc_percpu(struct disk_stats);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->bdev.bd_stats);
+
+ ctx->bh_folio = folio_alloc(GFP_KERNEL, 0);
+ if (!ctx->bh_folio) {
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "folio_alloc failed");
+ }
+
+ fc = fs_context_for_mount(&ind_test_fs_type, 0);
+ if (IS_ERR(fc)) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "fs_context_for_mount failed: %ld",
+ PTR_ERR(fc));
+ }
+ sb = sget_fc(fc, NULL, ind_test_set_super);
+ put_fs_context(fc);
+ if (IS_ERR(sb)) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "sget_fc failed: %ld", PTR_ERR(sb));
+ }
+ ctx->sb = sb;
+ up_write(&sb->s_umount);
+
+ ret = kunit_add_action_or_reset(test, ind_test_cleanup_action, ctx);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ sbi = &ctx->sbi;
+ sb->s_fs_info = sbi;
+ sb->s_op = &ind_test_sops;
+ sb->s_flags |= SB_ACTIVE;
+ sb->s_blocksize = IND_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = IND_TEST_BLK_BITS;
+ sb->s_magic = EXT4_SUPER_MAGIC;
+ sb->s_maxbytes = MAX_LFS_FILESIZE;
+ INIT_LIST_HEAD(&sb->s_inodes);
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
+#endif
+
+ strscpy(sb->s_id, "ext4_indirect_kunit", sizeof(sb->s_id));
+ inode_init_once(&ctx->bd_inode);
+ ctx->bd_inode.i_sb = sb;
+ ctx->bd_inode.i_blkbits = IND_TEST_BLK_BITS;
+ ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data;
+ ctx->bd_inode.i_data.host = &ctx->bd_inode;
+ 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;
+
+ sbi->s_sb = sb;
+ sbi->s_blockgroup_lock = &ctx->bgl;
+ bgl_lock_init(&ctx->bgl);
+ INIT_WORK(&sbi->s_sb_upd_work, ind_test_sb_upd_work);
+ timer_setup(&sbi->s_err_report, ind_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, 5 * HZ, 10);
+ ratelimit_state_init(&sbi->s_warning_ratelimit_state, 5 * HZ, 10);
+ ratelimit_state_init(&sbi->s_msg_ratelimit_state, 5 * HZ, 10);
+
+ ret = percpu_init_rwsem(&sbi->s_writepages_rwsem);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->rwsem_inited = true;
+
+ ret = percpu_counter_init(&sbi->s_freeclusters_counter, 4096,
+ GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = percpu_counter_init(&sbi->s_freeinodes_counter, 200, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->counters_inited = true;
+
+ ret = ext4_es_register_shrinker(sbi);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->es_shrinker_inited = true;
+
+ sbi->s_addr_per_block_bits = ilog2(EXT4_ADDR_PER_BLOCK(sb));
+ sbi->s_blocks_per_group = IND_TEST_BLOCKS_PER_GRP;
+ sbi->s_clusters_per_group = IND_TEST_BLOCKS_PER_GRP;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_cluster_bits = 0;
+ sbi->s_groups_count = IND_TEST_NGROUPS;
+ sbi->s_inodes_per_group = IND_TEST_INODES_PER_GRP;
+ sbi->s_inode_size = IND_TEST_INODE_SIZE;
+ sbi->s_inodes_per_block = IND_TEST_BLK_SIZE / IND_TEST_INODE_SIZE;
+ sbi->s_first_ino = 11;
+ sbi->s_mount_state = EXT4_VALID_FS;
+ sbi->s_bitmap_maxbytes = ext4_max_bitmap_size_test(IND_TEST_BLK_BITS,
+ 1);
+ set_opt(sb, ERRORS_CONT);
+
+ sbh = ind_get_or_create_bh(ctx, IND_TEST_FIRST_DATA_BLK);
+ KUNIT_ASSERT_NOT_NULL(test, sbh);
+ clear_buffer_mapped(sbh);
+ brelse(sbh);
+ sbi->s_sbh = sbh;
+ ctx->es = (struct ext4_super_block *)sbh->b_data;
+ sbi->s_es = ctx->es;
+
+ 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_data_block = cpu_to_le32(IND_TEST_FIRST_DATA_BLK);
+ ctx->es->s_blocks_per_group = cpu_to_le32(IND_TEST_BLOCKS_PER_GRP);
+ ctx->es->s_clusters_per_group = cpu_to_le32(IND_TEST_BLOCKS_PER_GRP);
+ ctx->es->s_inodes_per_group = cpu_to_le32(IND_TEST_INODES_PER_GRP);
+ ctx->es->s_inodes_count = cpu_to_le32(IND_TEST_NGROUPS *
+ IND_TEST_INODES_PER_GRP);
+ ctx->es->s_inode_size = cpu_to_le16(IND_TEST_INODE_SIZE);
+ ctx->es->s_checksum_type = EXT4_CRC32C_CHKSUM;
+ generate_random_uuid(ctx->es->s_uuid);
+ ext4_blocks_count_set(ctx->es, IND_TEST_TOTAL_BLKS);
+ ext4_set_feature_64bit(sb);
+ ext4_set_feature_metadata_csum(sb);
+ sbi->s_csum_seed = ext4_chksum(~0, ctx->es->s_uuid,
+ sizeof(ctx->es->s_uuid));
+ ext4_superblock_csum_set(sb);
+
+ active_ind_test = test;
+ kunit_activate_static_stub(test, ext4_sb_getblk, ind_sb_getblk_stub);
+ kunit_activate_static_stub(test, ext4_sb_bread_nofail,
+ ind_sb_bread_nofail_stub);
+ kunit_activate_static_stub(test, ext4_read_bh, ind_read_bh_stub);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ ind_handle_dirty_metadata_stub);
+ kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+ ind_mark_inode_dirty_stub);
+ kunit_activate_static_stub(test, ext4_mb_new_blocks,
+ ind_mb_new_blocks_stub);
+ kunit_activate_static_stub(test, ext4_free_blocks,
+ ind_free_blocks_stub);
+
+ return 0;
+}
+
+/*
+ * Test 1: ext4_block_to_path() across direct, single-, double-, and
+ * triple-indirect boundaries plus out-of-range block rejection, and
+ * ext4_ind_trans_blocks() credit estimation.
+ */
+static void test_block_to_path_and_trans_blocks(struct kunit *test)
+{
+ struct inode *inode = ind_make_test_inode(test, 12);
+ ext4_lblk_t offsets[4];
+ int boundary = -1;
+
+ /* Direct: block 0 and block 11 (boundary = 0) */
+ KUNIT_EXPECT_EQ(test,
+ ext4_block_to_path_test(inode, 0, offsets, &boundary),
+ 1);
+ KUNIT_EXPECT_EQ(test, offsets[0], 0U);
+ KUNIT_EXPECT_EQ(test, boundary, 11);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_block_to_path_test(inode, 11, offsets, &boundary),
+ 1);
+ KUNIT_EXPECT_EQ(test, offsets[0], 11U);
+ KUNIT_EXPECT_EQ(test, boundary, 0);
+
+ /* Single-indirect: first (12) and last (267) */
+ KUNIT_EXPECT_EQ(test,
+ ext4_block_to_path_test(inode, IND_TEST_IND_BASE,
+ offsets, &boundary), 2);
+ KUNIT_EXPECT_EQ(test, offsets[0], (ext4_lblk_t)EXT4_IND_BLOCK);
+ KUNIT_EXPECT_EQ(test, offsets[1], 0U);
+ KUNIT_EXPECT_EQ(test, boundary, 255);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_block_to_path_test(inode, IND_TEST_DIND_BASE - 1,
+ offsets, &boundary), 2);
+ KUNIT_EXPECT_EQ(test, offsets[0], (ext4_lblk_t)EXT4_IND_BLOCK);
+ KUNIT_EXPECT_EQ(test, offsets[1], 255U);
+ KUNIT_EXPECT_EQ(test, boundary, 0);
+
+ /* Double-indirect: first (268), end of first IND (523), last (65803) */
+ KUNIT_EXPECT_EQ(test,
+ ext4_block_to_path_test(inode, IND_TEST_DIND_BASE,
+ offsets, &boundary), 3);
+ KUNIT_EXPECT_EQ(test, offsets[0], (ext4_lblk_t)EXT4_DIND_BLOCK);
+ KUNIT_EXPECT_EQ(test, offsets[1], 0U);
+ KUNIT_EXPECT_EQ(test, offsets[2], 0U);
+ KUNIT_EXPECT_EQ(test, boundary, 255);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_block_to_path_test(inode, IND_TEST_DIND_BASE + 255,
+ offsets, &boundary), 3);
+ KUNIT_EXPECT_EQ(test, offsets[1], 0U);
+ KUNIT_EXPECT_EQ(test, offsets[2], 255U);
+ KUNIT_EXPECT_EQ(test, boundary, 0);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_block_to_path_test(inode, IND_TEST_TIND_BASE - 1,
+ offsets, &boundary), 3);
+ KUNIT_EXPECT_EQ(test, offsets[0], (ext4_lblk_t)EXT4_DIND_BLOCK);
+ KUNIT_EXPECT_EQ(test, offsets[1], 255U);
+ KUNIT_EXPECT_EQ(test, offsets[2], 255U);
+ KUNIT_EXPECT_EQ(test, boundary, 0);
+
+ /* Triple-indirect: first (65804) and last (16843019) */
+ KUNIT_EXPECT_EQ(test,
+ ext4_block_to_path_test(inode, IND_TEST_TIND_BASE,
+ offsets, &boundary), 4);
+ KUNIT_EXPECT_EQ(test, offsets[0], (ext4_lblk_t)EXT4_TIND_BLOCK);
+ KUNIT_EXPECT_EQ(test, offsets[1], 0U);
+ KUNIT_EXPECT_EQ(test, offsets[2], 0U);
+ KUNIT_EXPECT_EQ(test, offsets[3], 0U);
+ KUNIT_EXPECT_EQ(test, boundary, 255);
+
+ KUNIT_EXPECT_EQ(test,
+ ext4_block_to_path_test(inode, IND_TEST_MAX_LBLK - 1,
+ offsets, NULL), 4);
+ KUNIT_EXPECT_EQ(test, offsets[0], (ext4_lblk_t)EXT4_TIND_BLOCK);
+ KUNIT_EXPECT_EQ(test, offsets[1], 255U);
+ KUNIT_EXPECT_EQ(test, offsets[2], 255U);
+ KUNIT_EXPECT_EQ(test, offsets[3], 255U);
+
+ /* Out-of-range block returns 0 */
+ KUNIT_EXPECT_EQ(test,
+ ext4_block_to_path_test(inode, IND_TEST_MAX_LBLK,
+ offsets, NULL), 0);
+
+ /* ext4_ind_trans_blocks(): DIV_ROUND_UP(nrblocks, 256) + 4 */
+ KUNIT_EXPECT_EQ(test, ext4_ind_trans_blocks(inode, 1), 5);
+ KUNIT_EXPECT_EQ(test, ext4_ind_trans_blocks(inode, 256), 5);
+ KUNIT_EXPECT_EQ(test, ext4_ind_trans_blocks(inode, 257), 6);
+ KUNIT_EXPECT_EQ(test, ext4_ind_trans_blocks(inode, 512), 6);
+
+ iput(inode);
+}
+
+/*
+ * Test 2: ext4_ind_map_blocks() read-only lookup for contiguous mapped runs,
+ * EXT4_MAP_BOUNDARY detection, and multi-level unallocated hole span sizing.
+ */
+static void test_ind_map_blocks_lookup_mapped_and_holes(struct kunit *test)
+{
+ struct inode *inode = ind_make_test_inode(test, 13);
+ struct ext4_map_blocks map;
+ unsigned int i;
+ int ret;
+
+ /* Map direct [0..3] -> [100..103], hole at [4], [5..11] -> [205..211] */
+ for (i = 0; i < 4; i++)
+ EXT4_I(inode)->i_data[i] = cpu_to_le32(100 + i);
+ for (i = 5; i < EXT4_NDIR_BLOCKS; i++)
+ EXT4_I(inode)->i_data[i] = cpu_to_le32(200 + i);
+
+ /* Contiguous direct run [0..3] stops at hole [4], not at boundary */
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = 0;
+ map.m_len = 10;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(NULL, inode, &map, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 4);
+ KUNIT_EXPECT_EQ(test, map.m_pblk, 100ULL);
+ KUNIT_EXPECT_EQ(test, map.m_len, 4U);
+ KUNIT_EXPECT_TRUE(test, map.m_flags & EXT4_MAP_MAPPED);
+ KUNIT_EXPECT_FALSE(test, map.m_flags & EXT4_MAP_BOUNDARY);
+
+ /* Contiguous direct run [5..11] hits end of direct array -> BOUNDARY */
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = 5;
+ map.m_len = 10;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(NULL, inode, &map, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 7);
+ KUNIT_EXPECT_EQ(test, map.m_pblk, 205ULL);
+ KUNIT_EXPECT_EQ(test, map.m_len, 7U);
+ KUNIT_EXPECT_EQ(test, map.m_flags & (EXT4_MAP_MAPPED | EXT4_MAP_BOUNDARY),
+ (unsigned int)(EXT4_MAP_MAPPED | EXT4_MAP_BOUNDARY));
+
+ /* Direct hole at [4]: hole length is 1 */
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = 4;
+ map.m_len = 5;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(NULL, inode, &map, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, map.m_pblk, 0ULL);
+ KUNIT_EXPECT_EQ(test, map.m_len, 1U);
+
+ /* Absent IND subtree at lblk = 20 (offsets = [12, 8]) -> 256 - 8 = 248 */
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = IND_TEST_IND_BASE + 8;
+ map.m_len = 500;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(NULL, inode, &map, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, map.m_len, 248U);
+
+ /*
+ * Absent DIND subtree at offsets = [13, 2, 10]:
+ * remaining blocks in DIND = (255 - 2) * 256 + (256 - 10) = 65014.
+ */
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = IND_TEST_DIND_BASE + 2 * IND_TEST_PTRS_PER_BLK + 10;
+ map.m_len = 100000;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(NULL, inode, &map, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, map.m_len, 65014U);
+
+ iput(inode);
+}
+
+/*
+ * Test 3: ext4_ind_map_blocks() block allocation and branch splicing across
+ * direct, single-indirect, double-indirect, and triple-indirect levels.
+ */
+static void test_ind_map_blocks_alloc_direct_ind_dind_tind(struct kunit *test)
+{
+ struct inode *inode = ind_make_test_inode(test, 14);
+ struct ext4_map_blocks map, check;
+ handle_t *handle;
+ int ret;
+
+ handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, 8);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+
+ /* 1. Multi-block direct allocation at [2..5] */
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = 2;
+ map.m_len = 4;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 4);
+ KUNIT_EXPECT_EQ(test, map.m_flags & (EXT4_MAP_NEW | EXT4_MAP_MAPPED),
+ (unsigned int)(EXT4_MAP_NEW | EXT4_MAP_MAPPED));
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[1], 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[2]),
+ (u32)map.m_pblk);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[5]),
+ (u32)(map.m_pblk + 3));
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[6], 0);
+
+ /* 2. Direct allocation clamped at direct boundary [9..11] */
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = 9;
+ map.m_len = 6;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 3);
+ KUNIT_EXPECT_TRUE(test, map.m_flags & EXT4_MAP_BOUNDARY);
+
+ /* 3. Single-indirect allocation at [15..19] */
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = IND_TEST_IND_BASE + 3;
+ map.m_len = 5;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 5);
+ KUNIT_EXPECT_NE(test, EXT4_I(inode)->i_data[EXT4_IND_BLOCK], 0);
+
+ memset(&check, 0, sizeof(check));
+ check.m_lblk = IND_TEST_IND_BASE + 3;
+ check.m_len = 10;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(NULL, inode, &check, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 5);
+ KUNIT_EXPECT_EQ(test, check.m_pblk, map.m_pblk);
+
+ /* 4. Double-indirect allocation at DIND_BASE + 300 (len = 3) */
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = IND_TEST_DIND_BASE + 300;
+ map.m_len = 3;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 3);
+ KUNIT_EXPECT_NE(test, EXT4_I(inode)->i_data[EXT4_DIND_BLOCK], 0);
+
+ memset(&check, 0, sizeof(check));
+ check.m_lblk = IND_TEST_DIND_BASE + 300;
+ check.m_len = 5;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(NULL, inode, &check, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 3);
+ KUNIT_EXPECT_EQ(test, check.m_pblk, map.m_pblk);
+
+ /* 5. Triple-indirect allocation at TIND_BASE + 5 (len = 2) */
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = IND_TEST_TIND_BASE + 5;
+ map.m_len = 2;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 2);
+ KUNIT_EXPECT_NE(test, EXT4_I(inode)->i_data[EXT4_TIND_BLOCK], 0);
+
+ memset(&check, 0, sizeof(check));
+ check.m_lblk = IND_TEST_TIND_BASE + 5;
+ check.m_len = 4;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(NULL, inode, &check, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 2);
+ KUNIT_EXPECT_EQ(test, check.m_pblk, map.m_pblk);
+
+ ext4_journal_stop(handle);
+ iput(inode);
+}
+
+/*
+ * Test 4: ext4_ind_map_blocks() error paths and partial branch unwind in
+ * ext4_alloc_branch() when an intermediate indirect or leaf allocation fails.
+ */
+static void test_ind_map_blocks_errors_and_alloc_branch_unwind(struct kunit *test)
+{
+ struct ind_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *inode = ind_make_test_inode(test, 15);
+ struct ext4_map_blocks map;
+ struct buffer_head *ind_bh;
+ ext4_fsblk_t alloc_base;
+ handle_t *handle;
+ int ret;
+
+ handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, 8);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+
+ /* 1. Out-of-range m_lblk -> -EIO */
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = IND_TEST_MAX_LBLK;
+ map.m_len = 1;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(NULL, inode, &map, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+
+ /* 2. Allocating on an indirect inode with bigalloc -> -EFSCORRUPTED */
+ ext4_set_feature_bigalloc(sb);
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = 0;
+ map.m_len = 1;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+ ext4_clear_feature_bigalloc(sb);
+
+ /* 3. Out-of-range indirect block pointer on lookup -> -EFSCORRUPTED */
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] =
+ cpu_to_le32(IND_TEST_TOTAL_BLKS + 10);
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = IND_TEST_IND_BASE;
+ map.m_len = 1;
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(NULL, inode, &map, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+
+ /* 4. Non-uptodate IND block read I/O failure -> -EIO */
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] = cpu_to_le32(300);
+ ind_bh = ind_get_or_create_bh(ctx, 300);
+ KUNIT_ASSERT_NOT_NULL(test, ind_bh);
+ clear_buffer_uptodate(ind_bh);
+ ctx->read_bh_err = -EIO;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ ctx->read_bh_err = 0;
+
+ /*
+ * 5. Non-uptodate IND block containing invalid blockref (block 1 ==
+ * s_first_data_block) fails ext4_check_indirect_blockref() -> -EIO.
+ */
+ ((__le32 *)ind_bh->b_data)[0] = cpu_to_le32(IND_TEST_FIRST_DATA_BLK);
+ clear_buffer_uptodate(ind_bh);
+ down_read(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(NULL, inode, &map, 0);
+ up_read(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ brelse(ind_bh);
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] = 0;
+
+ /*
+ * 6. Partial TIND branch unwind: TIND (call 1), DIND (call 2), and
+ * IND (call 3) metadata blocks succeed; leaf data allocation (call 4)
+ * fails with -ENOSPC. All 3 metadata blocks must be freed and
+ * i_data[EXT4_TIND_BLOCK] must remain 0.
+ */
+ ctx->freed_cnt = 0;
+ ctx->alloc_calls = 0;
+ ctx->fail_alloc_on_call = 4;
+ ctx->fail_alloc_err = -ENOSPC;
+ alloc_base = ctx->next_alloc_pblk;
+
+ memset(&map, 0, sizeof(map));
+ map.m_lblk = IND_TEST_TIND_BASE + 9;
+ map.m_len = 2;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[EXT4_TIND_BLOCK], 0);
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, alloc_base));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, alloc_base + 1));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, alloc_base + 2));
+ KUNIT_EXPECT_EQ(test, inode->i_blocks, 0ULL);
+
+ ctx->fail_alloc_on_call = 0;
+ ctx->fail_alloc_err = 0;
+ ext4_journal_stop(handle);
+ iput(inode);
+}
+
+/*
+ * Test 5: ext4_ind_truncate() partial shared-branch truncation inside DIND,
+ * boundary truncation detaching IND, and full truncation to i_size = 0.
+ */
+static void test_ind_truncate_partial_and_full(struct kunit *test)
+{
+ struct ind_test_ctx *ctx = test->priv;
+ struct inode *inode = ind_make_test_inode(test, 16);
+ __le32 *ind, *dind, *dind_c0, *dind_c1, *tind, *tind_d0, *tind_i0;
+ handle_t *handle;
+ unsigned int i;
+
+ /* Direct [0..11] -> [100..111] */
+ for (i = 0; i < EXT4_NDIR_BLOCKS; i++)
+ EXT4_I(inode)->i_data[i] = cpu_to_le32(100 + i);
+
+ /* IND (500): [0..5] -> [600..605] */
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] = cpu_to_le32(500);
+ ind = ind_blk_ptrs(test, 500);
+ for (i = 0; i < 6; i++)
+ ind[i] = cpu_to_le32(600 + i);
+
+ /* DIND (501): child0 (502) -> [700..705], child1 (503) -> [710..713] */
+ EXT4_I(inode)->i_data[EXT4_DIND_BLOCK] = cpu_to_le32(501);
+ dind = ind_blk_ptrs(test, 501);
+ dind[0] = cpu_to_le32(502);
+ dind[1] = cpu_to_le32(503);
+ dind_c0 = ind_blk_ptrs(test, 502);
+ for (i = 0; i < 6; i++)
+ dind_c0[i] = cpu_to_le32(700 + i);
+ dind_c1 = ind_blk_ptrs(test, 503);
+ for (i = 0; i < 4; i++)
+ dind_c1[i] = cpu_to_le32(710 + i);
+
+ /* TIND (504) -> DIND (505) -> IND (506) -> data (800) */
+ EXT4_I(inode)->i_data[EXT4_TIND_BLOCK] = cpu_to_le32(504);
+ tind = ind_blk_ptrs(test, 504);
+ tind[0] = cpu_to_le32(505);
+ tind_d0 = ind_blk_ptrs(test, 505);
+ tind_d0[0] = cpu_to_le32(506);
+ tind_i0 = ind_blk_ptrs(test, 506);
+ tind_i0[0] = cpu_to_le32(800);
+
+ ext4_es_insert_extent(inode, 0, 100000, 100, EXTENT_STATUS_WRITTEN, 0);
+
+ handle = ext4_journal_start(inode, EXT4_HT_TRUNCATE, 8);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+
+ /*
+ * Step A: Truncate to last_block = DIND_BASE + 3.
+ * DIND[0][0..2] (700..702) survive; DIND[0][3..5] (703..705),
+ * DIND[1] (503, 710..713), and TIND (504, 505, 506, 800) are freed.
+ */
+ i_size_write(inode, (loff_t)(IND_TEST_DIND_BASE + 3) *
+ IND_TEST_BLK_SIZE);
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ext4_ind_truncate(handle, inode);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_disksize, i_size_read(inode));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[EXT4_DIND_BLOCK]),
+ 501U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(dind[0]), 502U);
+ KUNIT_EXPECT_EQ(test, dind[1], 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(dind_c0[2]), 702U);
+ KUNIT_EXPECT_EQ(test, dind_c0[3], 0);
+ KUNIT_EXPECT_FALSE(test, ind_was_freed(ctx, 702));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 703));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 705));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 503));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 710));
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[EXT4_TIND_BLOCK], 0);
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 504));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 506));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 800));
+
+ /*
+ * Step B: Truncate to last_block = IND_BASE (12).
+ * Direct [0..11] survive; IND (500) and remaining DIND (501, 502)
+ * are detached and freed.
+ */
+ ctx->freed_cnt = 0;
+ i_size_write(inode, (loff_t)IND_TEST_IND_BASE * IND_TEST_BLK_SIZE);
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ext4_ind_truncate(handle, inode);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[EXT4_IND_BLOCK], 0);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[EXT4_DIND_BLOCK], 0);
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 500));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 600));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 501));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 502));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[11]), 111U);
+
+ /* Step C: Truncate to 0 frees all remaining direct blocks */
+ ctx->freed_cnt = 0;
+ i_size_write(inode, 0);
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ext4_ind_truncate(handle, inode);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ for (i = 0; i < EXT4_NDIR_BLOCKS; i++)
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[i], 0);
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 100));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 111));
+
+ ext4_journal_stop(handle);
+ iput(inode);
+}
+
+/*
+ * Test 6: ext4_ind_remove_space() hole punching within direct blocks, across
+ * different tree levels (n < n2), and within the same indirect level (n == n2)
+ * converging on a shared IND or DIND block.
+ */
+static void test_ind_remove_space_levels_and_convergence(struct kunit *test)
+{
+ struct ind_test_ctx *ctx = test->priv;
+ struct inode *inode = ind_make_test_inode(test, 17);
+ __le32 *ind, *dind, *c0, *c1, *c2;
+ handle_t *handle;
+ unsigned int i;
+ int ret;
+
+ handle = ext4_journal_start(inode, EXT4_HT_TRUNCATE, 8);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+
+ /* Empty/out-of-range ranges are no-ops */
+ down_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ext4_ind_remove_space(handle, inode, 5, 5), 0);
+ KUNIT_EXPECT_EQ(test,
+ ext4_ind_remove_space(handle, inode,
+ IND_TEST_MAX_LBLK + 10,
+ IND_TEST_MAX_LBLK + 20), 0);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ /* Populate direct [0..11], IND (510), and DIND (511 -> 512, 513, 514) */
+ for (i = 0; i < EXT4_NDIR_BLOCKS; i++)
+ EXT4_I(inode)->i_data[i] = cpu_to_le32(120 + i);
+
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] = cpu_to_le32(510);
+ ind = ind_blk_ptrs(test, 510);
+ for (i = 0; i < 12; i++)
+ ind[i] = cpu_to_le32(620 + i);
+
+ EXT4_I(inode)->i_data[EXT4_DIND_BLOCK] = cpu_to_le32(511);
+ dind = ind_blk_ptrs(test, 511);
+ dind[0] = cpu_to_le32(512);
+ dind[1] = cpu_to_le32(513);
+ dind[2] = cpu_to_le32(514);
+ c0 = ind_blk_ptrs(test, 512);
+ c1 = ind_blk_ptrs(test, 513);
+ c2 = ind_blk_ptrs(test, 514);
+ for (i = 0; i < 8; i++) {
+ c0[i] = cpu_to_le32(720 + i);
+ c1[i] = cpu_to_le32(740 + i);
+ c2[i] = cpu_to_le32(760 + i);
+ }
+
+ /* 1. Direct-only hole punch [3, 8) */
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_remove_space(handle, inode, 3, 8);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[2]), 122U);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[3], 0);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[7], 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[8]), 128U);
+
+ /* 2. Same-level IND convergence [IND_BASE + 3, IND_BASE + 8) */
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_remove_space(handle, inode, IND_TEST_IND_BASE + 3,
+ IND_TEST_IND_BASE + 8);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ind[2]), 622U);
+ KUNIT_EXPECT_EQ(test, ind[3], 0);
+ KUNIT_EXPECT_EQ(test, ind[7], 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ind[8]), 628U);
+
+ /*
+ * 3. Same-level DIND convergence across 3 IND children:
+ * [DIND_BASE + 4, DIND_BASE + 2 * 256 + 3) frees c0[4..7], the entire
+ * middle c1 (513 + 740..747), and c2[0..2], preserving c0[0..3] and
+ * c2[3..7].
+ */
+ ctx->freed_cnt = 0;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_remove_space(handle, inode, IND_TEST_DIND_BASE + 4,
+ IND_TEST_DIND_BASE +
+ 2 * IND_TEST_PTRS_PER_BLK + 3);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(c0[3]), 723U);
+ KUNIT_EXPECT_EQ(test, c0[4], 0);
+ KUNIT_EXPECT_EQ(test, dind[1], 0);
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 513));
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 740));
+ KUNIT_EXPECT_EQ(test, c2[2], 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(c2[3]), 763U);
+
+ /*
+ * 4. Cross-level punch (n = 1 < n2 = 3): [10, DIND_BASE + 2) frees
+ * direct [10..11], entire IND (510), and c0[0..1], preserving c0[2..3].
+ */
+ ctx->freed_cnt = 0;
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ret = ext4_ind_remove_space(handle, inode, 10, IND_TEST_DIND_BASE + 2);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[9]), 129U);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[10], 0);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[EXT4_IND_BLOCK], 0);
+ KUNIT_EXPECT_TRUE(test, ind_was_freed(ctx, 510));
+ KUNIT_EXPECT_EQ(test, c0[1], 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(c0[2]), 722U);
+
+ ext4_journal_stop(handle);
+ iput(inode);
+}
+
+/*
+ * Test 7: Corrupt out-of-range block pointers in ext4_clear_blocks() and
+ * ext4_free_branches() record filesystem errors without freeing invalid blocks.
+ */
+static void test_ind_free_corrupt_block_numbers(struct kunit *test)
+{
+ struct ind_test_ctx *ctx = test->priv;
+ struct inode *inode = ind_make_test_inode(test, 18);
+ ext4_fsblk_t bad_blk = IND_TEST_TOTAL_BLKS + 77;
+ handle_t *handle;
+
+ handle = ext4_journal_start(inode, EXT4_HT_TRUNCATE, 4);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(bad_blk);
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] = cpu_to_le32(bad_blk + 1);
+ i_size_write(inode, 0);
+
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ext4_ind_truncate(handle, inode);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ KUNIT_EXPECT_TRUE(test, ctx->sbi.s_mount_state & EXT4_ERROR_FS);
+ KUNIT_EXPECT_FALSE(test, ind_was_freed(ctx, bad_blk));
+ KUNIT_EXPECT_FALSE(test, ind_was_freed(ctx, bad_blk + 1));
+
+ ext4_journal_stop(handle);
+ iput(inode);
+}
+
+static struct kunit_case ext4_indirect_test_cases[] = {
+ KUNIT_CASE(test_block_to_path_and_trans_blocks),
+ KUNIT_CASE(test_ind_map_blocks_lookup_mapped_and_holes),
+ KUNIT_CASE(test_ind_map_blocks_alloc_direct_ind_dind_tind),
+ KUNIT_CASE(test_ind_map_blocks_errors_and_alloc_branch_unwind),
+ KUNIT_CASE(test_ind_truncate_partial_and_full),
+ KUNIT_CASE(test_ind_remove_space_levels_and_convergence),
+ KUNIT_CASE(test_ind_free_corrupt_block_numbers),
+ {}
+};
+
+static struct kunit_suite ext4_indirect_test_suite = {
+ .name = "ext4_indirect_test",
+ .init = ind_test_init,
+ .test_cases = ext4_indirect_test_cases,
+};
+
+kunit_test_suites(&ext4_indirect_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 indirect block mapping and truncation");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/indirect.c b/fs/ext4/indirect.c
index 5aec759eed70..5b2b2be73a90 100644
--- a/fs/ext4/indirect.c
+++ b/fs/ext4/indirect.c
@@ -163,7 +163,7 @@ static Indirect *ext4_get_branch(struct inode *inode, int depth,
ret = -EFSCORRUPTED;
goto failure;
}
- bh = sb_getblk(sb, key);
+ bh = ext4_sb_getblk(sb, key);
if (unlikely(!bh)) {
ret = -ENOMEM;
goto failure;
@@ -354,7 +354,8 @@ static int ext4_alloc_branch(handle_t *handle,
if (i == 0)
continue;
- bh = branch[i].bh = sb_getblk(ar->inode->i_sb, new_blocks[i-1]);
+ bh = branch[i].bh = ext4_sb_getblk(ar->inode->i_sb,
+ new_blocks[i - 1]);
if (unlikely(!bh)) {
err = -ENOMEM;
goto failed;
@@ -671,6 +672,7 @@ int ext4_ind_map_blocks(handle_t *handle, struct inode *inode,
trace_ext4_ind_map_blocks_exit(inode, flags, map, err);
return err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ind_map_blocks);
/*
* Calculate number of indirect blocks touched by mapping @nrblocks logically
@@ -685,6 +687,7 @@ int ext4_ind_trans_blocks(struct inode *inode, int nrblocks)
*/
return DIV_ROUND_UP(nrblocks, EXT4_ADDR_PER_BLOCK(inode->i_sb)) + 4;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ind_trans_blocks);
static int ext4_ind_trunc_restart_fn(handle_t *handle, struct inode *inode,
struct buffer_head *bh, int *dropped)
@@ -1213,6 +1216,7 @@ void ext4_ind_truncate(handle_t *handle, struct inode *inode)
;
}
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ind_truncate);
/**
* ext4_ind_remove_space - remove space from the range
@@ -1472,3 +1476,13 @@ int ext4_ind_remove_space(handle_t *handle, struct inode *inode,
}
goto cleanup;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ind_remove_space);
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+int ext4_block_to_path_test(struct inode *inode, ext4_lblk_t i_block,
+ ext4_lblk_t offsets[4], int *boundary)
+{
+ return ext4_block_to_path(inode, i_block, offsets, boundary);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_to_path_test);
+#endif
diff --git a/fs/ext4/super.c b/fs/ext4/super.c
index 838dabda35c5..6cdcae00d569 100644
--- a/fs/ext4/super.c
+++ b/fs/ext4/super.c
@@ -285,11 +285,16 @@ struct buffer_head *ext4_sb_bread_unmovable(struct super_block *sb,
struct buffer_head *ext4_sb_bread_nofail(struct super_block *sb,
sector_t block)
{
- gfp_t gfp = mapping_gfp_constraint(sb->s_bdev->bd_mapping,
+ gfp_t gfp;
+
+ KUNIT_STATIC_STUB_REDIRECT(ext4_sb_bread_nofail, sb, block);
+
+ gfp = mapping_gfp_constraint(sb->s_bdev->bd_mapping,
~__GFP_FS) | __GFP_MOVABLE | __GFP_NOFAIL;
return __ext4_sb_bread_gfp(sb, block, 0, gfp);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_bread_nofail);
void ext4_sb_breadahead_unmovable(struct super_block *sb, sector_t block)
{
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 23/27] ext4: expand KUnit tests in fs/ext4/extents-test.c for file range operations
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (21 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 22/27] ext4: add KUnit tests for fs/ext4/indirect.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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
` (3 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Expand ext4_extents_test to cover high-level file-range operations and
reporting entry points in fs/ext4/extents.c:
- ext4_fallocate() mode validation (unsupported flags, encrypted
collapse/insert rejection, FALLOC_FL_WRITE_ZEROES device limit check,
non-extent inode rejection), preallocation with and without
FALLOC_FL_KEEP_SIZE, and hole punching via FALLOC_FL_PUNCH_HOLE
- ext4_zero_range() via FALLOC_FL_ZERO_RANGE (written-to-unwritten split
and EOF extension) and FALLOC_FL_WRITE_ZEROES (block zeroouts,
unwritten-to-written conversion, and orphan tracking across EOF)
- ext4_collapse_range() alignment/EOF validation, block freeing,
left-shift of trailing extents, and adjacent extent coalescing
- ext4_insert_range() alignment/EOF/maxbytes validation, mid-extent
splitting, right-shift of trailing extents, and i_size growth
- ext4_convert_unwritten_extents(),
ext4_convert_unwritten_extents_atomic(), and
ext4_convert_unwritten_io_end_vec() IO completion conversions
- ext4_fiemap() and ext4_get_es_cache() range checks, FIEMAP_FLAG_CACHE
precaching, external xattr reporting, and status extent reporting
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/extents-test.c | 519 ++++++++++++++++++++++++++++++++++++++++-
fs/ext4/extents.c | 6 +
2 files changed, 522 insertions(+), 3 deletions(-)
diff --git a/fs/ext4/extents-test.c b/fs/ext4/extents-test.c
index 7565a8aa4e10..4f1746be323f 100644
--- a/fs/ext4/extents-test.c
+++ b/fs/ext4/extents-test.c
@@ -15,8 +15,11 @@
#include <kunit/static_stub.h>
#include <linux/blkdev.h>
#include <linux/buffer_head.h>
+#include <linux/falloc.h>
+#include <linux/fiemap.h>
#include <linux/fs_context.h>
#include <linux/gfp_types.h>
+#include <linux/iversion.h>
#include <linux/stddef.h>
#include "ext4.h"
@@ -71,6 +74,13 @@ struct kunit_ctx {
int last_free_flags;
unsigned long total_freed_blocks;
+ unsigned int zeroout_calls;
+ ext4_lblk_t last_zeroout_lblk;
+ ext4_lblk_t last_zeroout_len;
+ unsigned int orphan_add_calls;
+ unsigned int orphan_del_calls;
+ struct dentry k_dentry;
+
bool fail_getblk;
} k_ctx;
@@ -200,20 +210,29 @@ static void ext_init_mock_inode(struct ext4_inode_info *ei,
inode->i_sb = sb;
inode->i_ino = ino;
inode->i_mode = S_IFREG | 0644;
+ set_nlink(inode, 1);
inode->i_blkbits = sb->s_blocksize_bits;
- inode->i_flags |= S_NOQUOTA;
+ inode->i_flags |= S_NOQUOTA | S_NOSEC;
inode->i_mapping = &inode->i_data;
+ inode_set_iversion(inode, 1);
spin_lock_init(&inode->i_lock);
init_rwsem(&inode->i_rwsem);
- init_rwsem(&inode->i_data.invalidate_lock);
+ address_space_init_once(&inode->i_data);
+ inode->i_data.host = inode;
inode_state_set_raw(inode, I_DIRTY_SYNC | I_DIRTY_DATASYNC);
ext4_es_init_tree(&ei->i_es_tree);
ext4_init_pending_tree(&ei->i_pending_tree);
rwlock_init(&ei->i_es_lock);
INIT_LIST_HEAD(&ei->i_es_list);
+ INIT_LIST_HEAD(&ei->i_orphan);
+ INIT_LIST_HEAD(&ei->i_fc_list);
+ INIT_LIST_HEAD(&ei->i_fc_dilist);
+ spin_lock_init(&ei->i_fc_lock);
init_rwsem(&ei->i_data_sem);
+ init_rwsem(&ei->xattr_sem);
+ spin_lock_init(&ei->i_raw_lock);
spin_lock_init(&ei->i_block_reservation_lock);
rwlock_init(&ei->i_prealloc_lock);
ei->i_prealloc_node = RB_ROOT;
@@ -267,6 +286,7 @@ static void extents_kunit_exit(struct kunit *test)
kfree(k_ctx.meta_blocks);
kfree(sbi->s_es);
+ kfree(sb->s_bdev->bd_queue);
kfree(sb->s_bdev->bd_mapping);
kfree(sb->s_bdev);
kfree(sbi);
@@ -490,16 +510,22 @@ static int extents_kunit_init(struct kunit *test)
err = -ENOMEM;
goto out_bdev;
}
+ sb->s_bdev->bd_queue = kzalloc_obj(struct request_queue);
+ if (!sb->s_bdev->bd_queue) {
+ err = -ENOMEM;
+ goto out_bdev_mapping;
+ }
sbi->s_es = kzalloc_obj(struct ext4_super_block);
if (!sbi->s_es) {
err = -ENOMEM;
- goto out_bdev_mapping;
+ goto out_bdev_queue;
}
sbi->s_es->s_first_data_block = cpu_to_le32(1);
sbi->s_es->s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
ext4_blocks_count_set(sbi->s_es, EXT_TOTAL_BLOCKS);
set_opt(sb, ERRORS_CONT);
+ sbi->s_bitmap_maxbytes = 1ULL << 40;
sbi->s_blocks_per_group = 32768;
sbi->s_clusters_per_group = 32768;
sbi->s_cluster_ratio = 1;
@@ -507,12 +533,20 @@ static int extents_kunit_init(struct kunit *test)
sbi->s_desc_per_block = 64;
sbi->s_groups_count = 32;
sbi->s_extent_max_zeroout_kb = 32;
+ mutex_init(&sbi->s_orphan_lock);
+ INIT_LIST_HEAD(&sbi->s_orphan);
spin_lock_init(&sbi->s_bdev_wb_lock);
spin_lock_init(&sbi->s_error_lock);
spin_lock_init(&sbi->s_md_lock);
+ ratelimit_state_init(&sbi->s_err_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL,
+ DEFAULT_RATELIMIT_BURST);
ratelimit_state_init(&sbi->s_warning_ratelimit_state,
DEFAULT_RATELIMIT_INTERVAL,
DEFAULT_RATELIMIT_BURST);
+ ratelimit_state_init(&sbi->s_msg_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL,
+ DEFAULT_RATELIMIT_BURST);
err = percpu_counter_init(&sbi->s_freeclusters_counter, 100000,
GFP_KERNEL);
@@ -621,6 +655,8 @@ static int extents_kunit_init(struct kunit *test)
percpu_counter_destroy(&sbi->s_freeclusters_counter);
out_es:
kfree(sbi->s_es);
+out_bdev_queue:
+ kfree(sb->s_bdev->bd_queue);
out_bdev_mapping:
kfree(sb->s_bdev->bd_mapping);
out_bdev:
@@ -2441,6 +2477,477 @@ static void test_ext_fast_commit_replay(struct kunit *test)
10ULL << (inode->i_sb->s_blocksize_bits - 9));
}
+static int ext_range_ext_zeroout_stub(struct inode *inode,
+ struct ext4_extent *ex)
+{
+ k_ctx.zeroout_calls++;
+ k_ctx.last_zeroout_lblk = le32_to_cpu(ex->ee_block);
+ k_ctx.last_zeroout_len = ext4_ext_get_actual_len(ex);
+ return 0;
+}
+
+static int ext_range_issue_zeroout_stub(struct inode *inode, ext4_lblk_t lblk,
+ ext4_fsblk_t pblk, ext4_lblk_t len)
+{
+ k_ctx.zeroout_calls++;
+ k_ctx.last_zeroout_lblk = lblk;
+ k_ctx.last_zeroout_len = len;
+ return 0;
+}
+
+static int ext_orphan_add_stub(handle_t *handle, struct inode *inode)
+{
+ k_ctx.orphan_add_calls++;
+ return 0;
+}
+
+static int ext_orphan_del_stub(handle_t *handle, struct inode *inode)
+{
+ k_ctx.orphan_del_calls++;
+ return 0;
+}
+
+static void ext_setup_range_test(struct kunit *test, struct inode *inode,
+ struct file *file)
+{
+ kunit_activate_static_stub(test, ext4_ext_zeroout,
+ ext_range_ext_zeroout_stub);
+ kunit_activate_static_stub(test, ext4_issue_zeroout,
+ ext_range_issue_zeroout_stub);
+ kunit_activate_static_stub(test, ext4_orphan_add, ext_orphan_add_stub);
+ kunit_activate_static_stub(test, ext4_orphan_del, ext_orphan_del_stub);
+
+ ext_reset_tree(inode);
+ inode->i_size = 0;
+ EXT4_I(inode)->i_disksize = 0;
+ inode->i_blocks = 0;
+
+ k_ctx.next_alloc_pblk = EXT_ALLOC_BASE_PBLK;
+ k_ctx.alloc_calls = 0;
+ k_ctx.free_calls = 0;
+ k_ctx.total_freed_blocks = 0;
+ k_ctx.zeroout_calls = 0;
+ k_ctx.orphan_add_calls = 0;
+ k_ctx.orphan_del_calls = 0;
+
+ if (file) {
+ struct file tmp = {
+ .f_path = { .dentry = &k_ctx.k_dentry },
+ .f_inode = inode,
+ .f_mapping = inode->i_mapping,
+ .f_mode = FMODE_READ | FMODE_WRITE |
+ FMODE_NOCMTIME,
+ };
+
+ memset(&k_ctx.k_dentry, 0, sizeof(k_ctx.k_dentry));
+ k_ctx.k_dentry.d_inode = inode;
+ memcpy(file, &tmp, sizeof(tmp));
+ }
+}
+
+static void ext_expect_extent_at(struct kunit *test, struct inode *inode,
+ ext4_lblk_t lblk, unsigned short exp_len,
+ ext4_fsblk_t exp_pblk, bool exp_unwritten)
+{
+ struct ext4_ext_path *path;
+ struct ext4_extent *ex;
+
+ down_read(&EXT4_I(inode)->i_data_sem);
+ path = ext4_find_extent(inode, lblk, NULL, EXT4_EX_NOCACHE);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+ ex = path[path->p_depth].p_ext;
+ KUNIT_ASSERT_NOT_NULL(test, ex);
+
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex->ee_block), lblk);
+ KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(ex), exp_len);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(ex), exp_pblk);
+ KUNIT_EXPECT_EQ(test, !!ext4_ext_is_unwritten(ex), exp_unwritten);
+
+ ext4_free_ext_path(path);
+ up_read(&EXT4_I(inode)->i_data_sem);
+}
+
+/*
+ * Verify ext4_fallocate() mode validation (unsupported flags, encrypted
+ * collapse/insert rejection, WRITE_ZEROES without bdev support, non-extent
+ * preallocation rejection), preallocation with and without FALLOC_FL_KEEP_SIZE,
+ * and hole punching via FALLOC_FL_PUNCH_HOLE.
+ */
+static void test_fallocate_modes_and_prealloc(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct file file;
+ long ret;
+
+ ext_setup_range_test(test, inode, &file);
+
+ /* Unsupported mode flag -> -EOPNOTSUPP */
+ ret = ext4_fallocate(&file, 0x40000000, 0, 4096);
+ KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP);
+
+ /* FALLOC_FL_WRITE_ZEROES without unmap sectors -> -EOPNOTSUPP */
+ ret = ext4_fallocate(&file, FALLOC_FL_WRITE_ZEROES, 0, 4096);
+ KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP);
+
+ /* Encrypted inode rejects COLLAPSE_RANGE and INSERT_RANGE */
+ inode->i_flags |= S_ENCRYPTED;
+ ret = ext4_fallocate(&file, FALLOC_FL_COLLAPSE_RANGE, 0, 4096);
+ KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP);
+ ret = ext4_fallocate(&file, FALLOC_FL_INSERT_RANGE, 0, 4096);
+ KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP);
+ inode->i_flags &= ~S_ENCRYPTED;
+
+ /* Non-extent (indirect) inode rejects fallocate preallocation */
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ret = ext4_fallocate(&file, 0, 0, 4096);
+ KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+
+ /* Preallocate [0, 4 blocks) with FALLOC_FL_KEEP_SIZE: i_size stays 0 */
+ ret = ext4_fallocate(&file, FALLOC_FL_KEEP_SIZE, 0, 4 * 4096);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, inode->i_size, 0LL);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_disksize, 0LL);
+ ext_expect_extent_at(test, inode, 0, 4, EXT_ALLOC_BASE_PBLK, true);
+
+ /* Extend allocation across [2, 6 blocks) without KEEP_SIZE */
+ ret = ext4_fallocate(&file, 0, 2 * 4096, 4 * 4096);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, inode->i_size, 6LL * 4096);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_disksize, 6LL * 4096);
+ ext_expect_extent_at(test, inode, 0, 6, EXT_ALLOC_BASE_PBLK, true);
+
+ /* Punch middle [2, 4) with FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE */
+ ret = ext4_fallocate(&file, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
+ 2 * 4096, 2 * 4096);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, inode->i_size, 6LL * 4096);
+ KUNIT_EXPECT_EQ(test, k_ctx.total_freed_blocks, 2UL);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ext_inode_hdr(inode)->eh_entries), 2);
+ ext_expect_extent_at(test, inode, 0, 2, EXT_ALLOC_BASE_PBLK, true);
+ ext_expect_extent_at(test, inode, 4, 2, EXT_ALLOC_BASE_PBLK + 4, true);
+}
+
+/*
+ * Verify ext4_zero_range() via both FALLOC_FL_ZERO_RANGE (splitting written
+ * extents to unwritten and allocating unwritten blocks past EOF) and
+ * FALLOC_FL_WRITE_ZEROES (issuing block zeroouts, converting to written, and
+ * tracking orphan state when extending i_disksize).
+ */
+static void test_zero_range_and_write_zeroes(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct super_block *sb = inode->i_sb;
+ struct file file;
+ long ret;
+
+ /* Part A: FALLOC_FL_ZERO_RANGE across written tail + past EOF */
+ ext_setup_range_test(test, inode, &file);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 0, 6, 200, false, 0), 0);
+ inode->i_size = 6LL * 4096;
+ EXT4_I(inode)->i_disksize = 6LL * 4096;
+
+ ret = ext4_fallocate(&file, FALLOC_FL_ZERO_RANGE, 2 * 4096, 6 * 4096);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, inode->i_size, 8LL * 4096);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_disksize, 8LL * 4096);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ext_inode_hdr(inode)->eh_entries), 3);
+ ext_expect_extent_at(test, inode, 0, 2, 200, false);
+ ext_expect_extent_at(test, inode, 2, 4, 202, true);
+ ext_expect_extent_at(test, inode, 6, 2, EXT_ALLOC_BASE_PBLK, true);
+
+ /* Part B: FALLOC_FL_WRITE_ZEROES over unwritten + hole past EOF */
+ ext_setup_range_test(test, inode, &file);
+ sb->s_bdev->bd_queue->limits.max_wzeroes_unmap_sectors = 1024;
+
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 0, 2, 300, true, 0), 0);
+ inode->i_size = 2LL * 4096;
+ EXT4_I(inode)->i_disksize = 2LL * 4096;
+
+ ret = ext4_fallocate(&file, FALLOC_FL_WRITE_ZEROES, 0, 4 * 4096);
+ sb->s_bdev->bd_queue->limits.max_wzeroes_unmap_sectors = 0;
+
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_GE(test, k_ctx.zeroout_calls, 2U);
+ KUNIT_EXPECT_GE(test, k_ctx.orphan_add_calls, 1U);
+ KUNIT_EXPECT_EQ(test, k_ctx.orphan_del_calls, 1U);
+ KUNIT_EXPECT_EQ(test, inode->i_size, 4LL * 4096);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_disksize, 4LL * 4096);
+ ext_expect_extent_at(test, inode, 0, 2, 300, false);
+ ext_expect_extent_at(test, inode, 2, 2, EXT_ALLOC_BASE_PBLK, false);
+}
+
+/*
+ * Verify ext4_collapse_range() validation checks, block removal, left-shift of
+ * subsequent extents, adjacent extent coalescing after collapse, and i_size
+ * reduction.
+ */
+static void test_collapse_range(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct file file;
+ long ret;
+
+ ext_setup_range_test(test, inode, &file);
+ inode->i_size = 10LL * 4096;
+ EXT4_I(inode)->i_disksize = 10LL * 4096;
+
+ /* Block-unaligned offset or length -> -EINVAL */
+ ret = ext4_fallocate(&file, FALLOC_FL_COLLAPSE_RANGE, 512, 4096);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+ ret = ext4_fallocate(&file, FALLOC_FL_COLLAPSE_RANGE, 4096, 512);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ /* Non-extent inode -> -EOPNOTSUPP */
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ret = ext4_fallocate(&file, FALLOC_FL_COLLAPSE_RANGE, 0, 4096);
+ KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+
+ /* Collapsing up to or beyond i_size -> -EINVAL */
+ ret = ext4_fallocate(&file, FALLOC_FL_COLLAPSE_RANGE,
+ 8 * 4096, 2 * 4096);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ /*
+ * Build [0, 2)@200 writ, [2, 4)@500 writ, [4, 7)@202 writ, and
+ * [8, 10)@600 unwrit. Collapsing [2, 4) frees [2, 4)@500, shifts
+ * [4, 7)@202 left to [2, 5)@202 where it merges with [0, 2)@200 into
+ * [0, 5)@200, and shifts [8, 10)@600 left to [6, 8)@600.
+ */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 0, 2, 200, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 2, 2, 500, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 4, 3, 202, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 8, 2, 600, true, 0), 0);
+
+ ret = ext4_fallocate(&file, FALLOC_FL_COLLAPSE_RANGE,
+ 2 * 4096, 2 * 4096);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, k_ctx.total_freed_blocks, 2UL);
+ KUNIT_EXPECT_EQ(test, inode->i_size, 8LL * 4096);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_disksize, 8LL * 4096);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ext_inode_hdr(inode)->eh_entries), 2);
+ ext_expect_extent_at(test, inode, 0, 5, 200, false);
+ ext_expect_extent_at(test, inode, 6, 2, 600, true);
+}
+
+/*
+ * Verify ext4_insert_range() alignment, EOF, and s_maxbytes checks, as well as
+ * mid-extent splitting, right-shift of trailing extents, and i_size growth.
+ */
+static void test_insert_range(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct file file;
+ long ret;
+
+ ext_setup_range_test(test, inode, &file);
+ inode->i_size = 10LL * 4096;
+ EXT4_I(inode)->i_disksize = 10LL * 4096;
+
+ /* Block-unaligned offset or length -> -EINVAL */
+ ret = ext4_fallocate(&file, FALLOC_FL_INSERT_RANGE, 1024, 4096);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+ ret = ext4_fallocate(&file, FALLOC_FL_INSERT_RANGE, 4096, 1024);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ /* Non-extent inode -> -EOPNOTSUPP */
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ret = ext4_fallocate(&file, FALLOC_FL_INSERT_RANGE, 0, 4096);
+ KUNIT_EXPECT_EQ(test, ret, -EOPNOTSUPP);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+
+ /* Offset at or beyond i_size -> -EINVAL */
+ ret = ext4_fallocate(&file, FALLOC_FL_INSERT_RANGE, 10 * 4096, 4096);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ /* Length exceeding s_maxbytes - i_size -> -EFBIG */
+ ret = ext4_fallocate(&file, FALLOC_FL_INSERT_RANGE, 4096,
+ round_down(inode->i_sb->s_maxbytes, 4096));
+ KUNIT_EXPECT_EQ(test, ret, -EFBIG);
+
+ /*
+ * Insert 3-block hole at lblk 2 inside [0, 6)@200 writ with trailing
+ * [8, 10)@400 unwrit: produces [0, 2)@200 writ, [5, 9)@202 writ, and
+ * [11, 13)@400 unwrit, with i_size = 13 blocks.
+ */
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 0, 6, 200, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 8, 2, 400, true, 0), 0);
+
+ ret = ext4_fallocate(&file, FALLOC_FL_INSERT_RANGE,
+ 2 * 4096, 3 * 4096);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, inode->i_size, 13LL * 4096);
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_disksize, 13LL * 4096);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ext_inode_hdr(inode)->eh_entries), 3);
+ ext_expect_extent_at(test, inode, 0, 2, 200, false);
+ ext_expect_extent_at(test, inode, 5, 4, 202, false);
+ ext_expect_extent_at(test, inode, 11, 2, 400, true);
+}
+
+/*
+ * Verify unwritten-to-written IO completion conversion helpers:
+ * ext4_convert_unwritten_extents(), ext4_convert_unwritten_extents_atomic()
+ * across a split unwritten mapping, and ext4_convert_unwritten_io_end_vec().
+ */
+static void test_convert_unwritten_extents_and_io_end(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ ext4_io_end_t io_end = {};
+ struct ext4_io_end_vec vec1 = {}, vec2 = {};
+ ext4_lblk_t converted = 0;
+ int err;
+
+ /* 1. ext4_convert_unwritten_extents() on middle of unwritten extent */
+ ext_setup_range_test(test, inode, NULL);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 0, 6, 200, true, 0), 0);
+
+ err = ext4_convert_unwritten_extents(NULL, inode, 2 * 4096, 2 * 4096,
+ &converted);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, converted, 2U);
+ ext_expect_extent_at(test, inode, 0, 2, 200, true);
+ ext_expect_extent_at(test, inode, 2, 2, 202, false);
+ ext_expect_extent_at(test, inode, 4, 2, 204, true);
+
+ /* 2. ext4_convert_unwritten_extents_atomic() across split unwritten */
+ ext_setup_range_test(test, inode, NULL);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 0, 2, 200, true, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 2, 3, 300, true, 0), 0);
+
+ err = ext4_convert_unwritten_extents_atomic(NULL, inode, 0, 5 * 4096);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ ext_expect_extent_at(test, inode, 0, 2, 200, false);
+ ext_expect_extent_at(test, inode, 2, 3, 300, false);
+
+ /* 3. ext4_convert_unwritten_io_end_vec() across multiple vecs */
+ ext_setup_range_test(test, inode, NULL);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 0, 8, 200, true, 0), 0);
+
+ io_end.inode = inode;
+ INIT_LIST_HEAD(&io_end.list_vec);
+ vec1.offset = 1 * 4096;
+ vec1.size = 2 * 4096;
+ vec2.offset = 5 * 4096;
+ vec2.size = 2 * 4096;
+ list_add_tail(&vec1.list, &io_end.list_vec);
+ list_add_tail(&vec2.list, &io_end.list_vec);
+
+ err = ext4_convert_unwritten_io_end_vec(NULL, &io_end);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ ext_expect_extent_at(test, inode, 0, 1, 200, true);
+ ext_expect_extent_at(test, inode, 1, 2, 201, false);
+ ext_expect_extent_at(test, inode, 3, 2, 203, true);
+ ext_expect_extent_at(test, inode, 5, 2, 205, false);
+ ext_expect_extent_at(test, inode, 7, 1, 207, true);
+}
+
+/*
+ * Verify ext4_fiemap() (range checks, FIEMAP_FLAG_CACHE precaching, data and
+ * external xattr reporting) and ext4_get_es_cache() (flag checks, precaching,
+ * and reporting mapped + delayed status extents).
+ */
+static void test_ext_fiemap_and_get_es_cache(struct kunit *test)
+{
+ struct inode *inode = &k_ctx.k_ei->vfs_inode;
+ struct fiemap_extent_info fieinfo = {};
+ struct extent_status es = {};
+ int err;
+
+ ext_setup_range_test(test, inode, NULL);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 0, 3, 200, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 6, 2, 300, true, 0), 0);
+ inode->i_size = 10LL * 4096;
+ EXT4_I(inode)->i_disksize = 10LL * 4096;
+
+ /* Zero-length FIEMAP -> -EINVAL */
+ err = ext4_fiemap(inode, &fieinfo, 0, 0);
+ KUNIT_EXPECT_EQ(test, err, -EINVAL);
+
+ /* Start past maxbytes on non-extent inode -> -EFBIG */
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ err = ext4_fiemap(inode, &fieinfo,
+ EXT4_SB(inode->i_sb)->s_bitmap_maxbytes + 4096, 4096);
+ KUNIT_EXPECT_EQ(test, err, -EFBIG);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+
+ /* Count-only data FIEMAP with FIEMAP_FLAG_CACHE populates ES cache */
+ memset(&fieinfo, 0, sizeof(fieinfo));
+ fieinfo.fi_flags = FIEMAP_FLAG_CACHE;
+ err = ext4_fiemap(inode, &fieinfo, 0, 10 * 4096);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, fieinfo.fi_flags & FIEMAP_FLAG_CACHE, 0U);
+ KUNIT_EXPECT_EQ(test, fieinfo.fi_extents_mapped, 2U);
+ KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 0, NULL, &es, NULL),
+ 1);
+
+ /* FIEMAP_FLAG_XATTR with no xattrs vs external i_file_acl block */
+ memset(&fieinfo, 0, sizeof(fieinfo));
+ fieinfo.fi_flags = FIEMAP_FLAG_XATTR;
+ err = ext4_fiemap(inode, &fieinfo, 0, 4096);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, fieinfo.fi_extents_mapped, 0U);
+
+ EXT4_I(inode)->i_file_acl = 750;
+ memset(&fieinfo, 0, sizeof(fieinfo));
+ fieinfo.fi_flags = FIEMAP_FLAG_XATTR;
+ err = ext4_fiemap(inode, &fieinfo, 0, 4096);
+ EXT4_I(inode)->i_file_acl = 0;
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, fieinfo.fi_extents_mapped, 1U);
+
+ /* ext4_get_es_cache: unsupported flag -> -EBADR, len == 0 -> -EINVAL */
+ memset(&fieinfo, 0, sizeof(fieinfo));
+ fieinfo.fi_flags = FIEMAP_FLAG_XATTR;
+ err = ext4_get_es_cache(inode, &fieinfo, 0, 4096);
+ KUNIT_EXPECT_EQ(test, err, -EBADR);
+
+ memset(&fieinfo, 0, sizeof(fieinfo));
+ err = ext4_get_es_cache(inode, &fieinfo, 0, 0);
+ KUNIT_EXPECT_EQ(test, err, -EINVAL);
+
+ /*
+ * Populate ES tree with written, hole, delayed, and unwritten entries
+ * and verify ext4_get_es_cache reports all 4 entries.
+ */
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ ext4_es_insert_extent(inode, 0, 3, 200, EXTENT_STATUS_WRITTEN, 0);
+ ext4_es_insert_extent(inode, 3, 1, ~0, EXTENT_STATUS_HOLE, 0);
+ ext4_es_insert_delayed_extent(inode, 4, 2, false, false);
+ ext4_es_insert_extent(inode, 6, 2, 300, EXTENT_STATUS_UNWRITTEN, 0);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ memset(&fieinfo, 0, sizeof(fieinfo));
+ err = ext4_get_es_cache(inode, &fieinfo, 0, 10 * 4096);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, fieinfo.fi_extents_mapped, 4U);
+
+ /*
+ * Build a depth-1 tree (5 discontiguous extents), clear ES cache, and
+ * verify FIEMAP_FLAG_CACHE precaches external leaf extents + holes.
+ */
+ ext_setup_range_test(test, inode, NULL);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 0, 1, 200, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 2, 1, 202, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 4, 1, 204, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 6, 1, 206, false, 0), 0);
+ KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 8, 1, 208, false, 0), 0);
+ inode->i_size = 10LL * 4096;
+ EXT4_I(inode)->i_disksize = 10LL * 4096;
+
+ down_write(&EXT4_I(inode)->i_data_sem);
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ up_write(&EXT4_I(inode)->i_data_sem);
+
+ memset(&fieinfo, 0, sizeof(fieinfo));
+ fieinfo.fi_flags = FIEMAP_FLAG_CACHE;
+ err = ext4_get_es_cache(inode, &fieinfo, 0, 10 * 4096);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, fieinfo.fi_flags & FIEMAP_FLAG_CACHE, 0U);
+ KUNIT_EXPECT_GE(test, fieinfo.fi_extents_mapped, 5U);
+}
+
static const struct kunit_ext_test_param test_split_convert_params[] = {
/* unwrit to writ splits */
{ .desc = "split unwrit extent to 2 extents and convert 1st half writ",
@@ -3001,6 +3508,12 @@ static struct kunit_case extents_test_cases[] = {
KUNIT_CASE(test_clu_mapped),
KUNIT_CASE(test_ext_credits_calculation),
KUNIT_CASE(test_ext_fast_commit_replay),
+ KUNIT_CASE(test_fallocate_modes_and_prealloc),
+ KUNIT_CASE(test_zero_range_and_write_zeroes),
+ KUNIT_CASE(test_collapse_range),
+ KUNIT_CASE(test_insert_range),
+ KUNIT_CASE(test_convert_unwritten_extents_and_io_end),
+ KUNIT_CASE(test_ext_fiemap_and_get_es_cache),
{}
};
diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
index efbee7aaa95d..cbada2846190 100644
--- a/fs/ext4/extents.c
+++ b/fs/ext4/extents.c
@@ -5034,6 +5034,7 @@ long ext4_fallocate(struct file *file, int mode, loff_t offset, loff_t len)
inode_unlock(inode);
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fallocate);
/*
* This function converts a range of blocks to written extents. The caller of
@@ -5121,6 +5122,7 @@ int ext4_convert_unwritten_extents_atomic(handle_t *handle, struct inode *inode,
return ret > 0 ? ret2 : ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_convert_unwritten_extents_atomic);
/*
* This function convert a range of blocks to written extents
@@ -5202,6 +5204,7 @@ int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode,
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_convert_unwritten_extents);
int ext4_convert_unwritten_io_end_vec(handle_t *handle, ext4_io_end_t *io_end)
{
@@ -5233,6 +5236,7 @@ int ext4_convert_unwritten_io_end_vec(handle_t *handle, ext4_io_end_t *io_end)
return ret < 0 ? ret : err;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_convert_unwritten_io_end_vec);
static int ext4_iomap_xattr_fiemap(struct inode *inode, struct iomap *iomap)
{
@@ -5345,6 +5349,7 @@ int ext4_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo,
inode_unlock_shared(inode);
return error;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fiemap);
int ext4_get_es_cache(struct inode *inode, struct fiemap_extent_info *fieinfo,
__u64 start, __u64 len)
@@ -5392,6 +5397,7 @@ int ext4_get_es_cache(struct inode *inode, struct fiemap_extent_info *fieinfo,
*/
return ext4_fill_es_cache_info(inode, start_blk, len_blks, fieinfo);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_es_cache);
/*
* ext4_ext_shift_path_extents:
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 24/27] ext4: add KUnit tests for fs/ext4/fsmap.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (22 preceding siblings ...)
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:06 ` 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
` (2 subsequent siblings)
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add KUnit test suite ext4_fsmap_test in fs/ext4/fsmap-test.c covering
FS_IOC_GETFSMAP reverse-mapping queries in fs/ext4/fsmap.c:
- ext4_fsmap_from_internal() and ext4_fsmap_to_internal() block/byte
conversions across 1K, 4K, and 64K block sizes
- ext4_getfsmap() validation of flags, device IDs, inverted keys,
out-of-range physical offsets, and missing group descriptors
- fixed per-group metadata synthesis (superblock, GDT, reserved GDT,
block bitmap, inode bitmap, inode table), free extents, and
EXT4_FMR_OWN_UNKNOWN gaps in the middle and at EOFS
- flex_bg adjacent fixed-metadata merging and cross-group free-extent
coalescing via gfi_lastfree
- 1K s_first_data_block offset handling, bigalloc cluster scaling, and
META_BG reserved-GDT suppression
- count-only queries, bounded continuation-key pagination, formatter
abort propagation, and external journal device reporting
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 2 +-
fs/ext4/fsmap-test.c | 787 +++++++++++++++++++++++++++++++++++++++++++
fs/ext4/fsmap.c | 3 +
fs/ext4/mballoc.c | 3 +
4 files changed, 794 insertions(+), 1 deletion(-)
create mode 100644 fs/ext4/fsmap-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 503acc946409..e1964407be24 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -23,7 +23,7 @@ ext4-test-objs += inode-test.o mballoc-test.o \
block_validity-test.o super-test.o \
sysfs-test.o ioctl-test.o \
resize-test.o migrate-test.o \
- indirect-test.o
+ indirect-test.o fsmap-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/fsmap-test.c b/fs/ext4/fsmap-test.c
new file mode 100644
index 000000000000..63a1061b413a
--- /dev/null
+++ b/fs/ext4/fsmap-test.c
@@ -0,0 +1,787 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/fsmap.c (FS_IOC_GETFSMAP reverse-mapping queries).
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/bitmap.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/fs_context.h>
+#include <linux/fsmap.h>
+#include <linux/kdev_t.h>
+#include <linux/log2.h>
+#include <linux/part_stat.h>
+#include <linux/percpu.h>
+
+#include "ext4.h"
+#include "fsmap.h"
+#include "mballoc.h"
+
+#define FSMAP_TEST_MAX_GROUPS 4
+#define FSMAP_TEST_MAX_CLUSTERS_PER_GRP 1024
+#define FSMAP_TEST_MAX_RECS 24
+#define FSMAP_TEST_DATADEV MKDEV(8, 1)
+#define FSMAP_TEST_LOGDEV MKDEV(8, 2)
+
+/*
+ * Matches struct bdev_inode layout in block/bdev.c so file_bdev() resolves
+ * &bdev via container_of(bdev_file->f_mapping->host, ...).
+ */
+struct fsmap_mock_bdev_inode {
+ struct block_device bdev;
+ struct inode vfs_inode;
+};
+
+struct fsmap_rec_sink {
+ struct ext4_fsmap recs[FSMAP_TEST_MAX_RECS];
+ unsigned int count;
+ unsigned int abort_after;
+ int fail_err;
+};
+
+struct fsmap_test_ctx {
+ struct super_block *sb;
+ struct ext4_sb_info sbi;
+ struct ext4_super_block es;
+ struct buffer_head sbh;
+ char sbh_data[4096];
+
+ struct fsmap_mock_bdev_inode data_bdev;
+ struct fsmap_mock_bdev_inode log_bdev;
+ struct file log_bdev_file;
+ journal_t mock_journal;
+
+ struct buffer_head gdb_bh;
+ struct buffer_head *gdb_bh_arr[1];
+ u8 gdb_data[4096];
+
+ DECLARE_BITMAP(grp_bms[FSMAP_TEST_MAX_GROUPS],
+ FSMAP_TEST_MAX_CLUSTERS_PER_GRP);
+};
+
+static struct fsmap_test_ctx *fsmap_active_ctx;
+
+static void fsmap_noop_work(struct work_struct *work)
+{
+}
+
+static void fsmap_noop_timer(struct timer_list *timer)
+{
+}
+
+static const struct super_operations fsmap_test_sops = {};
+
+static int fsmap_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static int fsmap_test_set_super(struct super_block *sb, struct fs_context *fc)
+{
+ return set_anon_super_fc(sb, fc);
+}
+
+static struct file_system_type fsmap_test_fs_type = {
+ .name = "ext4-fsmap-test",
+ .init_fs_context = fsmap_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static void fsmap_cleanup_ctx(void *data)
+{
+ struct fsmap_test_ctx *ctx = data;
+
+ fsmap_active_ctx = NULL;
+ cancel_work_sync(&ctx->sbi.s_sb_upd_work);
+ timer_delete_sync(&ctx->sbi.s_err_report);
+
+ if (ctx->sb)
+ deactivate_super(ctx->sb);
+ if (ctx->data_bdev.bdev.bd_stats)
+ free_percpu(ctx->data_bdev.bdev.bd_stats);
+ if (ctx->log_bdev.bdev.bd_stats)
+ free_percpu(ctx->log_bdev.bdev.bd_stats);
+}
+
+static int fsmap_query_range_stub(struct super_block *sb,
+ ext4_group_t group,
+ ext4_grpblk_t first,
+ ext4_grpblk_t end,
+ ext4_mballoc_query_range_fn meta_formatter,
+ ext4_mballoc_query_range_fn formatter,
+ void *priv)
+{
+ struct fsmap_test_ctx *ctx = fsmap_active_ctx;
+ ext4_grpblk_t max_clu = EXT4_CLUSTERS_PER_GROUP(sb);
+ ext4_grpblk_t first_free, start, next;
+ unsigned long *bm;
+ int error;
+
+ if (!ctx || group >= FSMAP_TEST_MAX_GROUPS)
+ return -EINVAL;
+
+ bm = ctx->grp_bms[group];
+ if ((unsigned long)end >= EXT4_CLUSTERS_PER_GROUP(sb))
+ end = max_clu - 1;
+
+ first_free = find_next_zero_bit(bm, max_clu, 0);
+ start = max_t(ext4_grpblk_t, first_free, first);
+
+ if (meta_formatter && start != first) {
+ if (start > end)
+ start = end;
+ error = meta_formatter(sb, group, first, start - first, priv);
+ if (error)
+ return error;
+ }
+
+ while (start <= end) {
+ start = find_next_zero_bit(bm, end + 1, start);
+ if (start > end)
+ break;
+ next = find_next_bit(bm, end + 1, start);
+
+ error = formatter(sb, group, start, next - start, priv);
+ if (error)
+ return error;
+
+ start = next + 1;
+ }
+
+ return 0;
+}
+
+static int fsmap_collect_cb(struct ext4_fsmap *fmr, void *priv)
+{
+ struct fsmap_rec_sink *sink = priv;
+
+ if (sink->fail_err)
+ return sink->fail_err;
+
+ if (sink->count < FSMAP_TEST_MAX_RECS)
+ sink->recs[sink->count] = *fmr;
+ sink->count++;
+
+ if (sink->abort_after && sink->count >= sink->abort_after)
+ return EXT4_QUERY_RANGE_ABORT;
+
+ return EXT4_QUERY_RANGE_CONTINUE;
+}
+
+static void fsmap_mark_clusters_used(struct fsmap_test_ctx *ctx,
+ ext4_group_t agno,
+ ext4_grpblk_t start_clu,
+ ext4_grpblk_t len_clu)
+{
+ bitmap_set(ctx->grp_bms[agno], start_clu, len_clu);
+}
+
+static void fsmap_set_group_tables(struct fsmap_test_ctx *ctx,
+ ext4_group_t agno,
+ ext4_fsblk_t blk_bm,
+ ext4_fsblk_t ino_bm,
+ ext4_fsblk_t itable)
+{
+ struct super_block *sb = ctx->sb;
+ struct ext4_group_desc *gdp;
+
+ gdp = (struct ext4_group_desc *)(ctx->gdb_data +
+ agno * EXT4_DESC_SIZE(sb));
+ ext4_block_bitmap_set(sb, gdp, blk_bm);
+ ext4_inode_bitmap_set(sb, gdp, ino_bm);
+ ext4_inode_table_set(sb, gdp, itable);
+}
+
+static struct fsmap_test_ctx *fsmap_create_ctx(struct kunit *test,
+ unsigned int blocksize_bits,
+ unsigned int cluster_bits,
+ ext4_group_t groups_count,
+ ext4_grpblk_t blocks_per_group)
+{
+ struct fsmap_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ unsigned int blocksize = 1U << blocksize_bits;
+ ext4_fsblk_t bofs = (blocksize == 1024) ? 1 : 0;
+ int err;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+
+ inode_init_once(&ctx->data_bdev.vfs_inode);
+ address_space_init_once(&ctx->data_bdev.vfs_inode.i_data);
+ ctx->data_bdev.vfs_inode.i_data.host = &ctx->data_bdev.vfs_inode;
+ ctx->data_bdev.bdev.bd_dev = FSMAP_TEST_DATADEV;
+ ctx->data_bdev.bdev.bd_mapping = &ctx->data_bdev.vfs_inode.i_data;
+ ctx->data_bdev.bdev.bd_stats = alloc_percpu(struct disk_stats);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->data_bdev.bdev.bd_stats);
+
+ inode_init_once(&ctx->log_bdev.vfs_inode);
+ address_space_init_once(&ctx->log_bdev.vfs_inode.i_data);
+ ctx->log_bdev.vfs_inode.i_data.host = &ctx->log_bdev.vfs_inode;
+ ctx->log_bdev.bdev.bd_dev = FSMAP_TEST_LOGDEV;
+ ctx->log_bdev.bdev.bd_mapping = &ctx->log_bdev.vfs_inode.i_data;
+ ctx->log_bdev.bdev.bd_stats = alloc_percpu(struct disk_stats);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->log_bdev.bdev.bd_stats);
+ ctx->log_bdev_file.f_mapping = &ctx->log_bdev.vfs_inode.i_data;
+
+ sbi = &ctx->sbi;
+ INIT_WORK(&sbi->s_sb_upd_work, fsmap_noop_work);
+ timer_setup(&sbi->s_err_report, fsmap_noop_timer, 0);
+ spin_lock_init(&sbi->s_error_lock);
+ spin_lock_init(&sbi->s_bdev_wb_lock);
+ ratelimit_state_init(&sbi->s_err_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL,
+ DEFAULT_RATELIMIT_BURST);
+
+ fc = fs_context_for_mount(&fsmap_test_fs_type, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fc));
+ sb = sget_fc(fc, NULL, fsmap_test_set_super);
+ put_fs_context(fc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(sb));
+ up_write(&sb->s_umount);
+
+ ctx->sb = sb;
+ err = kunit_add_action_or_reset(test, fsmap_cleanup_ctx, ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ sb->s_fs_info = sbi;
+ sb->s_op = &fsmap_test_sops;
+ sb->s_bdev = &ctx->data_bdev.bdev;
+ sb->s_blocksize = blocksize;
+ sb->s_blocksize_bits = blocksize_bits;
+ strscpy(sb->s_id, "ext4-fsmap", sizeof(sb->s_id));
+
+ ctx->sbh.b_data = ctx->sbh_data;
+ ctx->sbh.b_size = blocksize;
+ ctx->sbh.b_bdev = sb->s_bdev;
+ atomic_set(&ctx->sbh.b_count, 2);
+ set_buffer_uptodate(&ctx->sbh);
+ clear_buffer_mapped(&ctx->sbh);
+
+ sbi->s_sb = sb;
+ sbi->s_es = &ctx->es;
+ sbi->s_sbh = &ctx->sbh;
+ set_opt(sb, ERRORS_CONT);
+
+ 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_data_block = cpu_to_le32(bofs);
+ ctx->es.s_feature_incompat = cpu_to_le32(EXT4_FEATURE_INCOMPAT_64BIT);
+ ctx->es.s_desc_size = cpu_to_le16(64);
+ ext4_blocks_count_set(&ctx->es,
+ bofs + (ext4_fsblk_t)groups_count *
+ blocks_per_group);
+
+ sbi->s_desc_size = 64;
+ sbi->s_desc_per_block = blocksize / sbi->s_desc_size;
+ sbi->s_desc_per_block_bits = ilog2(sbi->s_desc_per_block);
+ sbi->s_addr_per_block_bits = ilog2(EXT4_ADDR_PER_BLOCK(sb));
+ sbi->s_groups_count = groups_count;
+ sbi->s_blocks_per_group = blocks_per_group;
+ sbi->s_cluster_bits = cluster_bits;
+ sbi->s_cluster_ratio = 1U << cluster_bits;
+ sbi->s_clusters_per_group = blocks_per_group >> cluster_bits;
+ sbi->s_itb_per_group = 8;
+ sbi->s_gdb_count = 1;
+
+ ctx->gdb_bh.b_data = ctx->gdb_data;
+ ctx->gdb_bh.b_size = blocksize;
+ ctx->gdb_bh.b_bdev = sb->s_bdev;
+ atomic_set(&ctx->gdb_bh.b_count, 2);
+ set_buffer_uptodate(&ctx->gdb_bh);
+ ctx->gdb_bh_arr[0] = &ctx->gdb_bh;
+ rcu_assign_pointer(sbi->s_group_desc, ctx->gdb_bh_arr);
+
+ fsmap_active_ctx = ctx;
+ kunit_activate_static_stub(test, ext4_mballoc_query_range,
+ fsmap_query_range_stub);
+
+ return ctx;
+}
+
+static void fsmap_init_full_range_head(struct ext4_fsmap_head *head,
+ unsigned int count)
+{
+ memset(head, 0, sizeof(*head));
+ head->fmh_count = count;
+ head->fmh_keys[1].fmr_device = UINT_MAX;
+ head->fmh_keys[1].fmr_physical = U64_MAX;
+ head->fmh_keys[1].fmr_owner = U64_MAX;
+ head->fmh_keys[1].fmr_flags = UINT_MAX;
+}
+
+static void fsmap_expect_rec(struct kunit *test, const struct ext4_fsmap *rec,
+ u32 exp_dev, u64 exp_phys, u64 exp_len,
+ u64 exp_owner)
+{
+ KUNIT_EXPECT_EQ(test, (u32)rec->fmr_device, exp_dev);
+ KUNIT_EXPECT_EQ(test, rec->fmr_physical, exp_phys);
+ KUNIT_EXPECT_EQ(test, rec->fmr_length, exp_len);
+ KUNIT_EXPECT_EQ(test, rec->fmr_owner, exp_owner);
+ KUNIT_EXPECT_EQ(test, rec->fmr_flags, (u32)FMR_OF_SPECIAL_OWNER);
+}
+
+/*
+ * Verify ext4_fsmap_from_internal() and ext4_fsmap_to_internal() block-to-byte
+ * and byte-to-block scaling across 1K, 4K, and 64K block sizes.
+ */
+static void test_fsmap_unit_conversions(struct kunit *test)
+{
+ static const unsigned int shifts[] = { 10, 12, 16 };
+ struct fsmap_test_ctx *ctx;
+ struct ext4_fsmap internal = {};
+ struct ext4_fsmap roundtrip = {};
+ struct fsmap uapi = {};
+ int i;
+
+ ctx = fsmap_create_ctx(test, 12, 0, 1, 256);
+
+ for (i = 0; i < ARRAY_SIZE(shifts); i++) {
+ unsigned int bits = shifts[i];
+
+ ctx->sb->s_blocksize_bits = bits;
+ ctx->sb->s_blocksize = 1U << bits;
+
+ memset(&uapi, 0xaa, sizeof(uapi));
+ internal.fmr_device = new_encode_dev(FSMAP_TEST_DATADEV);
+ internal.fmr_flags = FMR_OF_SPECIAL_OWNER;
+ internal.fmr_physical = 42;
+ internal.fmr_owner = EXT4_FMR_OWN_INODES;
+ internal.fmr_length = 17;
+
+ ext4_fsmap_from_internal(ctx->sb, &uapi, &internal);
+ KUNIT_EXPECT_EQ(test, uapi.fmr_device,
+ (u32)internal.fmr_device);
+ KUNIT_EXPECT_EQ(test, uapi.fmr_flags, internal.fmr_flags);
+ KUNIT_EXPECT_EQ(test, uapi.fmr_physical, 42ULL << bits);
+ KUNIT_EXPECT_EQ(test, uapi.fmr_owner, EXT4_FMR_OWN_INODES);
+ KUNIT_EXPECT_EQ(test, uapi.fmr_offset, 0ULL);
+ KUNIT_EXPECT_EQ(test, uapi.fmr_length, 17ULL << bits);
+ KUNIT_EXPECT_EQ(test, uapi.fmr_reserved[0], 0U);
+ KUNIT_EXPECT_EQ(test, uapi.fmr_reserved[1], 0U);
+ KUNIT_EXPECT_EQ(test, uapi.fmr_reserved[2], 0U);
+
+ /* Non-block-aligned byte tails truncate down to whole blocks */
+ uapi.fmr_physical += (1ULL << bits) - 1;
+ uapi.fmr_length += (1ULL << bits) - 1;
+ ext4_fsmap_to_internal(ctx->sb, &roundtrip, &uapi);
+ KUNIT_EXPECT_EQ(test, roundtrip.fmr_device,
+ internal.fmr_device);
+ KUNIT_EXPECT_EQ(test, roundtrip.fmr_flags, internal.fmr_flags);
+ KUNIT_EXPECT_EQ(test, roundtrip.fmr_physical, 42ULL);
+ KUNIT_EXPECT_EQ(test, roundtrip.fmr_owner, EXT4_FMR_OWN_INODES);
+ KUNIT_EXPECT_EQ(test, roundtrip.fmr_length, 17ULL);
+ }
+}
+
+/*
+ * Verify ext4_getfsmap() input flag, device ID, key ordering, out-of-range
+ * physical start, and missing group descriptor (-EFSCORRUPTED) checks.
+ */
+static void test_getfsmap_invalid_flags_devices_and_keys(struct kunit *test)
+{
+ struct fsmap_test_ctx *ctx;
+ struct ext4_fsmap_head head;
+ struct fsmap_rec_sink *sink;
+ u32 datadev = new_encode_dev(FSMAP_TEST_DATADEV);
+ int err;
+
+ sink = kunit_kzalloc(test, sizeof(*sink), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, sink);
+ ctx = fsmap_create_ctx(test, 12, 0, 1, 256);
+ fsmap_set_group_tables(ctx, 0, 2, 3, 4);
+ fsmap_mark_clusters_used(ctx, 0, 0, 12);
+
+ /* Invalid fmh_iflags -> -EINVAL */
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ head.fmh_iflags = ~FMH_IF_VALID;
+ KUNIT_EXPECT_EQ(test, ext4_getfsmap(ctx->sb, &head,
+ fsmap_collect_cb, sink), -EINVAL);
+
+ /* Unrecognized low or high device ID -> -EINVAL */
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ head.fmh_keys[0].fmr_device = new_encode_dev(MKDEV(9, 9));
+ KUNIT_EXPECT_EQ(test, ext4_getfsmap(ctx->sb, &head,
+ fsmap_collect_cb, sink), -EINVAL);
+
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ head.fmh_keys[1].fmr_device = new_encode_dev(MKDEV(9, 9));
+ KUNIT_EXPECT_EQ(test, ext4_getfsmap(ctx->sb, &head,
+ fsmap_collect_cb, sink), -EINVAL);
+
+ /* Inverted device, physical block, or owner keys -> -EINVAL */
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ head.fmh_keys[0].fmr_device = UINT_MAX;
+ head.fmh_keys[1].fmr_device = 0;
+ KUNIT_EXPECT_EQ(test, ext4_getfsmap(ctx->sb, &head,
+ fsmap_collect_cb, sink), -EINVAL);
+
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ head.fmh_keys[0].fmr_device = datadev;
+ head.fmh_keys[1].fmr_device = datadev;
+ head.fmh_keys[0].fmr_physical = 50;
+ head.fmh_keys[1].fmr_physical = 40;
+ KUNIT_EXPECT_EQ(test, ext4_getfsmap(ctx->sb, &head,
+ fsmap_collect_cb, sink), -EINVAL);
+
+ /* Equal low+len and high physical with owner 0 == 0 -> -EINVAL */
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ head.fmh_keys[0].fmr_device = datadev;
+ head.fmh_keys[1].fmr_device = datadev;
+ head.fmh_keys[0].fmr_physical = 20;
+ head.fmh_keys[0].fmr_length = 10;
+ head.fmh_keys[1].fmr_physical = 30;
+ head.fmh_keys[1].fmr_owner = 0;
+ KUNIT_EXPECT_EQ(test, ext4_getfsmap(ctx->sb, &head,
+ fsmap_collect_cb, sink), -EINVAL);
+
+ /* Low key starting past eofs returns 0 entries cleanly */
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ head.fmh_keys[0].fmr_device = datadev;
+ head.fmh_keys[1].fmr_device = datadev;
+ head.fmh_keys[0].fmr_physical = 500;
+ head.fmh_keys[1].fmr_physical = 600;
+ err = ext4_getfsmap(ctx->sb, &head, fsmap_collect_cb, sink);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, head.fmh_entries, 0U);
+ KUNIT_EXPECT_EQ(test, head.fmh_oflags, (u32)FMH_OF_DEV_T);
+
+ /* Missing group descriptor returns -EFSCORRUPTED */
+ ctx->gdb_bh_arr[0] = NULL;
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ set_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+ err = ext4_getfsmap(ctx->sb, &head, fsmap_collect_cb, sink);
+ clear_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+ ctx->gdb_bh_arr[0] = &ctx->gdb_bh;
+ KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+}
+
+/*
+ * Verify synthesis of fixed per-group metadata (SB, GDT, reserved GDT, block
+ * bitmap, inode bitmap, inode table), free extents, and EXT4_FMR_OWN_UNKNOWN
+ * gaps both in the middle and at the very end of the filesystem.
+ */
+static void test_getfsmap_fixed_metadata_and_unknown_gaps(struct kunit *test)
+{
+ struct fsmap_test_ctx *ctx;
+ struct ext4_fsmap_head head;
+ struct fsmap_rec_sink *sink;
+ u32 datadev = new_encode_dev(FSMAP_TEST_DATADEV);
+ int err;
+
+ sink = kunit_kzalloc(test, sizeof(*sink), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, sink);
+ ctx = fsmap_create_ctx(test, 12, 0, 1, 256);
+ ctx->es.s_reserved_gdt_blocks = cpu_to_le16(2);
+
+ /*
+ * Group 0 layout [0, 256):
+ * 0 : superblock (OWN_FS, len 1)
+ * 1 : GDT (OWN_GDT, len 1)
+ * 2..3 : reserved GDT (OWN_RESV_GDT, len 2)
+ * 4 : block bitmap (OWN_BLKBM, len 1)
+ * 5 : inode bitmap (OWN_INOBM, len 1)
+ * 6..13 : inode table (OWN_INODES, len 8)
+ * 14..19 : allocated user data (OWN_UNKNOWN, len 6)
+ * 20..99 : free space (OWN_FREE, len 80)
+ * 100..109: allocated user data (OWN_UNKNOWN, len 10)
+ * 110..249: free space (OWN_FREE, len 140)
+ * 250..255: allocated user data at EOFS (OWN_UNKNOWN, len 6)
+ */
+ fsmap_set_group_tables(ctx, 0, 4, 5, 6);
+ fsmap_mark_clusters_used(ctx, 0, 0, 20);
+ fsmap_mark_clusters_used(ctx, 0, 100, 10);
+ fsmap_mark_clusters_used(ctx, 0, 250, 6);
+
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ err = ext4_getfsmap(ctx->sb, &head, fsmap_collect_cb, sink);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_ASSERT_EQ(test, head.fmh_entries, 11U);
+ KUNIT_ASSERT_EQ(test, sink->count, 11U);
+
+ fsmap_expect_rec(test, &sink->recs[0], datadev, 0, 1, EXT4_FMR_OWN_FS);
+ fsmap_expect_rec(test, &sink->recs[1], datadev, 1, 1, EXT4_FMR_OWN_GDT);
+ fsmap_expect_rec(test, &sink->recs[2], datadev, 2, 2,
+ EXT4_FMR_OWN_RESV_GDT);
+ fsmap_expect_rec(test, &sink->recs[3], datadev, 4, 1,
+ EXT4_FMR_OWN_BLKBM);
+ fsmap_expect_rec(test, &sink->recs[4], datadev, 5, 1,
+ EXT4_FMR_OWN_INOBM);
+ fsmap_expect_rec(test, &sink->recs[5], datadev, 6, 8,
+ EXT4_FMR_OWN_INODES);
+ fsmap_expect_rec(test, &sink->recs[6], datadev, 14, 6,
+ EXT4_FMR_OWN_UNKNOWN);
+ fsmap_expect_rec(test, &sink->recs[7], datadev, 20, 80,
+ EXT4_FMR_OWN_FREE);
+ fsmap_expect_rec(test, &sink->recs[8], datadev, 100, 10,
+ EXT4_FMR_OWN_UNKNOWN);
+ fsmap_expect_rec(test, &sink->recs[9], datadev, 110, 140,
+ EXT4_FMR_OWN_FREE);
+ fsmap_expect_rec(test, &sink->recs[10], datadev, 250, 6,
+ EXT4_FMR_OWN_UNKNOWN);
+}
+
+/*
+ * Verify flex_bg adjacent fixed-metadata merging via
+ * ext4_getfsmap_merge_fixed_metadata() and cross-group free-extent coalescing
+ * via info->gfi_lastfree.
+ */
+static void test_getfsmap_cross_group_free_and_fixed_meta_coalescing(
+ struct kunit *test)
+{
+ struct fsmap_test_ctx *ctx;
+ struct ext4_fsmap_head head;
+ struct fsmap_rec_sink *sink;
+ u32 datadev = new_encode_dev(FSMAP_TEST_DATADEV);
+ int err;
+
+ sink = kunit_kzalloc(test, sizeof(*sink), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, sink);
+ ctx = fsmap_create_ctx(test, 12, 0, 2, 256);
+ /* SPARSE_SUPER2 with 0 backup groups -> group 1 has no SB/GDT */
+ ctx->es.s_feature_compat = cpu_to_le32(EXT4_FEATURE_COMPAT_SPARSE_SUPER2);
+
+ /*
+ * Flex_bg-packed metadata in group 0 for both groups 0 and 1:
+ * 0 : SB (len 1)
+ * 1 : GDT (len 1)
+ * 2..3 : group 0 + group 1 block bitmaps -> merged len 2!
+ * 4..5 : group 0 + group 1 inode bitmaps -> merged len 2!
+ * 6..21 : group 0 + group 1 inode tables (8 + 8) -> merged len 16!
+ * 22..255 (grp 0) + 256..511 (grp 1): contiguous free space across
+ * group boundary -> coalesced via gfi_lastfree into [22, 512) len 490!
+ */
+ fsmap_set_group_tables(ctx, 0, 2, 4, 6);
+ fsmap_set_group_tables(ctx, 1, 3, 5, 14);
+ fsmap_mark_clusters_used(ctx, 0, 0, 22);
+
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ err = ext4_getfsmap(ctx->sb, &head, fsmap_collect_cb, sink);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, head.fmh_entries, 6U);
+
+ fsmap_expect_rec(test, &sink->recs[0], datadev, 0, 1, EXT4_FMR_OWN_FS);
+ fsmap_expect_rec(test, &sink->recs[1], datadev, 1, 1, EXT4_FMR_OWN_GDT);
+ fsmap_expect_rec(test, &sink->recs[2], datadev, 2, 2,
+ EXT4_FMR_OWN_BLKBM);
+ fsmap_expect_rec(test, &sink->recs[3], datadev, 4, 2,
+ EXT4_FMR_OWN_INOBM);
+ fsmap_expect_rec(test, &sink->recs[4], datadev, 6, 16,
+ EXT4_FMR_OWN_INODES);
+ fsmap_expect_rec(test, &sink->recs[5], datadev, 22, 490,
+ EXT4_FMR_OWN_FREE);
+}
+
+/*
+ * Verify 1K blocksize (s_first_data_block == 1), bigalloc cluster-to-block
+ * scaling, and META_BG suppression of reserved GDT blocks.
+ */
+static void test_getfsmap_1k_bofs_bigalloc_and_meta_bg(struct kunit *test)
+{
+ struct fsmap_test_ctx *ctx;
+ struct ext4_fsmap_head head;
+ struct fsmap_rec_sink *sink;
+ u32 datadev = new_encode_dev(FSMAP_TEST_DATADEV);
+ int err;
+
+ sink = kunit_kzalloc(test, sizeof(*sink), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, sink);
+
+ /*
+ * 1K blocks (s_first_data_block = 1), cluster_bits = 2 (4 blks/clu),
+ * 1 group of 256 blocks (64 clusters) covering fsblks [1, 257).
+ * Enable META_BG with s_first_meta_bg = 0 and verify s_reserved_gdt_blocks
+ * is ignored.
+ */
+ ctx = fsmap_create_ctx(test, 10, 2, 1, 256);
+ ext4_set_feature_meta_bg(ctx->sb);
+ ext4_set_feature_bigalloc(ctx->sb);
+ ctx->es.s_first_meta_bg = 0;
+ ctx->es.s_reserved_gdt_blocks = cpu_to_le16(5);
+
+ /*
+ * Cluster 0 = fsblks [1, 5): SB(1), GDT(2), BLKBM(3), INOBM(4)
+ * Clusters 1..2 = fsblks [5, 13): INODES (len 8)
+ * Clusters 3..4 = fsblks [13, 21): allocated data (OWN_UNKNOWN, len 8)
+ * Clusters 5..63 = fsblks [21, 257): free (OWN_FREE, len 236)
+ */
+ fsmap_set_group_tables(ctx, 0, 3, 4, 5);
+ fsmap_mark_clusters_used(ctx, 0, 0, 5);
+
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ err = ext4_getfsmap(ctx->sb, &head, fsmap_collect_cb, sink);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ /* Note: block 0 before s_first_data_block=1 is reported as gap [0, 1) */
+ KUNIT_ASSERT_EQ(test, head.fmh_entries, 8U);
+
+ fsmap_expect_rec(test, &sink->recs[0], datadev, 0, 1,
+ EXT4_FMR_OWN_UNKNOWN);
+ fsmap_expect_rec(test, &sink->recs[1], datadev, 1, 1, EXT4_FMR_OWN_FS);
+ fsmap_expect_rec(test, &sink->recs[2], datadev, 2, 1, EXT4_FMR_OWN_GDT);
+ fsmap_expect_rec(test, &sink->recs[3], datadev, 3, 1,
+ EXT4_FMR_OWN_BLKBM);
+ fsmap_expect_rec(test, &sink->recs[4], datadev, 4, 1,
+ EXT4_FMR_OWN_INOBM);
+ fsmap_expect_rec(test, &sink->recs[5], datadev, 5, 8,
+ EXT4_FMR_OWN_INODES);
+ fsmap_expect_rec(test, &sink->recs[6], datadev, 13, 8,
+ EXT4_FMR_OWN_UNKNOWN);
+ fsmap_expect_rec(test, &sink->recs[7], datadev, 21, 236,
+ EXT4_FMR_OWN_FREE);
+}
+
+/*
+ * Verify count-only queries (fmh_count == 0), bounded pagination with
+ * continuation keys (non-zero low fmr_length), and formatter abort propagation.
+ */
+static void test_getfsmap_count_only_pagination_and_abort(struct kunit *test)
+{
+ struct fsmap_test_ctx *ctx;
+ struct ext4_fsmap_head head;
+ struct fsmap_rec_sink *full, *page, *cb_abort;
+ unsigned int collected = 0;
+ int err;
+
+ full = kunit_kzalloc(test, sizeof(*full), GFP_KERNEL);
+ page = kunit_kzalloc(test, sizeof(*page), GFP_KERNEL);
+ cb_abort = kunit_kzalloc(test, sizeof(*cb_abort), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, full);
+ KUNIT_ASSERT_NOT_NULL(test, page);
+ KUNIT_ASSERT_NOT_NULL(test, cb_abort);
+
+ ctx = fsmap_create_ctx(test, 12, 0, 1, 256);
+ fsmap_set_group_tables(ctx, 0, 2, 3, 4);
+ fsmap_mark_clusters_used(ctx, 0, 0, 16);
+
+ /* Reference full run: SB(0), GDT(1), BLKBM(2), INOBM(3), INODES(4..11),
+ * UNKNOWN(12..15), FREE(16..255) -> 7 records.
+ */
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ err = ext4_getfsmap(ctx->sb, &head, fsmap_collect_cb, full);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_ASSERT_EQ(test, full->count, 7U);
+
+ /* 1. Count-only mode (fmh_count == 0) does not invoke formatter */
+ fsmap_init_full_range_head(&head, 0);
+ err = ext4_getfsmap(ctx->sb, &head, NULL, NULL);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, head.fmh_entries, 7U);
+
+ /* 2. Paginate 2 records at a time using last record as low key */
+ fsmap_init_full_range_head(&head, 2);
+ while (true) {
+ unsigned int prev_count = page->count;
+
+ err = ext4_getfsmap(ctx->sb, &head, fsmap_collect_cb, page);
+ collected += head.fmh_entries;
+ if (err == 0)
+ break;
+ KUNIT_ASSERT_EQ(test, err, EXT4_QUERY_RANGE_ABORT);
+ KUNIT_ASSERT_EQ(test, head.fmh_entries, 2U);
+ KUNIT_ASSERT_GT(test, page->count, prev_count);
+ head.fmh_keys[0] = page->recs[page->count - 1];
+ }
+ KUNIT_ASSERT_EQ(test, collected, 7U);
+ KUNIT_ASSERT_EQ(test, page->count, 7U);
+ for (unsigned int i = 0; i < 7; i++) {
+ KUNIT_EXPECT_EQ(test, page->recs[i].fmr_physical,
+ full->recs[i].fmr_physical);
+ KUNIT_EXPECT_EQ(test, page->recs[i].fmr_length,
+ full->recs[i].fmr_length);
+ KUNIT_EXPECT_EQ(test, page->recs[i].fmr_owner,
+ full->recs[i].fmr_owner);
+ }
+
+ /* 3. Formatter-initiated abort stops immediately */
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ cb_abort->abort_after = 3;
+ err = ext4_getfsmap(ctx->sb, &head, fsmap_collect_cb, cb_abort);
+ KUNIT_EXPECT_EQ(test, err, EXT4_QUERY_RANGE_ABORT);
+ KUNIT_EXPECT_EQ(test, cb_abort->count, 3U);
+}
+
+/*
+ * Verify external journal device reporting (EXT4_FMR_OWN_LOG and leading gap)
+ * across multi-device wildcard queries and log-device-targeted queries.
+ */
+static void test_getfsmap_external_logdev_and_multi_device(struct kunit *test)
+{
+ struct fsmap_test_ctx *ctx;
+ struct ext4_fsmap_head head;
+ struct fsmap_rec_sink *sink;
+ u32 datadev = new_encode_dev(FSMAP_TEST_DATADEV);
+ u32 logdev = new_encode_dev(FSMAP_TEST_LOGDEV);
+ int err;
+
+ sink = kunit_kzalloc(test, sizeof(*sink), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, sink);
+ ctx = fsmap_create_ctx(test, 12, 0, 1, 128);
+ fsmap_set_group_tables(ctx, 0, 2, 3, 4);
+ fsmap_mark_clusters_used(ctx, 0, 0, 12);
+
+ KUNIT_ASSERT_PTR_EQ(test, file_bdev(&ctx->log_bdev_file),
+ &ctx->log_bdev.bdev);
+ ctx->mock_journal.j_blk_offset = 16;
+ ctx->mock_journal.j_total_len = 64;
+ ctx->sbi.s_journal = &ctx->mock_journal;
+ ctx->sbi.s_journal_bdev_file = &ctx->log_bdev_file;
+
+ /* Multi-device query [0, UINT_MAX] emits datadev (6 recs) + logdev (2) */
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ err = ext4_getfsmap(ctx->sb, &head, fsmap_collect_cb, sink);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_ASSERT_EQ(test, head.fmh_entries, 8U);
+ KUNIT_EXPECT_EQ(test, (u32)sink->recs[0].fmr_device, datadev);
+ fsmap_expect_rec(test, &sink->recs[6], logdev, 0, 16,
+ EXT4_FMR_OWN_UNKNOWN);
+ fsmap_expect_rec(test, &sink->recs[7], logdev, 16, 64,
+ EXT4_FMR_OWN_LOG);
+
+ /* Targeted query against only logdev starting at 0 */
+ memset(sink, 0, sizeof(*sink));
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ head.fmh_keys[0].fmr_device = logdev;
+ head.fmh_keys[1].fmr_device = logdev;
+ err = ext4_getfsmap(ctx->sb, &head, fsmap_collect_cb, sink);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_ASSERT_EQ(test, head.fmh_entries, 2U);
+ fsmap_expect_rec(test, &sink->recs[0], logdev, 0, 16,
+ EXT4_FMR_OWN_UNKNOWN);
+ fsmap_expect_rec(test, &sink->recs[1], logdev, 16, 64,
+ EXT4_FMR_OWN_LOG);
+
+ /* Logdev query with physical > 0 returns 0 entries */
+ memset(sink, 0, sizeof(*sink));
+ fsmap_init_full_range_head(&head, FSMAP_TEST_MAX_RECS);
+ head.fmh_keys[0].fmr_device = logdev;
+ head.fmh_keys[0].fmr_physical = 80;
+ head.fmh_keys[1].fmr_device = logdev;
+ err = ext4_getfsmap(ctx->sb, &head, fsmap_collect_cb, sink);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, head.fmh_entries, 0U);
+}
+
+static struct kunit_case fsmap_test_cases[] = {
+ KUNIT_CASE(test_fsmap_unit_conversions),
+ KUNIT_CASE(test_getfsmap_invalid_flags_devices_and_keys),
+ KUNIT_CASE(test_getfsmap_fixed_metadata_and_unknown_gaps),
+ KUNIT_CASE(test_getfsmap_cross_group_free_and_fixed_meta_coalescing),
+ KUNIT_CASE(test_getfsmap_1k_bofs_bigalloc_and_meta_bg),
+ KUNIT_CASE(test_getfsmap_count_only_pagination_and_abort),
+ KUNIT_CASE(test_getfsmap_external_logdev_and_multi_device),
+ {}
+};
+
+static struct kunit_suite fsmap_test_suite = {
+ .name = "ext4_fsmap_test",
+ .test_cases = fsmap_test_cases,
+};
+
+kunit_test_suites(&fsmap_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 FS_IOC_GETFSMAP implementation");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/fsmap.c b/fs/ext4/fsmap.c
index 8b1138d2dc8e..f41c2298d196 100644
--- a/fs/ext4/fsmap.c
+++ b/fs/ext4/fsmap.c
@@ -26,6 +26,7 @@ void ext4_fsmap_from_internal(struct super_block *sb, struct fsmap *dest,
dest->fmr_reserved[1] = 0;
dest->fmr_reserved[2] = 0;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fsmap_from_internal);
/* Convert an fsmap to an ext4_fsmap. */
void ext4_fsmap_to_internal(struct super_block *sb, struct ext4_fsmap *dest,
@@ -37,6 +38,7 @@ void ext4_fsmap_to_internal(struct super_block *sb, struct ext4_fsmap *dest,
dest->fmr_owner = src->fmr_owner;
dest->fmr_length = src->fmr_length >> sb->s_blocksize_bits;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fsmap_to_internal);
/* getfsmap query state */
struct ext4_getfsmap_info {
@@ -790,3 +792,4 @@ int ext4_getfsmap(struct super_block *sb, struct ext4_fsmap_head *head,
head->fmh_oflags = FMH_OF_DEV_T;
return error;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_getfsmap);
diff --git a/fs/ext4/mballoc.c b/fs/ext4/mballoc.c
index 3f06b0996063..3d3512ede16f 100644
--- a/fs/ext4/mballoc.c
+++ b/fs/ext4/mballoc.c
@@ -7151,6 +7151,9 @@ ext4_mballoc_query_range(
struct ext4_buddy e4b;
int error;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_mballoc_query_range, sb, group, first,
+ end, meta_formatter, formatter, priv);
+
error = ext4_mb_load_buddy(sb, group, &e4b);
if (error)
return error;
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 25/27] ext4: add KUnit tests for fs/ext4/move_extent.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (23 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 24/27] ext4: add KUnit tests for fs/ext4/fsmap.c Theodore Ts'o
@ 2026-11-12 4:06 ` 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:06 ` [PATCH 27/27] ext4: add KUnit tests for fs/ext4/dir.c Theodore Ts'o
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add a KUnit test suite (ext4_move_extent_test) covering online extent
swapping and validation helpers in fs/ext4/move_extent.c:
- Ordered i_data_sem lock acquisition/release in
ext4_double_down_write_data_sem() and ext4_double_up_write_data_sem()
- File, feature, and mode checks in mext_check_validity() (same inode,
cross-sb, non-regular, bigalloc, DAX, data journaling, encrypted,
non-extent, SUID/SGID, immutable/append, swapfile, quota, zero size)
- Sub-page alignment, EXT_MAX_BLOCKS bounds, and EOF clamping in
mext_check_adjust_range()
- End-to-end ext4_move_extents() across origin/donor holes, unwritten
extent moves, written MEXT_COPY_DATA with extent splitting and page
cache preservation, unwritten-origin zeroing, and -EBUSY release
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 3 +-
fs/ext4/ext4.h | 8 +
fs/ext4/move_extent-test.c | 997 +++++++++++++++++++++++++++++++++++++
fs/ext4/move_extent.c | 21 +-
4 files changed, 1026 insertions(+), 3 deletions(-)
create mode 100644 fs/ext4/move_extent-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index e1964407be24..5cf735df7656 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -23,7 +23,8 @@ ext4-test-objs += inode-test.o mballoc-test.o \
block_validity-test.o super-test.o \
sysfs-test.o ioctl-test.o \
resize-test.o migrate-test.o \
- indirect-test.o fsmap-test.o
+ indirect-test.o fsmap-test.o \
+ move_extent-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 4db096244a81..8bc7269af580 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -4180,6 +4180,14 @@ extern void ext4_double_up_write_data_sem(struct inode *orig_inode,
extern int ext4_move_extents(struct file *o_filp, struct file *d_filp,
__u64 start_orig, __u64 start_donor,
__u64 len, __u64 *moved_len);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+extern int ext4_mext_check_validity_test(struct inode *orig_inode,
+ struct inode *donor_inode);
+extern int ext4_mext_check_adjust_range_test(struct inode *orig_inode,
+ struct inode *donor_inode,
+ __u64 orig_start,
+ __u64 donor_start, __u64 *len);
+#endif
/* page-io.c */
extern int __init ext4_init_pageio(void);
diff --git a/fs/ext4/move_extent-test.c b/fs/ext4/move_extent-test.c
new file mode 100644
index 000000000000..e29a392eeebe
--- /dev/null
+++ b/fs/ext4/move_extent-test.c
@@ -0,0 +1,997 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/move_extent.c
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/fscrypt.h>
+#include <linux/iversion.h>
+#include <linux/mm.h>
+#include <linux/module.h>
+#include <linux/pagemap.h>
+#include <linux/part_stat.h>
+#include <linux/percpu.h>
+#include <linux/percpu_counter.h>
+#include <linux/quotaops.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/timer.h>
+#include <linux/workqueue.h>
+
+#include "ext4.h"
+#include "ext4_extents.h"
+#include "ext4_jbd2.h"
+
+#define MEXT_TEST_BLK_SIZE 4096U
+#define MEXT_TEST_BLK_BITS 12U
+#define MEXT_TEST_BLOCKS_PER_GRP 32768U
+#define MEXT_TEST_NGROUPS 8U
+#define MEXT_TEST_FIRST_DATA_BLK 1U
+#define MEXT_TEST_TOTAL_BLKS (MEXT_TEST_FIRST_DATA_BLK + \
+ MEXT_TEST_NGROUPS * \
+ MEXT_TEST_BLOCKS_PER_GRP)
+#define MEXT_MAX_META_BLKS 16U
+#define MEXT_META_BASE_PBLK 1000U
+
+struct mext_meta_blk {
+ ext4_fsblk_t pblk;
+ struct buffer_head bh;
+ u8 data[MEXT_TEST_BLK_SIZE];
+ bool in_use;
+};
+
+struct mext_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 address_space bdev_mapping;
+ struct buffer_head sbh;
+ u8 sbh_data[MEXT_TEST_BLK_SIZE];
+ struct mext_meta_blk *meta_blks;
+ ext4_fsblk_t next_meta_pblk;
+ struct inode *orig_inode;
+ struct inode *donor_inode;
+ struct dentry orig_dentry;
+ struct dentry donor_dentry;
+ struct file orig_file;
+ struct file donor_file;
+ bool es_shrinker_inited;
+ bool counters_inited;
+};
+
+static struct kunit *active_mext_test;
+
+static void mext_noop_work(struct work_struct *work)
+{
+}
+
+static void mext_noop_timer(struct timer_list *t)
+{
+}
+
+static unsigned long mext_dummy_shrinker_count(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return 0;
+}
+
+static unsigned long mext_dummy_shrinker_scan(struct shrinker *shrink,
+ struct shrink_control *sc)
+{
+ return SHRINK_STOP;
+}
+
+static struct inode *mext_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 = sizeof(struct ext4_inode) - EXT4_GOOD_OLD_INODE_SIZE;
+ ei->i_last_alloc_group = ~0U;
+
+ return &ei->vfs_inode;
+}
+
+static void mext_test_free_inode(struct inode *inode)
+{
+ kfree(EXT4_I(inode));
+}
+
+static void mext_test_evict_inode(struct inode *inode)
+{
+ truncate_inode_pages_final(&inode->i_data);
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ clear_inode(inode);
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **mext_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+
+static qsize_t *mext_get_reserved_space(struct inode *inode)
+{
+ return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations mext_dummy_qops = {
+ .get_reserved_space = mext_get_reserved_space,
+};
+#endif
+
+static const struct super_operations mext_test_sops = {
+ .alloc_inode = mext_test_alloc_inode,
+ .free_inode = mext_test_free_inode,
+ .evict_inode = mext_test_evict_inode,
+#ifdef CONFIG_QUOTA
+ .get_dquots = mext_get_dquots,
+#endif
+};
+
+static int mext_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static int mext_set_super(struct super_block *sb, struct fs_context *fc)
+{
+ return set_anon_super_fc(sb, fc);
+}
+
+static struct file_system_type mext_test_fs_type = {
+ .name = "ext4_move_extent_test",
+ .init_fs_context = mext_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static int mext_ext_dirty_stub(const char *where, unsigned int line,
+ handle_t *handle, struct inode *inode,
+ struct ext4_ext_path *path)
+{
+ if (path && path->p_bh) {
+ ext4_extent_block_csum_set_test(inode,
+ ext_block_hdr(path->p_bh));
+ set_buffer_verified(path->p_bh);
+ }
+ return 0;
+}
+
+static int mext_handle_dirty_metadata_stub(const char *where,
+ unsigned int line,
+ handle_t *handle,
+ struct inode *inode,
+ struct buffer_head *bh)
+{
+ if (bh)
+ set_buffer_uptodate(bh);
+ return 0;
+}
+
+static int mext_mark_inode_dirty_stub(handle_t *handle, struct inode *inode,
+ const char *func, unsigned int line)
+{
+ return 0;
+}
+
+static struct buffer_head *mext_ext_getblk_stub(struct inode *inode,
+ ext4_fsblk_t pblk,
+ gfp_t gfp_flags)
+{
+ struct kunit *test = active_mext_test;
+ struct mext_test_ctx *ctx;
+ struct mext_meta_blk *slot = NULL;
+ unsigned int i;
+
+ if (!test)
+ return NULL;
+ ctx = test->priv;
+ if (!ctx || !ctx->meta_blks)
+ return NULL;
+
+ for (i = 0; i < MEXT_MAX_META_BLKS; i++) {
+ if (ctx->meta_blks[i].in_use &&
+ ctx->meta_blks[i].pblk == pblk) {
+ slot = &ctx->meta_blks[i];
+ break;
+ }
+ }
+
+ if (!slot) {
+ for (i = 0; i < MEXT_MAX_META_BLKS; i++) {
+ if (!ctx->meta_blks[i].in_use) {
+ slot = &ctx->meta_blks[i];
+ break;
+ }
+ }
+ if (!slot)
+ return NULL;
+
+ memset(slot, 0, sizeof(*slot));
+ slot->in_use = true;
+ slot->pblk = pblk;
+ slot->bh.b_data = slot->data;
+ slot->bh.b_size = inode->i_sb->s_blocksize;
+ slot->bh.b_blocknr = pblk;
+ slot->bh.b_bdev = inode->i_sb->s_bdev;
+ atomic_set(&slot->bh.b_count, 1);
+ set_buffer_uptodate(&slot->bh);
+ set_buffer_mapped(&slot->bh);
+ }
+
+ get_bh(&slot->bh);
+ return &slot->bh;
+}
+
+static ext4_fsblk_t mext_mb_new_blocks_stub(handle_t *handle,
+ struct ext4_allocation_request *ar,
+ int *errp)
+{
+ struct kunit *test = active_mext_test;
+ struct mext_test_ctx *ctx;
+ ext4_fsblk_t pblk;
+
+ if (!test) {
+ *errp = -EIO;
+ return 0;
+ }
+ ctx = test->priv;
+ pblk = ctx->next_meta_pblk;
+ ctx->next_meta_pblk += ar->len;
+ *errp = 0;
+ return pblk;
+}
+
+static void mext_free_blocks_stub(handle_t *handle, struct inode *inode,
+ struct buffer_head *bh, ext4_fsblk_t block,
+ unsigned long count, int flags)
+{
+}
+
+static int mext_journal_data_mode_stub(struct inode *inode)
+{
+ return EXT4_INODE_JOURNAL_DATA_MODE;
+}
+
+static void mext_init_mock_file(struct file *filp, struct dentry *dentry,
+ struct inode *inode)
+{
+ struct file tmp = {
+ .f_path = { .dentry = dentry },
+ .f_inode = inode,
+ .f_mapping = inode->i_mapping,
+ .f_mode = FMODE_READ | FMODE_WRITE | FMODE_NOCMTIME,
+ };
+
+ memset(dentry, 0, sizeof(*dentry));
+ dentry->d_inode = inode;
+ memcpy(filp, &tmp, sizeof(*filp));
+}
+
+static struct inode *mext_create_test_inode(struct kunit *test,
+ struct super_block *sb,
+ unsigned long ino,
+ ext4_lblk_t size_blks)
+{
+ struct ext4_extent_header *eh;
+ struct inode *inode;
+
+ inode = new_inode(sb);
+ KUNIT_ASSERT_NOT_NULL(test, inode);
+
+ inode->i_ino = ino;
+ inode->i_mode = S_IFREG | 0644;
+ set_nlink(inode, 1);
+ inode->i_blkbits = sb->s_blocksize_bits;
+ /*
+ * Note: leave S_NOQUOTA clear so ext4_is_quota_file(inode) is false.
+ */
+ inode->i_flags |= S_NOSEC;
+ inode_state_set_raw(inode,
+ I_DIRTY_SYNC | I_DIRTY_DATASYNC | I_DIRTY_PAGES);
+ mapping_set_gfp_mask(&inode->i_data, GFP_KERNEL);
+
+ EXT4_I(inode)->i_csum_seed = 0x13572468U;
+ EXT4_I(inode)->i_disksize = (loff_t)size_blks << sb->s_blocksize_bits;
+ i_size_write(inode, EXT4_I(inode)->i_disksize);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ext4_set_aops(inode);
+
+ eh = ext_inode_hdr(inode);
+ memset(EXT4_I(inode)->i_data, 0, sizeof(EXT4_I(inode)->i_data));
+ eh->eh_magic = EXT4_EXT_MAGIC;
+ eh->eh_entries = 0;
+ eh->eh_max = cpu_to_le16(ext4_ext_space_root_idx_test(inode, 0));
+ eh->eh_depth = 0;
+
+ return inode;
+}
+
+static void mext_insert_extent(struct kunit *test, struct inode *inode,
+ ext4_lblk_t lblk, u16 len, ext4_fsblk_t pblk,
+ bool unwritten)
+{
+ struct ext4_extent newex = {};
+ struct ext4_ext_path *path;
+
+ newex.ee_block = cpu_to_le32(lblk);
+ newex.ee_len = cpu_to_le16(len);
+ if (unwritten)
+ ext4_ext_mark_unwritten(&newex);
+ ext4_ext_store_pblock(&newex, pblk);
+
+ inode_lock(inode);
+ down_write(&EXT4_I(inode)->i_data_sem);
+ path = ext4_find_extent(inode, lblk, NULL, 0);
+ if (!IS_ERR(path))
+ path = ext4_ext_insert_extent(NULL, inode, path, &newex, 0);
+ up_write(&EXT4_I(inode)->i_data_sem);
+ inode_unlock(inode);
+
+ KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+ ext4_free_ext_path(path);
+}
+
+static void mext_populate_folio_block(struct kunit *test, struct inode *inode,
+ ext4_lblk_t lblk, u8 pattern)
+{
+ loff_t pos = (loff_t)lblk << inode->i_blkbits;
+ size_t bsize = i_blocksize(inode);
+ struct folio *folio;
+ u8 *addr;
+
+ folio = __filemap_get_folio(inode->i_mapping, pos >> PAGE_SHIFT,
+ FGP_CREAT | FGP_LOCK, GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+
+ addr = folio_address(folio);
+ memset(addr + offset_in_folio(folio, pos), pattern, bsize);
+ folio_mark_uptodate(folio);
+ folio_unlock(folio);
+ folio_put(folio);
+}
+
+static void mext_assert_mapped_block(struct kunit *test, struct inode *inode,
+ ext4_lblk_t lblk, ext4_fsblk_t exp_pblk,
+ unsigned int exp_flag)
+{
+ struct ext4_map_blocks map = {
+ .m_lblk = lblk,
+ .m_len = 1,
+ };
+ int ret;
+
+ inode_lock(inode);
+ ret = ext4_map_blocks(NULL, inode, &map, 0);
+ inode_unlock(inode);
+
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, map.m_pblk, exp_pblk);
+ KUNIT_EXPECT_TRUE(test, (map.m_flags & exp_flag) != 0);
+ if (exp_flag == EXT4_MAP_MAPPED)
+ KUNIT_EXPECT_EQ(test, map.m_flags & EXT4_MAP_UNWRITTEN, 0U);
+ else if (exp_flag == EXT4_MAP_UNWRITTEN)
+ KUNIT_EXPECT_EQ(test, map.m_flags & EXT4_MAP_MAPPED, 0U);
+}
+
+static void mext_cleanup_ctx(void *data)
+{
+ struct mext_test_ctx *ctx = data;
+
+ active_mext_test = NULL;
+
+ if (ctx->orig_inode) {
+ iput(ctx->orig_inode);
+ ctx->orig_inode = NULL;
+ }
+ if (ctx->donor_inode) {
+ iput(ctx->donor_inode);
+ ctx->donor_inode = NULL;
+ }
+
+ cancel_work_sync(&ctx->sbi.s_sb_upd_work);
+ timer_delete_sync(&ctx->sbi.s_err_report);
+
+ if (ctx->es_shrinker_inited) {
+ ext4_es_unregister_shrinker(&ctx->sbi);
+ ctx->es_shrinker_inited = false;
+ }
+ if (ctx->counters_inited) {
+ percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+ percpu_counter_destroy(&ctx->sbi.s_dirtyclusters_counter);
+ ctx->counters_inited = false;
+ }
+
+ if (ctx->sb) {
+ ctx->sb->s_fs_info = NULL;
+ deactivate_super(ctx->sb);
+ ctx->sb = NULL;
+ }
+ if (ctx->bdev.bd_stats) {
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ }
+}
+
+static struct mext_test_ctx *mext_setup_ctx(struct kunit *test)
+{
+ struct mext_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ int err;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ ctx->es = kunit_kzalloc(test, sizeof(*ctx->es), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->es);
+ ctx->meta_blks = kunit_kcalloc(test, MEXT_MAX_META_BLKS,
+ sizeof(*ctx->meta_blks), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->meta_blks);
+ ctx->next_meta_pblk = MEXT_META_BASE_PBLK;
+
+ fc = fs_context_for_mount(&mext_test_fs_type, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fc));
+ sb = sget_fc(fc, NULL, mext_set_super);
+ put_fs_context(fc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(sb));
+ up_write(&sb->s_umount);
+ ctx->sb = sb;
+
+ INIT_WORK(&ctx->sbi.s_sb_upd_work, mext_noop_work);
+ timer_setup(&ctx->sbi.s_err_report, mext_noop_timer, 0);
+
+ err = kunit_add_action_or_reset(test, mext_cleanup_ctx, ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ ctx->bdev.bd_stats = alloc_percpu(struct disk_stats);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->bdev.bd_stats);
+ 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->bdev_mapping;
+ bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+
+ sb->s_bdev = &ctx->bdev;
+ sb->s_blocksize = MEXT_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = MEXT_TEST_BLK_BITS;
+ sb->s_maxbytes = MAX_LFS_FILESIZE;
+ sb->s_magic = EXT4_SUPER_MAGIC;
+ sb->s_op = &mext_test_sops;
+#ifdef CONFIG_QUOTA
+ sb->dq_op = &mext_dummy_qops;
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
+#endif
+ sb->s_fs_info = &ctx->sbi;
+ strscpy(sb->s_id, "ext4_mext_kunit", sizeof(sb->s_id));
+
+ ctx->sbh.b_data = ctx->sbh_data;
+ ctx->sbh.b_size = MEXT_TEST_BLK_SIZE;
+ ctx->sbh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->sbh.b_count, 2);
+ set_buffer_uptodate(&ctx->sbh);
+ clear_buffer_mapped(&ctx->sbh);
+
+ 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_data_block = cpu_to_le32(MEXT_TEST_FIRST_DATA_BLK);
+ ctx->es->s_log_block_size = cpu_to_le32(2);
+ ctx->es->s_log_cluster_size = cpu_to_le32(2);
+ ctx->es->s_blocks_per_group = cpu_to_le32(MEXT_TEST_BLOCKS_PER_GRP);
+ ctx->es->s_clusters_per_group = cpu_to_le32(MEXT_TEST_BLOCKS_PER_GRP);
+ ctx->es->s_inodes_per_group = cpu_to_le32(8192);
+ ctx->es->s_inodes_count = cpu_to_le32(MEXT_TEST_NGROUPS * 8192);
+ ctx->es->s_feature_incompat = cpu_to_le32(EXT4_FEATURE_INCOMPAT_EXTENTS);
+ ext4_blocks_count_set(ctx->es, MEXT_TEST_TOTAL_BLKS);
+
+ bgl_lock_init(&ctx->bgl);
+ ctx->sbi.s_sb = sb;
+ ctx->sbi.s_es = ctx->es;
+ ctx->sbi.s_sbh = &ctx->sbh;
+ ctx->sbi.s_blockgroup_lock = &ctx->bgl;
+ ctx->sbi.s_blocks_per_group = MEXT_TEST_BLOCKS_PER_GRP;
+ ctx->sbi.s_clusters_per_group = MEXT_TEST_BLOCKS_PER_GRP;
+ ctx->sbi.s_cluster_ratio = 1;
+ ctx->sbi.s_cluster_bits = 0;
+ ctx->sbi.s_groups_count = MEXT_TEST_NGROUPS;
+ ctx->sbi.s_desc_size = EXT4_MIN_DESC_SIZE;
+ ctx->sbi.s_desc_per_block = MEXT_TEST_BLK_SIZE / EXT4_MIN_DESC_SIZE;
+ ctx->sbi.s_inodes_per_group = 8192;
+ ctx->sbi.s_inode_size = sizeof(struct ext4_inode);
+ ctx->sbi.s_inodes_per_block = MEXT_TEST_BLK_SIZE /
+ sizeof(struct ext4_inode);
+ ctx->sbi.s_bitmap_maxbytes = MAX_LFS_FILESIZE;
+ set_opt(sb, ERRORS_CONT);
+
+ mutex_init(&ctx->sbi.s_orphan_lock);
+ INIT_LIST_HEAD(&ctx->sbi.s_orphan);
+ spin_lock_init(&ctx->sbi.s_bdev_wb_lock);
+ spin_lock_init(&ctx->sbi.s_error_lock);
+ spin_lock_init(&ctx->sbi.s_md_lock);
+ INIT_LIST_HEAD(&ctx->sbi.s_fc_q[FC_Q_MAIN]);
+ INIT_LIST_HEAD(&ctx->sbi.s_fc_q[FC_Q_STAGING]);
+ INIT_LIST_HEAD(&ctx->sbi.s_fc_dentry_q[FC_Q_MAIN]);
+ INIT_LIST_HEAD(&ctx->sbi.s_fc_dentry_q[FC_Q_STAGING]);
+ mutex_init(&ctx->sbi.s_fc_lock);
+ ratelimit_state_init(&ctx->sbi.s_err_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL,
+ DEFAULT_RATELIMIT_BURST);
+ ratelimit_state_init(&ctx->sbi.s_warning_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL,
+ DEFAULT_RATELIMIT_BURST);
+ ratelimit_state_init(&ctx->sbi.s_msg_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL,
+ DEFAULT_RATELIMIT_BURST);
+
+ err = percpu_counter_init(&ctx->sbi.s_freeclusters_counter, 65536,
+ GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ err = percpu_counter_init(&ctx->sbi.s_dirtyclusters_counter, 0,
+ GFP_KERNEL);
+ if (err) {
+ percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
+ KUNIT_FAIL_AND_ABORT(test, "dirtyclusters counter init failed");
+ }
+ ctx->counters_inited = true;
+
+ err = ext4_es_register_shrinker(&ctx->sbi);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ ctx->sbi.s_es_shrinker->count_objects = mext_dummy_shrinker_count;
+ ctx->sbi.s_es_shrinker->scan_objects = mext_dummy_shrinker_scan;
+ ctx->es_shrinker_inited = true;
+
+ ctx->orig_inode = mext_create_test_inode(test, sb, 21, 16);
+ ctx->donor_inode = mext_create_test_inode(test, sb, 22, 16);
+ mext_init_mock_file(&ctx->orig_file, &ctx->orig_dentry,
+ ctx->orig_inode);
+ mext_init_mock_file(&ctx->donor_file, &ctx->donor_dentry,
+ ctx->donor_inode);
+
+ test->priv = ctx;
+ active_mext_test = test;
+
+ kunit_activate_static_stub(test, __ext4_ext_dirty,
+ mext_ext_dirty_stub);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ mext_handle_dirty_metadata_stub);
+ kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+ mext_mark_inode_dirty_stub);
+ kunit_activate_static_stub(test, ext4_ext_getblk,
+ mext_ext_getblk_stub);
+ kunit_activate_static_stub(test, ext4_mb_new_blocks,
+ mext_mb_new_blocks_stub);
+ kunit_activate_static_stub(test, ext4_free_blocks,
+ mext_free_blocks_stub);
+
+ return ctx;
+}
+
+static void test_double_data_sem_lock_order(struct kunit *test)
+{
+ struct mext_test_ctx *ctx = mext_setup_ctx(test);
+ struct inode *low = ctx->orig_inode;
+ struct inode *high = ctx->donor_inode;
+
+ if (low > high)
+ swap(low, high);
+
+ /* Acquire with first < second */
+ ext4_double_down_write_data_sem(low, high);
+ KUNIT_EXPECT_TRUE(test, rwsem_is_locked(&EXT4_I(low)->i_data_sem));
+ KUNIT_EXPECT_TRUE(test, rwsem_is_locked(&EXT4_I(high)->i_data_sem));
+ ext4_double_up_write_data_sem(low, high);
+ KUNIT_EXPECT_FALSE(test, rwsem_is_locked(&EXT4_I(low)->i_data_sem));
+ KUNIT_EXPECT_FALSE(test, rwsem_is_locked(&EXT4_I(high)->i_data_sem));
+
+ /* Acquire with first > second */
+ ext4_double_down_write_data_sem(high, low);
+ KUNIT_EXPECT_TRUE(test, rwsem_is_locked(&EXT4_I(low)->i_data_sem));
+ KUNIT_EXPECT_TRUE(test, rwsem_is_locked(&EXT4_I(high)->i_data_sem));
+ ext4_double_up_write_data_sem(high, low);
+ KUNIT_EXPECT_FALSE(test, rwsem_is_locked(&EXT4_I(low)->i_data_sem));
+ KUNIT_EXPECT_FALSE(test, rwsem_is_locked(&EXT4_I(high)->i_data_sem));
+}
+
+static void test_mext_check_validity_rejections(struct kunit *test)
+{
+ struct mext_test_ctx *ctx = mext_setup_ctx(test);
+ struct inode *orig = ctx->orig_inode;
+ struct inode *donor = ctx->donor_inode;
+ struct super_block *other_sb;
+ __u64 moved_len = 99;
+ int ret;
+
+ /* Valid baseline */
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor), 0);
+
+ /* Same inode rejected both directly and via ext4_move_extents() */
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, orig),
+ -EINVAL);
+ ret = ext4_move_extents(&ctx->orig_file, &ctx->orig_file, 0, 0, 1,
+ &moved_len);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+ KUNIT_EXPECT_EQ(test, moved_len, 0ULL);
+
+ /* Cross-superblock rejected */
+ other_sb = kunit_kzalloc(test, sizeof(*other_sb), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, other_sb);
+ donor->i_sb = other_sb;
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EINVAL);
+ donor->i_sb = ctx->sb;
+
+ /* Non-regular files rejected */
+ orig->i_mode = S_IFIFO | 0644;
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EINVAL);
+ ret = ext4_move_extents(&ctx->orig_file, &ctx->donor_file, 0, 0, 1,
+ &moved_len);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+ orig->i_mode = S_IFREG | 0644;
+
+ donor->i_mode = S_IFDIR | 0755;
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EINVAL);
+ donor->i_mode = S_IFREG | 0644;
+
+ /* Bigalloc feature rejected */
+ ext4_set_feature_bigalloc(ctx->sb);
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EOPNOTSUPP);
+ ext4_clear_feature_bigalloc(ctx->sb);
+
+#if IS_ENABLED(CONFIG_FS_DAX)
+ /* DAX inode rejected */
+ orig->i_flags |= S_DAX;
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EOPNOTSUPP);
+ orig->i_flags &= ~S_DAX;
+#endif
+
+ /* Data journaling mode rejected */
+ kunit_activate_static_stub(test, ext4_inode_journal_mode,
+ mext_journal_data_mode_stub);
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EOPNOTSUPP);
+ kunit_deactivate_static_stub(test, ext4_inode_journal_mode);
+
+ /* Encrypted inode rejected */
+ donor->i_flags |= S_ENCRYPTED;
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EOPNOTSUPP);
+ donor->i_flags &= ~S_ENCRYPTED;
+
+ /* Non-extent inode rejected */
+ ext4_clear_inode_flag(orig, EXT4_INODE_EXTENTS);
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EOPNOTSUPP);
+ ext4_set_inode_flag(orig, EXT4_INODE_EXTENTS);
+
+ /* SUID / SGID on donor rejected */
+ donor->i_mode |= S_ISUID;
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EINVAL);
+ donor->i_mode = (donor->i_mode & ~S_ISUID) | S_ISGID;
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EINVAL);
+ donor->i_mode &= ~S_ISGID;
+
+ /* Immutable / append-only donor rejected with -EPERM */
+ donor->i_flags |= S_IMMUTABLE;
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EPERM);
+ donor->i_flags = (donor->i_flags & ~S_IMMUTABLE) | S_APPEND;
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EPERM);
+ donor->i_flags &= ~S_APPEND;
+
+ /* Swapfile rejected with -ETXTBSY */
+ orig->i_flags |= S_SWAPFILE;
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -ETXTBSY);
+ orig->i_flags &= ~S_SWAPFILE;
+
+ /* Quota file (S_NOQUOTA without EXT4_INODE_EA_INODE) rejected */
+ orig->i_flags |= S_NOQUOTA;
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EOPNOTSUPP);
+ ext4_set_inode_flag(orig, EXT4_INODE_EA_INODE);
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor), 0);
+ ext4_clear_inode_flag(orig, EXT4_INODE_EA_INODE);
+ orig->i_flags &= ~S_NOQUOTA;
+
+ /* Zero-byte file rejected with -EINVAL */
+ i_size_write(donor, 0);
+ KUNIT_EXPECT_EQ(test, ext4_mext_check_validity_test(orig, donor),
+ -EINVAL);
+ i_size_write(donor, EXT4_I(donor)->i_disksize);
+}
+
+static void test_mext_check_adjust_range(struct kunit *test)
+{
+ struct mext_test_ctx *ctx = mext_setup_ctx(test);
+ struct inode *orig = ctx->orig_inode;
+ struct inode *donor = ctx->donor_inode;
+ __u64 len;
+ int ret;
+
+ /* Sub-page block alignment check when blocksize (1024) < PAGE_SIZE */
+ orig->i_blkbits = 10;
+ donor->i_blkbits = 10;
+ i_size_write(orig, 32 * 1024);
+ i_size_write(donor, 32 * 1024);
+
+ len = 4;
+ ret = ext4_mext_check_adjust_range_test(orig, donor, 1, 2, &len);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+
+ len = 4;
+ ret = ext4_mext_check_adjust_range_test(orig, donor, 1,
+ 1 + (PAGE_SIZE >> 10), &len);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, len, 4ULL);
+
+ orig->i_blkbits = MEXT_TEST_BLK_BITS;
+ donor->i_blkbits = MEXT_TEST_BLK_BITS;
+
+ /* EXT_MAX_BLOCKS boundary and sum-overflow checks */
+ len = 1;
+ KUNIT_EXPECT_EQ(test,
+ ext4_mext_check_adjust_range_test(orig, donor,
+ EXT_MAX_BLOCKS,
+ EXT_MAX_BLOCKS, &len),
+ -EINVAL);
+
+ len = (u64)EXT_MAX_BLOCKS + 1;
+ KUNIT_EXPECT_EQ(test,
+ ext4_mext_check_adjust_range_test(orig, donor, 0, 0,
+ &len),
+ -EINVAL);
+
+ len = 4;
+ KUNIT_EXPECT_EQ(test,
+ ext4_mext_check_adjust_range_test(orig, donor,
+ EXT_MAX_BLOCKS - 4,
+ EXT_MAX_BLOCKS - 4,
+ &len),
+ -EINVAL);
+
+ /* Clamping to shorter EOF (including partial trailing block i_size) */
+ i_size_write(orig, 5 * MEXT_TEST_BLK_SIZE + 100); /* orig_eof = 6 */
+ i_size_write(donor, 4 * MEXT_TEST_BLK_SIZE); /* donor_eof = 4 */
+
+ len = 12;
+ ret = ext4_mext_check_adjust_range_test(orig, donor, 1, 1, &len);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, len, 3ULL);
+
+ /* Start at or beyond EOF clamps len to 0 and returns -EINVAL */
+ len = 4;
+ ret = ext4_mext_check_adjust_range_test(orig, donor, 4, 4, &len);
+ KUNIT_EXPECT_EQ(test, ret, -EINVAL);
+ KUNIT_EXPECT_EQ(test, len, 0ULL);
+}
+
+static void test_move_extents_holes_and_unwritten(struct kunit *test)
+{
+ struct mext_test_ctx *ctx = mext_setup_ctx(test);
+ struct inode *orig = ctx->orig_inode;
+ struct inode *donor = ctx->donor_inode;
+ __u64 moved_len = 0;
+ int ret;
+
+ /*
+ * orig: [0, 2) hole, [2, 4) unwritten -> pblk 202..203,
+ * [4, 6) unwritten -> pblk 204..205
+ * donor: [0, 2) unwritten -> pblk 300..301, [2, 4) hole,
+ * [4, 6) unwritten -> pblk 304..305
+ *
+ * Expected across [0, 6) (clamped from requested len = 16 to EOF = 6):
+ * - [0, 2): skipped in ext4_move_extents() because orig is a hole
+ * - [2, 4): skipped via MEXT_SKIP_EXTENT because donor is a hole
+ * - [4, 6): swapped via MEXT_MOVE_EXTENT (both unwritten)
+ */
+ i_size_write(orig, 6 * MEXT_TEST_BLK_SIZE);
+ EXT4_I(orig)->i_disksize = 6 * MEXT_TEST_BLK_SIZE;
+ i_size_write(donor, 6 * MEXT_TEST_BLK_SIZE);
+ EXT4_I(donor)->i_disksize = 6 * MEXT_TEST_BLK_SIZE;
+
+ mext_insert_extent(test, orig, 2, 4, 202, true);
+ mext_insert_extent(test, donor, 0, 2, 300, true);
+ mext_insert_extent(test, donor, 4, 2, 304, true);
+
+ ret = ext4_move_extents(&ctx->orig_file, &ctx->donor_file, 0, 0, 16,
+ &moved_len);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, moved_len, 2ULL);
+
+ /* [2, 4) on orig remained on pblk 202..203; [4, 6) swapped to 304..305 */
+ mext_assert_mapped_block(test, orig, 2, 202, EXT4_MAP_UNWRITTEN);
+ mext_assert_mapped_block(test, orig, 3, 203, EXT4_MAP_UNWRITTEN);
+ mext_assert_mapped_block(test, orig, 4, 304, EXT4_MAP_UNWRITTEN);
+ mext_assert_mapped_block(test, orig, 5, 305, EXT4_MAP_UNWRITTEN);
+
+ /* [0, 2) on donor remained 300..301; [4, 6) received 204..205 */
+ mext_assert_mapped_block(test, donor, 0, 300, EXT4_MAP_UNWRITTEN);
+ mext_assert_mapped_block(test, donor, 4, 204, EXT4_MAP_UNWRITTEN);
+ mext_assert_mapped_block(test, donor, 5, 205, EXT4_MAP_UNWRITTEN);
+}
+
+static void test_move_extents_written_copy_data_and_split(struct kunit *test)
+{
+ struct mext_test_ctx *ctx = mext_setup_ctx(test);
+ struct inode *orig = ctx->orig_inode;
+ struct inode *donor = ctx->donor_inode;
+ ext4_lblk_t lblk;
+ __u64 moved_len = 0;
+ int ret;
+
+ /*
+ * orig: [0, 6) written -> pblk 400..405, uptodate folios pre-filled
+ * donor: [1, 2) written -> pblk 501, [2, 3) hole,
+ * [3, 5) unwritten -> pblk 503..504
+ *
+ * Moving [1, 5) exercises MEXT_COPY_DATA, left/right extent splits on
+ * orig, donor hole skipping at lblk 2, written preservation on orig,
+ * mark_unwritten=1 on donor, and page cache data preservation.
+ */
+ i_size_write(orig, 6 * MEXT_TEST_BLK_SIZE);
+ EXT4_I(orig)->i_disksize = 6 * MEXT_TEST_BLK_SIZE;
+ i_size_write(donor, 6 * MEXT_TEST_BLK_SIZE);
+ EXT4_I(donor)->i_disksize = 6 * MEXT_TEST_BLK_SIZE;
+
+ mext_insert_extent(test, orig, 0, 6, 400, false);
+ mext_insert_extent(test, donor, 1, 1, 501, false);
+ mext_insert_extent(test, donor, 3, 2, 503, true);
+
+ for (lblk = 0; lblk < 6; lblk++)
+ mext_populate_folio_block(test, orig, lblk, 0xA0 + lblk);
+
+ ret = ext4_move_extents(&ctx->orig_file, &ctx->donor_file, 1, 1, 4,
+ &moved_len);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, moved_len, 3ULL);
+
+ /* Verify orig mappings: 0, 2, 5 unchanged; 1 -> 501, 3..4 -> 503..504 */
+ mext_assert_mapped_block(test, orig, 0, 400, EXT4_MAP_MAPPED);
+ mext_assert_mapped_block(test, orig, 1, 501, EXT4_MAP_MAPPED);
+ mext_assert_mapped_block(test, orig, 2, 402, EXT4_MAP_MAPPED);
+ mext_assert_mapped_block(test, orig, 3, 503, EXT4_MAP_MAPPED);
+ mext_assert_mapped_block(test, orig, 4, 504, EXT4_MAP_MAPPED);
+ mext_assert_mapped_block(test, orig, 5, 405, EXT4_MAP_MAPPED);
+
+ /* Donor's swapped extents are marked unwritten to avoid leaking data */
+ mext_assert_mapped_block(test, donor, 1, 401, EXT4_MAP_UNWRITTEN);
+ mext_assert_mapped_block(test, donor, 3, 403, EXT4_MAP_UNWRITTEN);
+ mext_assert_mapped_block(test, donor, 4, 404, EXT4_MAP_UNWRITTEN);
+
+ /* Verify cached bytes in orig's moved folios were preserved & dirtied */
+ for (lblk = 1; lblk <= 4; lblk++) {
+ loff_t pos = (loff_t)lblk << orig->i_blkbits;
+ struct folio *folio;
+ u8 *addr;
+
+ if (lblk == 2)
+ continue;
+ folio = filemap_get_folio(orig->i_mapping, pos >> PAGE_SHIFT);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ KUNIT_EXPECT_TRUE(test, folio_test_uptodate(folio));
+ KUNIT_EXPECT_TRUE(test, folio_test_dirty(folio));
+ addr = folio_address(folio);
+ KUNIT_EXPECT_EQ(test, addr[offset_in_folio(folio, pos)],
+ (u8)(0xA0 + lblk));
+ folio_put(folio);
+ }
+}
+
+static void test_move_extents_unwritten_orig_and_ebusy(struct kunit *test)
+{
+ struct mext_test_ctx *ctx = mext_setup_ctx(test);
+ struct inode *orig = ctx->orig_inode;
+ struct inode *donor = ctx->donor_inode;
+ struct folio *donor_folio;
+ __u64 moved_len = 0;
+ int ret;
+
+ /*
+ * Part 1: orig unwritten [0, 2) -> 600..601 (no pre-populated folios),
+ * donor written [0, 2) -> 700..701. Exercises MEXT_COPY_DATA through
+ * mext_folio_mkuptodate()'s non-uptodate unwritten buffer zeroing path.
+ */
+ i_size_write(orig, 4 * MEXT_TEST_BLK_SIZE);
+ EXT4_I(orig)->i_disksize = 4 * MEXT_TEST_BLK_SIZE;
+ i_size_write(donor, 4 * MEXT_TEST_BLK_SIZE);
+ EXT4_I(donor)->i_disksize = 4 * MEXT_TEST_BLK_SIZE;
+
+ mext_insert_extent(test, orig, 0, 2, 600, true);
+ mext_insert_extent(test, donor, 0, 2, 700, false);
+
+ ret = ext4_move_extents(&ctx->orig_file, &ctx->donor_file, 0, 0, 2,
+ &moved_len);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, moved_len, 2ULL);
+
+ mext_assert_mapped_block(test, orig, 0, 700, EXT4_MAP_UNWRITTEN);
+ mext_assert_mapped_block(test, orig, 1, 701, EXT4_MAP_UNWRITTEN);
+ mext_assert_mapped_block(test, donor, 0, 600, EXT4_MAP_UNWRITTEN);
+ mext_assert_mapped_block(test, donor, 1, 601, EXT4_MAP_UNWRITTEN);
+
+ /*
+ * Part 2: non-releasable folio on donor triggers -EBUSY from
+ * filemap_release_folio() without altering extent mappings.
+ */
+ mext_insert_extent(test, orig, 2, 1, 602, true);
+ mext_insert_extent(test, donor, 2, 1, 702, true);
+
+ donor_folio = __filemap_get_folio(donor->i_mapping,
+ ((loff_t)2 << donor->i_blkbits) >>
+ PAGE_SHIFT,
+ FGP_CREAT | FGP_LOCK, GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(donor_folio));
+ if (!folio_buffers(donor_folio))
+ create_empty_buffers(donor_folio, MEXT_TEST_BLK_SIZE, 0);
+ folio_set_checked(donor_folio);
+ folio_unlock(donor_folio);
+
+ ret = ext4_move_extents(&ctx->orig_file, &ctx->donor_file, 2, 2, 1,
+ &moved_len);
+ folio_clear_checked(donor_folio);
+ folio_put(donor_folio);
+
+ KUNIT_EXPECT_EQ(test, ret, -EBUSY);
+ KUNIT_EXPECT_EQ(test, moved_len, 0ULL);
+ mext_assert_mapped_block(test, orig, 2, 602, EXT4_MAP_UNWRITTEN);
+ mext_assert_mapped_block(test, donor, 2, 702, EXT4_MAP_UNWRITTEN);
+}
+
+static struct kunit_case move_extent_test_cases[] = {
+ KUNIT_CASE(test_double_data_sem_lock_order),
+ KUNIT_CASE(test_mext_check_validity_rejections),
+ KUNIT_CASE(test_mext_check_adjust_range),
+ KUNIT_CASE(test_move_extents_holes_and_unwritten),
+ KUNIT_CASE(test_move_extents_written_copy_data_and_split),
+ KUNIT_CASE(test_move_extents_unwritten_orig_and_ebusy),
+ {}
+};
+
+static struct kunit_suite move_extent_test_suite = {
+ .name = "ext4_move_extent_test",
+ .test_cases = move_extent_test_cases,
+};
+
+kunit_test_suites(&move_extent_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 move_extent");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/move_extent.c b/fs/ext4/move_extent.c
index d1405e763912..49adc348dda0 100644
--- a/fs/ext4/move_extent.c
+++ b/fs/ext4/move_extent.c
@@ -40,6 +40,7 @@ ext4_double_down_write_data_sem(struct inode *first, struct inode *second)
down_write_nested(&EXT4_I(first)->i_data_sem, I_DATA_SEM_OTHER);
}
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_double_down_write_data_sem);
/**
* ext4_double_up_write_data_sem - Release two inodes' write lock of i_data_sem
@@ -55,6 +56,7 @@ ext4_double_up_write_data_sem(struct inode *orig_inode,
up_write(&EXT4_I(orig_inode)->i_data_sem);
up_write(&EXT4_I(donor_inode)->i_data_sem);
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_double_up_write_data_sem);
/* Grab and lock folio on both @inode1 and @inode2 by inode order. */
static int mext_folio_double_lock(struct inode *inode1, struct inode *inode2,
@@ -656,8 +658,23 @@ int ext4_move_extents(struct file *o_filp, struct file *d_filp, __u64 orig_blk,
unlock_two_nondirectories(orig_inode, donor_inode);
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_move_extents);
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_double_down_write_data_sem);
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_double_up_write_data_sem);
+int ext4_mext_check_validity_test(struct inode *orig_inode,
+ struct inode *donor_inode)
+{
+ return mext_check_validity(orig_inode, donor_inode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mext_check_validity_test);
+
+int ext4_mext_check_adjust_range_test(struct inode *orig_inode,
+ struct inode *donor_inode,
+ __u64 orig_start, __u64 donor_start,
+ __u64 *len)
+{
+ return mext_check_adjust_range(orig_inode, donor_inode, orig_start,
+ donor_start, len);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mext_check_adjust_range_test);
#endif
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 26/27] ext4: add KUnit tests for fs/ext4/mmp.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (24 preceding siblings ...)
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:06 ` 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
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add a KUnit test suite (ext4_mmp_test) covering multi-mount protection
checksums, block I/O validation, and mount state transitions in
fs/ext4/mmp.c:
- CRC32C generation and verification in ext4_mmp_csum_set() and
ext4_mmp_csum_verify() with and without METADATA_CSUM
- Block read/write error handling, magic validation (-EFSCORRUPTED),
CRC mismatch handling (-EFSBADCRC), and buffer cleanup in
read_mmp_block() and write_mmp_block()
- ext4_multi_mount_protect() validation of out-of-range MMP blocks,
EXT4_MMP_SEQ_FSCK rejection (-EBUSY), EXT4_MMP_SEQ_CLEAN claim and
ext4_stop_mmpd() teardown, active/racing node sequence conflicts,
check_interval clamping, interrupted waits (-ETIMEDOUT), and kthread
startup failure
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 2 +-
fs/ext4/ext4.h | 20 +-
fs/ext4/mmp-test.c | 661 +++++++++++++++++++++++++++++++++++++++++++++
fs/ext4/mmp.c | 117 ++++++--
4 files changed, 770 insertions(+), 30 deletions(-)
create mode 100644 fs/ext4/mmp-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 5cf735df7656..cb0d7b6edf6d 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -24,7 +24,7 @@ ext4-test-objs += inode-test.o mballoc-test.o \
sysfs-test.o ioctl-test.o \
resize-test.o migrate-test.o \
indirect-test.o fsmap-test.o \
- move_extent-test.o
+ move_extent-test.o mmp-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 8bc7269af580..aaf4722e709e 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -4207,9 +4207,25 @@ extern struct ext4_io_end_vec *ext4_last_io_end_vec(ext4_io_end_t *io_end);
/* mmp.c */
extern int ext4_multi_mount_protect(struct super_block *, ext4_fsblk_t);
-
-/* mmp.c */
extern void ext4_stop_mmpd(struct ext4_sb_info *sbi);
+extern int ext4_mmp_submit_write(struct buffer_head *bh);
+extern int ext4_mmp_load_bh(struct super_block *sb, struct buffer_head **bh,
+ ext4_fsblk_t mmp_block);
+extern long ext4_mmp_wait_timeout(long timeout);
+extern struct task_struct *ext4_mmp_run_thread(struct super_block *sb,
+ const char *bdevname,
+ int bdevname_len);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+extern int ext4_mmp_csum_verify_test(struct super_block *sb,
+ struct mmp_struct *mmp);
+extern void ext4_mmp_csum_set_test(struct super_block *sb,
+ struct mmp_struct *mmp);
+extern int ext4_read_mmp_block_test(struct super_block *sb,
+ struct buffer_head **bh,
+ ext4_fsblk_t mmp_block);
+extern int ext4_write_mmp_block_test(struct super_block *sb,
+ struct buffer_head *bh);
+#endif
/* verity.c */
extern const struct fsverity_operations ext4_verityops;
diff --git a/fs/ext4/mmp-test.c b/fs/ext4/mmp-test.c
new file mode 100644
index 000000000000..008857631394
--- /dev/null
+++ b/fs/ext4/mmp-test.c
@@ -0,0 +1,661 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/mmp.c
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/kthread.h>
+#include <linux/module.h>
+#include <linux/part_stat.h>
+#include <linux/percpu.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/timer.h>
+#include <linux/workqueue.h>
+
+#include "ext4.h"
+
+#define MMP_TEST_BLK_SIZE 4096U
+#define MMP_TEST_BLK_BITS 12U
+#define MMP_TEST_FIRST_DATA_BLK 1ULL
+#define MMP_TEST_TOTAL_BLKS 1024ULL
+#define MMP_TEST_VALID_BLK 10ULL
+#define MMP_MAX_WAITS 8U
+
+struct mmp_test_ctx {
+ struct super_block *sb;
+ struct ext4_sb_info sbi;
+ struct ext4_super_block *es;
+ struct gendisk bdev_disk;
+ struct block_device bdev;
+ struct request_queue bdev_queue;
+ struct address_space bdev_mapping;
+ struct buffer_head sbh;
+ u8 sbh_data[MMP_TEST_BLK_SIZE];
+
+ /* Simulated on-disk MMP block and in-memory buffer_head */
+ u8 disk_buf[MMP_TEST_BLK_SIZE];
+ struct buffer_head mmp_bh;
+ u8 bh_buf[MMP_TEST_BLK_SIZE];
+
+ /* Deterministic I/O, sleep, and kthread fault injection */
+ int load_nomem;
+ int load_err_on_call;
+ unsigned int load_calls;
+ ext4_fsblk_t last_load_blk;
+
+ int write_err_on_call;
+ unsigned int write_calls;
+
+ unsigned int wait_calls;
+ long wait_timeouts[MMP_MAX_WAITS];
+ long wait_ret_on_call;
+ unsigned int wait_ret_target_call;
+ unsigned int mutate_seq_on_wait_call;
+ u32 mutated_seq;
+
+ int run_thread_err;
+ unsigned int run_thread_calls;
+};
+
+static struct kunit *active_mmp_test;
+
+static void mmp_noop_work(struct work_struct *work)
+{
+}
+
+static void mmp_noop_timer(struct timer_list *t)
+{
+}
+
+static const struct super_operations mmp_test_sops = {};
+
+static int mmp_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static int mmp_set_super(struct super_block *sb, struct fs_context *fc)
+{
+ return set_anon_super_fc(sb, fc);
+}
+
+static struct file_system_type mmp_test_fs_type = {
+ .name = "ext4_mmp_test",
+ .init_fs_context = mmp_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static int mmp_dummy_kthread_fn(void *data)
+{
+ while (!kthread_should_stop()) {
+ set_current_state(TASK_INTERRUPTIBLE);
+ if (!kthread_should_stop())
+ schedule();
+ }
+ __set_current_state(TASK_RUNNING);
+ return 0;
+}
+
+static int mmp_submit_write_stub(struct buffer_head *bh)
+{
+ struct kunit *test = active_mmp_test;
+ struct mmp_test_ctx *ctx;
+
+ if (!test)
+ return -EIO;
+ ctx = test->priv;
+ ctx->write_calls++;
+ if (ctx->write_err_on_call &&
+ ctx->write_calls == (unsigned int)ctx->write_err_on_call) {
+ clear_buffer_uptodate(bh);
+ return -EIO;
+ }
+
+ memcpy(ctx->disk_buf, bh->b_data, MMP_TEST_BLK_SIZE);
+ set_buffer_uptodate(bh);
+ return 0;
+}
+
+static int mmp_load_bh_stub(struct super_block *sb, struct buffer_head **bh,
+ ext4_fsblk_t mmp_block)
+{
+ struct kunit *test = active_mmp_test;
+ struct mmp_test_ctx *ctx;
+
+ if (!test)
+ return -EIO;
+ ctx = test->priv;
+ ctx->load_calls++;
+ ctx->last_load_blk = mmp_block;
+
+ if (ctx->load_nomem && !*bh)
+ return -ENOMEM;
+
+ if (!*bh) {
+ *bh = &ctx->mmp_bh;
+ get_bh(*bh);
+ }
+
+ if (ctx->load_err_on_call &&
+ ctx->load_calls == (unsigned int)ctx->load_err_on_call)
+ return -EIO;
+
+ memcpy((*bh)->b_data, ctx->disk_buf, MMP_TEST_BLK_SIZE);
+ set_buffer_uptodate(*bh);
+ return 0;
+}
+
+static long mmp_wait_timeout_stub(long timeout)
+{
+ struct kunit *test = active_mmp_test;
+ struct mmp_test_ctx *ctx;
+
+ if (!test)
+ return 1;
+ ctx = test->priv;
+ if (ctx->wait_calls < MMP_MAX_WAITS)
+ ctx->wait_timeouts[ctx->wait_calls] = timeout;
+ ctx->wait_calls++;
+
+ if (ctx->mutate_seq_on_wait_call &&
+ ctx->wait_calls == ctx->mutate_seq_on_wait_call) {
+ struct mmp_struct *disk_mmp = (struct mmp_struct *)ctx->disk_buf;
+
+ disk_mmp->mmp_seq = cpu_to_le32(ctx->mutated_seq);
+ strscpy(disk_mmp->mmp_nodename, "other-node",
+ sizeof(disk_mmp->mmp_nodename));
+ ext4_mmp_csum_set_test(ctx->sb, disk_mmp);
+ }
+
+ if (ctx->wait_ret_target_call &&
+ ctx->wait_calls == ctx->wait_ret_target_call)
+ return ctx->wait_ret_on_call;
+
+ return 0;
+}
+
+static struct task_struct *mmp_run_thread_stub(struct super_block *sb,
+ const char *bdevname,
+ int bdevname_len)
+{
+ struct kunit *test = active_mmp_test;
+ struct mmp_test_ctx *ctx;
+
+ if (!test)
+ return ERR_PTR(-EINVAL);
+ ctx = test->priv;
+ ctx->run_thread_calls++;
+ if (ctx->run_thread_err)
+ return ERR_PTR(ctx->run_thread_err);
+
+ return kthread_run(mmp_dummy_kthread_fn, NULL, "kunit-mmpd");
+}
+
+static void mmp_init_disk_block(struct mmp_test_ctx *ctx, u32 seq,
+ u16 check_interval)
+{
+ struct mmp_struct *mmp = (struct mmp_struct *)ctx->disk_buf;
+
+ memset(ctx->disk_buf, 0, sizeof(ctx->disk_buf));
+ mmp->mmp_magic = cpu_to_le32(EXT4_MMP_MAGIC);
+ mmp->mmp_seq = cpu_to_le32(seq);
+ mmp->mmp_time = cpu_to_le64(1700000000ULL);
+ strscpy(mmp->mmp_nodename, "init-node", sizeof(mmp->mmp_nodename));
+ strscpy(mmp->mmp_bdevname, "init-bdev", sizeof(mmp->mmp_bdevname));
+ mmp->mmp_check_interval = cpu_to_le16(check_interval);
+ ext4_mmp_csum_set_test(ctx->sb, mmp);
+}
+
+static void mmp_reset_counters(struct mmp_test_ctx *ctx)
+{
+ ctx->load_nomem = 0;
+ ctx->load_err_on_call = 0;
+ ctx->load_calls = 0;
+ ctx->last_load_blk = 0;
+ ctx->write_err_on_call = 0;
+ ctx->write_calls = 0;
+ ctx->wait_calls = 0;
+ memset(ctx->wait_timeouts, 0, sizeof(ctx->wait_timeouts));
+ ctx->wait_ret_on_call = 0;
+ ctx->wait_ret_target_call = 0;
+ ctx->mutate_seq_on_wait_call = 0;
+ ctx->mutated_seq = 0;
+ ctx->run_thread_err = 0;
+ ctx->run_thread_calls = 0;
+}
+
+static void mmp_cleanup_ctx(void *data)
+{
+ struct mmp_test_ctx *ctx = data;
+
+ active_mmp_test = NULL;
+
+ if (ctx->sbi.s_mmp_tsk)
+ ext4_stop_mmpd(&ctx->sbi);
+ ctx->sbi.s_mmp_bh = NULL;
+
+ cancel_work_sync(&ctx->sbi.s_sb_upd_work);
+ timer_delete_sync(&ctx->sbi.s_err_report);
+
+ if (ctx->sb) {
+ ctx->sb->s_fs_info = NULL;
+ deactivate_super(ctx->sb);
+ ctx->sb = NULL;
+ }
+ if (ctx->bdev.bd_stats) {
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ }
+}
+
+static struct mmp_test_ctx *mmp_setup_ctx(struct kunit *test)
+{
+ struct mmp_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ int err;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ ctx->es = kunit_kzalloc(test, sizeof(*ctx->es), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->es);
+
+ fc = fs_context_for_mount(&mmp_test_fs_type, 0);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(fc));
+ sb = sget_fc(fc, NULL, mmp_set_super);
+ put_fs_context(fc);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(sb));
+ up_write(&sb->s_umount);
+ ctx->sb = sb;
+
+ INIT_WORK(&ctx->sbi.s_sb_upd_work, mmp_noop_work);
+ timer_setup(&ctx->sbi.s_err_report, mmp_noop_timer, 0);
+
+ err = kunit_add_action_or_reset(test, mmp_cleanup_ctx, ctx);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ ctx->bdev.bd_stats = alloc_percpu(struct disk_stats);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->bdev.bd_stats);
+ strscpy(ctx->bdev_disk.disk_name, "mmp_bdev",
+ sizeof(ctx->bdev_disk.disk_name));
+ 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->bdev_mapping;
+
+ sb->s_bdev = &ctx->bdev;
+ sb->s_blocksize = MMP_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = MMP_TEST_BLK_BITS;
+ sb->s_magic = EXT4_SUPER_MAGIC;
+ sb->s_op = &mmp_test_sops;
+ sb->s_fs_info = &ctx->sbi;
+ strscpy(sb->s_id, "ext4_mmp_kunit", sizeof(sb->s_id));
+
+ ctx->sbh.b_data = ctx->sbh_data;
+ ctx->sbh.b_size = MMP_TEST_BLK_SIZE;
+ ctx->sbh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->sbh.b_count, 2);
+ set_buffer_uptodate(&ctx->sbh);
+ clear_buffer_mapped(&ctx->sbh);
+
+ ctx->mmp_bh.b_data = ctx->bh_buf;
+ ctx->mmp_bh.b_size = MMP_TEST_BLK_SIZE;
+ ctx->mmp_bh.b_blocknr = MMP_TEST_VALID_BLK;
+ ctx->mmp_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->mmp_bh.b_count, 2);
+ set_buffer_mapped(&ctx->mmp_bh);
+
+ ctx->es->s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+ ctx->es->s_first_data_block = cpu_to_le32(MMP_TEST_FIRST_DATA_BLK);
+ ctx->es->s_mmp_block = cpu_to_le64(MMP_TEST_VALID_BLK);
+ ctx->es->s_mmp_update_interval = cpu_to_le16(EXT4_MMP_MIN_CHECK_INTERVAL);
+ ctx->es->s_feature_incompat = cpu_to_le32(EXT4_FEATURE_INCOMPAT_MMP);
+ ctx->es->s_feature_ro_compat =
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+ ext4_blocks_count_set(ctx->es, MMP_TEST_TOTAL_BLKS);
+
+ ctx->sbi.s_sb = sb;
+ ctx->sbi.s_es = ctx->es;
+ ctx->sbi.s_sbh = &ctx->sbh;
+ ctx->sbi.s_csum_seed = 0x9abcdef0U;
+ set_opt(sb, ERRORS_CONT);
+ spin_lock_init(&ctx->sbi.s_bdev_wb_lock);
+ spin_lock_init(&ctx->sbi.s_error_lock);
+ ratelimit_state_init(&ctx->sbi.s_err_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL,
+ DEFAULT_RATELIMIT_BURST);
+ ratelimit_state_init(&ctx->sbi.s_warning_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL,
+ DEFAULT_RATELIMIT_BURST);
+ ratelimit_state_init(&ctx->sbi.s_msg_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL,
+ DEFAULT_RATELIMIT_BURST);
+
+ test->priv = ctx;
+ active_mmp_test = test;
+
+ kunit_activate_static_stub(test, ext4_mmp_submit_write,
+ mmp_submit_write_stub);
+ kunit_activate_static_stub(test, ext4_mmp_load_bh,
+ mmp_load_bh_stub);
+ kunit_activate_static_stub(test, ext4_mmp_wait_timeout,
+ mmp_wait_timeout_stub);
+ kunit_activate_static_stub(test, ext4_mmp_run_thread,
+ mmp_run_thread_stub);
+
+ return ctx;
+}
+
+static void test_mmp_csum_set_and_verify(struct kunit *test)
+{
+ struct mmp_test_ctx *ctx = mmp_setup_ctx(test);
+ struct mmp_struct *mmp;
+ __le32 orig_csum;
+
+ mmp = kunit_kzalloc(test, sizeof(*mmp), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, mmp);
+
+ mmp->mmp_magic = cpu_to_le32(EXT4_MMP_MAGIC);
+ mmp->mmp_seq = cpu_to_le32(EXT4_MMP_SEQ_CLEAN);
+ strscpy(mmp->mmp_nodename, "node-a", sizeof(mmp->mmp_nodename));
+
+ /* Without METADATA_CSUM, set is a no-op and verify always returns 1 */
+ ext4_clear_feature_metadata_csum(ctx->sb);
+ mmp->mmp_checksum = cpu_to_le32(0xdeadbeefU);
+ ext4_mmp_csum_set_test(ctx->sb, mmp);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(mmp->mmp_checksum), 0xdeadbeefU);
+ KUNIT_EXPECT_EQ(test, ext4_mmp_csum_verify_test(ctx->sb, mmp), 1);
+
+ /* With METADATA_CSUM, set computes CRC32C and verify detects tampering */
+ ext4_set_feature_metadata_csum(ctx->sb);
+ mmp->mmp_checksum = 0;
+ ext4_mmp_csum_set_test(ctx->sb, mmp);
+ orig_csum = mmp->mmp_checksum;
+ KUNIT_EXPECT_NE(test, le32_to_cpu(orig_csum), 0U);
+ KUNIT_EXPECT_EQ(test, ext4_mmp_csum_verify_test(ctx->sb, mmp), 1);
+
+ /* Flipping payload field or csum seed invalidates verification */
+ mmp->mmp_seq = cpu_to_le32(7);
+ KUNIT_EXPECT_EQ(test, ext4_mmp_csum_verify_test(ctx->sb, mmp), 0);
+ mmp->mmp_seq = cpu_to_le32(EXT4_MMP_SEQ_CLEAN);
+ KUNIT_EXPECT_EQ(test, ext4_mmp_csum_verify_test(ctx->sb, mmp), 1);
+
+ ctx->sbi.s_csum_seed ^= 1U;
+ KUNIT_EXPECT_EQ(test, ext4_mmp_csum_verify_test(ctx->sb, mmp), 0);
+}
+
+static void test_read_and_write_mmp_block(struct kunit *test)
+{
+ struct mmp_test_ctx *ctx = mmp_setup_ctx(test);
+ struct buffer_head *bh = NULL;
+ struct mmp_struct *mmp;
+ int base_ref;
+ int ret;
+
+ /* Write populates checksum and persists block into disk_buf */
+ mmp = (struct mmp_struct *)ctx->mmp_bh.b_data;
+ memset(ctx->mmp_bh.b_data, 0, MMP_TEST_BLK_SIZE);
+ mmp->mmp_magic = cpu_to_le32(EXT4_MMP_MAGIC);
+ mmp->mmp_seq = cpu_to_le32(19);
+ ret = ext4_write_mmp_block_test(ctx->sb, &ctx->mmp_bh);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->write_calls, 1U);
+ KUNIT_EXPECT_EQ(test,
+ ext4_mmp_csum_verify_test(ctx->sb,
+ (struct mmp_struct *)ctx->disk_buf), 1);
+
+ /* Simulated write I/O error returns -EIO */
+ ctx->write_err_on_call = 2;
+ KUNIT_EXPECT_EQ(test, ext4_write_mmp_block_test(ctx->sb, &ctx->mmp_bh),
+ -EIO);
+ ctx->write_err_on_call = 0;
+
+ /* Valid read allocates/populates bh and verifies magic + checksum */
+ base_ref = atomic_read(&ctx->mmp_bh.b_count);
+ ret = ext4_read_mmp_block_test(ctx->sb, &bh, MMP_TEST_VALID_BLK);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_ASSERT_PTR_EQ(test, bh, &ctx->mmp_bh);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->mmp_bh.b_count), base_ref + 1);
+
+ /* Re-reading with existing bh refreshes contents without leaking ref */
+ ((struct mmp_struct *)ctx->disk_buf)->mmp_seq = cpu_to_le32(23);
+ ext4_mmp_csum_set_test(ctx->sb, (struct mmp_struct *)ctx->disk_buf);
+ ret = ext4_read_mmp_block_test(ctx->sb, &bh, MMP_TEST_VALID_BLK);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test,
+ le32_to_cpu(((struct mmp_struct *)bh->b_data)->mmp_seq),
+ 23U);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->mmp_bh.b_count), base_ref + 1);
+ brelse(bh);
+ bh = NULL;
+
+ /* -ENOMEM when bh allocation fails */
+ ctx->load_nomem = 1;
+ KUNIT_EXPECT_EQ(test,
+ ext4_read_mmp_block_test(ctx->sb, &bh,
+ MMP_TEST_VALID_BLK),
+ -ENOMEM);
+ KUNIT_EXPECT_NULL(test, bh);
+ ctx->load_nomem = 0;
+
+ /* -EIO read failure releases bh and resets *bh to NULL */
+ mmp_reset_counters(ctx);
+ ctx->load_err_on_call = 1;
+ KUNIT_EXPECT_EQ(test,
+ ext4_read_mmp_block_test(ctx->sb, &bh,
+ MMP_TEST_VALID_BLK),
+ -EIO);
+ KUNIT_EXPECT_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->mmp_bh.b_count), base_ref);
+
+ /* Bad magic returns -EFSCORRUPTED and cleans up bh */
+ mmp_reset_counters(ctx);
+ ((struct mmp_struct *)ctx->disk_buf)->mmp_magic = cpu_to_le32(0x1234U);
+ KUNIT_EXPECT_EQ(test,
+ ext4_read_mmp_block_test(ctx->sb, &bh,
+ MMP_TEST_VALID_BLK),
+ -EFSCORRUPTED);
+ KUNIT_EXPECT_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->mmp_bh.b_count), base_ref);
+
+ /* Bad checksum returns -EFSBADCRC and cleans up bh */
+ mmp_init_disk_block(ctx, EXT4_MMP_SEQ_CLEAN, 5);
+ ((struct mmp_struct *)ctx->disk_buf)->mmp_checksum ^= cpu_to_le32(1U);
+ KUNIT_EXPECT_EQ(test,
+ ext4_read_mmp_block_test(ctx->sb, &bh,
+ MMP_TEST_VALID_BLK),
+ -EFSBADCRC);
+ KUNIT_EXPECT_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->mmp_bh.b_count), base_ref);
+}
+
+static void test_mmp_invalid_block_and_io_errors(struct kunit *test)
+{
+ struct mmp_test_ctx *ctx = mmp_setup_ctx(test);
+
+ /* Block below s_first_data_block (1) or >= blocks_count (1024) */
+ KUNIT_EXPECT_EQ(test, ext4_multi_mount_protect(ctx->sb, 0), -EINVAL);
+ KUNIT_EXPECT_EQ(test,
+ ext4_multi_mount_protect(ctx->sb, MMP_TEST_TOTAL_BLKS),
+ -EINVAL);
+ KUNIT_EXPECT_EQ(test, ctx->load_calls, 0U);
+
+ /* Initial read failure propagates error */
+ mmp_init_disk_block(ctx, EXT4_MMP_SEQ_CLEAN, 5);
+ mmp_reset_counters(ctx);
+ ctx->load_err_on_call = 1;
+ KUNIT_EXPECT_EQ(test,
+ ext4_multi_mount_protect(ctx->sb, MMP_TEST_VALID_BLK),
+ -EIO);
+
+ /* Write failure when claiming MMP block */
+ mmp_reset_counters(ctx);
+ ctx->write_err_on_call = 1;
+ KUNIT_EXPECT_EQ(test,
+ ext4_multi_mount_protect(ctx->sb, MMP_TEST_VALID_BLK),
+ -EIO);
+
+ /* Post-write verification read failure */
+ mmp_init_disk_block(ctx, EXT4_MMP_SEQ_CLEAN, 5);
+ mmp_reset_counters(ctx);
+ ctx->load_err_on_call = 2;
+ KUNIT_EXPECT_EQ(test,
+ ext4_multi_mount_protect(ctx->sb, MMP_TEST_VALID_BLK),
+ -EIO);
+}
+
+static void test_mmp_clean_and_fsck_sequences(struct kunit *test)
+{
+ struct mmp_test_ctx *ctx = mmp_setup_ctx(test);
+ struct mmp_struct *disk_mmp = (struct mmp_struct *)ctx->disk_buf;
+ int base_ref;
+ int ret;
+
+ /* EXT4_MMP_SEQ_FSCK fails immediately with -EBUSY without sleeping */
+ mmp_init_disk_block(ctx, EXT4_MMP_SEQ_FSCK, 5);
+ mmp_reset_counters(ctx);
+ ret = ext4_multi_mount_protect(ctx->sb, MMP_TEST_VALID_BLK);
+ KUNIT_EXPECT_EQ(test, ret, -EBUSY);
+ KUNIT_EXPECT_EQ(test, ctx->wait_calls, 0U);
+ KUNIT_EXPECT_EQ(test, ctx->write_calls, 0U);
+
+ /*
+ * EXT4_MMP_SEQ_CLEAN skips pre-claim wait, writes a valid sequence
+ * <= EXT4_MMP_SEQ_MAX, starts kmmpd, and ext4_stop_mmpd() cleans up.
+ */
+ mmp_init_disk_block(ctx, EXT4_MMP_SEQ_CLEAN, 5);
+ mmp_reset_counters(ctx);
+ base_ref = atomic_read(&ctx->mmp_bh.b_count);
+
+ ret = ext4_multi_mount_protect(ctx->sb, MMP_TEST_VALID_BLK);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->wait_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->write_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->run_thread_calls, 1U);
+ KUNIT_EXPECT_LE(test, le32_to_cpu(disk_mmp->mmp_seq), EXT4_MMP_SEQ_MAX);
+ KUNIT_EXPECT_NOT_NULL(test, ctx->sbi.s_mmp_tsk);
+ KUNIT_EXPECT_PTR_EQ(test, ctx->sbi.s_mmp_bh, &ctx->mmp_bh);
+ KUNIT_EXPECT_STREQ(test,
+ ((struct mmp_struct *)ctx->mmp_bh.b_data)->mmp_bdevname,
+ "mmp_bdev");
+
+ ext4_stop_mmpd(&ctx->sbi);
+ KUNIT_EXPECT_NULL(test, ctx->sbi.s_mmp_tsk);
+ KUNIT_EXPECT_EQ(test, atomic_read(&ctx->mmp_bh.b_count), base_ref);
+ ctx->sbi.s_mmp_bh = NULL;
+}
+
+static void test_mmp_active_seq_conflict_and_claim(struct kunit *test)
+{
+ struct mmp_test_ctx *ctx = mmp_setup_ctx(test);
+ int ret;
+
+ /*
+ * Case 1: Active sequence on disk; s_mmp_update_interval = 3 (floored
+ * to 5), disk mmp_check_interval = 12 (overrides 5 -> wait_time = 25s).
+ * Remote node updates mmp_seq during wait #1 -> fails with -EBUSY.
+ */
+ ctx->es->s_mmp_update_interval = cpu_to_le16(3);
+ mmp_init_disk_block(ctx, 100, 12);
+ mmp_reset_counters(ctx);
+ ctx->mutate_seq_on_wait_call = 1;
+ ctx->mutated_seq = 101;
+
+ ret = ext4_multi_mount_protect(ctx->sb, MMP_TEST_VALID_BLK);
+ KUNIT_EXPECT_EQ(test, ret, -EBUSY);
+ KUNIT_EXPECT_EQ(test, ctx->wait_calls, 1U);
+ KUNIT_EXPECT_EQ(test, ctx->wait_timeouts[0], (long)(25 * HZ));
+ KUNIT_EXPECT_EQ(test, ctx->write_calls, 0U);
+
+ /*
+ * Case 2: Stale sequence unchanged on wait #1, but racing mounter
+ * overwrites mmp_seq during post-claim wait #2 -> fails with -EBUSY.
+ */
+ mmp_init_disk_block(ctx, 100, 12);
+ mmp_reset_counters(ctx);
+ ctx->mutate_seq_on_wait_call = 2;
+ ctx->mutated_seq = 0x5555U;
+
+ ret = ext4_multi_mount_protect(ctx->sb, MMP_TEST_VALID_BLK);
+ KUNIT_EXPECT_EQ(test, ret, -EBUSY);
+ KUNIT_EXPECT_EQ(test, ctx->wait_calls, 2U);
+ KUNIT_EXPECT_EQ(test, ctx->wait_timeouts[0], (long)(25 * HZ));
+ KUNIT_EXPECT_EQ(test, ctx->wait_timeouts[1], (long)(25 * HZ));
+ KUNIT_EXPECT_EQ(test, ctx->write_calls, 1U);
+
+ /*
+ * Case 3: Stale sequence with large check_interval = 80 (wait_time
+ * capped to 80 + 60 = 140s via min(2*x+1, x+60)) succeeds cleanly.
+ */
+ mmp_init_disk_block(ctx, 100, 80);
+ mmp_reset_counters(ctx);
+
+ ret = ext4_multi_mount_protect(ctx->sb, MMP_TEST_VALID_BLK);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->wait_calls, 2U);
+ KUNIT_EXPECT_EQ(test, ctx->wait_timeouts[0], (long)(140 * HZ));
+ KUNIT_EXPECT_EQ(test, ctx->wait_timeouts[1], (long)(140 * HZ));
+
+ ext4_stop_mmpd(&ctx->sbi);
+ ctx->sbi.s_mmp_bh = NULL;
+}
+
+static void test_mmp_interrupted_waits_and_kthread_failure(struct kunit *test)
+{
+ struct mmp_test_ctx *ctx = mmp_setup_ctx(test);
+
+ /* Signal interruption on pre-claim wait returns -ETIMEDOUT */
+ mmp_init_disk_block(ctx, 77, 5);
+ mmp_reset_counters(ctx);
+ ctx->wait_ret_target_call = 1;
+ ctx->wait_ret_on_call = 3 * HZ;
+ KUNIT_EXPECT_EQ(test,
+ ext4_multi_mount_protect(ctx->sb, MMP_TEST_VALID_BLK),
+ -ETIMEDOUT);
+
+ /* Signal interruption on post-claim wait returns -ETIMEDOUT */
+ mmp_init_disk_block(ctx, 77, 5);
+ mmp_reset_counters(ctx);
+ ctx->wait_ret_target_call = 2;
+ ctx->wait_ret_on_call = 2 * HZ;
+ KUNIT_EXPECT_EQ(test,
+ ext4_multi_mount_protect(ctx->sb, MMP_TEST_VALID_BLK),
+ -ETIMEDOUT);
+
+ /* kthread creation failure returns -ENOMEM and leaves s_mmp_tsk NULL */
+ mmp_init_disk_block(ctx, EXT4_MMP_SEQ_CLEAN, 5);
+ mmp_reset_counters(ctx);
+ ctx->run_thread_err = -ENOMEM;
+ KUNIT_EXPECT_EQ(test,
+ ext4_multi_mount_protect(ctx->sb, MMP_TEST_VALID_BLK),
+ -ENOMEM);
+ KUNIT_EXPECT_NULL(test, ctx->sbi.s_mmp_tsk);
+ ctx->sbi.s_mmp_bh = NULL;
+}
+
+static struct kunit_case mmp_test_cases[] = {
+ KUNIT_CASE(test_mmp_csum_set_and_verify),
+ KUNIT_CASE(test_read_and_write_mmp_block),
+ KUNIT_CASE(test_mmp_invalid_block_and_io_errors),
+ KUNIT_CASE(test_mmp_clean_and_fsck_sequences),
+ KUNIT_CASE(test_mmp_active_seq_conflict_and_claim),
+ KUNIT_CASE(test_mmp_interrupted_waits_and_kthread_failure),
+ {}
+};
+
+static struct kunit_suite mmp_test_suite = {
+ .name = "ext4_mmp_test",
+ .test_cases = mmp_test_cases,
+};
+
+kunit_test_suites(&mmp_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 multi-mount protection (MMP)");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/mmp.c b/fs/ext4/mmp.c
index 7ce361484b38..025c9f332726 100644
--- a/fs/ext4/mmp.c
+++ b/fs/ext4/mmp.c
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: GPL-2.0
+#include <kunit/static_stub.h>
#include <linux/fs.h>
#include <linux/random.h>
#include <linux/buffer_head.h>
@@ -35,6 +36,20 @@ static void ext4_mmp_csum_set(struct super_block *sb, struct mmp_struct *mmp)
mmp->mmp_checksum = ext4_mmp_csum(sb, mmp);
}
+int ext4_mmp_submit_write(struct buffer_head *bh)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_mmp_submit_write, bh);
+
+ lock_buffer(bh);
+ bh_submit(bh, REQ_OP_WRITE | REQ_SYNC | REQ_META | REQ_PRIO,
+ bh_end_write);
+ wait_on_buffer(bh);
+ if (unlikely(!buffer_uptodate(bh)))
+ return -EIO;
+ return 0;
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mmp_submit_write);
+
/*
* Write the MMP block using REQ_SYNC to try to get the block on-disk
* faster.
@@ -45,13 +60,7 @@ static int write_mmp_block_thawed(struct super_block *sb,
struct mmp_struct *mmp = (struct mmp_struct *)(bh->b_data);
ext4_mmp_csum_set(sb, mmp);
- lock_buffer(bh);
- bh_submit(bh, REQ_OP_WRITE | REQ_SYNC | REQ_META | REQ_PRIO,
- bh_end_write);
- wait_on_buffer(bh);
- if (unlikely(!buffer_uptodate(bh)))
- return -EIO;
- return 0;
+ return ext4_mmp_submit_write(bh);
}
static int write_mmp_block(struct super_block *sb, struct buffer_head *bh)
@@ -64,6 +73,35 @@ static int write_mmp_block(struct super_block *sb, struct buffer_head *bh)
return write_mmp_block_thawed(sb, bh);
}
+int ext4_mmp_load_bh(struct super_block *sb, struct buffer_head **bh,
+ ext4_fsblk_t mmp_block)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_mmp_load_bh, sb, bh, mmp_block);
+
+ /*
+ * This would be sb_bread(sb, mmp_block), except we need to be sure
+ * that the MD RAID device cache has been bypassed, and that the read
+ * is not blocked in the elevator.
+ */
+ if (!*bh) {
+ *bh = sb_getblk(sb, mmp_block);
+ if (!*bh)
+ return -ENOMEM;
+ }
+
+ lock_buffer(*bh);
+ return ext4_read_bh(*bh, REQ_META | REQ_PRIO, NULL, false);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mmp_load_bh);
+
+long ext4_mmp_wait_timeout(long timeout)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_mmp_wait_timeout, timeout);
+
+ return schedule_timeout_interruptible(timeout);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mmp_wait_timeout);
+
/*
* Read the MMP block. It _must_ be read from disk and hence we clear the
* uptodate flag on the buffer.
@@ -77,19 +115,7 @@ static int read_mmp_block(struct super_block *sb, struct buffer_head **bh,
if (*bh)
clear_buffer_uptodate(*bh);
- /* This would be sb_bread(sb, mmp_block), except we need to be sure
- * that the MD RAID device cache has been bypassed, and that the read
- * is not blocked in the elevator. */
- if (!*bh) {
- *bh = sb_getblk(sb, mmp_block);
- if (!*bh) {
- ret = -ENOMEM;
- goto warn_exit;
- }
- }
-
- lock_buffer(*bh);
- ret = ext4_read_bh(*bh, REQ_META | REQ_PRIO, NULL, false);
+ ret = ext4_mmp_load_bh(sb, bh, mmp_block);
if (ret)
goto warn_exit;
@@ -185,8 +211,7 @@ static int kmmpd(void *data)
diff = jiffies - last_update_time;
if (diff < mmp_update_interval * HZ)
- schedule_timeout_interruptible(mmp_update_interval *
- HZ - diff);
+ ext4_mmp_wait_timeout(mmp_update_interval * HZ - diff);
/*
* We need to make sure that more than mmp_check_interval
@@ -250,6 +275,16 @@ static int kmmpd(void *data)
return retval;
}
+struct task_struct *ext4_mmp_run_thread(struct super_block *sb,
+ const char *bdevname, int bdevname_len)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_mmp_run_thread, sb, bdevname,
+ bdevname_len);
+
+ return kthread_run(kmmpd, sb, "kmmpd-%.*s", bdevname_len, bdevname);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mmp_run_thread);
+
void ext4_stop_mmpd(struct ext4_sb_info *sbi)
{
if (sbi->s_mmp_tsk) {
@@ -258,6 +293,7 @@ void ext4_stop_mmpd(struct ext4_sb_info *sbi)
sbi->s_mmp_tsk = NULL;
}
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_stop_mmpd);
/*
* Get a random new sequence number but make sure it is not greater than
@@ -323,7 +359,7 @@ int ext4_multi_mount_protect(struct super_block *sb,
ext4_warning(sb, "MMP interval %u higher than expected, please"
" wait.\n", wait_time * 2);
- if (schedule_timeout_interruptible(HZ * wait_time) != 0) {
+ if (ext4_mmp_wait_timeout(HZ * wait_time) != 0) {
ext4_warning(sb, "MMP startup interrupted, failing mount\n");
retval = -ETIMEDOUT;
goto failed;
@@ -358,7 +394,7 @@ int ext4_multi_mount_protect(struct super_block *sb,
/*
* wait for MMP interval and check mmp_seq.
*/
- if (schedule_timeout_interruptible(HZ * wait_time) != 0) {
+ if (ext4_mmp_wait_timeout(HZ * wait_time) != 0) {
ext4_warning(sb, "MMP startup interrupted, failing mount");
retval = -ETIMEDOUT;
goto failed;
@@ -384,9 +420,8 @@ int ext4_multi_mount_protect(struct super_block *sb,
/*
* Start a kernel thread to update the MMP block periodically.
*/
- EXT4_SB(sb)->s_mmp_tsk = kthread_run(kmmpd, sb, "kmmpd-%.*s",
- (int)sizeof(mmp->mmp_bdevname),
- mmp->mmp_bdevname);
+ EXT4_SB(sb)->s_mmp_tsk = ext4_mmp_run_thread(sb, mmp->mmp_bdevname,
+ (int)sizeof(mmp->mmp_bdevname));
if (IS_ERR(EXT4_SB(sb)->s_mmp_tsk)) {
EXT4_SB(sb)->s_mmp_tsk = NULL;
ext4_warning(sb, "Unable to create kmmpd thread for %s.",
@@ -401,3 +436,31 @@ int ext4_multi_mount_protect(struct super_block *sb,
brelse(bh);
return retval;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_multi_mount_protect);
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+int ext4_mmp_csum_verify_test(struct super_block *sb, struct mmp_struct *mmp)
+{
+ return ext4_mmp_csum_verify(sb, mmp);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mmp_csum_verify_test);
+
+void ext4_mmp_csum_set_test(struct super_block *sb, struct mmp_struct *mmp)
+{
+ ext4_mmp_csum_set(sb, mmp);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mmp_csum_set_test);
+
+int ext4_read_mmp_block_test(struct super_block *sb, struct buffer_head **bh,
+ ext4_fsblk_t mmp_block)
+{
+ return read_mmp_block(sb, bh, mmp_block);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_mmp_block_test);
+
+int ext4_write_mmp_block_test(struct super_block *sb, struct buffer_head *bh)
+{
+ return write_mmp_block(sb, bh);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_write_mmp_block_test);
+#endif
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* [PATCH 27/27] ext4: add KUnit tests for fs/ext4/dir.c
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
` (25 preceding siblings ...)
2026-11-12 4:06 ` [PATCH 26/27] ext4: add KUnit tests for fs/ext4/mmp.c Theodore Ts'o
@ 2026-11-12 4:06 ` Theodore Ts'o
2026-11-12 4:41 ` sashiko-bot
26 siblings, 1 reply; 55+ messages in thread
From: Theodore Ts'o @ 2026-11-12 4:06 UTC (permalink / raw)
To: Ext4 Developers List; +Cc: Theodore Ts'o
Add a KUnit test suite (ext4_dir_test) in fs/ext4/dir-test.c covering
directory entry validation, hash-cookie helpers, directory llseek, and
both linear and HTree directory iteration in fs/ext4/dir.c:
- __ext4_check_dir_entry() and ext4_check_all_de() validation across
valid blocks (including fake '.', '..', and metadata_csum tail
entries, as well as encrypted+casefolded extended-hash entries) and
corrupt rec_len, alignment, name_len, block-end slack, overrun,
out-of-bounds inode numbers, and full-block '.' entries.
- is_dx_dir(), get_dtype(), and 32-bit vs 64-bit HTree telldir hash
position conversions (hash2pos, pos2maj_hash, pos2min_hash, and
ext4_get_htree_eof).
- ext4_dir_operations open(), llseek() (64-bit and 32-bit HTree EOF
bounds vs non-DX s_bitmap_maxbytes/SEEK_END bounds and cookie
invalidation), and release().
- ext4_htree_store_dirent() rbtree ordering across major/minor hashes,
64-bit hash collision chaining, and ext4_htree_free_dir_info().
- Linear ext4_readdir() across multi-block directories with sparse
holes, deleted entries, stop/resume, unaligned f_pos revalidation on
i_version change, bad checksum and corrupt entry skipping, and
ERR_BAD_DX_DIR fallback clearing EXT4_INODE_INDEX.
- HTree ext4_dx_readdir() across multi-leaf next_hash continuations,
mid-collision extra_fname stop/resume, EOF handling, seek reset, and
mid-iteration i_version refill.
Export ext4_dir_operations, expose thin KUnit wrappers for static
helpers in fs/ext4/dir.c, and add a KUNIT_STATIC_STUB_REDIRECT hook to
ext4_htree_fill_tree().
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 3 +-
fs/ext4/dir-test.c | 992 +++++++++++++++++++++++++++++++++++++++++++++
fs/ext4/dir.c | 33 ++
fs/ext4/ext4.h | 8 +
fs/ext4/namei.c | 4 +
5 files changed, 1039 insertions(+), 1 deletion(-)
create mode 100644 fs/ext4/dir-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index cb0d7b6edf6d..a427f6f361e5 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -24,7 +24,8 @@ ext4-test-objs += inode-test.o mballoc-test.o \
sysfs-test.o ioctl-test.o \
resize-test.o migrate-test.o \
indirect-test.o fsmap-test.o \
- move_extent-test.o mmp-test.o
+ move_extent-test.o mmp-test.o \
+ dir-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/dir-test.c b/fs/ext4/dir-test.c
new file mode 100644
index 000000000000..4d7c7c89e8b9
--- /dev/null
+++ b/fs/ext4/dir-test.c
@@ -0,0 +1,992 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for ext4 directory entry validation, linear/HTree readdir,
+ * hash-cookie conversions, and directory llseek (fs/ext4/dir.c).
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/iversion.h>
+#include <linux/module.h>
+#include <linux/rbtree.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+
+#define DIR_TEST_BLK_SIZE 1024
+#define DIR_TEST_BLK_BITS 10
+#define DIR_TEST_INODE_SIZE 256
+#define DIR_TEST_EXTRA_ISIZE (sizeof(struct ext4_inode) - \
+ EXT4_GOOD_OLD_INODE_SIZE)
+#define DIR_TEST_FIRST_PBLK 200
+#define DIR_TEST_MAX_BLOCKS 16
+#define DIR_TEST_MAX_LBLOCKS 8
+#define DIR_TEST_CSUM_SEED 0x4d3c2b1aU
+
+#define DIR_TEST_ROOT_INO EXT4_ROOT_INO
+#define DIR_TEST_DIR_INO 12
+#define DIR_TEST_MAX_EMIT 32
+
+struct dir_mock_block {
+ bool in_use;
+ struct buffer_head bh;
+ char data[DIR_TEST_BLK_SIZE] __aligned(8);
+};
+
+struct dir_emit_record {
+ char name[32];
+ u64 ino;
+ loff_t offset;
+ unsigned int d_type;
+};
+
+struct dir_capture_ctx {
+ struct dir_context ctx;
+ int count;
+ int max_emit;
+ struct inode *bump_iversion_after_first;
+ struct dir_emit_record recs[DIR_TEST_MAX_EMIT];
+};
+
+struct dir_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 ext4_inode_info dir_ei;
+ ext4_fsblk_t lblk_to_pblk[DIR_TEST_MAX_LBLOCKS];
+ struct dir_mock_block blocks[DIR_TEST_MAX_BLOCKS];
+
+ struct dentry dentry;
+ struct file filp;
+ struct dir_capture_ctx cap;
+
+ int fail_map_blocks;
+ int fail_getblk;
+ int htree_mode;
+ int htree_fill_calls;
+ __u32 last_fill_start_hash;
+ __u32 last_fill_start_minor;
+};
+
+enum dir_htree_stub_mode {
+ DIR_HTREE_STUB_NONE = 0,
+ DIR_HTREE_STUB_BAD_DX,
+ DIR_HTREE_STUB_TWO_BATCH,
+};
+
+static const struct super_operations dir_test_sops = {
+};
+
+static void init_mock_dir_file(struct dir_test_ctx *ctx, fmode_t extra_mode)
+{
+ struct inode *dir = &ctx->dir_ei.vfs_inode;
+ struct file tmp = {
+ .f_path = { .dentry = &ctx->dentry },
+ .f_inode = dir,
+ .f_mapping = dir->i_mapping,
+ .f_op = &ext4_dir_operations,
+ .f_mode = FMODE_READ | FMODE_LSEEK | FMODE_PREAD | extra_mode,
+ };
+
+ memset(&ctx->dentry, 0, sizeof(ctx->dentry));
+ spin_lock_init(&ctx->dentry.d_lock);
+ seqcount_spinlock_init(&ctx->dentry.d_seq, &ctx->dentry.d_lock);
+ ctx->dentry.d_parent = &ctx->dentry;
+ ctx->dentry.d_sb = &ctx->sb;
+ ctx->dentry.d_inode = dir;
+ ctx->dentry.__d_name.name = (const unsigned char *)"testdir";
+ ctx->dentry.__d_name.len = 7;
+
+ memcpy(&ctx->filp, &tmp, sizeof(ctx->filp));
+ spin_lock_init(&ctx->filp.f_lock);
+ mutex_init(&ctx->filp.f_pos_lock);
+ /*
+ * Pre-populate the readahead window across our mock block range so
+ * ext4_readdir() does not issue block-device pagecache readahead I/O.
+ */
+ ctx->filp.f_ra.start = 0;
+ ctx->filp.f_ra.size = 4096;
+}
+
+static bool dir_capture_actor(struct dir_context *dctx, const char *name,
+ int namelen, loff_t offset, u64 ino,
+ unsigned int d_type)
+{
+ struct dir_capture_ctx *cap =
+ container_of(dctx, struct dir_capture_ctx, ctx);
+
+ if (cap->max_emit > 0 && cap->count >= cap->max_emit)
+ return false;
+
+ if (cap->count < DIR_TEST_MAX_EMIT) {
+ struct dir_emit_record *rec = &cap->recs[cap->count];
+ int copy_len = min_t(int, namelen, sizeof(rec->name) - 1);
+
+ memcpy(rec->name, name, copy_len);
+ rec->name[copy_len] = '\0';
+ rec->ino = ino;
+ rec->offset = offset;
+ rec->d_type = d_type;
+ }
+ cap->count++;
+
+ if (cap->bump_iversion_after_first && cap->count == 1) {
+ inode_inc_iversion(cap->bump_iversion_after_first);
+ cap->bump_iversion_after_first = NULL;
+ }
+
+ return true;
+}
+
+static void reset_capture(struct dir_capture_ctx *cap, loff_t pos, int max_emit)
+{
+ memset(cap, 0, sizeof(*cap));
+ cap->ctx.actor = dir_capture_actor;
+ cap->ctx.pos = pos;
+ cap->max_emit = max_emit;
+}
+
+static struct buffer_head *alloc_dir_lblk(struct dir_test_ctx *ctx,
+ ext4_lblk_t lblk)
+{
+ int i;
+
+ if (lblk >= DIR_TEST_MAX_LBLOCKS)
+ return NULL;
+
+ if (ctx->lblk_to_pblk[lblk]) {
+ unsigned long idx = ctx->lblk_to_pblk[lblk] -
+ DIR_TEST_FIRST_PBLK;
+
+ return &ctx->blocks[idx].bh;
+ }
+
+ for (i = 0; i < DIR_TEST_MAX_BLOCKS; i++) {
+ if (!ctx->blocks[i].in_use) {
+ struct buffer_head *bh = &ctx->blocks[i].bh;
+
+ ctx->blocks[i].in_use = true;
+ memset(ctx->blocks[i].data, 0, DIR_TEST_BLK_SIZE);
+ set_buffer_uptodate(bh);
+ clear_buffer_verified(bh);
+ ctx->lblk_to_pblk[lblk] = DIR_TEST_FIRST_PBLK + i;
+ return bh;
+ }
+ }
+ return NULL;
+}
+
+static struct ext4_dir_entry_2 *write_de(char *buf, unsigned int offset,
+ u32 ino, u16 rec_len, u8 file_type,
+ const char *name)
+{
+ struct ext4_dir_entry_2 *de = (struct ext4_dir_entry_2 *)(buf + offset);
+ size_t nlen = strlen(name);
+
+ memset(de, 0, rec_len);
+ de->inode = cpu_to_le32(ino);
+ de->rec_len = ext4_rec_len_to_disk(rec_len, DIR_TEST_BLK_SIZE);
+ de->name_len = (u8)nlen;
+ de->file_type = file_type;
+ memcpy(de->name, name, nlen);
+ return de;
+}
+
+static int store_stub_dirent(struct file *dir_file, __u32 hash,
+ __u32 minor_hash, u32 ino, u8 file_type,
+ const char *name)
+{
+ struct ext4_dir_entry_2 de = {
+ .inode = cpu_to_le32(ino),
+ .file_type = file_type,
+ };
+ struct fscrypt_str ent_name = {
+ .name = (unsigned char *)name,
+ .len = strlen(name),
+ };
+
+ return ext4_htree_store_dirent(dir_file, hash, minor_hash,
+ &de, &ent_name);
+}
+
+/* --- KUnit Static Stubs --- */
+
+static int dir_map_blocks_stub(handle_t *handle, struct inode *inode,
+ struct ext4_map_blocks *map, int flags)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct dir_test_ctx *ctx = test->priv;
+ ext4_fsblk_t pblk;
+
+ if (ctx->fail_map_blocks)
+ return ctx->fail_map_blocks;
+ if (map->m_lblk >= DIR_TEST_MAX_LBLOCKS)
+ return 0;
+
+ pblk = ctx->lblk_to_pblk[map->m_lblk];
+ if (!pblk) {
+ map->m_pblk = 0;
+ map->m_len = 1;
+ map->m_flags = 0;
+ return 0;
+ }
+
+ map->m_pblk = pblk;
+ map->m_len = 1;
+ map->m_flags = EXT4_MAP_MAPPED;
+ return 1;
+}
+
+static struct buffer_head *dir_getblk_stub(handle_t *handle,
+ struct inode *inode,
+ ext4_lblk_t block, int map_flags)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct dir_test_ctx *ctx = test->priv;
+ ext4_fsblk_t pblk;
+ struct buffer_head *bh;
+
+ if (ctx->fail_getblk)
+ return ERR_PTR(ctx->fail_getblk);
+ if (block >= DIR_TEST_MAX_LBLOCKS)
+ return NULL;
+
+ pblk = ctx->lblk_to_pblk[block];
+ if (!pblk)
+ return NULL;
+
+ bh = &ctx->blocks[pblk - DIR_TEST_FIRST_PBLK].bh;
+ get_bh(bh);
+ return bh;
+}
+
+static int dir_dirty_metadata_stub(const char *where, unsigned int line,
+ handle_t *handle, struct inode *inode,
+ struct buffer_head *bh)
+{
+ return 0;
+}
+
+static int dir_htree_fill_tree_stub(struct file *dir_file, __u32 start_hash,
+ __u32 start_minor_hash, __u32 *next_hash)
+{
+ struct kunit *test = kunit_get_current_test();
+ struct dir_test_ctx *ctx = test->priv;
+ int err;
+
+ ctx->htree_fill_calls++;
+ ctx->last_fill_start_hash = start_hash;
+ ctx->last_fill_start_minor = start_minor_hash;
+
+ if (ctx->htree_mode == DIR_HTREE_STUB_BAD_DX)
+ return ERR_BAD_DX_DIR;
+
+ if (ctx->htree_mode == DIR_HTREE_STUB_TWO_BATCH) {
+ int count = 0;
+
+ if (start_hash < 0x2000) {
+ if (start_hash == 0 && start_minor_hash == 0) {
+ err = store_stub_dirent(dir_file, 0, 0,
+ DIR_TEST_DIR_INO,
+ EXT4_FT_DIR, ".");
+ if (err)
+ return err;
+ count++;
+ }
+ if (start_hash < 2 ||
+ (start_hash == 2 && start_minor_hash == 0)) {
+ err = store_stub_dirent(dir_file, 2, 0,
+ DIR_TEST_ROOT_INO,
+ EXT4_FT_DIR, "..");
+ if (err)
+ return err;
+ count++;
+ }
+ if (start_hash < 0x1000 ||
+ (start_hash == 0x1000 &&
+ start_minor_hash <= 0x55)) {
+ err = store_stub_dirent(dir_file, 0x1000, 0x55,
+ 21, EXT4_FT_REG_FILE,
+ "coll_a");
+ if (err)
+ return err;
+ err = store_stub_dirent(dir_file, 0x1000, 0x55,
+ 22, EXT4_FT_SYMLINK,
+ "coll_b");
+ if (err)
+ return err;
+ count += 2;
+ }
+ *next_hash = 0x2000;
+ return count;
+ }
+
+ if (start_hash <= 0x2000) {
+ err = store_stub_dirent(dir_file, 0x2000, 0x09,
+ 23, EXT4_FT_DIR, "leaf2_sub");
+ if (err)
+ return err;
+ *next_hash = ~0U;
+ return 1;
+ }
+
+ *next_hash = ~0U;
+ return 0;
+ }
+
+ *next_hash = ~0U;
+ return 0;
+}
+
+static int dir_test_init(struct kunit *test)
+{
+ struct dir_test_ctx *ctx;
+ struct inode *dir;
+ int i;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+ test->priv = ctx;
+
+ ctx->sb.s_blocksize = DIR_TEST_BLK_SIZE;
+ ctx->sb.s_blocksize_bits = DIR_TEST_BLK_BITS;
+ ctx->sb.s_magic = EXT4_SUPER_MAGIC;
+ ctx->sb.s_maxbytes = 64ULL * DIR_TEST_BLK_SIZE;
+ ctx->sb.s_op = &dir_test_sops;
+ ctx->sb.s_fs_info = &ctx->sbi;
+ ctx->sb.s_bdev = &ctx->bdev;
+ ctx->bdev.bd_mapping = &ctx->bd_mapping;
+ bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+ init_rwsem(&ctx->sb.s_umount);
+ strscpy(ctx->sb.s_id, "ext4_dir_kunit", sizeof(ctx->sb.s_id));
+
+ ctx->sbi.s_sb = &ctx->sb;
+ ctx->sbi.s_es = &ctx->es;
+ ctx->sbi.s_sbh = &ctx->sb_bh;
+ ctx->sbi.s_inode_size = DIR_TEST_INODE_SIZE;
+ ctx->sbi.s_first_ino = EXT4_GOOD_OLD_FIRST_INO;
+ ctx->sbi.s_bitmap_maxbytes = 16ULL * DIR_TEST_BLK_SIZE;
+ ctx->sbi.s_csum_seed = DIR_TEST_CSUM_SEED;
+ set_opt(&ctx->sb, ERRORS_CONT);
+ spin_lock_init(&ctx->sbi.s_bdev_wb_lock);
+ spin_lock_init(&ctx->sbi.s_error_lock);
+ spin_lock_init(&ctx->sbi.s_md_lock);
+ ratelimit_state_init(&ctx->sbi.s_err_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+ ratelimit_state_init(&ctx->sbi.s_warning_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+ ratelimit_state_init(&ctx->sbi.s_msg_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+
+ ctx->es.s_inodes_count = cpu_to_le32(1024);
+ ctx->es.s_first_ino = cpu_to_le32(EXT4_GOOD_OLD_FIRST_INO);
+ ctx->es.s_inode_size = cpu_to_le16(DIR_TEST_INODE_SIZE);
+ ctx->es.s_feature_incompat = cpu_to_le32(EXT4_FEATURE_INCOMPAT_FILETYPE);
+ ctx->es.s_feature_compat = cpu_to_le32(EXT4_FEATURE_COMPAT_DIR_INDEX);
+ ctx->es.s_feature_ro_compat =
+ cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+
+ ctx->sb_bh.b_data = (char *)&ctx->es;
+ ctx->sb_bh.b_size = DIR_TEST_BLK_SIZE;
+ ctx->sb_bh.b_blocknr = 1;
+ atomic_set(&ctx->sb_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->sb_bh);
+
+ for (i = 0; i < DIR_TEST_MAX_BLOCKS; i++) {
+ struct buffer_head *bh = &ctx->blocks[i].bh;
+
+ bh->b_data = ctx->blocks[i].data;
+ bh->b_size = DIR_TEST_BLK_SIZE;
+ bh->b_blocknr = DIR_TEST_FIRST_PBLK + i;
+ atomic_set(&bh->b_count, 1);
+ set_buffer_uptodate(bh);
+ }
+
+ dir = &ctx->dir_ei.vfs_inode;
+ dir->i_sb = &ctx->sb;
+ dir->i_ino = DIR_TEST_DIR_INO;
+ dir->i_mode = S_IFDIR | 0755;
+ dir->i_blkbits = DIR_TEST_BLK_BITS;
+ dir->i_mapping = &dir->i_data;
+ dir->i_data.host = dir;
+ set_nlink(dir, 2);
+ atomic_set(&dir->i_count, 1);
+ spin_lock_init(&dir->i_lock);
+ init_rwsem(&dir->i_rwsem);
+ INIT_LIST_HEAD(&dir->i_io_list);
+ INIT_LIST_HEAD(&dir->i_wb_list);
+ INIT_LIST_HEAD(&dir->i_lru);
+ INIT_HLIST_NODE(&dir->i_hash);
+ inode_state_set_raw(dir, I_DIRTY);
+ inode_set_iversion_raw(dir, 10);
+
+ init_rwsem(&ctx->dir_ei.xattr_sem);
+ init_rwsem(&ctx->dir_ei.i_data_sem);
+ INIT_LIST_HEAD(&ctx->dir_ei.i_orphan);
+ INIT_LIST_HEAD(&ctx->dir_ei.i_fc_list);
+ spin_lock_init(&ctx->dir_ei.i_fc_lock);
+ spin_lock_init(&ctx->dir_ei.i_raw_lock);
+ ctx->dir_ei.i_extra_isize = DIR_TEST_EXTRA_ISIZE;
+ ctx->dir_ei.i_csum_seed = DIR_TEST_CSUM_SEED ^ DIR_TEST_DIR_INO;
+
+ init_mock_dir_file(ctx, FMODE_64BITHASH);
+
+ kunit_activate_static_stub(test, ext4_map_blocks, dir_map_blocks_stub);
+ kunit_activate_static_stub(test, ext4_getblk, dir_getblk_stub);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ dir_dirty_metadata_stub);
+
+ return 0;
+}
+
+/*
+ * Test 1: Validate __ext4_check_dir_entry() and ext4_check_all_de() across
+ * valid directory blocks (including fake '.' / '..' / csum tail entries and
+ * casefold+encrypted extended-hash entries) and every corruption condition.
+ */
+static void test_check_dir_entry_and_all_de(struct kunit *test)
+{
+ struct dir_test_ctx *ctx = test->priv;
+ struct inode *dir = &ctx->dir_ei.vfs_inode;
+ struct buffer_head *bh;
+ struct ext4_dir_entry_2 *de;
+ unsigned int tail_len = sizeof(struct ext4_dir_entry_tail);
+ char *buf;
+
+ bh = alloc_dir_lblk(ctx, 0);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ buf = bh->b_data;
+
+ /* Valid block: '.', '..', 'alpha', and a 12-byte csum tail */
+ write_de(buf, 0, DIR_TEST_DIR_INO, 12, EXT4_FT_DIR, ".");
+ write_de(buf, 12, DIR_TEST_ROOT_INO, 12, EXT4_FT_DIR, "..");
+ write_de(buf, 24, 15, DIR_TEST_BLK_SIZE - 24 - tail_len,
+ EXT4_FT_REG_FILE, "alpha");
+ ext4_initialize_dirent_tail(bh, DIR_TEST_BLK_SIZE);
+
+ KUNIT_EXPECT_EQ(test, ext4_check_all_de(dir, bh, buf,
+ DIR_TEST_BLK_SIZE), 0);
+
+ /* 1. rec_len smaller than minimal (8 < 12) */
+ de = (struct ext4_dir_entry_2 *)(buf + 24);
+ de->rec_len = cpu_to_le16(8);
+ KUNIT_EXPECT_EQ(test, __ext4_check_dir_entry(__func__, __LINE__, dir,
+ &ctx->filp, de, bh, buf,
+ DIR_TEST_BLK_SIZE, 24), 1);
+ KUNIT_EXPECT_EQ(test, ext4_check_all_de(dir, bh, buf,
+ DIR_TEST_BLK_SIZE),
+ -EFSCORRUPTED);
+
+ /* 2. rec_len not a multiple of 4 (14 % 4 != 0) */
+ de->rec_len = cpu_to_le16(14);
+ KUNIT_EXPECT_EQ(test, __ext4_check_dir_entry(__func__, __LINE__, dir,
+ NULL, de, bh, buf,
+ DIR_TEST_BLK_SIZE, 24), 1);
+
+ /* 3. rec_len too small for name_len (name_len=10 needs 20 bytes) */
+ de->name_len = 10;
+ de->rec_len = cpu_to_le16(16);
+ KUNIT_EXPECT_EQ(test, __ext4_check_dir_entry(__func__, __LINE__, dir,
+ NULL, de, bh, buf,
+ DIR_TEST_BLK_SIZE, 24), 1);
+
+ /*
+ * 4. Casefolded + encrypted directory requires space for
+ * ext4_dir_entry_hash (+8 bytes) on regular entries, while fake
+ * entries ('.', '..', csum tail) remain 12 bytes.
+ */
+ dir->i_flags |= S_CASEFOLD | S_ENCRYPTED;
+ write_de(buf, 24, 15, 16, EXT4_FT_REG_FILE, "ab");
+ de = (struct ext4_dir_entry_2 *)(buf + 24);
+ KUNIT_EXPECT_EQ(test, __ext4_check_dir_entry(__func__, __LINE__, dir,
+ NULL, de, bh, buf,
+ DIR_TEST_BLK_SIZE, 24), 1);
+ write_de(buf, 24, 15, DIR_TEST_BLK_SIZE - 24 - tail_len,
+ EXT4_FT_REG_FILE, "ab");
+ ext4_initialize_dirent_tail(bh, DIR_TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, ext4_check_all_de(dir, bh, buf,
+ DIR_TEST_BLK_SIZE), 0);
+ dir->i_flags &= ~(S_CASEFOLD | S_ENCRYPTED);
+
+ /* 5. Directory entry overrun (next_offset > size) */
+ de = (struct ext4_dir_entry_2 *)(buf + 24);
+ de->rec_len = cpu_to_le16(DIR_TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, __ext4_check_dir_entry(__func__, __LINE__, dir,
+ NULL, de, bh, buf,
+ DIR_TEST_BLK_SIZE, 24), 1);
+
+ /* 6. Entry too close to block end (leaves only 8 bytes < 12) */
+ de->rec_len = cpu_to_le16(DIR_TEST_BLK_SIZE - 24 - 8);
+ KUNIT_EXPECT_EQ(test, __ext4_check_dir_entry(__func__, __LINE__, dir,
+ NULL, de, bh, buf,
+ DIR_TEST_BLK_SIZE, 24), 1);
+
+ /* 7. Inode number out of bounds (> s_inodes_count) */
+ de->rec_len = cpu_to_le16(DIR_TEST_BLK_SIZE - 24 - tail_len);
+ de->inode = cpu_to_le32(2048);
+ KUNIT_EXPECT_EQ(test, __ext4_check_dir_entry(__func__, __LINE__, dir,
+ NULL, de, bh, buf,
+ DIR_TEST_BLK_SIZE, 24), 1);
+
+ /* 8. '.' directory entry cannot span to the end of the block */
+ write_de(buf, 0, DIR_TEST_DIR_INO, DIR_TEST_BLK_SIZE, EXT4_FT_DIR, ".");
+ de = (struct ext4_dir_entry_2 *)buf;
+ KUNIT_EXPECT_EQ(test, __ext4_check_dir_entry(__func__, __LINE__, dir,
+ NULL, de, bh, buf,
+ DIR_TEST_BLK_SIZE, 0), 1);
+}
+
+/*
+ * Test 2: Verify is_dx_dir(), get_dtype(), and 32-bit / 64-bit HTree telldir
+ * hash-position cookie helpers.
+ */
+static void test_dx_detection_and_hash_pos_cookies(struct kunit *test)
+{
+ struct dir_test_ctx *ctx = test->priv;
+ struct inode *dir = &ctx->dir_ei.vfs_inode;
+ __u32 major = 0xaabbccdeU;
+ __u32 minor = 0x12345678U;
+ loff_t pos;
+
+ /* Without DIR_INDEX feature, is_dx_dir() is always 0 */
+ ext4_clear_feature_dir_index(&ctx->sb);
+ ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
+ i_size_write(dir, DIR_TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, ext4_is_dx_dir_test(dir), 0);
+
+ /* With DIR_INDEX: multi-block dir without INDEX or inline is non-dx */
+ ext4_set_feature_dir_index(&ctx->sb);
+ ext4_clear_inode_flag(dir, EXT4_INODE_INDEX);
+ i_size_write(dir, 2 * DIR_TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, ext4_is_dx_dir_test(dir), 0);
+
+ /* Single-block dir with DIR_INDEX is treated as dx-capable */
+ i_size_write(dir, DIR_TEST_BLK_SIZE);
+ KUNIT_EXPECT_EQ(test, ext4_is_dx_dir_test(dir), 1);
+
+ /* Multi-block dir with EXT4_INODE_INDEX is dx */
+ i_size_write(dir, 4 * DIR_TEST_BLK_SIZE);
+ ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
+ KUNIT_EXPECT_EQ(test, ext4_is_dx_dir_test(dir), 1);
+ ext4_clear_inode_flag(dir, EXT4_INODE_INDEX);
+
+ /* Inline-data dir with DIR_INDEX is treated as dx */
+ ext4_set_inode_flag(dir, EXT4_INODE_INLINE_DATA);
+ ctx->dir_ei.i_inline_off = 160;
+ KUNIT_EXPECT_EQ(test, ext4_is_dx_dir_test(dir), 1);
+ ext4_clear_inode_flag(dir, EXT4_INODE_INLINE_DATA);
+ ctx->dir_ei.i_inline_off = 0;
+
+ /* get_dtype() mapping with and without FILETYPE feature */
+ KUNIT_EXPECT_EQ(test, get_dtype(&ctx->sb, EXT4_FT_REG_FILE), DT_REG);
+ KUNIT_EXPECT_EQ(test, get_dtype(&ctx->sb, EXT4_FT_DIR), DT_DIR);
+ KUNIT_EXPECT_EQ(test, get_dtype(&ctx->sb, EXT4_FT_SYMLINK), DT_LNK);
+ KUNIT_EXPECT_EQ(test, get_dtype(&ctx->sb, EXT4_FT_DIR_CSUM),
+ DT_UNKNOWN);
+ ext4_clear_feature_filetype(&ctx->sb);
+ KUNIT_EXPECT_EQ(test, get_dtype(&ctx->sb, EXT4_FT_REG_FILE),
+ DT_UNKNOWN);
+ ext4_set_feature_filetype(&ctx->sb);
+
+ /* 64-bit hash cookie round-trip */
+ ctx->filp.f_mode = FMODE_READ | FMODE_64BITHASH;
+ KUNIT_EXPECT_EQ(test, ext4_get_htree_eof_test(&ctx->filp),
+ (loff_t)EXT4_HTREE_EOF_64BIT);
+ pos = ext4_dir_hash2pos_test(&ctx->filp, major, minor);
+ KUNIT_EXPECT_EQ(test, ext4_dir_pos2maj_hash_test(&ctx->filp, pos),
+ major & ~1U);
+ KUNIT_EXPECT_EQ(test, ext4_dir_pos2min_hash_test(&ctx->filp, pos),
+ minor);
+
+ /* 32-bit hash cookie round-trip */
+ ctx->filp.f_mode = FMODE_READ | FMODE_32BITHASH;
+ KUNIT_EXPECT_EQ(test, ext4_get_htree_eof_test(&ctx->filp),
+ (loff_t)EXT4_HTREE_EOF_32BIT);
+ pos = ext4_dir_hash2pos_test(&ctx->filp, major, minor);
+ KUNIT_EXPECT_EQ(test, pos, (loff_t)(major >> 1));
+ KUNIT_EXPECT_EQ(test, ext4_dir_pos2maj_hash_test(&ctx->filp, pos),
+ major & ~1U);
+ KUNIT_EXPECT_EQ(test, ext4_dir_pos2min_hash_test(&ctx->filp, pos), 0U);
+}
+
+/*
+ * Test 3: Verify ext4_dir_operations open(), llseek(), and release() across
+ * 64-bit dx dirs, 32-bit dx dirs, and non-dx indirect/extent linear dirs.
+ */
+static void test_dir_open_llseek_and_release(struct kunit *test)
+{
+ struct dir_test_ctx *ctx = test->priv;
+ struct inode *dir = &ctx->dir_ei.vfs_inode;
+ struct dir_private_info *info;
+ loff_t ret;
+
+ KUNIT_ASSERT_EQ(test, ext4_dir_operations.open(dir, &ctx->filp), 0);
+ info = ctx->filp.private_data;
+ KUNIT_ASSERT_NOT_NULL(test, info);
+
+ /* 1. DX directory with 64-bit hash cookies */
+ ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
+ i_size_write(dir, 2 * DIR_TEST_BLK_SIZE);
+ ctx->filp.f_mode = FMODE_READ | FMODE_LSEEK | FMODE_64BITHASH;
+ info->cookie = inode_query_iversion(dir);
+
+ ret = ext4_dir_operations.llseek(&ctx->filp, EXT4_HTREE_EOF_64BIT,
+ SEEK_SET);
+ KUNIT_EXPECT_EQ(test, ret, (loff_t)EXT4_HTREE_EOF_64BIT);
+ KUNIT_EXPECT_FALSE(test, inode_eq_iversion(dir, info->cookie));
+
+ ret = ext4_dir_operations.llseek(&ctx->filp, -1, SEEK_SET);
+ KUNIT_EXPECT_EQ(test, ret, (loff_t)-EINVAL);
+
+ /* 2. DX directory with 32-bit hash cookies caps at 32-bit EOF */
+ ctx->filp.f_mode = FMODE_READ | FMODE_LSEEK | FMODE_32BITHASH;
+ ret = ext4_dir_operations.llseek(&ctx->filp, EXT4_HTREE_EOF_32BIT,
+ SEEK_SET);
+ KUNIT_EXPECT_EQ(test, ret, (loff_t)EXT4_HTREE_EOF_32BIT);
+ ret = ext4_dir_operations.llseek(&ctx->filp,
+ (loff_t)EXT4_HTREE_EOF_32BIT + 1,
+ SEEK_SET);
+ KUNIT_EXPECT_EQ(test, ret, (loff_t)-EINVAL);
+
+ /* 3. Non-DX indirect directory uses s_bitmap_maxbytes and i_size */
+ ext4_clear_inode_flag(dir, EXT4_INODE_INDEX);
+ ext4_clear_inode_flag(dir, EXT4_INODE_EXTENTS);
+ i_size_write(dir, 3 * DIR_TEST_BLK_SIZE);
+ info->cookie = inode_query_iversion(dir);
+
+ ret = ext4_dir_operations.llseek(&ctx->filp, 0, SEEK_END);
+ KUNIT_EXPECT_EQ(test, ret, (loff_t)(3 * DIR_TEST_BLK_SIZE));
+ KUNIT_EXPECT_FALSE(test, inode_eq_iversion(dir, info->cookie));
+
+ ret = ext4_dir_operations.llseek(&ctx->filp,
+ ctx->sbi.s_bitmap_maxbytes + 1,
+ SEEK_SET);
+ KUNIT_EXPECT_EQ(test, ret, (loff_t)-EINVAL);
+
+ KUNIT_EXPECT_EQ(test, ext4_dir_operations.release(dir, &ctx->filp), 0);
+ ctx->filp.private_data = NULL;
+}
+
+/*
+ * Test 4: Verify ext4_htree_store_dirent() rbtree sorting across major and
+ * minor hashes, exact 64-bit hash collision chaining, and cleanup via
+ * ext4_htree_free_dir_info().
+ */
+static void test_htree_store_dirent_and_free(struct kunit *test)
+{
+ struct dir_test_ctx *ctx = test->priv;
+ struct inode *dir = &ctx->dir_ei.vfs_inode;
+ struct dir_private_info *info;
+ int err;
+
+ ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
+ i_size_write(dir, 2 * DIR_TEST_BLK_SIZE);
+ KUNIT_ASSERT_EQ(test, ext4_dir_operations.open(dir, &ctx->filp), 0);
+ info = ctx->filp.private_data;
+
+ /* Insert out of hash order, including 3 colliding entries at 0x200:7 */
+ KUNIT_ASSERT_EQ(test, store_stub_dirent(&ctx->filp, 0x400, 0x10, 34,
+ EXT4_FT_REG_FILE, "fourth"), 0);
+ KUNIT_ASSERT_EQ(test, store_stub_dirent(&ctx->filp, 0x100, 0x20, 31,
+ EXT4_FT_DIR, "first"), 0);
+ KUNIT_ASSERT_EQ(test, store_stub_dirent(&ctx->filp, 0x400, 0x05, 33,
+ EXT4_FT_SYMLINK, "third"), 0);
+ KUNIT_ASSERT_EQ(test, store_stub_dirent(&ctx->filp, 0x200, 0x07, 32,
+ EXT4_FT_REG_FILE, "c1"), 0);
+ KUNIT_ASSERT_EQ(test, store_stub_dirent(&ctx->filp, 0x200, 0x07, 35,
+ EXT4_FT_REG_FILE, "c2"), 0);
+ KUNIT_ASSERT_EQ(test, store_stub_dirent(&ctx->filp, 0x200, 0x07, 36,
+ EXT4_FT_REG_FILE, "c3"), 0);
+
+ /* Pre-prime cookie and next_hash so ext4_dx_readdir drains info->root */
+ info->cookie = inode_query_iversion(dir);
+ info->next_hash = ~0U;
+ reset_capture(&ctx->cap, 0, 0);
+
+ inode_lock_shared(dir);
+ err = ext4_dir_operations.iterate_shared(&ctx->filp, &ctx->cap.ctx);
+ inode_unlock_shared(dir);
+
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_ASSERT_EQ(test, ctx->cap.count, 6);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[0].name, "first");
+ KUNIT_EXPECT_EQ(test, ctx->cap.recs[0].d_type, (unsigned int)DT_DIR);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[1].name, "c1");
+ /* Collision list inserts after head: c1 -> c3 -> c2 */
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[2].name, "c3");
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[3].name, "c2");
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[4].name, "third");
+ KUNIT_EXPECT_EQ(test, ctx->cap.recs[4].d_type, (unsigned int)DT_LNK);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[5].name, "fourth");
+ KUNIT_EXPECT_EQ(test, ctx->cap.ctx.pos, (loff_t)EXT4_HTREE_EOF_64BIT);
+
+ /* Re-populate nodes so release exercises non-empty free_rb_tree_fname */
+ KUNIT_ASSERT_EQ(test, store_stub_dirent(&ctx->filp, 0x300, 0x01, 40,
+ EXT4_FT_REG_FILE, "leftover1"),
+ 0);
+ KUNIT_ASSERT_EQ(test, store_stub_dirent(&ctx->filp, 0x300, 0x01, 41,
+ EXT4_FT_REG_FILE, "leftover2"),
+ 0);
+ KUNIT_EXPECT_EQ(test, ext4_dir_operations.release(dir, &ctx->filp), 0);
+ ctx->filp.private_data = NULL;
+}
+
+/*
+ * Test 5: Verify linear ext4_readdir() across multi-block directories with
+ * sparse holes, deleted entries, dir_emit() stop/resume, i_version change
+ * mid-entry revalidation, checksum/corruption block skipping, and
+ * ERR_BAD_DX_DIR fallback clearing EXT4_INODE_INDEX.
+ */
+static void test_linear_readdir(struct kunit *test)
+{
+ struct dir_test_ctx *ctx = test->priv;
+ struct inode *dir = &ctx->dir_ei.vfs_inode;
+ struct buffer_head *bh0, *bh2, *bh3;
+ unsigned int tail_len = sizeof(struct ext4_dir_entry_tail);
+ loff_t resume_pos;
+ int err;
+
+ /*
+ * 4-block non-indexed linear directory:
+ * lblk 0: '.', '..', deleted (ino=0), 'file_a', 'sym_b' + csum tail
+ * lblk 1: sparse hole (unmapped)
+ * lblk 2: 'sub_c', 'fifo_d' + csum tail
+ * lblk 3: 'last_e' + csum tail
+ */
+ ext4_clear_inode_flag(dir, EXT4_INODE_INDEX);
+ i_size_write(dir, 4 * DIR_TEST_BLK_SIZE);
+ dir->i_blocks = (4 * DIR_TEST_BLK_SIZE) >> 9;
+
+ bh0 = alloc_dir_lblk(ctx, 0);
+ bh2 = alloc_dir_lblk(ctx, 2);
+ bh3 = alloc_dir_lblk(ctx, 3);
+ KUNIT_ASSERT_NOT_NULL(test, bh0);
+ KUNIT_ASSERT_NOT_NULL(test, bh2);
+ KUNIT_ASSERT_NOT_NULL(test, bh3);
+
+ write_de(bh0->b_data, 0, DIR_TEST_DIR_INO, 12, EXT4_FT_DIR, ".");
+ write_de(bh0->b_data, 12, DIR_TEST_ROOT_INO, 12, EXT4_FT_DIR, "..");
+ write_de(bh0->b_data, 24, 0, 16, EXT4_FT_REG_FILE, "deleted");
+ write_de(bh0->b_data, 40, 51, 20, EXT4_FT_REG_FILE, "file_a");
+ write_de(bh0->b_data, 60, 52, DIR_TEST_BLK_SIZE - 60 - tail_len,
+ EXT4_FT_SYMLINK, "sym_b");
+ ext4_initialize_dirent_tail(bh0, DIR_TEST_BLK_SIZE);
+ KUNIT_ASSERT_EQ(test, ext4_handle_dirty_dirblock(NULL, dir, bh0), 0);
+
+ write_de(bh2->b_data, 0, 53, 16, EXT4_FT_DIR, "sub_c");
+ write_de(bh2->b_data, 16, 54, DIR_TEST_BLK_SIZE - 16 - tail_len,
+ EXT4_FT_FIFO, "fifo_d");
+ ext4_initialize_dirent_tail(bh2, DIR_TEST_BLK_SIZE);
+ KUNIT_ASSERT_EQ(test, ext4_handle_dirty_dirblock(NULL, dir, bh2), 0);
+
+ write_de(bh3->b_data, 0, 55, DIR_TEST_BLK_SIZE - tail_len,
+ EXT4_FT_REG_FILE, "last_e");
+ ext4_initialize_dirent_tail(bh3, DIR_TEST_BLK_SIZE);
+ KUNIT_ASSERT_EQ(test, ext4_handle_dirty_dirblock(NULL, dir, bh3), 0);
+
+ KUNIT_ASSERT_EQ(test, ext4_dir_operations.open(dir, &ctx->filp), 0);
+
+ /* 1. Stop after 3 emitted entries ('.', '..', 'file_a'), then resume */
+ reset_capture(&ctx->cap, 0, 3);
+ inode_lock_shared(dir);
+ err = ext4_dir_operations.iterate_shared(&ctx->filp, &ctx->cap.ctx);
+ inode_unlock_shared(dir);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_ASSERT_EQ(test, ctx->cap.count, 3);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[0].name, ".");
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[1].name, "..");
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[2].name, "file_a");
+ KUNIT_EXPECT_EQ(test, ctx->cap.recs[2].d_type, (unsigned int)DT_REG);
+
+ resume_pos = ctx->cap.ctx.pos;
+ KUNIT_EXPECT_EQ(test, resume_pos, (loff_t)60);
+
+ reset_capture(&ctx->cap, resume_pos, 0);
+ inode_lock_shared(dir);
+ err = ext4_dir_operations.iterate_shared(&ctx->filp, &ctx->cap.ctx);
+ inode_unlock_shared(dir);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_ASSERT_EQ(test, ctx->cap.count, 4);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[0].name, "sym_b");
+ KUNIT_EXPECT_EQ(test, ctx->cap.recs[0].d_type, (unsigned int)DT_LNK);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[1].name, "sub_c");
+ KUNIT_EXPECT_EQ(test, ctx->cap.recs[1].d_type, (unsigned int)DT_DIR);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[2].name, "fifo_d");
+ KUNIT_EXPECT_EQ(test, ctx->cap.recs[2].d_type, (unsigned int)DT_FIFO);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[3].name, "last_e");
+
+ /*
+ * 2. Unaligned ctx->pos (44, inside 'file_a' at [40..60)) combined
+ * with an i_version bump triggers block-start rescan and snaps to 60.
+ */
+ inode_inc_iversion(dir);
+ reset_capture(&ctx->cap, 44, 1);
+ inode_lock_shared(dir);
+ err = ext4_dir_operations.iterate_shared(&ctx->filp, &ctx->cap.ctx);
+ inode_unlock_shared(dir);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_ASSERT_EQ(test, ctx->cap.count, 1);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[0].name, "sym_b");
+ KUNIT_EXPECT_EQ(test, ctx->cap.recs[0].offset, (loff_t)60);
+
+ /*
+ * 3. Corrupt block 0 checksum and corrupt second entry in block 2:
+ * ext4_readdir() skips block 0, emits 'sub_c' from block 2 before
+ * skipping the rest of block 2, and still emits 'last_e' from block 3.
+ */
+ clear_buffer_verified(bh0);
+ bh0->b_data[DIR_TEST_BLK_SIZE - 1] ^= 0xff;
+ ((struct ext4_dir_entry_2 *)(bh2->b_data + 16))->rec_len =
+ cpu_to_le16(8);
+
+ reset_capture(&ctx->cap, 0, 0);
+ inode_lock_shared(dir);
+ err = ext4_dir_operations.iterate_shared(&ctx->filp, &ctx->cap.ctx);
+ inode_unlock_shared(dir);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_ASSERT_EQ(test, ctx->cap.count, 2);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[0].name, "sub_c");
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[1].name, "last_e");
+
+ /*
+ * 4. ERR_BAD_DX_DIR fallback when !metadata_csum clears
+ * EXT4_INODE_INDEX and falls back to linear block scanning.
+ */
+ ext4_clear_feature_metadata_csum(&ctx->sb);
+ ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
+ write_de(bh3->b_data, 0, 55, DIR_TEST_BLK_SIZE,
+ EXT4_FT_REG_FILE, "last_e");
+ kunit_activate_static_stub(test, ext4_htree_fill_tree,
+ dir_htree_fill_tree_stub);
+ ctx->htree_mode = DIR_HTREE_STUB_BAD_DX;
+
+ reset_capture(&ctx->cap, 3 * DIR_TEST_BLK_SIZE, 0);
+ inode_lock_shared(dir);
+ err = ext4_dir_operations.iterate_shared(&ctx->filp, &ctx->cap.ctx);
+ inode_unlock_shared(dir);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_test_inode_flag(dir, EXT4_INODE_INDEX));
+ KUNIT_ASSERT_EQ(test, ctx->cap.count, 1);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[0].name, "last_e");
+
+ KUNIT_EXPECT_EQ(test, ext4_dir_operations.release(dir, &ctx->filp), 0);
+ ctx->filp.private_data = NULL;
+}
+
+/*
+ * Test 6: Verify ext4_dx_readdir() across multi-batch leaf refills,
+ * mid-collision-chain stop & extra_fname resumption, EOF short-circuit,
+ * f_pos seek tree reset, and mid-stream i_version invalidation.
+ */
+static void test_dx_readdir(struct kunit *test)
+{
+ struct dir_test_ctx *ctx = test->priv;
+ struct inode *dir = &ctx->dir_ei.vfs_inode;
+ loff_t saved_pos, seek_pos;
+ int err;
+
+ ext4_set_inode_flag(dir, EXT4_INODE_INDEX);
+ i_size_write(dir, 2 * DIR_TEST_BLK_SIZE);
+ kunit_activate_static_stub(test, ext4_htree_fill_tree,
+ dir_htree_fill_tree_stub);
+ ctx->htree_mode = DIR_HTREE_STUB_TWO_BATCH;
+
+ KUNIT_ASSERT_EQ(test, ext4_dir_operations.open(dir, &ctx->filp), 0);
+
+ /*
+ * 1. Stop after 3 entries ('.', '..', and first collision entry):
+ * leaves the second colliding entry queued on info->extra_fname.
+ */
+ reset_capture(&ctx->cap, 0, 3);
+ inode_lock_shared(dir);
+ err = ext4_dir_operations.iterate_shared(&ctx->filp, &ctx->cap.ctx);
+ inode_unlock_shared(dir);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->htree_fill_calls, 1);
+ KUNIT_ASSERT_EQ(test, ctx->cap.count, 3);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[0].name, ".");
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[1].name, "..");
+ /* Collision chain at 0x1000:0x55 emits coll_a then coll_b */
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[2].name, "coll_a");
+ saved_pos = ctx->cap.ctx.pos;
+
+ /*
+ * 2. Resume from saved_pos: emits leftover 'coll_b' from extra_fname
+ * without refilling batch 1, then refills at next_hash=0x2000 for
+ * 'leaf2_sub', and finishes at EXT4_HTREE_EOF_64BIT.
+ */
+ reset_capture(&ctx->cap, saved_pos, 0);
+ inode_lock_shared(dir);
+ err = ext4_dir_operations.iterate_shared(&ctx->filp, &ctx->cap.ctx);
+ inode_unlock_shared(dir);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->htree_fill_calls, 2);
+ KUNIT_EXPECT_EQ(test, ctx->last_fill_start_hash, 0x2000U);
+ KUNIT_ASSERT_EQ(test, ctx->cap.count, 2);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[0].name, "coll_b");
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[1].name, "leaf2_sub");
+ KUNIT_EXPECT_EQ(test, ctx->cap.ctx.pos, (loff_t)EXT4_HTREE_EOF_64BIT);
+
+ /* 3. Calling again at HTREE_EOF returns 0 immediately without refill */
+ reset_capture(&ctx->cap, ctx->cap.ctx.pos, 0);
+ inode_lock_shared(dir);
+ err = ext4_dir_operations.iterate_shared(&ctx->filp, &ctx->cap.ctx);
+ inode_unlock_shared(dir);
+ KUNIT_EXPECT_EQ(test, err, 0);
+ KUNIT_EXPECT_EQ(test, ctx->cap.count, 0);
+ KUNIT_EXPECT_EQ(test, ctx->htree_fill_calls, 2);
+
+ /*
+ * 4. Seeking to a 64-bit hash cookie resets the cached state and
+ * refills starting from (pos2maj_hash, pos2min_hash); bumping
+ * i_version after the first entry forces an rbtree refill mid-walk.
+ */
+ seek_pos = ext4_dir_hash2pos_test(&ctx->filp, 0x1000, 0x55);
+ reset_capture(&ctx->cap, seek_pos, 0);
+ ctx->cap.bump_iversion_after_first = dir;
+
+ inode_lock_shared(dir);
+ err = ext4_dir_operations.iterate_shared(&ctx->filp, &ctx->cap.ctx);
+ inode_unlock_shared(dir);
+ KUNIT_ASSERT_EQ(test, err, 0);
+ KUNIT_EXPECT_GE(test, ctx->htree_fill_calls, 4);
+ KUNIT_EXPECT_GE(test, ctx->cap.count, 3);
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[0].name, "coll_a");
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[1].name, "coll_b");
+ KUNIT_EXPECT_STREQ(test, ctx->cap.recs[ctx->cap.count - 1].name,
+ "leaf2_sub");
+
+ KUNIT_EXPECT_EQ(test, ext4_dir_operations.release(dir, &ctx->filp), 0);
+ ctx->filp.private_data = NULL;
+}
+
+static struct kunit_case ext4_dir_test_cases[] = {
+ KUNIT_CASE(test_check_dir_entry_and_all_de),
+ KUNIT_CASE(test_dx_detection_and_hash_pos_cookies),
+ KUNIT_CASE(test_dir_open_llseek_and_release),
+ KUNIT_CASE(test_htree_store_dirent_and_free),
+ KUNIT_CASE(test_linear_readdir),
+ KUNIT_CASE(test_dx_readdir),
+ {}
+};
+
+static struct kunit_suite ext4_dir_test_suite = {
+ .name = "ext4_dir_test",
+ .init = dir_test_init,
+ .test_cases = ext4_dir_test_cases,
+};
+
+kunit_test_suites(&ext4_dir_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 directory operations");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/dir.c b/fs/ext4/dir.c
index 4214034b5aba..ad14875c3a1f 100644
--- a/fs/ext4/dir.c
+++ b/fs/ext4/dir.c
@@ -712,3 +712,36 @@ const struct file_operations ext4_dir_operations = {
.release = ext4_release_dir,
.setlease = generic_setlease,
};
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dir_operations);
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+int ext4_is_dx_dir_test(struct inode *inode)
+{
+ return is_dx_dir(inode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_is_dx_dir_test);
+
+loff_t ext4_dir_hash2pos_test(struct file *filp, __u32 major, __u32 minor)
+{
+ return hash2pos(filp, major, minor);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dir_hash2pos_test);
+
+__u32 ext4_dir_pos2maj_hash_test(struct file *filp, loff_t pos)
+{
+ return pos2maj_hash(filp, pos);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dir_pos2maj_hash_test);
+
+__u32 ext4_dir_pos2min_hash_test(struct file *filp, loff_t pos)
+{
+ return pos2min_hash(filp, pos);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dir_pos2min_hash_test);
+
+loff_t ext4_get_htree_eof_test(struct file *filp)
+{
+ return ext4_get_htree_eof(filp);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_htree_eof_test);
+#endif
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index aaf4722e709e..c7d64bb21c57 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3045,6 +3045,14 @@ static inline unsigned char get_dtype(struct super_block *sb, int filetype)
}
extern int ext4_check_all_de(struct inode *dir, struct buffer_head *bh,
void *buf, int buf_size);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+extern int ext4_is_dx_dir_test(struct inode *inode);
+extern loff_t ext4_dir_hash2pos_test(struct file *filp, __u32 major,
+ __u32 minor);
+extern __u32 ext4_dir_pos2maj_hash_test(struct file *filp, loff_t pos);
+extern __u32 ext4_dir_pos2min_hash_test(struct file *filp, loff_t pos);
+extern loff_t ext4_get_htree_eof_test(struct file *filp);
+#endif
/* fsync.c */
extern int ext4_sync_file(struct file *, loff_t, loff_t, int);
diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
index 63d01a9711a2..e44e306d5ca6 100644
--- a/fs/ext4/namei.c
+++ b/fs/ext4/namei.c
@@ -36,6 +36,7 @@
#include <linux/bio.h>
#include <linux/iversion.h>
#include <linux/unicode.h>
+#include <kunit/static_stub.h>
#include "ext4.h"
#include "ext4_jbd2.h"
@@ -1002,6 +1003,9 @@ int ext4_htree_fill_tree(struct file *dir_file, __u32 start_hash,
__u32 hashval;
struct fscrypt_str tmp_str;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_htree_fill_tree, dir_file, start_hash,
+ start_minor_hash, next_hash);
+
dxtrace(printk(KERN_DEBUG "In htree_fill_tree, start hash: %x:%x\n",
start_hash, start_minor_hash));
dir = file_inode(dir_file);
--
2.56.0.542.g3ef89b7020-goog
^ permalink raw reply related [flat|nested] 55+ messages in thread
* Re: [PATCH 15/27] ext4: check multi-block overlap on left traversal in add_system_zone()
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:10 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
> When walking system_blks->root to insert [start_blk, start_blk + count),
> add_system_zone() takes the left branch whenever start_blk <
> entry->start_blk instead of checking that the entire range ends at or
> before entry->start_blk (unlike ext4_sb_block_valid(), which checks
> start_blk + count - 1 < entry->start_blk).
>
> As a result, a corrupt multi-block metadata range (such as a group's
> inode table or a reserved journal inode extent mapped in
> ext4_protect_reserved_inode() before sbi->s_system_blks is published)
> that starts strictly before an existing system zone and extends into or
> across it traverses rb_left instead of returning -EFSCORRUPTED. This
> inserts overlapping intervals into system_blks->root and breaks the
> disjoint-interval invariant relied upon by ext4_sb_block_valid() (for
> example, a lookup on blocks past entry->start_blk + entry->count
> branches right and misses the overlapping zone stored in rb_left). A
> corrupt 64-bit start_blk whose start_blk + count wraps around u64 can
> similarly corrupt the rbtree ordering.
>
> Reject zero-length or u64-wrapping ranges up front and require
> start_blk + count <= entry->start_blk before taking rb_left in
> add_system_zone(). Add KUnit regression tests in
> fs/ext4/block_validity-test.c covering group metadata and journal
> extents that partially overlap from the left, completely enclose an
> existing system zone, or wrap 64-bit block numbers.
>
> Fixes: bf9a379d0980 ("ext4: don't allow overlapping system zones")
> Signed-off-by: Theodore Ts'o <tytso@mit.edu>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=15
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 03/27] ext4: add KUnit tests for ext4_ext_split() leaf and index splitting
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:15 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Low] ext4: unexplained test_swap_extents() changes and stale Fixes
SHA
--
commit 55141b7904f9009059aca2d2a02dc5c501ed9bde
Author: Theodore Ts'o <tytso@mit.edu>
> ext4: add KUnit tests for ext4_ext_split() leaf and index splitting
>
> Commit 7831b3664109 ("ext4: add KUnit tests for fs/ext4/extents.c")
> updated both block-allocation paths in ext4_ext_split() to call
> ext4_ext_getblk() and noted node splitting in the commit message, but
> none of the test cases in fs/ext4/extents-test.c actually reached
> ext4_ext_split(). In test_insert_extent_grow_and_merge_up(), inserting a
> 5th extent into a full depth-0 root triggers ext4_ext_grow_indepth(),
> moving the 4 extents into an external leaf block with eh_max == 340 and
> returning from ext4_ext_create_new_leaf() without splitting.
>
> Add two KUnit test cases to exercise ext4_ext_split() across leaf and
> intermediate index levels:
>
> - test_insert_extent_leaf_split(): fills a depth-1 external leaf block
> to capacity (340 extents), inserts in the middle to trigger a leaf
> split that moves tail extents into a new leaf block, and then fills
> the second leaf and appends past its end to exercise the m == 0
> leaf-addition path.
> - test_insert_extent_index_split(): exercises depth 1 -> 2 tree growth
> followed by leaf splitting via the repeat loop in
> ext4_ext_create_new_leaf(), and then clamps the depth-1 index block's
> eh_max to exercise simultaneous leaf and intermediate index block
> splitting (k == 1) in ext4_ext_split().
>
> Fixes: 7831b3664109 ("ext4: add KUnit tests for fs/ext4/extents.c")
> Signed-off-by: Theodore Ts'o <tytso@mit.edu>
> diff --git a/fs/ext4/extents-test.c b/fs/ext4/extents-test.c
> index 7056481862188..47ddeb94e2bf4 100644
> --- a/fs/ext4/extents-test.c
> +++ b/fs/ext4/extents-test.c
[ ... ]
> @@ -2073,17 +2265,13 @@ static void test_swap_extents(struct kunit *test)
> KUNIT_ASSERT_EQ(test, ext_insert_one(inode1, 10, 10, 100, false, 0), 0);
> KUNIT_ASSERT_EQ(test, ext_insert_one(inode2, 10, 10, 200, false, 0), 0);
>
> - inode_lock(inode1);
> - inode_lock(inode2);
> - down_write(&EXT4_I(inode1)->i_data_sem);
> - down_write(&EXT4_I(inode2)->i_data_sem);
> + lock_two_nondirectories(inode1, inode2);
> + ext4_double_down_write_data_sem(inode1, inode2);
>
> replaced = ext4_swap_extents(NULL, inode1, inode2, 12, 12, 5, 1, &err);
>
> - up_write(&EXT4_I(inode2)->i_data_sem);
> - up_write(&EXT4_I(inode1)->i_data_sem);
> - inode_unlock(inode2);
> - inode_unlock(inode1);
> + ext4_double_up_write_data_sem(inode1, inode2);
> + unlock_two_nondirectories(inode1, inode2);
>
> KUNIT_EXPECT_EQ(test, err, 0);
> KUNIT_EXPECT_EQ(test, replaced, 5);
[ ... ]
> diff --git a/fs/ext4/move_extent.c b/fs/ext4/move_extent.c
> index 3329b7ad5dbd8..d1405e7639128 100644
> --- a/fs/ext4/move_extent.c
> +++ b/fs/ext4/move_extent.c
> @@ -656,3 +656,8 @@ int ext4_move_extents(struct file *o_filp, struct file *d_filp, __u64 orig_blk,
> unlock_two_nondirectories(orig_inode, donor_inode);
> return ret;
> }
> +
> +#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_double_down_write_data_sem);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_double_up_write_data_sem);
> +#endif
[Severity: Low]
This isn't a bug, but the commit message does not mention updating
test_swap_extents() to use lock_two_nondirectories() and
ext4_double_down_write_data_sem() (and exporting those helpers from
fs/ext4/move_extent.c) to avoid lockdep warnings on i_rwsem and
i_data_sem.
Also, the commit message and Fixes: trailer cite a stale pre-rebase sha
for commit "ext4: add KUnit tests for fs/ext4/extents.c" rather than the
parent commit in this branch.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=3
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 12/27] ext4: add KUnit tests for fs/ext4/balloc.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:16 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
> Add the ext4_balloc_test KUnit test suite in fs/ext4/balloc-test.c (29
> test cases, 43 subtests total) to comprehensively exercise block bitmap
> management, group descriptor and overhead calculations, cluster pool
> reservation, allocation retry policy, metadata block allocation, and
> goal block selection in fs/ext4/balloc.c using an in-memory VFS
> superblock, mock block device, and deterministic block pool without
> requiring a mounted block device:
>
> - Block-to-group and cluster-offset mapping (ext4_get_group_number and
> ext4_get_group_no_and_offset) across 1K and 4K block sizes, standard
> shift-based vs. non-standard do_div group geometries, bigalloc
> multi-block clusters, s_first_data_block offsets, >32-bit physical
> block numbers, and optional NULL output pointers.
> - Backup superblock placement (ext4_bg_has_super) across dense
> filesystems, sparse_super powers of 3, 5, and 7, and sparse_super2
> [ ... ]
> through ext4_sb_getblk() so KUnit tests can intercept buffer-cache
> lookups via its existing static stub hook, and export the remaining
> non-static balloc.c entry points with EXPORT_SYMBOL_FOR_EXT4_TEST.
>
> Signed-off-by: Theodore Ts'o <tytso@mit.edu>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=12
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 08/27] ext4: zero s_fc_replay_state in ext4_fc_replay_cleanup()
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:16 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found no new issues
in this patch, but noticed 1 related pre-existing issue it does not
introduce:
- [High] ext4: missing s_ea_inode_work flush before
ext4_fc_replay_cleanup() [1]
--
commit a4ccfbc7c2db4e8403230f07464457a51d148eaa
Author: Theodore Ts'o <tytso@mit.edu>
ext4: zero s_fc_replay_state in ext4_fc_replay_cleanup()
This commit zeroes sbi->s_fc_replay_state in ext4_fc_replay_cleanup()
after freeing fc_regions and fc_modified_inodes so that repeated calls to
ext4_fc_replay_cleanup() are idempotent.
> diff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c
> index 0cac890cf3702..15bd040c2329b 100644
> --- a/fs/ext4/fast_commit.c
> +++ b/fs/ext4/fast_commit.c
> @@ -2398,6 +2398,7 @@ void ext4_fc_replay_cleanup(struct super_block *sb)
> sbi->s_mount_state &= ~EXT4_FC_REPLAY;
> kfree(sbi->s_fc_replay_state.fc_regions);
> kfree(sbi->s_fc_replay_state.fc_modified_inodes);
> + memset(&sbi->s_fc_replay_state, 0, sizeof(sbi->s_fc_replay_state));
> }
[Severity: High]
This problem wasn't introduced by this patch, but can
ext4_fc_replay_cleanup() clear EXT4_FC_REPLAY before sbi->s_ea_inode_work
is flushed [1]?
When fast-commit replay unlinks an inode that owns an extended attribute
inode, ext4_xattr_inode_dec_ref_all() -> ext4_put_ea_inode() queues the EA
inode onto sbi->s_ea_inode_to_free and schedules sbi->s_ea_inode_work.
Unlike the failed_mount3a error path, the normal mount path in
__ext4_fill_super() calls ext4_fc_replay_cleanup() without flushing
sbi->s_ea_inode_work first, clearing EXT4_FC_REPLAY before ext4_mb_init()
initializes sbi->s_group_info:
fs/ext4/super.c:__ext4_fill_super() {
...
ext4_fc_replay_cleanup(sb);
ext4_ext_init(sb);
/* Register extent status tree shrinker */
err = ext4_es_register_shrinker(sbi);
if (err)
goto failed_mount3;
err = ext4_Inode_table_init(sb);
...
err = ext4_mb_init(sb);
...
failed_mount3a:
/* Drain deferred EA inode iputs from journal replay */
flush_delayed_work(&sbi->s_ea_inode_work);
ext4_fc_replay_cleanup(sb);
...
}
If ext4_ea_inode_work() runs before ext4_mb_init() populates
sbi->s_group_info (or when mount failure jumps to failed_mount_wq after
ext4_mb_release()), ext4_evict_inode() -> ext4_free_inode() runs with
EXT4_FC_REPLAY cleared and ext4_get_group_info() returning NULL, which
triggers ext4_std_error(sb, -EFSCORRUPTED).
Should ext4_fc_replay_cleanup() (or __ext4_fill_super() right before it)
call flush_delayed_work(&sbi->s_ea_inode_work) before clearing
EXT4_FC_REPLAY?
[1] https://sashiko.dev/#/bug/linux-f893d2ed-6353-413a-8dd1-c6bc439b880a
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=8
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 13/27] ext4: add KUnit tests for fs/ext4/orphan.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:18 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Medium] ext4: unexported ext4_superblock_csum_set() breaks modular
KUnit build
--
commit 7b2d7b8d20233b110a9621f769fa6154acdc1c4b
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/orphan.c
This commit adds a KUnit test suite for orphan inode tracking, orphan file
management, and mount-time orphan recovery in fs/ext4/orphan-test.c, along
with a static stub redirect in ext4_truncate() and test symbol exports.
> diff --git a/fs/ext4/orphan-test.c b/fs/ext4/orphan-test.c
> new file mode 100644
> index 0000000000000..f93057df1d31d
> --- /dev/null
> +++ b/fs/ext4/orphan-test.c
[ ... ]
> +static int orphan_test_init(struct kunit *test)
> +{
[ ... ]
> + /* Keep sb_bh unmapped so ext4_commit_super() skips bh_submit I/O */
> + 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;
> + ext4_superblock_csum_set(sb);
[Severity: Medium]
Does calling ext4_superblock_csum_set() here break the build when
CONFIG_EXT4_KUNIT_TESTS=m? orphan-test.o is linked into ext4-test.ko while
ext4_superblock_csum_set() in fs/ext4/super.c is not exported with
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_superblock_csum_set) until a later commit
in the series, commit "ext4: add KUnit tests for fs/ext4/super.c", causing
modpost to fail with an undefined symbol error and breaking bisection.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=13
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 01/27] ext4: add KUinit tests for fs/ext4/extents_status.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:18 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found no new issues
in this patch, but noticed 1 related pre-existing issue it does not
introduce:
- [Medium] ext4: data race in __es_shrink() [1]
--
commit 45574cf43b458dd12781bdd04f9e4dfbf9ef71c4
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUinit tests for fs/ext4/extents_status.c
This commit adds KUnit tests for the ext4 extent status tree and bigalloc
pending reservations, and exports the required symbols for the test module.
> diff --git a/fs/ext4/extents_status.c b/fs/ext4/extents_status.c
> index 6e4a191e82191..64bac0c4c1fa0 100644
> --- a/fs/ext4/extents_status.c
> +++ b/fs/ext4/extents_status.c
> @@ -2390,3 +2390,22 @@ static int __revise_pending(struct inode *inode, ext4_lblk_t lblk,
> out:
> return (ret < 0) ? ret : pendings;
> }
> +
> +#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_init_tree);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_insert_extent);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_cache_extent);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_remove_extent);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_find_extent_range);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_lookup_extent);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_scan_range);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_scan_clu);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_register_shrinker);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_es_unregister_shrinker);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_seq_es_shrinker_info_show);
[Severity: Medium]
This problem wasn't introduced by this patch, but can concurrent shrinker
runs race on the 64-bit statistics fields in sbi->s_es_stats [1]?
fs/ext4/extents_status.c:__es_shrink() {
...
spin_unlock(&sbi->s_es_lock);
...
out:
scan_time = ktime_to_ns(ktime_sub(ktime_get(), start_time));
if (likely(es_stats->es_stats_scan_time))
es_stats->es_stats_scan_time = (scan_time +
es_stats->es_stats_scan_time*3) / 4;
else
es_stats->es_stats_scan_time = scan_time;
if (scan_time > es_stats->es_stats_max_scan_time)
es_stats->es_stats_max_scan_time = scan_time;
if (likely(es_stats->es_stats_shrunk))
es_stats->es_stats_shrunk = (nr_shrunk +
es_stats->es_stats_shrunk*3) / 4;
else
es_stats->es_stats_shrunk = nr_shrunk;
}
__es_shrink() drops sbi->s_es_lock before reaching the out label, so
concurrent memory reclaim threads can update es_stats_scan_time,
es_stats_max_scan_time, and es_stats_shrunk simultaneously without
synchronization. On 32-bit architectures, these plain 64-bit read-modify-write
accesses can tear and corrupt the shrinker metrics.
[1] https://sashiko.dev/#/bug/linux-ffbda43a-8d25-4cbd-be48-64c4f05ffa5f
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=1
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 14/27] ext4: add KUnit tests for fs/ext4/block_validity.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:21 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
> Add the ext4_block_validity_test KUnit test suite in
> fs/ext4/block_validity-test.c (16 test cases, 40 subtests total) to
> comprehensively exercise system-zone rbtree construction, RCU release,
> block range validation, reserved journal inode protection, and indirect
> block reference checking in fs/ext4/block_validity.c using an in-memory
> VFS superblock and mock block device without requiring a mounted disk:
>
> - Bounds and 64-bit overflow checks (ext4_sb_block_valid and
> ext4_inode_block_valid) across 1KiB (s_first_data_block = 1) and 4KiB
> (s_first_data_block = 0) geometries, exact end-of-filesystem single
> and multi-block spans, 64-bit u64 addition wrap-around, >32-bit block
> counts under EXT4_FEATURE_INCOMPAT_64BIT, and high-32-bit masking when
> 64bit is cleared.
> - System-zone lifecycle management (ext4_setup_system_zone and
> ext4_release_system_zone), covering pre-setup NULL behavior, RCU
> [ ... ]
>
> Export the five runtime entry points in fs/ext4/block_validity.c with
> EXPORT_SYMBOL_FOR_EXT4_TEST for the ext4-test KUnit module.
>
> Signed-off-by: Theodore Ts'o <tytso@mit.edu>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=14
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 05/27] ext4: add KUnit tests for fs/ext4/namei.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:21 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found no new issues
in this patch, but noticed 1 related pre-existing issue it does not
introduce:
- [High] ext4: out-of-bounds read due to missing dx_entry validation in
ext4_htree_next_block() [1]
--
commit 2ae6fd1b46635dda6b69ee78cd382ff9c0d0450a
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/namei.c
This commit adds a KUnit test suite for directory lookup, HTree indexing,
and namespace operations in fs/ext4/namei.c, and exports the internal
HTree definitions and helper wrappers needed by the tests.
> diff --git a/fs/ext4/namei.c b/fs/ext4/namei.c
> index a6386c1d237f1..63d01a9711a27 100644
> --- a/fs/ext4/namei.c
> +++ b/fs/ext4/namei.c
[ ... ]
> @@ -4249,3 +4103,159 @@ const struct inode_operations ext4_special_inode_operations = {
> .get_inode_acl = ext4_get_acl,
> .set_acl = ext4_set_acl,
> };
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_special_inode_operations);
> +
> +#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
[ ... ]
> +int ext4_htree_next_block_test(struct inode *dir, __u32 hash,
> + struct dx_frame *frame,
> + struct dx_frame *frames, __u32 *start_hash)
> +{
> + return ext4_htree_next_block(dir, hash, frame, frames, start_hash);
> +}
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_htree_next_block_test);
[Severity: High]
This problem wasn't introduced by this patch, but can
ext4_htree_next_block() read out of bounds past bh->b_data [1] when
loading a sibling interior dx_node block?
fs/ext4/namei.c:ext4_htree_next_block() {
...
while (1) {
if (++(p->at) < p->entries + dx_get_count(p->entries))
break;
if (p == frames)
return 0;
num_frames++;
p--;
}
...
while (num_frames--) {
bh = ext4_read_dirblock(dir, dx_get_block(p->at), INDEX);
if (IS_ERR(bh))
return PTR_ERR(bh);
p++;
brelse(p->bh);
p->bh = bh;
p->at = p->entries = ((struct dx_node *) bh->b_data)->entries;
}
return 1;
}
Unlike dx_probe(), which checks dx_get_limit() against dx_node_limit() and
verifies that dx_get_count() is non-zero and within dx_get_limit(),
ext4_htree_next_block() assigns p->entries directly from bh->b_data without
validating the new node's count or limit. If a corrupted interior node has
an out-of-bounds count (for example, when metadata_csum is disabled or
buffer_verified(bh) is already set), later calls to ext4_htree_next_block()
advance p->at up to p->entries + dx_get_count(p->entries) and dereference
it via dx_get_hash() and dx_get_block(), reading past the end of bh->b_data.
[1] https://sashiko.dev/#/bug/linux-c6639d9a-25ec-49eb-a6a4-468135552f9a
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=5
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 16/27] ext4: enable full feature coverage in fs/ext4/.kunitconfig
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:24 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
> Enable CONFIG_EXT4_FS_POSIX_ACL, CONFIG_EXT4_FS_SECURITY,
> CONFIG_FS_VERITY, CONFIG_FS_ENCRYPTION, CONFIG_QUOTA, CONFIG_QFMT_V2,
> and CONFIG_UNICODE in fs/ext4/.kunitconfig so conditional ext4 source
> files and feature-gated code paths (POSIX ACLs, security xattrs,
> fs-verity, fscrypt, quota accounting, and UTF-8 casefolding) are
> compiled and exercised during KUnit runs.
>
> Fix mock superblock and test setup bugs in existing KUnit suites exposed
> by enabling these Kconfig options:
>
> - Provide .get_dquots callbacks in mock super_operations under
> CONFIG_QUOTA in block_validity-test.c, extents-test.c,
> fast_commit-test.c, inline-test.c, inode-test.c, mballoc-test.c,
> namei-test.c, and xattr-test.c so dquot_drop() does not dereference a
> NULL callback during ext4_clear_inode().
> - Populate sb->s_cop = &ext4_cryptops under CONFIG_FS_ENCRYPTION in
> balloc-test.c, extents-test.c, fast_commit-test.c, inline-test.c,
> inode-test.c, mballoc-test.c, and xattr-test.c so
> fscrypt_put_encryption_info() can resolve inode_info_offs safely on
> eviction.
> - Fix test_namei_casefold_lookup_and_strict() under CONFIG_UNICODE in
> namei-test.c to use SB_ENC_STRICT_MODE_FL (instead of undeclared
> SB_STRICT_ENCODING) and propagate EXT4_INODE_CASEFOLD to S_CASEFOLD on
> the VFS directory inode via ext4_set_inode_flags().
>
> Signed-off-by: Theodore Ts'o <tytso@mit.edu>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=16
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 11/27] ext4: add KUnit tests for fs/ext4/ialloc.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:25 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Medium] ext4: UAF on super_block in ialloc_test_cleanup() from
undrained fserror work
It also noticed 1 related pre-existing issue this patch does not
introduce:
- [Critical] ext4: missing error handling for blkdev_issue_flush() in
ext4_fc_commit() [1]
--
commit 52f553b981802c3fefc912614c6427cda362ae6f
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/ialloc.c
This commit adds a KUnit test suite for fs/ext4/ialloc.c along with static
stub hooks and helper exports to test inode allocation, freeing, bitmap
validation, and lazy inode table initialization.
> diff --git a/fs/ext4/ialloc-test.c b/fs/ext4/ialloc-test.c
> new file mode 100644
> index 0000000000000..a3066b788ce6a
> --- /dev/null
> +++ b/fs/ext4/ialloc-test.c
[ ... ]
> +static void ialloc_test_cleanup(void *data)
> +{
> + struct ialloc_test_ctx *ctx = data;
> + handle_t *handle;
> + unsigned int i;
> +
> + ctx->evict_skip_free = true;
> + ctx->sbi.s_journal = NULL;
> +
> + while ((handle = ext4_journal_current_handle()) != NULL)
> + ext4_journal_stop(handle);
> +
> + for (i = 0; i < ctx->nr_tracked_inodes; i++) {
> + struct inode *ti = ctx->tracked_inodes[i];
> +
> + if (ti) {
> + if (inode_state_read_once(ti) & I_NEW)
> + unlock_new_inode(ti);
> + spin_lock(&ti->i_lock);
> + inode_state_clear(ti, I_DIRTY_TIME);
> + spin_unlock(&ti->i_lock);
> + set_nlink(ti, 1);
> + iput(ti);
> + ctx->tracked_inodes[i] = NULL;
> + }
> + }
> + ctx->nr_tracked_inodes = 0;
> +
> + ctx->sb->s_root = NULL;
> + if (ctx->root_inode) {
> + set_nlink(ctx->root_inode, 2);
> + iput(ctx->root_inode);
> + ctx->root_inode = NULL;
> + }
> +
> + deactivate_super(ctx->sb);
[Severity: Medium]
Can clearing ctx->sb->s_root before deactivate_super(ctx->sb) cause a
use-after-free on ctx->sb in fserror_worker()?
ialloc_test_init() sets SB_ACTIVE on ctx->sb, so tests that trigger
ext4_error() or ext4_std_error() queue fserror_worker() items referencing
ctx->sb via fserror_report_metadata(). In generic_shutdown_super(),
fserror_unmount(sb) is only called inside the if (sb->s_root) block, so
nulling s_root beforehand skips draining those pending work items before
ctx->sb is freed:
CPU0 kworker
ialloc_test_validate_ibitmap_csum()
ext4_error()
fserror_report_metadata()
queue_work(..., &event->work)
ialloc_test_cleanup()
ctx->sb->s_root = NULL
deactivate_super(ctx->sb)
generic_shutdown_super()
/* skips fserror_unmount() */
put_super()
...
kfree(sb)
fserror_worker()
sb = event->sb
if (sb->s_flags & SB_ACTIVE)
<-- use-after-free
Omitting SB_ACTIVE in ialloc_test_init() would avoid queueing fserror
events in the first place.
> diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c
> index 02546b828836c..66215b6359320 100644
> --- a/fs/ext4/ialloc.c
> +++ b/fs/ext4/ialloc.c
[ ... ]
> @@ -1521,6 +1527,23 @@ unsigned long ext4_count_dirs(struct super_block * sb)
> }
> return count;
> }
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_count_dirs);
> +
> +int ext4_itable_zeroout(struct super_block *sb, ext4_fsblk_t blk, int num,
> + int barrier)
> +{
> + int ret;
> +
> + KUNIT_STATIC_STUB_REDIRECT(ext4_itable_zeroout, sb, blk, num, barrier);
> +
> + ret = sb_issue_zeroout(sb, blk, num, GFP_NOFS);
> + if (ret < 0)
> + return ret;
> + if (barrier)
> + blkdev_issue_flush(sb->s_bdev);
> + return ret;
> +}
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_itable_zeroout);
[Severity: Critical]
This problem wasn't introduced by this patch, but should
ext4_itable_zeroout() check and return the error from
blkdev_issue_flush() [1]?
When barrier is set and blkdev_issue_flush() fails, ext4_itable_zeroout()
still returns 0, so ext4_init_inode_table() proceeds to set
EXT4_BG_INODE_ZEROED in the group descriptor even though the zeroed inode
table blocks may not have been flushed to stable storage before a crash.
(The same unchecked blkdev_issue_flush() pattern also exists in
ext4_fc_perform_commit() and ext4_fc_replay_inode() in
fs/ext4/fast_commit.c.)
[1] https://sashiko.dev/#/bug/linux-d85ff7fd-2937-4bb0-8016-be8fb0499fbd
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=11
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 06/27] ext4: expand KUnit tests for fs/ext4/mballoc.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:26 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 6 potential
issues to consider:
- [Medium] ext4: test_mb_good_group() and test_mb_prefetch() fail on
16K/64K PAGE_SIZE
- [Low] ext4: unpinned per-CPU locality group lookups across sleeps in
mballoc-test.c
- [Low] ext4: unaligned __u32 accesses in mb_set_bits() and
mb_test_and_clear_bits()
- [Low] ext4: test_mb_regular_allocator() step 3 skips CR_BEST_AVAIL_LEN
trimming
- [Low] ext4: missing bb_free update in
test_mb_simple_and_aligned_scan()
- [Low] ext4: unused *_test() wrapper functions in fs/ext4/mballoc.c
--
commit 72a0b9666c2cc1afd5fa22724c492669c1b39ccf
Author: Theodore Ts'o <tytso@mit.edu>
ext4: expand KUnit tests for fs/ext4/mballoc.c
This commit expands the ext4_mballoc_test KUnit suite in
fs/ext4/mballoc-test.c with 25 new parameterized tests and exports the
corresponding helper wrappers from fs/ext4/mballoc.c.
> diff --git a/fs/ext4/mballoc-test.c b/fs/ext4/mballoc-test.c
> index d31780075c21e..402f4efdf25a4 100644
> --- a/fs/ext4/mballoc-test.c
> +++ b/fs/ext4/mballoc-test.c
[ ... ]
> @@ -949,6 +1008,1747 @@ static void test_mb_mark_used_cost(struct kunit *test)
> ext4_mb_unload_buddy_test(&e4b);
> }
>
> +static void test_mb_bit_ops(struct kunit *test)
> +{
> + u8 *buf;
> + void *unaligned;
> + int bit, i;
> +
> + buf = kunit_kzalloc(test, 128, GFP_KERNEL);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
> +
> + /*
> + * Use an unaligned pointer (offset by 3 bytes) to exercise
> + * mb_correct_addr_and_bit() on 64-bit/32-bit architectures.
> + */
> + unaligned = buf + 3;
[ ... ]
> + /* Multi-word unaligned range spanning head bits, 32-bit words, and tail */
> + mb_set_bits(unaligned, 5, 75);
> + KUNIT_EXPECT_EQ(test, mb_find_next_bit_test(unaligned, 256, 0), 5);
> + KUNIT_EXPECT_EQ(test, mb_find_next_zero_bit_test(unaligned, 256, 5), 80);
> + /* Clamping when no zero bit exists before max */
> + KUNIT_EXPECT_EQ(test, mb_find_next_zero_bit_test(unaligned, 50, 5), 50);
> +
> + /* Clear a sub-range across 32-bit boundaries */
> + mb_clear_bits_test(unaligned, 13, 50);
> + for (i = 0; i < 100; i++) {
> + int expected = (i >= 5 && i < 13) || (i >= 63 && i < 80);
> +
> + KUNIT_EXPECT_EQ(test, !!mb_test_bit_test(i, unaligned), expected);
> + }
> +}
> +
> +static void test_mb_test_and_clear_bits(struct kunit *test)
> +{
> + u8 *buf;
> + void *bm;
> + int ret, i;
> +
> + buf = kunit_kzalloc(test, 128, GFP_KERNEL);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, buf);
> + bm = buf + 3;
> +
> + /* Case 1: All bits in range [5, 85) are set -> returns -1 and clears all */
> + memset(buf, 0xff, 128);
> + ret = mb_test_and_clear_bits_test(bm, 5, 80);
[Severity: Low]
Does passing buf + 3 to mb_set_bits() and mb_test_and_clear_bits_test()
cause misaligned 32-bit accesses?
Unlike the single-bit helpers that align the base address via
mb_correct_addr_and_bit(), mb_set_bits() and mb_test_and_clear_bits() take
a 32-bit word fast path when (cur & 31) == 0 && (len - cur) >= 32 and
dereference __u32 *addr = bm + (cur >> 3) directly. When cur reaches 32,
addr is buf + 7, which triggers UBSAN alignment warnings under
CONFIG_UBSAN_ALIGNMENT or alignment faults on strict-alignment
architectures.
[ ... ]
> +static void test_mb_good_group(struct kunit *test)
> +{
> + struct super_block *sb = (struct super_block *)test->priv;
> + struct ext4_sb_info *sbi = EXT4_SB(sb);
> + struct ext4_buddy e4b;
> + struct ext4_allocation_context ac = {};
> + struct ext4_group_info *grp1, *grp2;
> + int ret;
> +
> + if (sb->s_blocksize > PAGE_SIZE)
> + kunit_skip(test, "blocksize exceeds pagesize");
> +
> + /* Initialize group 1 buddy cache so bb_fragments and orders are populated */
> + ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + ext4_mb_unload_buddy_test(&e4b);
[ ... ]
> + /*
> + * Test ext4_mb_good_group_nolock on uninitialized group 2:
> + * At inexpensive CR_POWER2_ALIGNED, it skips initializing group 2 and returns 0.
> + * At expensive CR_GOAL_LEN_SLOW (with STRICT_CHECK), it initializes group 2
> + * and returns 1.
> + */
> + grp2 = ext4_get_group_info(sb, 2);
> + KUNIT_ASSERT_NOT_NULL(test, grp2);
> + KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_NEED_INIT(grp2));
> +
> + ac.ac_criteria = CR_POWER2_ALIGNED;\n> + ret = ext4_mb_good_group_nolock_test(&ac, 2, CR_POWER2_ALIGNED);
> + KUNIT_EXPECT_EQ(test, ret, 0);
> + KUNIT_EXPECT_TRUE(test, EXT4_MB_GRP_NEED_INIT(grp2));
[Severity: Medium]
Will test_mb_good_group() and test_mb_prefetch() fail on 16K or 64K
PAGE_SIZE kernels?
ext4_mb_init_cache() initializes groups_per_folio =
DIV_ROUND_UP(folio_size(folio) / blocksize, 2) consecutive groups at once
whenever a buddy-cache folio is populated. When blocksize is 1K on a 16K
page kernel, or 1K/4K on a 64K page kernel, groups_per_folio is at least
8, so loading group 1's buddy (or calling ext4_mb_prefetch_fini() on
groups [0, 2) in test_mb_prefetch()) also initializes group 2 on folio 0
and clears EXT4_GROUP_INFO_NEED_INIT_BIT on grp2, causing the
EXT4_MB_GRP_NEED_INIT(grp2) expectations in both tests to fail.
[ ... ]
> +static void test_mb_simple_and_aligned_scan(struct kunit *test)
> +{
> + struct super_block *sb = (struct super_block *)test->priv;
> + struct ext4_sb_info *sbi = EXT4_SB(sb);
> + struct ext4_buddy e4b;
> + struct ext4_allocation_context ac = {};
> + struct inode *inode;
> + int ret;
> +
> + if (sb->s_blocksize > PAGE_SIZE)
> + kunit_skip(test, "blocksize exceeds pagesize");
> +
> + inode = mbt_alloc_test_inode(test, sb, S_IFREG);
> +
> + /*
> + * Mark cluster 0 used in group 1 so:
> + * - order 3 buddy [8, 16) is the first order-3 buddy
> + * - first 16-cluster stripe [0, 16) is not completely free, so
> + * stripe-aligned scan must skip to cluster 16.
> + */
> + mbt_ctx_mark_used(sb, TEST_GOAL_GROUP, 0, 1);
> +
> + ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
> + KUNIT_ASSERT_EQ(test, ret, 0);
[Severity: Low]
Does marking cluster 0 used here without decrementing
ext4_get_group_info(sb, TEST_GOAL_GROUP)->bb_free trigger a false bitmap
corruption error?
When ext4_mb_load_buddy_test() calls ext4_mb_generate_buddy(), the bitmap
free count is 1 less than grp->bb_free, which logs an inconsistency error
via ext4_grp_locked_error() and sets EXT4_GROUP_INFO_BBITMAP_CORRUPT on
TEST_GOAL_GROUP.
[ ... ]
> +static void test_mb_regular_allocator(struct kunit *test)
> +{
[ ... ]
> + /*
> + * 3. MB_OPTIMIZE_SCAN with CR_BEST_AVAIL_LEN goal length trimming:
> + * Make group 2 the only group with free blocks, fragmented into two
> + * 8-cluster chunks [0, 8) and [16, 24) so largest_free_order = 3
> + * and avg_fragment_size = 8.
> + * Note: group 2 may have already been prefetched during step 2, so
> + * mark its blocks used via mb_mark_used_test on the loaded buddy.
> + */
[ ... ]
> + memset(&ac, 0, sizeof(ac));
> + ac.ac_sb = sb;
> + ac.ac_inode = inode;
> + ac.ac_status = AC_STATUS_CONTINUE;
> + ac.ac_flags = EXT4_MB_HINT_DATA;
> + ac.ac_g_ex.fe_group = 0;
> + ac.ac_g_ex.fe_len = 16;
> + ac.ac_orig_goal_len = 16;
> + ac.ac_o_ex.fe_len = 8;
> +
> + ret = ext4_mb_regular_allocator_test(&ac);
[Severity: Low]
Does setting ac_o_ex.fe_len = 8 prevent ext4_mb_scan_groups_best_avail()
from trimming ac_g_ex.fe_len down to 8 at CR_BEST_AVAIL_LEN?
In ext4_mb_scan_groups_best_avail(), order = fls(16) - 1 = 4 and min_order
starts at 4 - 3 = 1, but because (1 << min_order) < ac->ac_o_ex.fe_len, it
clamps min_order to fls(8) = 4:
fs/ext4/mballoc.c:ext4_mb_scan_groups_best_avail() {
...
order = fls(ac->ac_g_ex.fe_len) - 1;
...
min_order = order - sbi->s_mb_best_avail_max_trim_order;
if (min_order < 0)
min_order = 0;
if (1 << min_order < ac->ac_o_ex.fe_len)
min_order = fls(ac->ac_o_ex.fe_len);
for (i = order; i >= min_order; i--) {
...
}
As a result, the loop only checks i = 4 (fe_len = 16) and never trims to
i = 3 (fe_len = 8), so the allocation only succeeds after falling back to
CR_GOAL_LEN_SLOW and ext4_mb_try_best_found(). Setting ac_o_ex.fe_len <= 4
would allow min_order <= 3 so CR_BEST_AVAIL_LEN trims fe_len to 8.
[ ... ]
> +static void test_mb_group_pa_lifecycle(struct kunit *test)
> +{
> + struct super_block *sb = (struct super_block *)test->priv;
> + struct ext4_sb_info *sbi = EXT4_SB(sb);
> + struct inode *inode1, *inode2;
> + struct ext4_allocation_request ar = {};
> + struct ext4_group_info *grp;
> + ext4_fsblk_t blk1, blk2;
> + int err = 0, discarded;
> +
> + if (sb->s_blocksize > PAGE_SIZE)
> + kunit_skip(test, "blocksize exceeds pagesize");
> +
> + inode1 = mbt_alloc_test_inode(test, sb, S_IFREG);
> + inode2 = mbt_alloc_test_inode(test, sb, S_IFREG);
> + inode2->i_ino = 13;
> +
> + /* Small files (<= s_mb_stream_request) use locality group PA (MB_GROUP_PA) */
> + i_size_write(inode1, sb->s_blocksize);
> + i_size_write(inode2, sb->s_blocksize);
> +
> + ar.inode = inode1;
> + ar.logical = 0;
> + ar.len = 1;
> + ar.goal = ext4_group_first_block_no(sb, TEST_GOAL_GROUP);
> + ar.flags = EXT4_MB_HINT_DATA;
> +
> + blk1 = ext4_mb_new_blocks(NULL, &ar, &err);
> + KUNIT_ASSERT_EQ(test, err, 0);
> + KUNIT_EXPECT_EQ(test, blk1, ext4_group_first_block_no(sb, TEST_GOAL_GROUP));
> +
> + /* Second small file allocation reuses the locality group PA */
> + ar.inode = inode2;
> + ar.logical = 0;
> + ar.len = 1;
> + ar.goal = ext4_group_first_block_no(sb, TEST_GOAL_GROUP);
> + blk2 = ext4_mb_new_blocks(NULL, &ar, &err);
> + KUNIT_ASSERT_EQ(test, err, 0);
> + KUNIT_EXPECT_EQ(test, blk2, blk1 + EXT4_C2B(sbi, 1));
> +
> + /* Discard all group preallocations and verify free count is restored */
> + discarded = ext4_mb_discard_preallocations_test(sb, 0);
> + rcu_barrier();
> + KUNIT_EXPECT_EQ(test, discarded, (int)sbi->s_mb_group_prealloc - 2);
> +
> + grp = ext4_get_group_info(sb, TEST_GOAL_GROUP);
> + KUNIT_EXPECT_EQ(test, grp->bb_free, EXT4_CLUSTERS_PER_GROUP(sb) - 2);
> +}
> +
> +static void test_mb_lg_list_trim(struct kunit *test)
> +{
> + struct super_block *sb = (struct super_block *)test->priv;
> + struct ext4_sb_info *sbi = EXT4_SB(sb);
> + struct ext4_locality_group *lg;
> + struct inode *inode;
> + struct ext4_buddy e4b;
> + struct ext4_prealloc_space *pa;
> + int i, ret, count = 0;
> + int order = fls(15) - 1; /* pa_free = 15 -> order 3 */
> +
> + if (sb->s_blocksize > PAGE_SIZE)
> + kunit_skip(test, "blocksize exceeds pagesize");
> +
> + inode = mbt_alloc_test_inode(test, sb, S_IFREG);
> + sbi->s_mb_group_prealloc = 16;
> +
> + ret = ext4_mb_load_buddy_test(sb, TEST_GOAL_GROUP, &e4b);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> +
> + /*
> + * Create 9 locality-group PAs in bucket order 3 (each of length 16 with
> + * 1 cluster used -> pa_free = 15). When the 9th PA is added via
> + * ext4_mb_release_context -> ext4_mb_add_n_trim, lg_prealloc_count > 8
> + * triggers ext4_mb_discard_lg_preallocations() to trim the list to 5!
> + */
> + for (i = 0; i < 9; i++) {
> + struct ext4_allocation_context ac = {};
> + struct ext4_free_extent ex = {
> + .fe_group = TEST_GOAL_GROUP,
> + .fe_start = i * 32,
> + .fe_len = 16,
> + };
> +
> + ac.ac_sb = sb;
> + ac.ac_inode = inode;
> + ac.ac_flags = EXT4_MB_HINT_DATA | EXT4_MB_HINT_GROUP_ALLOC;
> + ac.ac_lg = raw_cpu_ptr(sbi->s_locality_groups);
> + mutex_lock(&ac.ac_lg->lg_mutex);
> +
> + ret = ext4_mb_pa_alloc_test(&ac);
> + if (ret) {
> + mutex_unlock(&ac.ac_lg->lg_mutex);
> + ext4_mb_unload_buddy_test(&e4b);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + }
> +
> + ext4_lock_group(sb, TEST_GOAL_GROUP);
> + mb_mark_used_test(&e4b, &ex);
> + ext4_unlock_group(sb, TEST_GOAL_GROUP);
> +
> + ac.ac_status = AC_STATUS_FOUND;
> + ac.ac_o_ex.fe_len = 1;
> + ac.ac_b_ex = ex;
> + ac.ac_f_ex = ex;
> + ext4_mb_new_preallocation_test(&ac);
> + /* Release context updates pa_free to 15 and runs ext4_mb_add_n_trim */
> + ext4_mb_release_context_test(&ac);
> + }
> +
> + ext4_mb_unload_buddy_test(&e4b);
> +
> + lg = raw_cpu_ptr(sbi->s_locality_groups);
> + spin_lock(&lg->lg_prealloc_lock);
> + list_for_each_entry(pa, &lg->lg_prealloc_list[order], pa_node.lg_list)
> + count++;
> + spin_unlock(&lg->lg_prealloc_lock);
> +
> + KUNIT_EXPECT_EQ(test, count, 5);
> +}
[Severity: Low]
Can CPU migration across sleeping calls cause test_mb_group_pa_lifecycle()
and test_mb_lg_list_trim() to use different per-CPU sbi->s_locality_groups
instances?
Both tests run in a preemptible KUnit kthread and call sleeping functions
(ext4_mb_new_blocks(), mutex_lock(), and ext4_mb_pa_alloc_test()). If the
kthread migrates CPUs between the two ext4_mb_new_blocks() calls in
test_mb_group_pa_lifecycle(), the second call looks up the new CPU's
empty lg_prealloc_list instead of reusing the first PA. Similarly,
test_mb_lg_list_trim() re-evaluates raw_cpu_ptr(sbi->s_locality_groups) on
each loop iteration and again when counting lg_prealloc_list[order], so a
migration splits the 9 PAs across per-CPU lists and fails the count == 5
check.
> diff --git a/fs/ext4/mballoc.c b/fs/ext4/mballoc.c
> index 0f73fb826dbf4..46ba07e017a31 100644
> --- a/fs/ext4/mballoc.c
> +++ b/fs/ext4/mballoc.c
[ ... ]
> +void mb_regenerate_buddy_test(struct ext4_buddy *e4b)
> +{
> + mb_regenerate_buddy(e4b);
> +}
> +EXPORT_SYMBOL_FOR_EXT4_TEST(mb_regenerate_buddy_test);
[ ... ]
> +void ext4_mb_measure_extent_test(struct ext4_allocation_context *ac,
> + struct ext4_free_extent *ex,
> + struct ext4_buddy *e4b)
> +{
> + ext4_mb_measure_extent(ac, ex, e4b);
> +}
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_measure_extent_test);
[ ... ]
> +bool ext4_mb_use_preallocated_test(struct ext4_allocation_context *ac)
> +{
> + return ext4_mb_use_preallocated(ac);
> +}
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_use_preallocated_test);
[ ... ]
> +void ext4_mb_pa_put_free_test(struct ext4_allocation_context *ac)
> +{
> + ext4_mb_pa_put_free(ac);
> +}
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_pa_put_free_test);
> +
> +void ext4_mb_generate_from_pa_test(struct super_block *sb, void *bitmap,
> + ext4_group_t group)
> +{
> + ext4_mb_generate_from_pa(sb, bitmap, group);
> +}
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_generate_from_pa_test);
> +
> +int ext4_mb_discard_group_preallocations_test(struct super_block *sb,
> + ext4_group_t group, int *busy)
> +{
> + return ext4_mb_discard_group_preallocations(sb, group, busy);
> +}
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_discard_group_preallocations_test);
[Severity: Low]
This isn't a bug, but mb_regenerate_buddy_test(),
ext4_mb_measure_extent_test(), ext4_mb_use_preallocated_test(),
ext4_mb_pa_put_free_test(), and
ext4_mb_discard_group_preallocations_test() have no callers in
fs/ext4/mballoc-test.c.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=6
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 04/27] ext4: add KUnit tests for fs/ext4/xattr.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:26 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 3 potential
issues to consider:
- [Medium] ext4: uninitialized e_name in test_xattr_check_inode()
bypasses value checks
- [Low] ext4: stack frame size warning in
test_xattr_credits_usage_expand_and_delete()
- [Low] ext4: missing i_size_ordered_init() in init_mock_inode_slot()
--
commit b81033fe08ff323ec6a5c24275644912bdfe1108
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/xattr.c
This commit adds a KUnit test suite in fs/ext4/xattr-test.c to exercise
extended attribute operations in fs/ext4/xattr.c using static stubs and
exports.
> diff --git a/fs/ext4/xattr-test.c b/fs/ext4/xattr-test.c
> new file mode 100644
> index 0000000000000..35be150353a20
> --- /dev/null
> +++ b/fs/ext4/xattr-test.c
[ ... ]
> +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);
[Severity: Low]
Should init_mock_inode_slot() also call i_size_ordered_init(inode)?
On 32-bit SMP builds with CONFIG_DEBUG_LOCK_ALLOC=y, struct inode embeds
i_size_seqcount, so calling i_size_write() or i_size_read() on the mock
EA inodes triggers a lockdep warning for an uninitialized key and
disables lockdep.
[ ... ]
> +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_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;
> + }
[Severity: Medium]
Do these four cases reach the value checks in check_xattrs()?
Because hdr is zeroed with memset() and e1->e_name[0] is left as NUL
while e1->e_name_len is 1, check_xattrs() rejects all four cases in the
entry-name loop before reaching the value-validation loop:
fs/ext4/xattr.c:check_xattrs() {
...
while (!IS_LAST_ENTRY(e)) {
...
if (strnlen(e->e_name, e->e_name_len) != e->e_name_len) {
err_str = "bad e_name length";
goto errout;
}
e = next;
}
/* Check the values */
while (!IS_LAST_ENTRY(entry)) {
...
}
In addition, check_xattrs() only inspects e_value_offs when
entry->e_value_inum == 0, so CHECK_NONZERO_OFFS_WITH_INUM does not test a
check that check_xattrs() performs; even with e1->e_name[0] set, it would
fail on the missing EXT4_FEATURE_INCOMPAT_EA_INODE feature or zero
e_value_size instead.
[ ... ]
> +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,
> + };
[Severity: Low]
Can placing fake_journal on the stack exceed CONFIG_FRAME_WARN?
journal_t (struct journal_s) is ~1.3 KB on 64-bit builds and over 2.2 KB
when lock debugging is enabled, which triggers -Wframe-larger-than=
warnings on 32-bit builds or 64-bit debug configurations. Moving
fake_journal into struct xattr_test_ctx or allocating it with
kunit_kzalloc() would avoid the large stack frame.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=4
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 17/27] ext4: add KUnit tests for fs/ext4/super.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:27 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [High] ext4: duplicate test symbol exports in super.c and mballoc.c
break modpost
--
commit bd8448103700f2d8698fe8da1a5ea9c5b818c8f5
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/super.c
This commit adds a KUnit test suite (ext4_super_test) for superblock
validation, geometry checks, metadata checksums, mount options, overhead
and statfs accounting, and error recording in fs/ext4/super.c, along with
test helper wrappers and symbol exports.
> diff --git a/fs/ext4/super.c b/fs/ext4/super.c
> index 88bd28cedd999..838dabda35c5d 100644
> --- a/fs/ext4/super.c
> +++ b/fs/ext4/super.c
[ ... ]
> @@ -379,6 +381,7 @@ __u32 ext4_free_group_clusters(struct super_block *sb,
> (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT ?
> (__u32)le16_to_cpu(bg->bg_free_blocks_count_hi) << 16 : 0);
> }
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_group_clusters);
>
> __u32 ext4_free_inodes_count(struct super_block *sb,
> struct ext4_group_desc *bg)
> @@ -441,6 +444,7 @@ void ext4_free_group_clusters_set(struct super_block *sb,
> if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT)
> bg->bg_free_blocks_count_hi = cpu_to_le16(count >> 16);
> }
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_group_clusters_set);
>
> void ext4_free_inodes_set(struct super_block *sb,
> struct ext4_group_desc *bg, __u32 count)
[Severity: High]
Does exporting ext4_free_group_clusters and ext4_free_group_clusters_set
here duplicate the existing exports in fs/ext4/mballoc.c?
fs/ext4/mballoc.c:
...
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_group_clusters);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_group_clusters_set);
...
Because super.o and mballoc.o are linked into the same module or vmlinux,
builds with CONFIG_MODULES=y and CONFIG_EXT4_KUNIT_TESTS enabled fail in
modpost with "symbol ... exported twice".
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=17
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 02/27] ext4: add KUnit tests for fs/ext4/extents.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:28 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Low] ext4: use-after-free of sb->s_bdev after deactivate_super() in
extents_kunit_exit()
- [Low] ext4: stale i_es_tree entries and warnings in extents-test.c
KUnit tests
It also noticed 1 related pre-existing issue this patch does not
introduce:
- [Medium] ext4: data block leak in ext4_ext_map_blocks() on -ENOMEM
extent insert [1]
--
commit 228dbe39d61604db8aef3d32655ec0608aa3210f
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/extents.c
This commit expands the ext4_extents_test KUnit suite in
fs/ext4/extents-test.c to cover extent tree initialization, validation,
navigation, insertion, merging, removal, block mapping, shifting, swapping,
and fast-commit replay helpers, while adding KUnit static stubs for metadata
and block allocation helpers.
> diff --git a/fs/ext4/extents-test.c b/fs/ext4/extents-test.c
> index c3836ecb89f9c..7056481862188 100644
> --- a/fs/ext4/extents-test.c
> +++ b/fs/ext4/extents-test.c
[ ... ]
> @@ -142,33 +182,201 @@ static struct file_system_type ext_fs_type = {
[ ... ]
> static void extents_kunit_exit(struct kunit *test)
> {
> struct ext4_sb_info *sbi;
> + struct super_block *sb;
>
> if (!k_ctx.k_ei)
> return;
>
> - sbi = k_ctx.k_ei->vfs_inode.i_sb->s_fs_info;
> + sb = k_ctx.k_ei->vfs_inode.i_sb;
> + sbi = sb->s_fs_info;
> +
> + if (k_ctx.k_ei2) {
> + ext_cleanup_inode(&k_ctx.k_ei2->vfs_inode);
> + kfree(k_ctx.k_ei2);
> + }
> + ext_cleanup_inode(&k_ctx.k_ei->vfs_inode);
> +
> + percpu_counter_destroy(&sbi->s_freeclusters_counter);
> + percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
> ext4_es_unregister_shrinker(sbi);
> - deactivate_super(sbi->s_sb);
> + deactivate_super(sb);
> +
> + kfree(k_ctx.meta_blocks);
> + kfree(sbi->s_es);
> + kfree(sb->s_bdev->bd_mapping);
> + kfree(sb->s_bdev);
> kfree(sbi);
[Severity: Low]
Can dereferencing sb->s_bdev after deactivate_super(sb) cause a
use-after-free?
deactivate_super(sb) drops the final active and passive references on sb
via kill_anon_super() and put_super(), queuing sb for RCU and workqueue
destruction (destroy_super_work() -> kfree(sb)). Freeing
sb->s_bdev->bd_mapping and sb->s_bdev before calling deactivate_super(sb),
as the error path in extents_kunit_init() does, avoids reading from sb
after its reference count reaches zero.
[ ... ]
> +static void test_convert_to_initialized_abut_neighbors(struct kunit *test)
> +{
> + struct inode *inode = &k_ctx.k_ei->vfs_inode;
> + struct ext4_extent_header *eh = ext_inode_hdr(inode);
> + struct ext4_extent *ex;
> + struct ext4_map_blocks map = {};
> + int ret;
> +
> + EXT4_SB(inode->i_sb)->s_extent_max_zeroout_kb = 0;
> + ext_reset_tree(inode);
> +
> + /*
> + * Setup: [10, 12) @ 100 (written), [12, 16) @ 102 (unwritten),
> + * [16, 18) @ 106 (written).
> + */
> + KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 2, 100, false, 0), 0);
> + KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 12, 4, 102, true, 0), 0);
> + KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 16, 2, 106, false, 0), 0);
> + KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
> +
> + /*
> + * Convert head of unwritten extent [12, 14): transfers 2 blocks to
> + * left neighbor [10, 12) -> [10, 14) written, [14, 16) unwritten.
> + */
> + map.m_lblk = 12;
> + map.m_len = 2;
> + map.m_flags = 0;
> + down_write(&EXT4_I(inode)->i_data_sem);
> + ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CREATE);
> + up_write(&EXT4_I(inode)->i_data_sem);
[ ... ]
> +static void test_ext_fast_commit_replay(struct kunit *test)
> +{
> + struct inode *inode = &k_ctx.k_ei->vfs_inode;
> + struct ext4_extent_header *eh = ext_inode_hdr(inode);
> + struct ext4_extent *ex;
> + int err;
> +
> + ext_reset_tree(inode);
> + /* Start with unwritten extent [10, 20) @ 100 */
> + KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 10, 100, true, 0), 0);
> +
> + /* Replay update converting middle [12, 16) to written @ 102 */
> + err = ext4_ext_replay_update_ex(inode, 12, 4, 0, 102);
[Severity: Low]
Do test_ext_fast_commit_replay() and
test_convert_to_initialized_abut_neighbors() leave stale unwritten entries
in i_es_tree that trigger ext4_es_cache_extent() conflict warnings?
In test_ext_fast_commit_replay(), EXT4_FC_REPLAY is not set in
EXT4_SB(inode->i_sb)->s_mount_state. The first ext4_ext_replay_update_ex()
call invokes ext4_find_extent(..., 0) -> ext4_cache_extents(), which caches
[10, 20) as unwritten in i_es_tree before converting [12, 16) to written in
the extent tree. Later calls to ext4_ext_replay_update_ex(),
ext4_ext_replay_shrink_inode(), and ext4_ext_replay_set_iblocks() then try
to cache the converted written extents over the stale unwritten entry in
i_es_tree, logging ext4_warning_inode() conflict messages and causing
ext4_ext_replay_set_iblocks() -> ext4_map_blocks() to hit the stale cache
instead of reading the extent tree.
Similarly, test_convert_to_initialized_abut_neighbors() calls
ext4_ext_map_blocks() directly without EXT4_EX_NOCACHE, bypassing
ext4_map_blocks() where i_es_tree is normally updated via
ext4_es_insert_extent(), so the second ext4_ext_map_blocks() call also hits
an ext4_es_cache_extent() conflict warning on [10, 14).
> diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
> index 6c018bcc01187..efbee7aaa95dc 100644
> --- a/fs/ext4/extents.c
> +++ b/fs/ext4/extents.c
[ ... ]
> @@ -1332,7 +1339,7 @@ static int ext4_ext_grow_indepth(handle_t *handle, struct inode *inode,
> if (newblock == 0)
> return err;
>
> - bh = sb_getblk_gfp(inode->i_sb, newblock, __GFP_MOVABLE | GFP_NOFS);
> + bh = ext4_ext_getblk(inode, newblock, __GFP_MOVABLE | GFP_NOFS);
> if (unlikely(!bh))
> return -ENOMEM;
[Severity: Medium]
This problem wasn't introduced by this patch, but does
ext4_ext_map_blocks() leak newly allocated data blocks [1] when
ext4_ext_insert_extent() fails with -ENOMEM?
Memory allocations in ext4_ext_insert_extent(), such as in
ext4_ext_create_new_leaf(), happen before the new extent is inserted into
the tree. However, the error path in ext4_ext_map_blocks() only calls
ext4_free_blocks() when err is -EDQUOT or -ENOSPC, leaving the unreferenced
blocks allocated in the block bitmap and charged to inode->i_blocks and
quota on -ENOMEM:
fs/ext4/extents.c:ext4_ext_map_blocks() {
...
path = ext4_ext_insert_extent(handle, inode, path, &newex, flags);
if (IS_ERR(path)) {
err = PTR_ERR(path);
if (allocated_clusters && (err == -EDQUOT || err == -ENOSPC)) {
...
ext4_free_blocks(handle, inode, NULL, newblock,
EXT4_C2B(sbi, allocated_clusters),
fb_flags);
}
goto out;
}
...
}
[1] https://sashiko.dev/#/bug/linux-d55bdb55-6626-49d1-965d-23a09e7996e3
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=2
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 09/27] ext4: add KUnit tests for fs/ext4/fast_commit.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:28 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 3 potential
issues to consider:
- [Medium] ext4: uninitialized j_trans_commit_map in
fast_commit_test_init()
- [Medium] ext4: excessive stack frame size in fast_commit-test.c test
cases
- [Low] ext4: resource leak and UAF on abort in fast_commit_test_init()
and cleanup
--
commit 0a44fff123afc676ec0d455ddc174806f24be58f
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/fast_commit.c
This commit adds the ext4_fast_commit_test KUnit test suite in
fs/ext4/fast_commit-test.c and exports fast commit helpers and static stub
hooks in fs/ext4/fast_commit.c and related files.
> diff --git a/fs/ext4/fast_commit-test.c b/fs/ext4/fast_commit-test.c
> new file mode 100644
> index 0000000000000..57745e51bea67
> --- /dev/null
> +++ b/fs/ext4/fast_commit-test.c
[ ... ]
> +/* --- Suite Setup & Teardown --- */
> +
> +static void fc_cleanup_all(void *priv)
> +{
> + struct fc_test_ctx *ctx = priv;
> + int i;
> +
> + ctx->sbi->s_mount_state &= ~EXT4_FC_REPLAY;
> + ext4_fc_cleanup_test(&ctx->journal, 1, 999999);
> + ext4_fc_replay_cleanup(ctx->sb);
> +
> + for (i = 0; i < TEST_MAX_INODES; i++) {
> + if (!ctx->inodes[i].allocated)
> + continue;
> + down_write(&ctx->inodes[i].ei.i_data_sem);
> + ext4_es_remove_extent(&ctx->inodes[i].ei.vfs_inode, 0,
> + EXT_MAX_BLOCKS);
> + up_write(&ctx->inodes[i].ei.i_data_sem);
> + ext4_fc_free_inode_snap_test(&ctx->inodes[i].ei.vfs_inode);
> + }
> +
> + ext4_es_unregister_shrinker(ctx->sbi);
> + deactivate_super(ctx->sb);
> +}
> +
> +static int fast_commit_test_init(struct kunit *test)
> +{
> + struct fc_test_ctx *ctx;
> + struct ext4_sb_info *sbi;
> + struct super_block *sb;
> + struct fs_context *fc;
> + int i, err;
> +
> + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
> +
> + sbi = kunit_kzalloc(test, sizeof(*sbi), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, sbi);
> +
> + fc = fs_context_for_mount(&fc_fs_type, 0);
> + KUNIT_ASSERT_FALSE(test, IS_ERR(fc));
> + sb = sget_fc(fc, NULL, fc_set_anon);
> + put_fs_context(fc);
> + KUNIT_ASSERT_FALSE(test, IS_ERR(sb));
[ ... ]
> + up_write(&sb->s_umount);
> +
> + err = ext4_es_register_shrinker(sbi);
> + KUNIT_ASSERT_EQ(test, err, 0);
> + ctx->shrinker_count = sbi->s_es_shrinker->count_objects;
> + ctx->shrinker_scan = sbi->s_es_shrinker->scan_objects;
> + WRITE_ONCE(sbi->s_es_shrinker->count_objects, fc_dummy_shrinker_count);
> + WRITE_ONCE(sbi->s_es_shrinker->scan_objects, fc_dummy_shrinker_scan);
> +
> + /* Mock journal, running transaction, and handle setup */
> + rwlock_init(&ctx->journal.j_state_lock);
> + spin_lock_init(&ctx->journal.j_list_lock);
> + mutex_init(&ctx->journal.j_barrier);
[Severity: Medium]
Does fast_commit_test_init() leave ctx->journal.j_trans_commit_map
uninitialized?
ctx is heap-allocated with kunit_kzalloc(). When test_fc_commit_paths()
calls ext4_fc_commit() -> ext4_fc_perform_commit() ->
jbd2_journal_lock_updates(), jbd2_might_wait_for_commit() passes
&ctx->journal.j_trans_commit_map to rwsem_acquire() and rwsem_release().
With CONFIG_LOCKDEP enabled, its NULL key in non-static memory triggers
"INFO: trying to register non-static key" and turns off lockdep via
debug_locks_off().
[ ... ]
> + kunit_activate_static_stub(test, ext4_fc_submit_bh,
> + stub_fc_submit_bh);
> + kunit_activate_static_stub(test, ext4_fc_issue_flush,
> + stub_fc_issue_flush);
> +
> + init_mock_inode_slot(ctx, &ctx->inodes[0], TEST_ROOT_INO,
> + S_IFDIR | 0755);
> + init_mock_inode_slot(ctx, &ctx->inodes[1], TEST_DIR_INO,
> + S_IFDIR | 0755);
> + init_mock_inode_slot(ctx, &ctx->inodes[2], TEST_FILE1_INO,
> + S_IFREG | 0644);
> + init_mock_inode_slot(ctx, &ctx->inodes[3], TEST_FILE2_INO,
> + S_IFREG | 0644);
> + init_mock_inode_slot(ctx, &ctx->inodes[4], TEST_SUBDIR_INO,
> + S_IFDIR | 0755);
> +
> + err = kunit_add_action_or_reset(test, fc_cleanup_all, ctx);
> + KUNIT_ASSERT_EQ(test, err, 0);
> +
> + return 0;
> +}
[Severity: Low]
Can an assertion failure in fast_commit_test_init() leak sb and leave
sbi->s_es_shrinker and sbi->s_es_stats percpu_counters registered after
KUnit frees sbi?
fc_cleanup_all() is only registered after ext4_es_register_shrinker() and
all the kunit_activate_static_stub() calls, which themselves assert on
allocation failure. Aborting in that window skips deactivate_super(sb) and
ext4_es_unregister_shrinker(sbi), causing a use-after-free once the
kunit_kzalloc()-allocated sbi is freed.
Also, in fc_cleanup_all(), calling ext4_fc_cleanup_test() only once drains
FC_Q_MAIN before splicing FC_Q_STAGING into FC_Q_MAIN, so any
ext4_fc_dentry_update entries on s_fc_dentry_q[FC_Q_STAGING] when a test
aborts are leaked.
> +
> +/* =========================================================================
> + * 1. Initialization, Eligibility, and Snapshot Stat Helpers
> + * =========================================================================
> + */
> +
> +static void test_fc_init_and_eligibility(struct kunit *test)
> +{
> + struct fc_test_ctx *ctx = test->priv;
> + struct super_block *sb = ctx->sb;
> + struct inode *inode = get_mock_inode(ctx, TEST_FILE1_INO);
> + struct ext4_inode_info *ei = EXT4_I(inode);
> + journal_t tmp_journal = {};
[Severity: Medium]
Does allocating tmp_journal on the stack here (and five struct dentry
objects on the stack in test_fc_replay_inode_and_dentries()) exceed
CONFIG_FRAME_WARN?
journal_t is over 1.3 KB (and over 2.2 KB with CONFIG_LOCKDEP enabled), and
the five dentries in test_fc_replay_inode_and_dentries() take ~1.6 KB with
CONFIG_LOCKDEP, triggering -Wframe-larger-than warnings or build failures
under CONFIG_WERROR. Could tmp_journal and the mock dentries be allocated
with kunit_kzalloc() or moved into struct fc_test_ctx?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=9
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 07/27] ext4: add KUnit tests for fs/ext4/inline.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:29 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 4 potential
issues to consider:
- [Low] ext4: unexercised dioread_nolock branch in inline conversion
KUnit test
- [Low] ext4: unexercised iversion revalidation in test_filldir_actor()
- [Low] ext4: inaccurate comment on ext4_inlinedir_to_tree() in
inline-test.c
- [Low] ext4: missing i_size_ordered_init() on mock inode in
inline_test_init()
It also noticed 2 related pre-existing issues this patch does not
introduce:
- [Critical] ext4: TOCTOU race condition in
ext4_convert_inline_data_nolock() [1]
- [Critical] ext4: 32-bit i_size truncation in
ext4_inline_data_truncate() [2]
--
commit 2474f0318caf555f2d6839372e8f1734082921e5
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/inline.c
This commit adds a KUnit test suite in fs/ext4/inline-test.c to exercise
ext4 inline data operations for regular files, symlinks, and directories,
along with static stub hooks and symbol exports for the tested functions.
> diff --git a/fs/ext4/inline-test.c b/fs/ext4/inline-test.c
> new file mode 100644
> index 0000000000000..a75f46475c4a9
> --- /dev/null
> +++ b/fs/ext4/inline-test.c
[ ... ]
> +static int inline_test_init(struct kunit *test)
> +{
[ ... ]
> + /* Initialize mock ext4_inode_info and embedded vfs_inode */
> + inode = &ctx->ei.vfs_inode;
> + inode->i_sb = &ctx->sb;
> + inode->i_ino = TEST_INO;
> + inode->i_blkbits = TEST_BLOCK_BITS;
> + set_nlink(inode, 1);
> + atomic_set(&inode->i_count, 1);
> + spin_lock_init(&inode->i_lock);
> + init_rwsem(&inode->i_rwsem);
[Severity: Low]
Does the mock inode initialization in inline_test_init() miss a call to
i_size_ordered_init(inode) (or inode_init_once())?
On 32-bit SMP kernels with CONFIG_DEBUG_LOCK_ALLOC enabled, struct inode
embeds seqcount_t i_size_seqcount. Because ctx is allocated from the heap
via kunit_kzalloc(), calling i_size_write() or i_size_read() in
create_inline_file() and the tested inline helpers accesses an
uninitialized lockdep_map key on inode->i_size_seqcount, triggering a
lockdep warning and disabling lockdep.
[ ... ]
> +static void test_write_triggers_inline_to_extent_conversion(struct kunit *test)
> +{
[ ... ]
> + /*
> + * 2. Conversion with DIOREAD_NOLOCK + DELALLOC enabled and journal_data
> + * mode (exercises ext4_should_dioread_nolock and ext4_should_journal_data
> + * branches in ext4_convert_inline_data_to_extent).
> + */
> + reset_inode_state(ctx, S_IFREG | 0644);
> + ret = create_inline_file(inode, buf, sizeof(buf));
> + KUNIT_ASSERT_EQ(test, ret, 0);
> +
> + set_opt(&ctx->sb, DIOREAD_NOLOCK);
> + set_opt(&ctx->sb, DELALLOC);
> + ctx->journal_mode = EXT4_INODE_JOURNAL_DATA_MODE;
> +
> + inode_lock(inode);
> + folio = NULL;
> + ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 200,
> + &folio);
> + inode_unlock(inode);
[Severity: Low]
Does setting ctx->journal_mode to EXT4_INODE_JOURNAL_DATA_MODE here prevent
the ext4_should_dioread_nolock() branch in
ext4_convert_inline_data_to_extent() from being exercised?
ext4_should_dioread_nolock() returns 0 whenever
ext4_should_journal_data(inode) is true:
fs/ext4/ext4_jbd2.h:ext4_should_dioread_nolock() {
...
if (ext4_should_journal_data(inode))
return 0;
...
}
Testing the DIOREAD_NOLOCK + DELALLOC unwritten-extent conversion path
requires a separate call with a non-journaled data mode such as
EXT4_INODE_WRITEBACK_DATA_MODE.
[ ... ]
> +static bool test_filldir_actor(struct dir_context *dctx, const char *name,
> + int namelen, loff_t offset, u64 ino,
> + unsigned int d_type)
> +{
> + struct test_dir_ctx *tctx =
> + container_of(dctx, struct test_dir_ctx, ctx);
> +
> + if (tctx->max_emit > 0 && tctx->count >= tctx->max_emit)
> + return false;
[ ... ]
> + /* Mutating inode iversion during iteration triggers offset revalidation */
> + memset(&env->dctx, 0, sizeof(env->dctx));
> + env->dctx.ctx.actor = test_filldir_actor;
> + env->dctx.ctx.pos = 0;
> + env->dctx.mutate_inode_on_second = dir;
> + ret = ext4_read_inline_dir(&env->filp, &env->dctx.ctx,
> + &has_inline_data);
> + KUNIT_EXPECT_EQ(test, ret, 0);
> + KUNIT_EXPECT_EQ(test, env->dctx.count, 6);
[Severity: Low]
Does this subtest ever exercise partial iteration or the iversion
revalidation loop in ext4_read_inline_dir()?
tctx->max_emit is never set to a non-zero value anywhere in the file, and
ext4_read_inline_dir() is only called once starting from ctx.pos = 0.
When test_filldir_actor() increments dir's iversion on the second entry,
ext4_read_inline_dir() has already passed the
!inode_eq_iversion(inode, info->cookie) check at the top of the function
and continues emitting all 6 entries in the same call without ever
re-entering at a non-zero ctx.pos.
[ ... ]
> +static void test_inline_dir_conversion_and_corruptions(struct kunit *test)
> +{
[ ... ]
> + /*
> + * 4. Corrupted inline directory checks:
> + * 4a. Parent ino ("..") == 0 -> empty_inline_dir returns false.
> + * 4b. Zero rec_len dirent -> empty_inline_dir returns false,
> + * ext4_inlinedir_to_tree returns -EFSCORRUPTED, and
> + * ext4_convert_inline_data_nolock aborts via ext4_check_all_de.
> + */
> + reset_inode_state(ctx, S_IFDIR | 0755);
> + handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
> + KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
> + ret = ext4_try_create_inline_dir(handle, &env->parent, dir);
> + ext4_journal_stop(handle);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> +
> + /* Corrupt parent ino to 0 */
> + *(__le32 *)raw_inline_i_block(ctx) = cpu_to_le32(0);
> + KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
> + *(__le32 *)raw_inline_i_block(ctx) = cpu_to_le32(TEST_PARENT_INO);
> +
> + /* Corrupt first dirent rec_len to 0 */
> + de = (struct ext4_dir_entry_2 *)((char *)raw_inline_i_block(ctx) +
> + EXT4_INLINE_DOTDOT_SIZE);
> + de->rec_len = cpu_to_le16(0);
> + KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
> +
> + /* ext4_convert_inline_data on corrupted inline dir fails in ext4_check_all_de */
> + ret = ext4_convert_inline_data(dir);
> + KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
> + KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(dir));
> +}
[Severity: Low]
This isn't a bug, but comment 4b states that a zero rec_len dirent causes
ext4_inlinedir_to_tree() to return -EFSCORRUPTED, whereas the test does
not call ext4_inlinedir_to_tree() here and ext4_inlinedir_to_tree() breaks
out of its loop and returns the count of already-processed entries (2 for
"." and "..") when ext4_check_dir_entry() fails.
> diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
> index 0e662a9f7574c..97cc2c61ce25d 100644
> --- a/fs/ext4/inline.c
> +++ b/fs/ext4/inline.c
[ ... ]
> @@ -1144,7 +1152,7 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
> goto out_restore;
> }
>
> - data_bh = sb_getblk(inode->i_sb, map.m_pblk);
> + data_bh = ext4_inline_getblk(inode->i_sb, map.m_pblk);
> if (!data_bh) {
> error = -ENOMEM;
> goto out_restore;
[Severity: Critical]
This problem wasn't introduced by this patch, but can unlocking data_bh
before ext4_init_dirblock() in ext4_convert_inline_data_nolock() expose an
uninitialized directory block to concurrent readers [1]?
CPU0 CPU1
ext4_convert_inline_data_nolock()
ext4_destroy_inline_data_nolock()
ext4_map_blocks()
data_bh = ext4_inline_getblk()
lock_buffer(data_bh)
memset(data_bh->b_data, 0, ...)
unlock_buffer(data_bh)
i_size_write(inode, s_blocksize)
ext4_get_parent()
ext4_find_entry()
...
ext4_dirblock_csum_verify()
<-- fails on zeroed block
ext4_init_dirblock(data_bh)
set_buffer_uptodate(data_bh)
set_buffer_verified(data_bh)
ext4_handle_dirty_dirblock()
In the S_ISDIR branch of ext4_convert_inline_data_nolock(), data_bh is
zeroed, unlocked, and published via i_size_write() before
ext4_init_dirblock() populates the directory entries and computes the
block checksum:
fs/ext4/inline.c:ext4_convert_inline_data_nolock() {
...
} else {
unlock_buffer(data_bh);
inode->i_size = inode->i_sb->s_blocksize;
i_size_write(inode, inode->i_sb->s_blocksize);
EXT4_I(inode)->i_disksize = inode->i_sb->s_blocksize;
error = ext4_init_dirblock(handle, inode, data_bh,
le32_to_cpu(((struct ext4_dir_entry_2 *)buf)->inode),
buf + EXT4_INLINE_DOTDOT_SIZE,
inline_size - EXT4_INLINE_DOTDOT_SIZE);
...
}
In addition, ext4_init_dirblock() sets buffer_uptodate and buffer_verified
before ext4_handle_dirty_dirblock() calculates the checksum. A concurrent
reader (such as NFS export lookup via ext4_get_parent()) can read the
partially initialized or unchecksummed block and fail
ext4_dirblock_csum_verify(), triggering an ext4 filesystem error (and a
read-only remount or panic).
> @@ -2018,5 +2026,25 @@ int ext4_convert_inline_data(struct inode *inode)
> }
>
> #if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
[ ... ]
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inline_data_truncate);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_convert_inline_data);
> #endif
[Severity: Critical]
This problem wasn't introduced by this patch, but can
ext4_inline_data_truncate() truncate the 64-bit inode->i_size into a
32-bit size_t on 32-bit architectures [2]?
fs/ext4/inline.c:ext4_inline_data_truncate() {
...
size_t i_size;
...
down_write(&EXT4_I(inode)->i_data_sem);
i_size = inode->i_size;
inline_size = ext4_get_inline_size(inode);
EXT4_I(inode)->i_disksize = i_size;
if (i_size < inline_size) {
if (!ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA)) {
ext4_lblk_t lblk = EXT4_B_TO_LBLK(inode, i_size);
ext4_es_remove_extent(inode, lblk,
EXT_MAX_BLOCKS - lblk);
}
...
}
When an inline file is converted to delayed-allocation extents via a
buffered write at an offset of 4 GiB or greater, EXT4_STATE_MAY_INLINE_DATA
is cleared while EXT4_INODE_INLINE_DATA remains set until writeback. If a
subsequent truncate or ext4_truncate_failed_write() calls
ext4_inline_data_truncate() before writeback on a 32-bit system, assigning
inode->i_size to the 32-bit i_size local variable discards the upper 32
bits, corrupting EXT4_I(inode)->i_disksize. When the lower 32 bits are
smaller than inline_size, it also enters the i_size < inline_size branch,
removing live delayed extents from the extent status tree and zeroing the
inline data buffer.
[1] https://sashiko.dev/#/bug/linux-48900fa2-4764-4787-990d-c127e7cf2417
[2] https://sashiko.dev/#/bug/linux-d1c56a9e-62e9-4eeb-addd-143c371dbdca
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=7
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 26/27] ext4: add KUnit tests for fs/ext4/mmp.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:29 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
> Add a KUnit test suite (ext4_mmp_test) covering multi-mount protection
> checksums, block I/O validation, and mount state transitions in
> fs/ext4/mmp.c:
>
> - CRC32C generation and verification in ext4_mmp_csum_set() and
> ext4_mmp_csum_verify() with and without METADATA_CSUM
> - Block read/write error handling, magic validation (-EFSCORRUPTED),
> CRC mismatch handling (-EFSBADCRC), and buffer cleanup in
> read_mmp_block() and write_mmp_block()
> - ext4_multi_mount_protect() validation of out-of-range MMP blocks,
> EXT4_MMP_SEQ_FSCK rejection (-EBUSY), EXT4_MMP_SEQ_CLEAN claim and
> ext4_stop_mmpd() teardown, active/racing node sequence conflicts,
> check_interval clamping, interrupted waits (-ETIMEDOUT), and kthread
> startup failure
>
> Signed-off-by: Theodore Ts'o <tytso@mit.edu>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=26
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 24/27] ext4: add KUnit tests for fs/ext4/fsmap.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:30 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Low] ext4: per-CPU disk_stats leak on assert abort in
fsmap_create_ctx()
- [Low] ext4: unused fail_err field in fsmap_rec_sink and
fsmap_collect_cb()
--
commit 26245fe40968f50951fa0187b785c405d62e3f12
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/fsmap.c
This commit adds a KUnit test suite covering FS_IOC_GETFSMAP reverse-mapping
queries in fs/ext4/fsmap.c.
> diff --git a/fs/ext4/fsmap-test.c b/fs/ext4/fsmap-test.c
> new file mode 100644
> index 0000000000000..63a1061b413a7
> --- /dev/null
> +++ b/fs/ext4/fsmap-test.c
> @@ -0,0 +1,787 @@
[ ... ]
> +struct fsmap_rec_sink {
> + struct ext4_fsmap recs[FSMAP_TEST_MAX_RECS];
> + unsigned int count;
> + unsigned int abort_after;
> + int fail_err;
> +};
[ ... ]
> +static int fsmap_collect_cb(struct ext4_fsmap *fmr, void *priv)
> +{
> + struct fsmap_rec_sink *sink = priv;
> +
> + if (sink->fail_err)
> + return sink->fail_err;
[Severity: Low]
Nothing in this file sets sink->fail_err, leaving the field and this check
in fsmap_collect_cb() unused.
[ ... ]
> +static struct fsmap_test_ctx *fsmap_create_ctx(struct kunit *test,
> + unsigned int blocksize_bits,
> + unsigned int cluster_bits,
> + ext4_group_t groups_count,
> + ext4_grpblk_t blocks_per_group)
> +{
> + struct fsmap_test_ctx *ctx;
> + struct fs_context *fc;
> + struct super_block *sb;
> + struct ext4_sb_info *sbi;
> + unsigned int blocksize = 1U << blocksize_bits;
> + ext4_fsblk_t bofs = (blocksize == 1024) ? 1 : 0;
> + int err;
> +
> + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_NULL(test, ctx);
> +
> + inode_init_once(&ctx->data_bdev.vfs_inode);
> + address_space_init_once(&ctx->data_bdev.vfs_inode.i_data);
> + ctx->data_bdev.vfs_inode.i_data.host = &ctx->data_bdev.vfs_inode;
> + ctx->data_bdev.bdev.bd_dev = FSMAP_TEST_DATADEV;
> + ctx->data_bdev.bdev.bd_mapping = &ctx->data_bdev.vfs_inode.i_data;
> + ctx->data_bdev.bdev.bd_stats = alloc_percpu(struct disk_stats);
> + KUNIT_ASSERT_NOT_NULL(test, ctx->data_bdev.bdev.bd_stats);
> +
> + inode_init_once(&ctx->log_bdev.vfs_inode);
> + address_space_init_once(&ctx->log_bdev.vfs_inode.i_data);
> + ctx->log_bdev.vfs_inode.i_data.host = &ctx->log_bdev.vfs_inode;
> + ctx->log_bdev.bdev.bd_dev = FSMAP_TEST_LOGDEV;
> + ctx->log_bdev.bdev.bd_mapping = &ctx->log_bdev.vfs_inode.i_data;
> + ctx->log_bdev.bdev.bd_stats = alloc_percpu(struct disk_stats);
> + KUNIT_ASSERT_NOT_NULL(test, ctx->log_bdev.bdev.bd_stats);
> + ctx->log_bdev_file.f_mapping = &ctx->log_bdev.vfs_inode.i_data;
> +
> + sbi = &ctx->sbi;
> + INIT_WORK(&sbi->s_sb_upd_work, fsmap_noop_work);
> + timer_setup(&sbi->s_err_report, fsmap_noop_timer, 0);
> + spin_lock_init(&sbi->s_error_lock);
> + spin_lock_init(&sbi->s_bdev_wb_lock);
> + ratelimit_state_init(&sbi->s_err_ratelimit_state,
> + DEFAULT_RATELIMIT_INTERVAL,
> + DEFAULT_RATELIMIT_BURST);
> +
> + fc = fs_context_for_mount(&fsmap_test_fs_type, 0);
> + KUNIT_ASSERT_FALSE(test, IS_ERR(fc));
> + sb = sget_fc(fc, NULL, fsmap_test_set_super);
> + put_fs_context(fc);
> + KUNIT_ASSERT_FALSE(test, IS_ERR(sb));
> + up_write(&sb->s_umount);
> +
> + ctx->sb = sb;
> + err = kunit_add_action_or_reset(test, fsmap_cleanup_ctx, ctx);
> + KUNIT_ASSERT_EQ(test, err, 0);
[Severity: Low]
Does this leak the per-CPU bd_stats allocations if one of the assertions
before kunit_add_action_or_reset() aborts the test? Registering
fsmap_cleanup_ctx() before calling alloc_percpu() would ensure they are
freed on early assertion failure.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=24
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 18/27] ext4: add KUnit tests for fs/ext4/sysfs.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:32 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 4 potential
issues to consider:
- [Medium] ext4: UAF of ctx->sb in fserror_worker() due to NULL
sb->s_root in sysfs-test.c
- [Low] ext4: preemptible part_stat_add() call in
test_computed_ro_and_error_time_attrs()
- [Low] ext4: s_last_error_func check fails under !CONFIG_PRINTK in
sysfs-test.c
- [Low] ext4: sysfs_test_cleanup() frees ctx->sb before canceling work
and timer
--
commit f2e9ba8068c01ef9b7c5be16e187d6640c8a57c0
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/sysfs.c
This commit adds a KUnit test suite covering ext4 sysfs attribute show and
store handlers, feature capability attributes, and sysfs/procfs registration.
> diff --git a/fs/ext4/sysfs-test.c b/fs/ext4/sysfs-test.c
> new file mode 100644
> index 0000000000000..c13785420aa11
> --- /dev/null
> +++ b/fs/ext4/sysfs-test.c
> @@ -0,0 +1,653 @@
[ ... ]
> +static void sysfs_test_cleanup(struct sysfs_test_ctx *ctx)
> +{
> + ctx->sbi.s_journal = NULL;
> +
> + deactivate_super(ctx->sb);
> + rcu_barrier();
> +
> + cancel_work_sync(&ctx->sbi.s_sb_upd_work);
> + timer_delete_sync(&ctx->sbi.s_err_report);
[Severity: Low]
Should cancel_work_sync() and timer_delete_sync() run before
deactivate_super(ctx->sb)?
Writing a non-zero value to err_report_sec in test_validated_rw_attrs()
replaces sbi->s_err_report's callback with print_daily_error_info() and arms
the timer, and print_daily_error_info() dereferences sbi->s_sb (ctx->sb).
Stopping the work item and timer before deactivating and RCU-freeing ctx->sb
ensures neither callback can access ctx->sb after it is freed.
[ ... ]
> + sbi->s_sb = sb;
> + sb->s_fs_info = sbi;
> + sb->s_op = &sysfs_test_sops;
> +#ifdef CONFIG_FS_ENCRYPTION
> + sb->s_cop = &ext4_cryptops;
> +#endif
> + sb->s_flags |= SB_ACTIVE;
> + sb->s_blocksize = SYSFS_TEST_BLK_SIZE;
> + sb->s_blocksize_bits = SYSFS_TEST_BLK_BITS;
> + strscpy(sb->s_id, "ext4_sysfs_kunit", sizeof(sb->s_id));
[Severity: Medium]
Can leaving sb->s_root NULL while setting SB_ACTIVE cause a use-after-free of
ctx->sb in fserror_worker()?
In test_feature_attrs_and_trigger_error(), writing to trigger_fs_error calls
trigger_test_error() -> ext4_error() -> __ext4_error() ->
fserror_report_metadata(). Because alloc_super() initialized
sb->s_pending_errors to 1 and SB_ACTIVE is set, fserror_report_metadata()
queues an asynchronous fserror_worker work item holding a raw pointer to
ctx->sb. However, generic_shutdown_super() only clears SB_ACTIVE and calls
fserror_unmount(sb) to drain pending events when sb->s_root is non-NULL:
CPU0 kworker
test_feature_attrs_and_trigger_error()
...
fserror_report_metadata(sb, ...)
schedule_work(&event->work)
sysfs_test_cleanup()
deactivate_super(ctx->sb)
kill_anon_super()
generic_shutdown_super()
/* sb->s_root is NULL:
* skips fserror_unmount(sb)
*/
put_super(sb)
rcu_barrier()
/* destroy_super_work() frees sb */
fserror_worker()
sb = event->sb;
if (!(sb->s_flags & SB_ACTIVE))
<-- use-after-free
Initializing sb->s_root with d_make_root() (or calling fserror_unmount(ctx->sb)
before deactivating the superblock) would ensure pending fserror events are
drained before ctx->sb is freed.
[ ... ]
> +static void test_computed_ro_and_error_time_attrs(struct kunit *test)
> +{
> + struct sysfs_test_ctx *ctx = test->priv;
> + struct ext4_sb_info *sbi = &ctx->sbi;
> + struct ext4_super_block *es = &ctx->es;
> + char expected_pid[32];
> + char *buf;
> +
> + buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL);
> + KUNIT_ASSERT_NOT_NULL(test, buf);
> +
> + /* 7 dirty clusters * 4 blocks/cluster == 28 delayed allocation blks */
> + percpu_counter_set(&sbi->s_dirtyclusters_counter, 7);
> + KUNIT_EXPECT_GT(test,
> + ext4_sysfs_attr_show_test(sbi,
> + "delayed_allocation_blocks", buf),
> + (ssize_t)0);
> + KUNIT_EXPECT_STREQ(test, buf, "28\n");
> +
> + /* (50 - 10) sectors >> 1 == 20 KB session, + 100 == 120 KB lifetime */
> + sbi->s_sectors_written_start = 10;
> + sbi->s_kbytes_written = 100;
> + part_stat_add(&ctx->bdev, sectors[STAT_WRITE], 50);
[Severity: Low]
Does calling part_stat_add() here without part_stat_lock() /
part_stat_unlock() trigger a CONFIG_DEBUG_PREEMPT warning?
part_stat_add() uses __this_cpu_add(), which requires preemption to be
disabled and logs a "BUG: using __this_cpu_add() in preemptible" warning when
called in a preemptible KUnit thread with CONFIG_DEBUG_PREEMPT enabled.
[ ... ]
> + /* Non-empty message records error state via ext4_error() */
> + KUNIT_EXPECT_EQ(test,
> + ext4_sysfs_attr_store_test(sbi, "trigger_fs_error",
> + "kunit injected\n", 15),
> + (ssize_t)15);
> + KUNIT_EXPECT_NE(test, sbi->s_mount_state & EXT4_ERROR_FS, 0);
> + KUNIT_EXPECT_STREQ(test, sbi->s_last_error_func, "trigger_test_error");
> + sbi->s_journal = NULL;
> +}
[Severity: Low]
Will the sbi->s_last_error_func assertion fail when CONFIG_PRINTK is disabled?
In fs/ext4/ext4.h, the !CONFIG_PRINTK definition of ext4_error() passes ""
instead of __func__ to __ext4_error(), so sbi->s_last_error_func is set to ""
rather than "trigger_test_error".
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=18
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 23/27] ext4: expand KUnit tests in fs/ext4/extents-test.c for file range operations
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:34 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 3 potential
issues to consider:
- [Medium] ext4: uninitialized invalidate_lock in ext_init_mock_inode()
- [Low] ext4: use-after-free of sb->s_bdev after deactivate_super() in
extents_kunit_exit()
- [Low] ext4: i_data_sem and path leak on assertion abort in
ext_expect_extent_at()
--
commit 0bbdba01f4f2a820793844a83a44fb01144fcc50
Author: Theodore Ts'o <tytso@mit.edu>
ext4: expand KUnit tests in fs/ext4/extents-test.c for file range operations
This commit expands the ext4 extents KUnit test suite to cover
ext4_fallocate(), ext4_zero_range(), ext4_collapse_range(),
ext4_insert_range(), unwritten extent conversion helpers, ext4_fiemap(),
and ext4_get_es_cache().
> diff --git a/fs/ext4/extents-test.c b/fs/ext4/extents-test.c
> index 7565a8aa4e106..4f1746be323ff 100644
> --- a/fs/ext4/extents-test.c
> +++ b/fs/ext4/extents-test.c
[ ... ]
> @@ -200,20 +210,29 @@ static void ext_init_mock_inode(struct ext4_inode_info *ei,
> inode->i_sb = sb;
> inode->i_ino = ino;
> inode->i_mode = S_IFREG | 0644;
> + set_nlink(inode, 1);
> inode->i_blkbits = sb->s_blocksize_bits;
> - inode->i_flags |= S_NOQUOTA;
> + inode->i_flags |= S_NOQUOTA | S_NOSEC;
> inode->i_mapping = &inode->i_data;
> + inode_set_iversion(inode, 1);
>
> spin_lock_init(&inode->i_lock);
> init_rwsem(&inode->i_rwsem);
> - init_rwsem(&inode->i_data.invalidate_lock);
> + address_space_init_once(&inode->i_data);
> + inode->i_data.host = inode;
> inode_state_set_raw(inode, I_DIRTY_SYNC | I_DIRTY_DATASYNC);
[Severity: Medium]
Does replacing init_rwsem(&inode->i_data.invalidate_lock) with
address_space_init_once(&inode->i_data) leave invalidate_lock
uninitialized?
address_space_init_once() zeroes inode->i_data and initializes only
i_pages, i_mmap_rwsem, i_private_lock, and i_mmap; in the VFS,
invalidate_lock is initialized separately in inode_init_always_gfp().
When the new fallocate tests call ext4_fallocate() with punch hole, zero
range, collapse range, or insert range, filemap_invalidate_lock() acquires
this zeroed rw_semaphore, which triggers lockdep or CONFIG_DEBUG_RWSEMS
warnings and deadlocks on CONFIG_PREEMPT_RT kernels. Keeping
init_rwsem(&inode->i_data.invalidate_lock) after address_space_init_once()
avoids this.
[ ... ]
> @@ -267,6 +286,7 @@ static void extents_kunit_exit(struct kunit *test)
>
> kfree(k_ctx.meta_blocks);
> kfree(sbi->s_es);
> + kfree(sb->s_bdev->bd_queue);
> kfree(sb->s_bdev->bd_mapping);
> kfree(sb->s_bdev);
> kfree(sbi);
[Severity: Low]
Can dereferencing sb->s_bdev here use sb after it has been freed?
deactivate_super(sb) is called just above this hunk, which drops the final
reference on sb and schedules its destruction via RCU and
destroy_super_work(), unlike the error path in extents_kunit_init() which
frees sb->s_bdev->bd_queue, sb->s_bdev->bd_mapping, and sb->s_bdev before
deactivating the superblock.
[ ... ]
> +static void ext_expect_extent_at(struct kunit *test, struct inode *inode,
> + ext4_lblk_t lblk, unsigned short exp_len,
> + ext4_fsblk_t exp_pblk, bool exp_unwritten)
> +{
> + struct ext4_ext_path *path;
> + struct ext4_extent *ex;
> +
> + down_read(&EXT4_I(inode)->i_data_sem);
> + path = ext4_find_extent(inode, lblk, NULL, EXT4_EX_NOCACHE);
> + KUNIT_ASSERT_FALSE(test, IS_ERR(path));
> + ex = path[path->p_depth].p_ext;
> + KUNIT_ASSERT_NOT_NULL(test, ex);
> +
> + KUNIT_EXPECT_EQ(test, le32_to_cpu(ex->ee_block), lblk);
> + KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(ex), exp_len);
> + KUNIT_EXPECT_EQ(test, ext4_ext_pblock(ex), exp_pblk);
> + KUNIT_EXPECT_EQ(test, !!ext4_ext_is_unwritten(ex), exp_unwritten);
> +
> + ext4_free_ext_path(path);
> + up_read(&EXT4_I(inode)->i_data_sem);
> +}
[Severity: Low]
If KUNIT_ASSERT_FALSE() or KUNIT_ASSERT_NOT_NULL() fails in
ext_expect_extent_at(), does the test abort with EXT4_I(inode)->i_data_sem
still held for read and path leaked?
Because KUNIT_ASSERT_* immediately terminates the test kthread,
extents_kunit_exit() -> ext_cleanup_inode() will then deadlock on
down_write(&ei->i_data_sem).
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=23
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 20/27] ext4: add KUnit tests for fs/ext4/resize.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:34 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 4 potential
issues to consider:
- [Medium] ext4: RCU callback context and barrier order in
resize_test_cleanup()
- [Low] ext4: uninitialized work/timer cancel in resize_test_init()
unwind
- [Low] ext4: leak of group_desc on assertion abort in
resize_configure_fs()
- [Low] ext4: partial percpu_counter_init() leak in
resize_configure_fs()
--
commit 2234a2c272479e93ed67d158ad4b60b46b408f78
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/resize.c
This commit adds a KUnit test suite for online filesystem resizing in
fs/ext4/resize.c and routes resize metadata buffer lookups and zeroout
calls through KUnit stubbed helpers.
> diff --git a/fs/ext4/resize-test.c b/fs/ext4/resize-test.c
> new file mode 100644
> index 0000000000000..081a5d6c6fd90
> --- /dev/null
> +++ b/fs/ext4/resize-test.c
[ ... ]
> +static void resize_test_free_inode(struct inode *inode)
> +{
> + ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
> + kfree(EXT4_I(inode));
> +}
> +
> +static void resize_test_evict_inode(struct inode *inode)
> +{
> + truncate_inode_pages_final(&inode->i_data);
> + clear_inode(inode);
> +}
[ ... ]
> +static void resize_test_cleanup(struct resize_test_ctx *ctx)
> +{
> + struct super_block *sb = ctx->sb;
> + struct ext4_sb_info *sbi = &ctx->sbi;
> + struct flex_groups **flex_groups;
> + struct resize_mock_blk *blk, *tmp;
> + unsigned int i;
> +
> + if (sb) {
> + cancel_work_sync(&sbi->s_sb_upd_work);
> + timer_delete_sync(&sbi->s_err_report);
> + }
> +
> + if (ctx->resize_inode) {
> + iput(ctx->resize_inode);
> + ctx->resize_inode = NULL;
> + }
> +
> + if (ctx->mb_inited && sb) {
> + ext4_mb_release(sb);
> + ctx->mb_inited = false;
> + }
> +
> + if (ctx->counters_inited) {
> + percpu_counter_destroy(&sbi->s_freeclusters_counter);
> + percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
> + percpu_counter_destroy(&sbi->s_freeinodes_counter);
> + ctx->counters_inited = false;
> + }
> +
> + flex_groups = rcu_access_pointer(sbi->s_flex_groups);
> + if (flex_groups) {
> + for (i = 0; i < sbi->s_flex_groups_allocated; i++)
> + kvfree(flex_groups[i]);
> + kvfree(flex_groups);
> + rcu_assign_pointer(sbi->s_flex_groups, NULL);
> + sbi->s_flex_groups_allocated = 0;
> + }
> +
> + rcu_barrier();
> + kvfree(rcu_access_pointer(sbi->s_group_desc));
> + rcu_assign_pointer(sbi->s_group_desc, NULL);
> +
> + if (sb) {
> + deactivate_super(sb);
> + ctx->sb = NULL;
> + }
[Severity: Medium]
Can resize_test_free_inode() and resize_test_cleanup() cause a
use-after-free on ctx->sbi and take i_es_lock in RCU softirq context?
The VFS invokes super_operations.free_inode from i_callback() in RCU
softirq context, but resize_test_free_inode() calls
ext4_es_remove_extent(), which takes write_lock(&EXT4_I(inode)->i_es_lock)
without disabling softirqs, reads EXT4_SB(inode->i_sb)->s_mount_state, and
can touch sbi->s_dirtyclusters_counter. In addition, resize_test_cleanup()
destroys the percpu counters before rcu_barrier() and calls
deactivate_super(sb) after rcu_barrier(); if a test aborts while an inode
such as rinode in test_convert_meta_bg_validation_and_conversion() is still
live, deactivate_super() -> evict_inodes() queues i_callback() after
rcu_barrier() has already returned, so resize_test_free_inode() can run
after KUnit frees ctx.
Moving ext4_es_remove_extent() into resize_test_evict_inode() (matching
ext4_clear_inode()) and calling deactivate_super(sb) before rcu_barrier()
and percpu_counter_destroy() would avoid both issues.
[ ... ]
> +static void resize_configure_fs(struct kunit *test, ext4_group_t ngroups,
> + u16 reserved_gdb, u8 log_flex,
> + u32 last_grp_blks)
> +{
[ ... ]
> + group_desc = kvmalloc_array(gdb_count, sizeof(*group_desc), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_NULL(test, group_desc);
> + for (g = 0; g < gdb_count; g++) {
> + struct buffer_head *bh;
> +
> + bh = resize_get_or_create_bh(ctx,
> + RESIZE_TEST_FIRST_DATA_BLK + 1 + g);
> + KUNIT_ASSERT_NOT_NULL(test, bh);
> + brelse(bh);
> + group_desc[g] = bh;
> + }
> + rcu_assign_pointer(sbi->s_group_desc, group_desc);
[Severity: Low]
Does this leak group_desc if KUNIT_ASSERT_NOT_NULL(test, bh) aborts the
test? Because sbi->s_group_desc is only assigned after the loop,
resize_test_cleanup() will not see group_desc to free it.
[ ... ]
> + ret = percpu_counter_init(&sbi->s_freeclusters_counter,
> + total_free_blks, GFP_KERNEL);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + ret = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0, GFP_KERNEL);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + ret = percpu_counter_init(&sbi->s_freeinodes_counter,
> + total_inodes - 10, GFP_KERNEL);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + ctx->counters_inited = true;
[Severity: Low]
What happens if the second or third percpu_counter_init() fails and
KUNIT_ASSERT_EQ() aborts before ctx->counters_inited is set?
resize_test_cleanup() skips percpu_counter_destroy() when
ctx->counters_inited is false, leaking the already-initialized counter(s)
and, under CONFIG_HOTPLUG_CPU, leaving their list_head entries inside
ctx->sbi linked on the global percpu_counters list after ctx is freed.
Since percpu_counter_destroy() is a no-op on zero-initialized counters,
resize_test_cleanup() can call percpu_counter_destroy() unconditionally.
[ ... ]
> +static int resize_test_init(struct kunit *test)
> +{
[ ... ]
> + ctx->sb = sb;
> + up_write(&sb->s_umount);
> +
> + ret = kunit_add_action_or_reset(test, resize_test_cleanup_action, ctx);
> + KUNIT_ASSERT_EQ(test, ret, 0);
[ ... ]
> + INIT_WORK(&sbi->s_sb_upd_work, resize_test_sb_upd_work);
> + timer_setup(&sbi->s_err_report, resize_test_err_timer, 0);
[Severity: Low]
If kunit_add_action_or_reset() fails to allocate the action, it immediately
invokes resize_test_cleanup() while ctx->sb is already set, before
sbi->s_sb_upd_work and sbi->s_err_report are initialized below. Would
cancel_work_sync() and timer_delete_sync() in resize_test_cleanup() then
trigger warnings on the uninitialized work and timer structures?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=20
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 19/27] ext4: add KUnit tests for fs/ext4/ioctl.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:35 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 4 potential
issues to consider:
- [Medium] ext4: uninitialized s_es_stats counters dereferenced in
ioctl_test_init()
- [Low] ext4: uninitialized work and timer cleanup in ioctl_test_init()
error paths
- [Low] ext4: NULL bh->b_bdev in ioctl_init_mock_bh() dereferenced by
tracepoint
- [Low] ext4: leaked inodes and dentries on KUNIT_ASSERT_NOT_NULL()
abort in ioctl-test
It also noticed 2 related pre-existing issues this patch does not
introduce:
- [High] ext4: missing s_encoding initialization in
ext4_ioctl_set_tune_sb() [1]
- [High] ext4: torn read of i_version in swap_inode_data() [2]
--
commit e7c759c842d0abec3f3d75ab9a035c69eb297d76
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/ioctl.c
This commit adds a KUnit test suite for superblock online tuning, backup
superblock updates, inode data swapping, fileattr handling, forced shutdown,
and ioctl dispatch in fs/ext4/ioctl.c, exporting helper wrappers for testing.
> diff --git a/fs/ext4/ioctl-test.c b/fs/ext4/ioctl-test.c
> new file mode 100644
> index 0000000000000..fa31a9cc5d920
> --- /dev/null
> +++ b/fs/ext4/ioctl-test.c
[ ... ]
> +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);
> +}
[Severity: Low]
Can leaving bh->b_bdev NULL here cause a NULL pointer dereference when the
block_dirty_buffer tracepoint is enabled?
test_update_superblocks_and_overhead() marks these mock buffer_heads dirty
via ext4_update_primary_sb() and ext4_update_backup_sb() ->
mark_buffer_dirty(), which calls trace_block_dirty_buffer(bh) and
dereferences bh->b_bdev->bd_dev.
[ ... ]
> +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);
[ ... ]
> +static int ioctl_test_init(struct kunit *test)
> +{
[ ... ]
> + ctx = kzalloc_obj(*ctx);
> + KUNIT_ASSERT_NOT_NULL(test, ctx);
> + test->priv = ctx;
> + active_ioctl_test = test;
[ ... ]
> + 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);
[Severity: Low]
If any of the percpu_counter_init() calls fail, does ioctl_test_cleanup() run
cancel_work_sync() and timer_delete_sync() on uninitialized s_sb_upd_work and
s_err_report?
Because INIT_WORK() and timer_setup() happen after these error branches,
cancel_work_sync() hits WARN_ON(!work->func) in __flush_work() and
timer_delete_sync() passes an uninitialized lockdep_map to
lock_map_acquire(). Also, the earlier allocation failure paths before
sget_fc() abort without resetting active_ioctl_test to NULL.
> + 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);
[Severity: Medium]
Does ioctl_test_init() leave the sbi->s_es_stats per-CPU counters
uninitialized?
test_swap_inode_data_and_seed() and test_force_shutdown_and_ioctl_dispatch()
insert and remove extents via ext4_es_insert_extent() and
ext4_es_remove_extent(), which call percpu_counter_inc() and
percpu_counter_dec() on sbi->s_es_stats.es_stats_shk_cnt and
sbi->s_es_stats.es_stats_all_cnt. Because neither percpu_counter_init() nor
ext4_es_register_shrinker() is called for sbi, running these tests on
CONFIG_SMP dereferences a NULL per-CPU pointer and uses an uninitialized
spinlock in percpu_counter_add_batch().
[ ... ]
> +static void test_swap_inode_data_and_seed(struct kunit *test)
> +{
[ ... ]
> + inode1 = new_inode(sb);
> + inode2 = new_inode(sb);
> + KUNIT_ASSERT_NOT_NULL(test, inode1);
> + KUNIT_ASSERT_NOT_NULL(test, inode2);
[Severity: Low]
If the second new_inode(sb) call returns NULL, does KUNIT_ASSERT_NOT_NULL()
abort the test while leaking inode1?
When ioctl_test_cleanup() subsequently calls deactivate_super(ctx->sb), the
unreleased inode remains on sb->s_inodes and triggers the busy-inodes
CHECK_DATA_CORRUPTION() in generic_shutdown_super(). The same pattern
appears in test_immutable_dax_and_fileattr() when allocating reg_inode,
dir_inode, reg_dentry, and dir_dentry before d_instantiate(), and in
test_force_shutdown_and_ioctl_dispatch() if d_alloc_anon() fails after
new_inode().
> diff --git a/fs/ext4/ioctl.c b/fs/ext4/ioctl.c
> index c8387e6a2c6e9..61e59c30e7611 100644
> --- a/fs/ext4/ioctl.c
> +++ b/fs/ext4/ioctl.c
[ ... ]
> @@ -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;
> }
> }
[Severity: High]
This problem wasn't introduced by this patch, but when
EXT4_FEATURE_INCOMPAT_CASEFOLD is enabled online via
EXT4_IOC_SET_TUNE_SB_PARAM [1], does anything initialize
sb->s_encoding in memory?
ext4_sb_setparams() sets EXT4_FEATURE_INCOMPAT_CASEFOLD in the superblock
without loading the unicode map into sb->s_encoding. Subsequent
EXT4_IOC_SETFLAGS calls can then set EXT4_CASEFOLD_FL on a directory, and a
lookup in that directory calls ext4_fname_setup_ci_filename() ->
utf8_casefold(dir->i_sb->s_encoding, ...), dereferencing a NULL
sb->s_encoding pointer.
[ ... ]
> +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);
[Severity: High]
This problem wasn't introduced by this patch, but can swap_inode_data()
cause torn reads of i_version on 32-bit architectures [2]?
fs/ext4/ioctl.c:swap_inode_data() {
...
swap(inode1->i_version, inode2->i_version);
...
}
Because i_version is an atomic64_t, exchanging it with swap() performs plain
struct assignment instead of using atomic64 accessors, allowing concurrent
lockless readers such as inode_peek_iversion_raw() to observe torn values on
32-bit SMP systems.
[1] https://sashiko.dev/#/bug/linux-4eae7256-2707-4887-b442-c774f5662e2b
[2] https://sashiko.dev/#/bug/linux-14d568d4-dbf5-4a52-9af4-757563dc37e3
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=19
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 10/27] ext4: expand KUnit tests for fs/ext4/inode.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:36 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] ext4: use-after-free of mock superblock and inodes in
inode_test_cleanup()
- [Medium] ext4: highmem NULL deref on bh->b_data in
inode_test_read_bh_lock_stub()
It also noticed 2 related pre-existing issues this patch does not
introduce:
- [Medium] ext4: user-triggerable WARN_ON_ONCE in ext4_iomap_begin() [1]
- [Critical] ext4: stale data leak to concurrent readers in
ext4_setattr() [2]
--
commit f093b56ce7ca8d901aefc501ae9f381a7877b64a
Author: Theodore Ts'o <tytso@mit.edu>
ext4: expand KUnit tests for fs/ext4/inode.c
This commit expands the ext4_inode_test KUnit test suite to exercise
fs/ext4/inode.c using an in-memory VFS superblock and mock block pool, and
adds KUnit static stub redirects and symbol exports for the tested entry
points.
> diff --git a/fs/ext4/inode-test.c b/fs/ext4/inode-test.c
> index 749af7ad4e09c..e8d19c1d42707 100644
> --- a/fs/ext4/inode-test.c
> +++ b/fs/ext4/inode-test.c
> @@ -267,8 +284,3432 @@ static void inode_test_xtimestamp_decoding(struct kunit *test)
[ ... ]
> +static void inode_test_read_bh_nowait_stub(struct buffer_head *bh,
> + blk_opf_t op_flags,
> + bio_end_io_t end_io,
> + bool simu_fail)
> +{
> + struct kunit *test = kunit_get_current_test();
> + struct inode_test_ctx *ctx = test ? test->priv : NULL;
> + struct inode_test_blk *slot;
> +
> + if (!ctx) {
> + unlock_buffer(bh);
> + return;
> + }
> + ctx->read_nowait_calls++;
> + if (ctx->fail_read_eio || simu_fail) {
> + clear_buffer_uptodate(bh);
> + unlock_buffer(bh);
> + return;
> + }
> + slot = inode_test_find_blk_by_bh(ctx, bh);
> + if (!slot)
> + slot = inode_test_find_blk(ctx, bh->b_blocknr);
> + if (slot) {
> + memcpy(bh->b_data, slot->disk_buf, bh->b_size);
> + set_buffer_uptodate(bh);
> + } else {
> + clear_buffer_uptodate(bh);
> + }
> + unlock_buffer(bh);
> +}
> +
> +static int inode_test_read_bh_lock_stub(struct buffer_head *bh,
> + blk_opf_t op_flags, bool wait)
> +{
> + struct kunit *test = kunit_get_current_test();
> + struct inode_test_ctx *ctx = test ? test->priv : NULL;
> + struct inode_test_blk *slot;
> +
> + if (!ctx)
> + return -EIO;
> + ctx->read_lock_calls++;
> + lock_buffer(bh);
> + if (ctx->fail_read_eio) {
> + clear_buffer_uptodate(bh);
> + unlock_buffer(bh);
> + return wait ? -EIO : 0;
> + }
> + slot = inode_test_find_blk_by_bh(ctx, bh);
> + if (!slot)
> + slot = inode_test_find_blk(ctx, bh->b_blocknr);
> + if (!slot) {
> + clear_buffer_uptodate(bh);
> + unlock_buffer(bh);
> + return wait ? -EIO : 0;
> + }
> + memcpy(bh->b_data, slot->disk_buf, bh->b_size);
> + set_buffer_uptodate(bh);
> + unlock_buffer(bh);
> + return 0;
> +}
[Severity: Medium]
Can copying directly to bh->b_data in inode_test_read_bh_lock_stub() and
inode_test_read_bh_nowait_stub() cause a NULL or low-address pointer
dereference on 32-bit CONFIG_HIGHMEM=y kernels?
inode_test_setup() configures inode->i_data with GFP_HIGHUSER_MOVABLE.
When inode_test_zero_partial_and_eof() or inode_test_setattr() calls
ext4_zero_partial_blocks() -> ext4_block_zero_range() -> ext4_load_tail_bh(),
the pagecache folio allocated for inode->i_mapping can come from highmem.
create_empty_buffers() -> folio_set_bh() sets bh->b_data on a highmem folio
to (char *)(0 + offset) rather than a kernel virtual address, so
memcpy(bh->b_data, ...) faults when ext4_load_tail_bh() calls
ext4_read_bh_lock(). Using memcpy_to_folio(bh->b_folio, bh_offset(bh), ...)
when bh->b_folio is present (or dropping __GFP_HIGHMEM from inode->i_data)
would avoid this.
[ ... ]
> +static void inode_test_cleanup(void *data)
> +{
> + struct inode_test_ctx *ctx = data;
> + struct inode *inode = &ctx->ei.vfs_inode;
> + struct mapping_metadata_bhs *mmb;
> + unsigned int i;
> +
> + ctx->sbi.s_journal = NULL;
> +
> + for (i = 0; i < ctx->nr_tracked_inodes; i++) {
> + struct inode *ti = ctx->tracked_inodes[i];
> +
> + if (ti) {
> + spin_lock(&ti->i_lock);
> + inode_state_clear(ti, I_DIRTY_TIME);
> + spin_unlock(&ti->i_lock);
> + iput(ti);
> + ctx->tracked_inodes[i] = NULL;
> + }
> + }
> + ctx->nr_tracked_inodes = 0;
> +
> + truncate_inode_pages_final(&inode->i_data);
> + inode_io_list_del(inode);
> + ext4_fc_del(inode);
> + mmb = ext4_i_metadata_bhs(inode);
> + if (mmb)
> + mmb_invalidate(mmb);
> + inode_test_cleanup_ext4_trees(inode);
> + if (ctx->ei.jinode) {
> + jbd2_free_inode(ctx->ei.jinode);
> + ctx->ei.jinode = NULL;
> + }
> +
> + deactivate_super(ctx->sb);
> + rcu_barrier();
> +
> + ext4_es_unregister_shrinker(&ctx->sbi);
> + percpu_counter_destroy(&ctx->sbi.s_dirtyclusters_counter);
> + percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter);
> +
> + kfree(mmb);
> + kfree(ctx);
> +}
> +
> +static struct inode_test_ctx *inode_test_setup(struct kunit *test)
> +{
> + struct inode_test_ctx *ctx;
> + struct fs_context *fc;
> + struct super_block *sb;
> + struct ext4_sb_info *sbi;
> + struct inode *inode;
> + int err, i;
> +
> + ctx = kzalloc_obj(*ctx);
> + KUNIT_ASSERT_NOT_NULL(test, ctx);
> + test->priv = ctx;
> +
> + fc = fs_context_for_mount(&inode_test_fs_type, 0);
> + if (IS_ERR(fc)) {
> + err = PTR_ERR(fc);
> + kfree(ctx);
> + KUNIT_ASSERT_EQ(test, err, 0);
> + }
> + sb = sget_fc(fc, NULL, set_anon_super_fc);
> + put_fs_context(fc);
> + if (IS_ERR(sb)) {
> + err = PTR_ERR(sb);
> + kfree(ctx);
> + KUNIT_ASSERT_EQ(test, err, 0);
> + }
> + ctx->sb = sb;
> + sbi = &ctx->sbi;
> +
> + sbi->s_sb = sb;
> + sb->s_fs_info = sbi;
> + sb->s_op = &inode_test_sops;
> +#ifdef CONFIG_QUOTA
> + sb->dq_op = &inode_test_qops;
> +#endif
> + sb->s_flags |= SB_ACTIVE;
[Severity: Medium]
Can setting SB_ACTIVE while leaving sb->s_root NULL cause a use-after-free
of ctx->sb, ctx->sbi, and inodes after inode_test_cleanup() runs?
sget_fc() -> alloc_super() calls fserror_mount(sb), which sets
sb->s_pending_errors to 1. When tests exercise corruption paths that call
__ext4_error() or __ext4_error_inode(), fserror_report() sees SB_ACTIVE and
s_pending_errors > 0, takes a reference on the inode via igrab(), and queues
fserror_worker() on system_dfl_wq. Because ctx->sb->s_root is NULL,
deactivate_super() -> generic_shutdown_super() skips fserror_unmount(sb):
CPU0 (KUnit test) CPU1 (kworker)
inode_test_iget_corruptions()
ext4_iget()
__ext4_error_inode()
fserror_report()
igrab(inode)
schedule_work(&event->work)
inode_test_cleanup()
deactivate_super(ctx->sb)
generic_shutdown_super()
/* s_root is NULL: skips
fserror_unmount() */
kfree(ctx)
fserror_worker()
/* accesses freed event->sb */
iput(event->inode)
ext4_evict_inode()
/* accesses freed ctx->sbi */
<-- use-after-free
In addition, when __ext4_error_inode() is called on the embedded
&ctx->ei.vfs_inode (for example in inode_test_expand_extra_isize()),
fserror_worker() races with inode_test_reset_mock_inode() and calls iput()
on the embedded inode after ctx is freed. Calling fserror_unmount(sb) in
inode_test_setup() (or before freeing ctx in inode_test_cleanup()) would
prevent fserror_report() from queuing async work.
> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 67d37b39db938..98444d8ddb400 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
[ ... ]
> @@ -3858,12 +3888,14 @@ int ext4_iomap_begin(struct inode *inode, loff_t offset, loff_t length,
>
> return 0;
> }
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_iomap_begin);
[Severity: Medium]
This problem wasn't introduced by this patch, but can an atomic write to an
encrypted file trigger WARN_ON_ONCE(1) in ext4_iomap_begin() [1]?
Before checking the IOMAP_ATOMIC length constraint, ext4_iomap_begin()
calls fscrypt_limit_io_blocks() to truncate map.m_len when an I/O crosses a
32-bit DUN boundary:
fs/ext4/inode.c:ext4_iomap_begin() {
...
map.m_len = fscrypt_limit_io_blocks(inode, map.m_lblk, map.m_len);
/*
* Before returning to iomap, let's ensure the allocated mapping
* covers the entire requested length for atomic writes.
*/
if (flags & IOMAP_ATOMIC) {
if (map.m_len < (length >> blkbits)) {
WARN_ON_ONCE(1);
return -EINVAL;
}
}
...
}
When an unprivileged user issues an atomic write across a DUN boundary on
an encrypted file, fscrypt_limit_io_blocks() shortens map.m_len below
length >> blkbits and triggers WARN_ON_ONCE(1).
[ ... ]
> @@ -6274,6 +6322,7 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
> error = rc;
> return error;
> }
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_setattr);
[Severity: Critical]
This problem wasn't introduced by this patch, but when extending a file via
ext4_setattr(), can updating i_size before calling
pagecache_isize_extended() expose stale post-EOF pagecache data to
concurrent readers [2]?
When extending a file whose old size is block-aligned (or where blocksize <
PAGE_SIZE) and whose tail folio already has uptodate post-EOF data in the
pagecache (such as from an earlier mmap write past EOF), ext4_setattr()
writes the new i_size under i_data_sem before stopping the journal handle
and zeroing the extended pagecache range:
CPU0 CPU1
ext4_setattr()
i_size_write(inode, attr->ia_size)
generic_file_read_iter()
filemap_read()
/* sees new i_size and copies
unzeroed tail folio <-- stale data */
ext4_journal_stop(handle)
pagecache_isize_extended()
Because buffered reads do not take i_rwsem, CPU1 can read the unzeroed
post-EOF portion of the folio before pagecache_isize_extended() zeroes it.
[1] https://sashiko.dev/#/bug/linux-38b43030-f0ce-4abf-8653-7266f768a948
[2] https://sashiko.dev/#/bug/linux-808e5d6d-ae12-4689-877c-3d6dc1f711d5
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=10
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 25/27] ext4: add KUnit tests for fs/ext4/move_extent.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:36 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] ext4: 64KB page failure in test_mext_check_adjust_range()
- [Medium] ext4: sub-folio dirty buffer -EBUSY in move_extent-test.c on
>4KB pages
--
commit abdff1b8eb818ad1bc77ee7f502a090b450df395
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/move_extent.c
This commit adds a KUnit test suite covering lock ordering, validity and
range checks, and extent swapping in fs/ext4/move_extent.c.
> diff --git a/fs/ext4/move_extent-test.c b/fs/ext4/move_extent-test.c
> new file mode 100644
> index 0000000000000..e29a392eeebe3
> --- /dev/null
> +++ b/fs/ext4/move_extent-test.c
[ ... ]
> +static void test_mext_check_adjust_range(struct kunit *test)
> +{
> + struct mext_test_ctx *ctx = mext_setup_ctx(test);
> + struct inode *orig = ctx->orig_inode;
> + struct inode *donor = ctx->donor_inode;
> + __u64 len;
> + int ret;
> +
> + /* Sub-page block alignment check when blocksize (1024) < PAGE_SIZE */
> + orig->i_blkbits = 10;
> + donor->i_blkbits = 10;
> + i_size_write(orig, 32 * 1024);
> + i_size_write(donor, 32 * 1024);
> +
> + len = 4;
> + ret = ext4_mext_check_adjust_range_test(orig, donor, 1, 2, &len);
> + KUNIT_EXPECT_EQ(test, ret, -EINVAL);
> +
> + len = 4;
> + ret = ext4_mext_check_adjust_range_test(orig, donor, 1,
> + 1 + (PAGE_SIZE >> 10), &len);
> + KUNIT_EXPECT_EQ(test, ret, 0);
> + KUNIT_EXPECT_EQ(test, len, 4ULL);
[Severity: Medium]
Does this sub-page alignment check fail on kernels with 64KB pages?
With i_blkbits set to 10 and i_size set to 32KB, donor_eof is 32 blocks,
while donor_start = 1 + (PAGE_SIZE >> 10) evaluates to 65 when PAGE_SIZE
is 65536. mext_check_adjust_range() sees donor_eof <= donor_start, clamps
*len to 0, and returns -EINVAL instead of 0. Sizing orig and donor
relative to PAGE_SIZE (for example, max_t(loff_t, 32 * 1024, 4 * PAGE_SIZE))
would avoid this.
[ ... ]
> +static void test_move_extents_written_copy_data_and_split(struct kunit *test)
> +{
> + struct mext_test_ctx *ctx = mext_setup_ctx(test);
> + struct inode *orig = ctx->orig_inode;
> + struct inode *donor = ctx->donor_inode;
> + ext4_lblk_t lblk;
> + __u64 moved_len = 0;
> + int ret;
> +
> + /*
> + * orig: [0, 6) written -> pblk 400..405, uptodate folios pre-filled
> + * donor: [1, 2) written -> pblk 501, [2, 3) hole,
> + * [3, 5) unwritten -> pblk 503..504
> + *
> + * Moving [1, 5) exercises MEXT_COPY_DATA, left/right extent splits on
> + * orig, donor hole skipping at lblk 2, written preservation on orig,
> + * mark_unwritten=1 on donor, and page cache data preservation.
> + */
> + i_size_write(orig, 6 * MEXT_TEST_BLK_SIZE);
> + EXT4_I(orig)->i_disksize = 6 * MEXT_TEST_BLK_SIZE;
> + i_size_write(donor, 6 * MEXT_TEST_BLK_SIZE);
> + EXT4_I(donor)->i_disksize = 6 * MEXT_TEST_BLK_SIZE;
> +
> + mext_insert_extent(test, orig, 0, 6, 400, false);
> + mext_insert_extent(test, donor, 1, 1, 501, false);
> + mext_insert_extent(test, donor, 3, 2, 503, true);
> +
> + for (lblk = 0; lblk < 6; lblk++)
> + mext_populate_folio_block(test, orig, lblk, 0xA0 + lblk);
> +
> + ret = ext4_move_extents(&ctx->orig_file, &ctx->donor_file, 1, 1, 4,
> + &moved_len);
> + KUNIT_EXPECT_EQ(test, ret, 0);
> + KUNIT_EXPECT_EQ(test, moved_len, 3ULL);
[Severity: Medium]
Does ext4_move_extents() fail with -EBUSY here when PAGE_SIZE > 4096?
With a 4KB block size on 16KB or 64KB page kernels, logical blocks 1 and 3
share folio 0 in orig->i_mapping. Moving lblk 1 via MEXT_COPY_DATA calls
mext_folio_mkwrite() -> block_commit_write(), which attaches buffer_heads
to orig's folio 0 and marks lblk 1's buffer dirty. After skipping the
donor hole at lblk 2, mext_move_extent() tries to move lblk 3..4 in the
same folio and calls filemap_release_folio(folio[0], 0) ->
ext4_release_folio() -> try_to_free_buffers(), which refuses to drop the
dirty buffer from lblk 1 and aborts the move with -EBUSY and moved_len = 1.
Similarly, in test_move_extents_unwritten_orig_and_ebusy(), moving
lblk 0..1 leaves dirty buffers on orig's folio 0 when PAGE_SIZE >= 16384,
so the subsequent move of lblk 2 fails filemap_release_folio() on orig
before reaching the PG_checked donor_folio under test.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=25
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 21/27] ext4: add KUnit tests for fs/ext4/migrate.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:37 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 4 potential
issues to consider:
- [High] ext4: UAF and invalid RCU context in migrate_test_free_inode()
and cleanup
- [Low] ext4: uninitialized work/timer teardown and counter leak in
migrate_test_init()
- [Low] ext4: skipped extent block checksum check in migrate-test due to
BH_Verified
- [Low] ext4: leaked ext4_ext_path and busy inodes on mid-test
KUNIT_ASSERT failure
It also noticed 1 related pre-existing issue this patch does not
introduce:
- [Critical] ext4: error clobbering and data loss in ext4_ext_migrate()
[1]
--
commit 5ed76678c1803e6d79647899ecaa6195b5ca46ef
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/migrate.c
This commit adds the ext4_migrate_test KUnit suite in fs/ext4/migrate-test.c
to test online inode format migration between indirect block maps and extent
trees, and exports the tested helpers via EXPORT_SYMBOL_FOR_EXT4_TEST.
> diff --git a/fs/ext4/migrate-test.c b/fs/ext4/migrate-test.c
> new file mode 100644
> index 0000000000000..e83d208a5117e
> --- /dev/null
> +++ b/fs/ext4/migrate-test.c
[ ... ]
> +static void migrate_test_free_inode(struct inode *inode)
> +{
> + ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
> + kfree(EXT4_I(inode));
> +}
> +
> +static void migrate_test_evict_inode(struct inode *inode)
> +{
> + truncate_inode_pages_final(&inode->i_data);
> + clear_inode(inode);
> +}
[ ... ]
> +static void migrate_test_cleanup(struct migrate_test_ctx *ctx)
> +{
> + struct super_block *sb = ctx->sb;
> + struct ext4_sb_info *sbi = &ctx->sbi;
> + struct migrate_mock_blk *blk, *tmp;
> +
> + if (sb) {
> + cancel_work_sync(&sbi->s_sb_upd_work);
> + timer_delete_sync(&sbi->s_err_report);
> + }
> +
> + if (ctx->es_shrinker_inited) {
> + ext4_es_unregister_shrinker(sbi);
> + ctx->es_shrinker_inited = false;
> + }
> +
> + if (ctx->counters_inited) {
> + percpu_counter_destroy(&sbi->s_freeclusters_counter);
> + percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
> + percpu_counter_destroy(&sbi->s_freeinodes_counter);
> + ctx->counters_inited = false;
> + }
> +
> + if (ctx->rwsem_inited) {
> + percpu_free_rwsem(&sbi->s_writepages_rwsem);
> + ctx->rwsem_inited = false;
> + }
> +
> + if (sb) {
> + deactivate_super(sb);
> + ctx->sb = NULL;
> + }
[Severity: High]
Does calling ext4_es_remove_extent() from migrate_test_free_inode() run in
RCU softirq context and race with migrate_test_cleanup()?
During ext4_ext_migrate(), finish_range() -> ext4_find_extent() caches
extents in EXT4_I(tmp_inode)->i_es_tree and links the inode onto
ctx->sbi.s_es_list. Because destroy_inode() invokes .free_inode via
call_rcu(), migrate_test_free_inode() runs in RCU softirq context, where
ext4_es_remove_extent() takes non-softirq-safe locks (i_es_lock and
s_es_lock) and can allocate with GFP_KERNEL.
In addition, migrate_test_cleanup() destroys the extent status shrinker's
percpu counters before deactivate_super(sb) and returns without
rcu_barrier() before KUnit frees ctx:
CPU0 CPU1 (<softirq>)
migrate_test_cleanup()
ext4_es_unregister_shrinker(sbi)
percpu_counter_destroy(...)
deactivate_super(sb)
/* KUnit frees ctx */
i_callback()
migrate_test_free_inode()
ext4_es_remove_extent()
/* dereferences freed ctx->sbi
* and NULL counter->counters
*/ <-- use-after-free
Calling ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS) in
migrate_test_evict_inode() before clear_inode(), moving deactivate_super(sb)
ahead of the sbi teardown in migrate_test_cleanup(), and calling
rcu_barrier() before returning would avoid this.
[ ... ]
> +static int migrate_test_init(struct kunit *test)
> +{
[ ... ]
> + ctx->sb = sb;
> + up_write(&sb->s_umount);
> +
> + ret = kunit_add_action_or_reset(test, migrate_test_cleanup_action, ctx);
> + KUNIT_ASSERT_EQ(test, ret, 0);
[ ... ]
> + INIT_WORK(&sbi->s_sb_upd_work, migrate_test_sb_upd_work);
> + timer_setup(&sbi->s_err_report, migrate_test_err_timer, 0);
[ ... ]
> + ret = percpu_counter_init(&sbi->s_freeclusters_counter, 4096,
> + GFP_KERNEL);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + ret = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0, GFP_KERNEL);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + ret = percpu_counter_init(&sbi->s_freeinodes_counter, 200, GFP_KERNEL);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + ctx->counters_inited = true;
[Severity: Low]
If kunit_add_action_or_reset() fails, can migrate_test_cleanup() call
cancel_work_sync() and timer_delete_sync() on s_sb_upd_work and s_err_report
before they are initialized?
Also, if the second or third percpu_counter_init() fails, KUNIT_ASSERT_EQ()
aborts before ctx->counters_inited is set, so migrate_test_cleanup() skips
percpu_counter_destroy() and leaks the already-initialized percpu counters.
[ ... ]
> +static void test_ext_migrate_dind_tind_and_multilevel_tree(struct kunit *test)
> +{
[ ... ]
> + for (i = 0; i < ARRAY_SIZE(expected); i++) {
> + path = ext4_find_extent(inode, expected[i].lblk, NULL, 0);
> + KUNIT_ASSERT_FALSE_MSG(test, IS_ERR(path),
> + "find_extent failed at idx %u", i);
> + KUNIT_ASSERT_NOT_NULL(test, path[path->p_depth].p_ext);
> + KUNIT_EXPECT_EQ_MSG(test,
> + le32_to_cpu(path[path->p_depth].p_ext->ee_block),
> + expected[i].lblk, "lblk idx %u", i);
> + KUNIT_EXPECT_EQ_MSG(test,
> + ext4_ext_pblock(path[path->p_depth].p_ext),
> + expected[i].pblk, "pblk idx %u", i);
> + ext4_free_ext_path(path);
> + }
[Severity: Low]
Does ext4_find_extent(inode, ..., 0) here skip verifying the extent block
checksum on the migrated inode?
While building the tree on tmp_inode, ext4_ext_grow_indepth() and
migrate_ext_dirty_stub() call set_buffer_verified() on the mock buffer_head.
When ext4_find_extent() is later called on inode with flags == 0,
__read_extent_tree_block() sees buffer_verified(bh) and returns without
running __ext4_ext_check(), so i_csum_seed propagation is never checked.
Passing EXT4_EX_FORCE_CACHE (or clearing buffer_verified() on the block)
would force checksum verification.
[Severity: Low]
In the same loop, if KUNIT_ASSERT_NOT_NULL(test, path[path->p_depth].p_ext)
fails, does the early abort leak path and leave inode referenced during
deactivate_super(sb)? Other KUNIT_ASSERT_* calls in these tests while
holding a test inode similarly skip iput(inode) on failure.
> diff --git a/fs/ext4/migrate.c b/fs/ext4/migrate.c
> index 5d60ef10fe110..8d76042f0caea 100644
> --- a/fs/ext4/migrate.c
> +++ b/fs/ext4/migrate.c
> @@ -597,6 +597,7 @@ int ext4_ext_migrate(struct inode *inode)
> iput(tmp_inode);
> return retval;
> }
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_migrate);
[Severity: Critical]
This problem wasn't introduced by this patch [1], but in
ext4_ext_migrate(), does assigning the result of
ext4_journal_ensure_credits() to retval after err_out clobber the migration
error and risk freeing the original file's data blocks?
fs/ext4/migrate.c:ext4_ext_migrate() {
...
err_out:
if (retval)
free_ext_block(handle, tmp_inode);
else {
...
}
/* We mark the tmp_inode dirty via ext4_ext_tree_init. */
retval = ext4_journal_ensure_credits(handle, 1, 0);
if (retval < 0)
goto out_stop;
...
tmp_inode->i_blocks = 0;
EXT4_I(tmp_inode)->i_csum_seed = tmp_csum_seed;
/* Reset the extent details */
ext4_ext_tree_init(handle, tmp_inode);
out_stop:
ext4_journal_stop(handle);
out_tmp_inode:
unlock_new_inode(tmp_inode);
out_unlock:
ext4_writepages_up_write(inode->i_sb, alloc_ctx);
iput(tmp_inode);
return retval;
}
When extent building fails (such as in
test_ext_migrate_error_rollback_frees_tmp_extent_blocks()),
ext4_journal_ensure_credits() overwrites the negative retval with 0 so
ext4_ext_migrate() returns success. Worse, if
ext4_journal_ensure_credits() fails, jumping to out_stop leaves tmp_inode
with i_nlink == 0, non-zero i_blocks, and in-inode root extents still
pointing at the original file's physical data blocks, which
iput(tmp_inode) -> ext4_evict_inode() will then free.
[1] https://sashiko.dev/#/bug/linux-29ab25ff-78b3-4378-b750-b1aca01c3a2d
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=21
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 27/27] ext4: add KUnit tests for fs/ext4/dir.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:41 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Low] ext4: uninitialized i_size_seqcount in dir_test_init() on 32-bit
SMP
It also noticed 1 related pre-existing issue this patch does not
introduce:
- [Critical] ext4: out-of-bounds read in ext4_readdir() on fake dirents
[1]
--
commit 789413bd80e96edcb89e48a8973879c9137951c1
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/dir.c
This commit adds a KUnit test suite for fs/ext4/dir.c covering directory
entry validation, hash-cookie helpers, directory llseek, and linear and
HTree directory iteration, and exports helper symbols for testing.
> diff --git a/fs/ext4/dir-test.c b/fs/ext4/dir-test.c
> new file mode 100644
> index 0000000000000..4d7c7c89e8b90
> --- /dev/null
> +++ b/fs/ext4/dir-test.c
[ ... ]
> +static int dir_test_init(struct kunit *test)
> +{
> + struct dir_test_ctx *ctx;
> + struct inode *dir;
> + int i;
> +
> + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
> + test->priv = ctx;
[ ... ]
> + dir = &ctx->dir_ei.vfs_inode;
> + dir->i_sb = &ctx->sb;
> + dir->i_ino = DIR_TEST_DIR_INO;
> + dir->i_mode = S_IFDIR | 0755;
> + dir->i_blkbits = DIR_TEST_BLK_BITS;
> + dir->i_mapping = &dir->i_data;
> + dir->i_data.host = dir;
> + set_nlink(dir, 2);\n> + atomic_set(&dir->i_count, 1);
> + spin_lock_init(&dir->i_lock);
> + init_rwsem(&dir->i_rwsem);
> + INIT_LIST_HEAD(&dir->i_io_list);
> + INIT_LIST_HEAD(&dir->i_wb_list);
> + INIT_LIST_HEAD(&dir->i_lru);
> + INIT_HLIST_NODE(&dir->i_hash);
> + inode_state_set_raw(dir, I_DIRTY);
> + inode_set_iversion_raw(dir, 10);
[Severity: Low]
Does dir_test_init() leave dir->i_size_seqcount uninitialized on 32-bit SMP
builds?
Because ctx is allocated on the heap via kunit_kzalloc() without calling
i_size_ordered_init(dir) (or inode_init_once(dir)), i_size_write(dir, ...)
and i_size_read(dir) in the test cases pass an uninitialized lockdep_map to
write_seqcount_begin() and read_seqcount_begin() when CONFIG_DEBUG_LOCK_ALLOC
is enabled, triggering a non-static key warning and disabling lockdep.
> +
> + init_rwsem(&ctx->dir_ei.xattr_sem);
[ ... ]
> +static void test_check_dir_entry_and_all_de(struct kunit *test)
> +{
[ ... ]
> + /*
> + * 4. Casefolded + encrypted directory requires space for
> + * ext4_dir_entry_hash (+8 bytes) on regular entries, while fake
> + * entries ('.', '..', csum tail) remain 12 bytes.
> + */
> + dir->i_flags |= S_CASEFOLD | S_ENCRYPTED;
> + write_de(buf, 24, 15, 16, EXT4_FT_REG_FILE, "ab");
> + de = (struct ext4_dir_entry_2 *)(buf + 24);
> + KUNIT_EXPECT_EQ(test, __ext4_check_dir_entry(__func__, __LINE__, dir,
> + NULL, de, bh, buf,
> + DIR_TEST_BLK_SIZE, 24), 1);
[Severity: Critical]
This problem wasn't introduced by this patch, but can ext4_readdir() perform
an 8-byte out-of-bounds read past the end of the directory block buffer on
fake entries [1]?
In encrypted and casefolded directories, __ext4_check_dir_entry() passes
NULL instead of dir to ext4_dir_rec_len() when is_fake_dir_entry(de) is
true, so it does not require space for the 8-byte ext4_dir_entry_hash:
fs/ext4/dir.c:__ext4_check_dir_entry() {
...
bool fake = is_fake_dir_entry(de);
...
if (unlikely(rlen < ext4_dir_rec_len(1, fake ? NULL : dir)))
error_msg = "rec_len is smaller than minimal";
else if (unlikely(rlen % 4 != 0))
error_msg = "rec_len % 4 != 0";
else if (unlikely(rlen < ext4_dir_rec_len(de->name_len,
fake ? NULL : dir)))
error_msg = "rec_len is too small for name_len";
...
}
However, ext4_readdir() only checks that de->inode is non-zero before
reading EXT4_DIRENT_HASH(de) and EXT4_DIRENT_MINOR_HASH(de):
fs/ext4/dir.c:ext4_readdir() {
...
if (le32_to_cpu(de->inode)) {
if (!IS_ENCRYPTED(inode)) {
...
} else {
...
if (IS_CASEFOLDED(inode)) {
hash = EXT4_DIRENT_HASH(de);
minor_hash = EXT4_DIRENT_MINOR_HASH(de);
}
...
}
If a corrupted directory places a fake entry (such as "." or "..", or an
entry with file_type set to EXT4_FT_DIR_CSUM and a non-zero de->inode) at
the end of the block with rlen sized without the 8-byte hash, reading the
hash accesses 8 bytes past bh->b_data + sb->s_blocksize, which can fault on
an unmapped page or leak adjacent memory to userspace via nokey_name.dirhash.
[1] https://sashiko.dev/#/bug/linux-7bcf348c-5fde-4625-bb87-69ecef239378
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=27
^ permalink raw reply [flat|nested] 55+ messages in thread
* Re: [PATCH 22/27] ext4: add KUnit tests for fs/ext4/indirect.c
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
0 siblings, 0 replies; 55+ messages in thread
From: sashiko-bot @ 2026-11-12 4:46 UTC (permalink / raw)
To: Theodore Ts'o, Ext4 Developers List
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] ext4: indirect-test: UAF in ind_test_free_inode() after RCU
grace period
- [Low] ext4: uncleaned percpu_counter and uninited work/timer in
ind_test_init()
It also noticed 1 related pre-existing issue this patch does not
introduce:
- [Critical] ext4: incomplete error check in ext4_ind_map_blocks() [1]
--
commit ebeb5ffec6cf1b1539b939ab7329a1923d9ae24c
Author: Theodore Ts'o <tytso@mit.edu>
ext4: add KUnit tests for fs/ext4/indirect.c
This commit adds the ext4_indirect_test KUnit test suite for indirect block
mapping, truncation, and hole punching, and routes indirect block buffer
lookups through ext4_sb_getblk().
> diff --git a/fs/ext4/indirect-test.c b/fs/ext4/indirect-test.c
> new file mode 100644
> index 0000000000000..add5662adc3ba
> --- /dev/null
> +++ b/fs/ext4/indirect-test.c
[ ... ]
> +static void ind_test_free_inode(struct inode *inode)
> +{
> + ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
> + kfree(EXT4_I(inode));
> +}
> +
> +static void ind_test_evict_inode(struct inode *inode)
> +{
> + truncate_inode_pages_final(&inode->i_data);
> + clear_inode(inode);
> +}
[Severity: Medium]
Can ind_test_free_inode() trigger a use-after-free on ctx->sbi after the
test finishes?
At the end of each test, iput() runs ind_test_evict_inode() synchronously
and queues i_callback() -> ind_test_free_inode() via call_rcu(). KUnit
then runs ind_test_cleanup() without an rcu_barrier() and frees ctx, so
when the RCU callback later runs ext4_es_remove_extent(), it dereferences
EXT4_SB(inode->i_sb) inside the freed ctx.
Moving ext4_es_remove_extent() into ind_test_evict_inode() and calling
rcu_barrier() in ind_test_cleanup() before deactivating sb would avoid
this.
[ ... ]
> +static void ind_test_cleanup(struct ind_test_ctx *ctx)
> +{
> + struct super_block *sb = ctx->sb;
> + struct ext4_sb_info *sbi = &ctx->sbi;
> + struct ind_mock_blk *blk, *tmp;
> +
> + if (sb) {
> + cancel_work_sync(&sbi->s_sb_upd_work);
> + timer_delete_sync(&sbi->s_err_report);
> + }
> +
> + if (ctx->es_shrinker_inited) {
> + ext4_es_unregister_shrinker(sbi);
> + ctx->es_shrinker_inited = false;
> + }
> +
> + if (ctx->counters_inited) {
> + percpu_counter_destroy(&sbi->s_freeclusters_counter);
> + percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
> + percpu_counter_destroy(&sbi->s_freeinodes_counter);
> + ctx->counters_inited = false;
> + }
[ ... ]
> + ctx->sb = sb;
> + up_write(&sb->s_umount);
> +
> + ret = kunit_add_action_or_reset(test, ind_test_cleanup_action, ctx);
> + KUNIT_ASSERT_EQ(test, ret, 0);
[ ... ]
> + INIT_WORK(&sbi->s_sb_upd_work, ind_test_sb_upd_work);
> + timer_setup(&sbi->s_err_report, ind_test_err_timer, 0);
[ ... ]
> + ret = percpu_counter_init(&sbi->s_freeclusters_counter, 4096,
> + GFP_KERNEL);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + ret = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0, GFP_KERNEL);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + ret = percpu_counter_init(&sbi->s_freeinodes_counter, 200, GFP_KERNEL);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> + ctx->counters_inited = true;
[Severity: Low]
If the second or third percpu_counter_init() fails, does KUNIT_ASSERT_EQ()
abort while ctx->counters_inited is still false? ind_test_cleanup() then
skips percpu_counter_destroy(), leaking the earlier counter and leaving it
linked in the global percpu_counters list when ctx is freed.
Also, ctx->sb is set before kunit_add_action_or_reset(), whereas
s_sb_upd_work and s_err_report are initialized later, so if
kunit_add_action_or_reset() fails, ind_test_cleanup() calls
cancel_work_sync() and timer_delete_sync() on uninitialized structures.
[ ... ]
> diff --git a/fs/ext4/indirect.c b/fs/ext4/indirect.c
> index 5aec759eed705..5b2b2be73a90a 100644
> --- a/fs/ext4/indirect.c
> +++ b/fs/ext4/indirect.c
> @@ -163,7 +163,7 @@ static Indirect *ext4_get_branch(struct inode *inode, int depth,
> ret = -EFSCORRUPTED;
> goto failure;
> }
> - bh = sb_getblk(sb, key);
> + bh = ext4_sb_getblk(sb, key);
> if (unlikely(!bh)) {
> ret = -ENOMEM;
> goto failure;
[Severity: Critical]
This problem wasn't introduced by this patch, but when ext4_get_branch()
fails with -EFSCORRUPTED or -ENOMEM [1], doesn't
ext4_ind_map_blocks() miss the error when EXT4_GET_BLOCKS_CREATE is set?
fs/ext4/indirect.c:ext4_ind_map_blocks() {
...
/* Failed read of indirect block */
if (err == -EIO)
goto cleanup;
...
}
Because ext4_ind_map_blocks() only checks for err == -EIO, it treats the
partial chain returned on -ENOMEM or -EFSCORRUPTED as a hole, allocates new
blocks, and splices them over the existing indirect block pointer,
orphaning the original subtree and corrupting the file.
[1] https://sashiko.dev/#/bug/linux-2bfbc9ba-6121-45d3-a664-11e7452ad5d6
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=22
^ permalink raw reply [flat|nested] 55+ messages in thread
end of thread, other threads:[~2026-11-12 4:46 UTC | newest]
Thread overview: 55+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox