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

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


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

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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261112040638.1576836-6-tytso@mit.edu \
    --to=tytso@mit.edu \
    --cc=linux-ext4@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox