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

Expand the ext4_extents_test KUnit test suite in fs/ext4/extents-test.c
to cover the core extent tree operations in fs/ext4/extents.c:

- 48-bit physical block encoding/decoding (ext4_ext_pblock,
  ext4_ext_store_pblock, ext4_idx_pblock, ext4_idx_store_pblock) and
  unwritten extent length encoding (ext4_ext_mark_unwritten,
  ext4_ext_is_unwritten, ext4_ext_get_actual_len,
  ext4_ext_mark_initialized).
- Extent tree initialization (ext4_ext_tree_init) and header/entry
  validation (ext4_ext_check_inode, __ext4_ext_check) across 13
  parameterized corruption cases (magic, depth, max, entries, empty
  non-root index, zero-length extent, logical block wrap-around,
  out-of-bounds physical blocks, overlapping/unsorted extents and
  indexes, and parent/leaf logical block mismatch).
- Metadata checksum calculation and verification on external extent
  blocks when EXT4_FEATURE_RO_COMPAT_METADATA_CSUM is enabled.
- Root and multi-level (depth >= 1) extent tree lookup and navigation
  (ext4_find_extent, ext4_ext_search_left, ext4_ext_search_right,
  ext4_ext_next_allocated_block, ext4_ext_next_leaf_block) and extent
  status precaching (ext4_ext_precache) using in-memory simulated
  metadata buffer_heads via an ext4_ext_getblk() KUnit static stub.
- Extent merge rules (ext4_can_extents_be_merged) and insertion
  (ext4_ext_insert_extent), including forward/backward/three-way
  merging, nomerge flag handling, tree depth growth
  (ext4_ext_grow_indepth), node splitting (ext4_ext_split), and tree
  collapse back into the root inode (ext4_ext_try_to_merge_up).
- Extent space removal and truncation (ext4_ext_remove_space,
  ext4_ext_truncate) across tail truncates, partial trims, middle hole
  punches, multi-level index removal (ext4_ext_rm_idx), and bigalloc
  partial-cluster nofree and pending cluster rereservation
  (ext4_rereserve_cluster).
- Block mapping and allocation (ext4_ext_map_blocks), including hole
  lookup, delayed-extent hole adjustment, QUERY_LAST_IN_LEAF, block
  allocation, overlap clamping, ENOSPC handling, unwritten-to-written
  conversion with neighbor merging, and bigalloc implied cluster
  allocation (ext4_map_cluster_from_extent).
- Extent shifting (ext4_ext_shift_extents) for FALLOC_FL_COLLAPSE_RANGE
  and FALLOC_FL_INSERT_RANGE, extent swapping (ext4_swap_extents),
  cluster mapping queries (ext4_clu_mapped), JBD2 transaction credit
  calculations (ext4_ext_calc_credits_for_single_extent,
  ext4_ext_index_trans_blocks), and fast-commit replay helpers
  (ext4_ext_replay_update_ex, ext4_ext_replay_shrink_inode,
  ext4_ext_replay_set_iblocks).
- Fix ext4_ext_path memory leaks in the existing test_split_convert()
  helpers and isolate the mock extent status shrinker from background
  reclaim.

Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
 fs/ext4/ext4_extents.h |   21 +
 fs/ext4/ext4_jbd2.c    |    8 +
 fs/ext4/extents-test.c | 1886 ++++++++++++++++++++++++++++++++++++++--
 fs/ext4/extents.c      |   83 +-
 fs/ext4/inode.c        |    5 +
 fs/ext4/mballoc.c      |    7 +
 6 files changed, 1938 insertions(+), 72 deletions(-)

diff --git a/fs/ext4/ext4_extents.h b/fs/ext4/ext4_extents.h
index ebaf7cc42430..40e6b3a56f71 100644
--- a/fs/ext4/ext4_extents.h
+++ b/fs/ext4/ext4_extents.h
@@ -268,6 +268,8 @@ extern int __ext4_ext_dirty(const char *where, unsigned int line,
 			    handle_t *handle, struct inode *inode,
 			    struct ext4_ext_path *path);
 extern int ext4_ext_zeroout(struct inode *inode, struct ext4_extent *ex);
+extern struct buffer_head *ext4_ext_getblk(struct inode *inode,
+					   ext4_fsblk_t pblk, gfp_t gfp_flags);
 #if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
 extern int ext4_ext_space_root_idx_test(struct inode *inode, int check);
 extern struct ext4_ext_path *ext4_split_convert_extents_test(
@@ -275,6 +277,25 @@ extern struct ext4_ext_path *ext4_split_convert_extents_test(
 				struct ext4_map_blocks *map,
 				struct ext4_ext_path *path,
 				int flags, unsigned int *allocated);
+extern int ext4_ext_check_test(struct inode *inode,
+			       struct ext4_extent_header *eh, int depth,
+			       ext4_fsblk_t pblk, ext4_lblk_t lblk);
+extern void ext4_extent_block_csum_set_test(struct inode *inode,
+					    struct ext4_extent_header *eh);
+extern int ext4_can_extents_be_merged_test(struct inode *inode,
+					   struct ext4_extent *ex1,
+					   struct ext4_extent *ex2);
+extern int ext4_ext_search_left_test(struct inode *inode,
+				     struct ext4_ext_path *path,
+				     ext4_lblk_t *logical, ext4_fsblk_t *phys);
+extern int ext4_ext_search_right_test(struct inode *inode,
+				      struct ext4_ext_path *path,
+				      ext4_lblk_t *logical, ext4_fsblk_t *phys,
+				      struct ext4_extent *ret_ex, int flags);
+extern ext4_lblk_t ext4_ext_next_leaf_block_test(struct ext4_ext_path *path);
+extern int ext4_ext_shift_extents_test(struct inode *inode, handle_t *handle,
+				       ext4_lblk_t start, ext4_lblk_t shift,
+				       enum SHIFT_DIRECTION direction);
 #endif
 #endif /* _EXT4_EXTENTS */
 
diff --git a/fs/ext4/ext4_jbd2.c b/fs/ext4/ext4_jbd2.c
index 53ddedb52a6f..d6ffb6f08606 100644
--- a/fs/ext4/ext4_jbd2.c
+++ b/fs/ext4/ext4_jbd2.c
@@ -4,6 +4,7 @@
  */
 
 #include "ext4_jbd2.h"
+#include <kunit/static_stub.h>
 
 #include <trace/events/ext4.h>
 
@@ -379,6 +380,9 @@ int __ext4_handle_dirty_metadata(const char *where, unsigned int line,
 {
 	int err = 0;
 
+	KUNIT_STATIC_STUB_REDIRECT(__ext4_handle_dirty_metadata, where, line,
+				   handle, inode, bh);
+
 	might_sleep();
 
 	set_buffer_meta(bh);
@@ -431,3 +435,7 @@ int __ext4_handle_dirty_metadata(const char *where, unsigned int line,
 	}
 	return err;
 }
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_handle_dirty_metadata);
+#endif
diff --git a/fs/ext4/extents-test.c b/fs/ext4/extents-test.c
index c3836ecb89f9..705648186218 100644
--- a/fs/ext4/extents-test.c
+++ b/fs/ext4/extents-test.c
@@ -3,60 +3,75 @@
  * Written by Ojaswin Mujoo <ojaswin@linux.ibm.com> (IBM)
  *
  * These Kunit tests are designed to test the functionality of
- * extent split and conversion in ext4.
- *
- * Currently, ext4 can split extents in 2 ways:
- * 1. By splitting the extents in the extent tree and optionally converting them
- *    to written or unwritten based on flags passed.
- * 2. In case 1 encounters an error, ext4 instead zerooes out the unwritten
- *    areas of the extent and marks the complete extent written.
- *
- * The primary function that handles this is ext4_split_convert_extents().
- *
- * We test both of the methods of split. The behavior we try to enforce is:
- * 1. When passing EXT4_GET_BLOCKS_CONVERT flag to ext4_split_convert_extents(),
- *    the split extent should be converted to initialized.
- * 2. When passing EXT4_GET_BLOCKS_CONVERT_UNWRITTEN flag to
- *    ext4_split_convert_extents(), the split extent should be converted to
- *    uninitialized.
- * 3. In case we use the zeroout method, then we should correctly write zeroes
- *    to the unwritten areas of the extent and we should not corrupt/leak any
- *    data.
- *
- * Enforcing 1 and 2 is straight forward, we just setup a minimal inode with
- * extent tree, call ext4_split_convert_extents() and check the final state of
- * the extent tree.
- *
- * For zeroout testing, we maintain a separate buffer which represents the disk
- * data corresponding to the extents. We then override ext4's zeroout functions
- * to instead write zeroes to our buffer. Then, we override
- * ext4_ext_insert_extent() to return -ENOSPC, which triggers the zeroout.
- * Finally, we check the state of the extent tree and zeroout buffer to confirm
- * everything went well.
+ * fs/ext4/extents.c, including extent tree initialization and validation,
+ * binary search and multi-level navigation, extent insertion, merging,
+ * splitting, tree depth growth and collapse, space removal and bigalloc
+ * partial cluster handling, block mapping, unwritten-to-written conversion,
+ * zeroout fallback, extent shifting, extent swapping, cluster mapping,
+ * transaction credit calculation, and fast-commit replay helpers.
  */
 
 #include <kunit/test.h>
 #include <kunit/static_stub.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
 #include <linux/fs_context.h>
 #include <linux/gfp_types.h>
 #include <linux/stddef.h>
 
 #include "ext4.h"
 #include "ext4_extents.h"
+#include "ext4_jbd2.h"
 
 #define EXT_DATA_PBLK 100
 #define EXT_DATA_LBLK 10
 #define EXT_DATA_LEN 3
 
+#define EXT_MAX_META_BLOCKS 16
+#define EXT_META_BASE_PBLK  500
+#define EXT_ALLOC_BASE_PBLK 1000
+#define EXT_TOTAL_BLOCKS    0x100000ULL
+
+struct kunit_meta_block {
+	ext4_fsblk_t pblk;
+	struct buffer_head bh;
+	char data[4096];
+	bool in_use;
+};
+
 struct kunit_ctx {
 	/*
-	 * Ext4 inode which has only 1 unwrit extent
+	 * Ext4 inode initialized with 1 extent by default
 	 */
 	struct ext4_inode_info *k_ei;
+	/*
+	 * Optional secondary inode (used for swap_extents tests)
+	 */
+	struct ext4_inode_info *k_ei2;
 	/*
 	 * Represents the underlying data area (used for zeroout testing)
 	 */
 	char *k_data;
+	/*
+	 * In-memory pool of external extent tree blocks (leaf/index blocks)
+	 */
+	struct kunit_meta_block *meta_blocks;
+	/*
+	 * Mock block allocator and free tracking
+	 */
+	ext4_fsblk_t next_alloc_pblk;
+	unsigned int alloc_calls;
+	ext4_fsblk_t last_alloc_pblk;
+	unsigned int last_alloc_len;
+	int fail_alloc_err;
+
+	unsigned int free_calls;
+	ext4_fsblk_t last_free_pblk;
+	unsigned long last_free_count;
+	int last_free_flags;
+	unsigned long total_freed_blocks;
+
+	bool fail_getblk;
 } k_ctx;
 
 /*
@@ -126,6 +141,31 @@ struct kunit_ext_test_param {
 	struct kunit_ext_data_state exp_data_state[3];
 };
 
+static unsigned long ext_dummy_shrinker_count(struct shrinker *shrink,
+					      struct shrink_control *sc)
+{
+	return 0;
+}
+
+static unsigned long ext_dummy_shrinker_scan(struct shrinker *shrink,
+					     struct shrink_control *sc)
+{
+	return SHRINK_STOP;
+}
+
+static const struct super_operations ext_dummy_sops = {};
+
+#ifdef CONFIG_QUOTA
+static qsize_t *ext_get_reserved_space(struct inode *inode)
+{
+	return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations ext_dummy_qops = {
+	.get_reserved_space = ext_get_reserved_space,
+};
+#endif
+
 static int ext_init_fs_context(struct fs_context *fc)
 {
 	return 0;
@@ -142,33 +182,201 @@ static struct file_system_type ext_fs_type = {
 	.kill_sb	 = kill_anon_super,
 };
 
+static void ext_init_mock_inode(struct ext4_inode_info *ei,
+				struct super_block *sb, unsigned long ino)
+{
+	struct inode *inode = &ei->vfs_inode;
+
+	memset(ei, 0, sizeof(*ei));
+	inode->i_sb = sb;
+	inode->i_ino = ino;
+	inode->i_mode = S_IFREG | 0644;
+	inode->i_blkbits = sb->s_blocksize_bits;
+	inode->i_flags |= S_NOQUOTA;
+	inode->i_mapping = &inode->i_data;
+
+	spin_lock_init(&inode->i_lock);
+	init_rwsem(&inode->i_rwsem);
+	init_rwsem(&inode->i_data.invalidate_lock);
+	inode_state_set_raw(inode, I_DIRTY_SYNC | I_DIRTY_DATASYNC);
+
+	ext4_es_init_tree(&ei->i_es_tree);
+	ext4_init_pending_tree(&ei->i_pending_tree);
+	rwlock_init(&ei->i_es_lock);
+	INIT_LIST_HEAD(&ei->i_es_list);
+	init_rwsem(&ei->i_data_sem);
+	spin_lock_init(&ei->i_block_reservation_lock);
+	rwlock_init(&ei->i_prealloc_lock);
+	ei->i_prealloc_node = RB_ROOT;
+	ei->i_csum_seed = 0x12345678;
+
+	ei->i_disksize = (EXT_DATA_LBLK + EXT_DATA_LEN + 10)
+			 << sb->s_blocksize_bits;
+	inode->i_size = ei->i_disksize;
+	ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+}
+
+static void ext_cleanup_inode(struct inode *inode)
+{
+	struct ext4_inode_info *ei = EXT4_I(inode);
+	struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+	struct rb_node *node;
+
+	down_write(&ei->i_data_sem);
+	ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+	up_write(&ei->i_data_sem);
+
+	while ((node = rb_first(&ei->i_pending_tree.root)) != NULL) {
+		struct pending_reservation *pr;
+
+		pr = rb_entry(node, struct pending_reservation, rb_node);
+		ext4_remove_pending(inode, EXT4_C2B(sbi, pr->lclu));
+	}
+}
+
 static void extents_kunit_exit(struct kunit *test)
 {
 	struct ext4_sb_info *sbi;
+	struct super_block *sb;
 
 	if (!k_ctx.k_ei)
 		return;
 
-	sbi = k_ctx.k_ei->vfs_inode.i_sb->s_fs_info;
+	sb = k_ctx.k_ei->vfs_inode.i_sb;
+	sbi = sb->s_fs_info;
+
+	if (k_ctx.k_ei2) {
+		ext_cleanup_inode(&k_ctx.k_ei2->vfs_inode);
+		kfree(k_ctx.k_ei2);
+	}
+	ext_cleanup_inode(&k_ctx.k_ei->vfs_inode);
+
+	percpu_counter_destroy(&sbi->s_freeclusters_counter);
+	percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
 	ext4_es_unregister_shrinker(sbi);
-	deactivate_super(sbi->s_sb);
+	deactivate_super(sb);
+
+	kfree(k_ctx.meta_blocks);
+	kfree(sbi->s_es);
+	kfree(sb->s_bdev->bd_mapping);
+	kfree(sb->s_bdev);
 	kfree(sbi);
 	kfree(k_ctx.k_ei);
 	kfree(k_ctx.k_data);
+	memset(&k_ctx, 0, sizeof(k_ctx));
 }
 
 static int __ext4_ext_dirty_stub(const char *where, unsigned int line,
 				 handle_t *handle, struct inode *inode,
 				 struct ext4_ext_path *path)
 {
+	if (path && path->p_bh) {
+		ext4_extent_block_csum_set_test(inode,
+						ext_block_hdr(path->p_bh));
+		set_buffer_verified(path->p_bh);
+	}
 	return 0;
 }
 
+static int __ext4_handle_dirty_metadata_stub(const char *where,
+					     unsigned int line,
+					     handle_t *handle,
+					     struct inode *inode,
+					     struct buffer_head *bh)
+{
+	if (bh)
+		set_buffer_uptodate(bh);
+	return 0;
+}
+
+static int __ext4_mark_inode_dirty_stub(handle_t *handle, struct inode *inode,
+					const char *func, unsigned int line)
+{
+	return 0;
+}
+
+static struct buffer_head *ext4_ext_getblk_stub(struct inode *inode,
+						ext4_fsblk_t pblk,
+						gfp_t gfp_flags)
+{
+	struct kunit_meta_block *slot = NULL;
+	int i;
+
+	if (k_ctx.fail_getblk || !k_ctx.meta_blocks)
+		return NULL;
+
+	for (i = 0; i < EXT_MAX_META_BLOCKS; i++) {
+		if (k_ctx.meta_blocks[i].in_use &&
+		    k_ctx.meta_blocks[i].pblk == pblk) {
+			slot = &k_ctx.meta_blocks[i];
+			break;
+		}
+	}
+
+	if (!slot) {
+		for (i = 0; i < EXT_MAX_META_BLOCKS; i++) {
+			if (!k_ctx.meta_blocks[i].in_use) {
+				slot = &k_ctx.meta_blocks[i];
+				break;
+			}
+		}
+		if (!slot)
+			return NULL;
+
+		memset(slot, 0, sizeof(*slot));
+		slot->in_use = true;
+		slot->pblk = pblk;
+		slot->bh.b_data = slot->data;
+		slot->bh.b_size = inode->i_sb->s_blocksize;
+		slot->bh.b_blocknr = pblk;
+		slot->bh.b_bdev = inode->i_sb->s_bdev;
+		atomic_set(&slot->bh.b_count, 1);
+		set_buffer_uptodate(&slot->bh);
+		set_buffer_mapped(&slot->bh);
+	}
+
+	get_bh(&slot->bh);
+	return &slot->bh;
+}
+
+static ext4_fsblk_t ext4_mb_new_blocks_stub(handle_t *handle,
+					    struct ext4_allocation_request *ar,
+					    int *errp)
+{
+	struct ext4_sb_info *sbi = EXT4_SB(ar->inode->i_sb);
+	ext4_fsblk_t pblk;
+
+	k_ctx.alloc_calls++;
+	if (k_ctx.fail_alloc_err) {
+		*errp = k_ctx.fail_alloc_err;
+		return 0;
+	}
+
+	pblk = k_ctx.next_alloc_pblk;
+	k_ctx.next_alloc_pblk += EXT4_C2B(sbi, ar->len);
+	k_ctx.last_alloc_pblk = pblk;
+	k_ctx.last_alloc_len = ar->len;
+	*errp = 0;
+	return pblk;
+}
+
+static void ext4_free_blocks_stub(handle_t *handle, struct inode *inode,
+				  struct buffer_head *bh, ext4_fsblk_t block,
+				  unsigned long count, int flags)
+{
+	k_ctx.free_calls++;
+	k_ctx.last_free_pblk = block;
+	k_ctx.last_free_count = count;
+	k_ctx.last_free_flags = flags;
+	k_ctx.total_freed_blocks += count;
+}
+
 static struct ext4_ext_path *
 ext4_ext_insert_extent_stub(handle_t *handle, struct inode *inode,
 			    struct ext4_ext_path *path,
 			    struct ext4_extent *newext, int gb_flags)
 {
+	ext4_free_ext_path(path);
 	return ERR_PTR(-ENOSPC);
 }
 
@@ -227,10 +435,11 @@ static int extents_kunit_init(struct kunit *test)
 	struct super_block *sb;
 	struct fs_context *fc;
 	struct ext4_sb_info *sbi = NULL;
-	struct kunit_ext_test_param *param =
-		(struct kunit_ext_test_param *)(test->param_value);
 	int err;
 
+	memset(&k_ctx, 0, sizeof(k_ctx));
+	k_ctx.next_alloc_pblk = EXT_ALLOC_BASE_PBLK;
+
 	sbi = kzalloc_obj(struct ext4_sb_info);
 	if (sbi == NULL)
 		return -ENOMEM;
@@ -249,16 +458,76 @@ static int extents_kunit_init(struct kunit *test)
 
 	sbi->s_sb = sb;
 	sb->s_fs_info = sbi;
-
+	sb->s_op = &ext_dummy_sops;
+#ifdef CONFIG_QUOTA
+	sb->dq_op = &ext_dummy_qops;
+#endif
 	sb->s_blocksize = 4096;
 	sb->s_blocksize_bits = 12;
+	sb->s_maxbytes = MAX_LFS_FILESIZE;
+	strscpy(sb->s_id, "extents_kunit", sizeof(sb->s_id));
+
+	sb->s_bdev = kzalloc_obj(struct block_device);
+	if (!sb->s_bdev) {
+		err = -ENOMEM;
+		goto out_deactivate;
+	}
+	bdev_set_flag(sb->s_bdev, BD_READ_ONLY);
+	sb->s_bdev->bd_mapping = kzalloc_obj(struct address_space);
+	if (!sb->s_bdev->bd_mapping) {
+		err = -ENOMEM;
+		goto out_bdev;
+	}
 
-	if (!param || !param->disable_zeroout)
-		sbi->s_extent_max_zeroout_kb = 32;
+	sbi->s_es = kzalloc_obj(struct ext4_super_block);
+	if (!sbi->s_es) {
+		err = -ENOMEM;
+		goto out_bdev_mapping;
+	}
+	sbi->s_es->s_first_data_block = cpu_to_le32(1);
+	sbi->s_es->s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+	ext4_blocks_count_set(sbi->s_es, EXT_TOTAL_BLOCKS);
+	set_opt(sb, ERRORS_CONT);
+	sbi->s_blocks_per_group = 32768;
+	sbi->s_clusters_per_group = 32768;
+	sbi->s_cluster_ratio = 1;
+	sbi->s_cluster_bits = 0;
+	sbi->s_desc_per_block = 64;
+	sbi->s_groups_count = 32;
+	sbi->s_extent_max_zeroout_kb = 32;
+	spin_lock_init(&sbi->s_bdev_wb_lock);
+	spin_lock_init(&sbi->s_error_lock);
+	spin_lock_init(&sbi->s_md_lock);
+	ratelimit_state_init(&sbi->s_warning_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL,
+			     DEFAULT_RATELIMIT_BURST);
+
+	err = percpu_counter_init(&sbi->s_freeclusters_counter, 100000,
+				  GFP_KERNEL);
+	if (err)
+		goto out_es;
+	err = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0,
+				  GFP_KERNEL);
+	if (err)
+		goto out_freeclusters;
 
 	err = ext4_es_register_shrinker(sbi);
 	if (err)
-		goto out_deactivate;
+		goto out_dirtyclusters;
+
+	/*
+	 * Isolate the mock shrinker from asynchronous background reclaim.
+	 */
+	sbi->s_es_shrinker->count_objects = ext_dummy_shrinker_count;
+	sbi->s_es_shrinker->scan_objects = ext_dummy_shrinker_scan;
+
+	k_ctx.meta_blocks = kcalloc(EXT_MAX_META_BLOCKS,
+				    sizeof(struct kunit_meta_block),
+				    GFP_KERNEL);
+	if (!k_ctx.meta_blocks) {
+		err = -ENOMEM;
+		goto out;
+	}
 
 	/* setup the mock inode */
 	k_ctx.k_ei = kzalloc_obj(struct ext4_inode_info);
@@ -268,19 +537,7 @@ static int extents_kunit_init(struct kunit *test)
 	}
 	ei = k_ctx.k_ei;
 	inode = &ei->vfs_inode;
-
-	ext4_es_init_tree(&ei->i_es_tree);
-	rwlock_init(&ei->i_es_lock);
-	INIT_LIST_HEAD(&ei->i_es_list);
-	ei->i_es_all_nr = 0;
-	ei->i_es_shk_nr = 0;
-	ei->i_es_shrink_lblk = 0;
-
-	ei->i_disksize = (EXT_DATA_LBLK + EXT_DATA_LEN + 10)
-			 << sb->s_blocksize_bits;
-	ei->i_flags = 0;
-	ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
-	inode->i_sb = sb;
+	ext_init_mock_inode(ei, sb, 12);
 
 	k_ctx.k_data = kzalloc(EXT_DATA_LEN * 4096, GFP_KERNEL);
 	if (k_ctx.k_data == NULL) {
@@ -293,9 +550,6 @@ static int extents_kunit_init(struct kunit *test)
 	 */
 	memset(k_ctx.k_data, 'X', EXT_DATA_LEN * 4096);
 
-	/* create a tree with depth 0 */
-	eh = (struct ext4_extent_header *)k_ctx.k_ei->i_data;
-
 	/* Fill extent header */
 	eh = ext_inode_hdr(&k_ctx.k_ei->vfs_inode);
 	eh->eh_depth = 0;
@@ -307,25 +561,34 @@ static int extents_kunit_init(struct kunit *test)
 
 	/*
 	 * add 1 extent in leaf node covering:
-	 * - lblks: [EXT_DATA_LBLK, EXT_DATA_LBLK * + EXT_DATA_LEN)
+	 * - lblks: [EXT_DATA_LBLK, EXT_DATA_LBLK + EXT_DATA_LEN)
 	 * - pblks: [EXT_DATA_PBLK, EXT_DATA_PBLK + EXT_DATA_LEN)
 	 */
 	EXT_FIRST_EXTENT(eh)->ee_block = cpu_to_le32(EXT_DATA_LBLK);
 	EXT_FIRST_EXTENT(eh)->ee_len = cpu_to_le16(EXT_DATA_LEN);
 	ext4_ext_store_pblock(EXT_FIRST_EXTENT(eh), EXT_DATA_PBLK);
-	if (!param || param->is_unwrit_at_start)
-		ext4_ext_mark_unwritten(EXT_FIRST_EXTENT(eh));
+	ext4_ext_mark_unwritten(EXT_FIRST_EXTENT(eh));
 
+	down_write(&ei->i_data_sem);
 	ext4_es_insert_extent(inode, EXT_DATA_LBLK, EXT_DATA_LEN, EXT_DATA_PBLK,
-			      ext4_ext_is_unwritten(EXT_FIRST_EXTENT(eh)) ?
-				      EXTENT_STATUS_UNWRITTEN :
-				      EXTENT_STATUS_WRITTEN,
-			      0);
+			      EXTENT_STATUS_UNWRITTEN, 0);
+	up_write(&ei->i_data_sem);
 
 	/* Add stubs */
 	kunit_activate_static_stub(test, __ext4_ext_dirty,
 				   __ext4_ext_dirty_stub);
-	kunit_activate_static_stub(test, ext4_ext_zeroout, ext4_ext_zeroout_stub);
+	kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+				   __ext4_handle_dirty_metadata_stub);
+	kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+				   __ext4_mark_inode_dirty_stub);
+	kunit_activate_static_stub(test, ext4_ext_getblk,
+				   ext4_ext_getblk_stub);
+	kunit_activate_static_stub(test, ext4_mb_new_blocks,
+				   ext4_mb_new_blocks_stub);
+	kunit_activate_static_stub(test, ext4_free_blocks,
+				   ext4_free_blocks_stub);
+	kunit_activate_static_stub(test, ext4_ext_zeroout,
+				   ext4_ext_zeroout_stub);
 	kunit_activate_static_stub(test, ext4_issue_zeroout,
 				   ext4_issue_zeroout_stub);
 	up_write(&sb->s_umount);
@@ -333,13 +596,23 @@ static int extents_kunit_init(struct kunit *test)
 	return 0;
 
 out:
-	kfree(k_ctx.k_ei);
-	k_ctx.k_ei = NULL;
-
 	kfree(k_ctx.k_data);
 	k_ctx.k_data = NULL;
-
+	kfree(k_ctx.k_ei);
+	k_ctx.k_ei = NULL;
+	kfree(k_ctx.meta_blocks);
+	k_ctx.meta_blocks = NULL;
 	ext4_es_unregister_shrinker(sbi);
+out_dirtyclusters:
+	percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
+out_freeclusters:
+	percpu_counter_destroy(&sbi->s_freeclusters_counter);
+out_es:
+	kfree(sbi->s_es);
+out_bdev_mapping:
+	kfree(sb->s_bdev->bd_mapping);
+out_bdev:
+	kfree(sb->s_bdev);
 out_deactivate:
 	deactivate_locked_super(sb);
 	kfree(sbi);
@@ -392,12 +665,24 @@ static void test_split_convert(struct kunit *test)
 {
 	struct ext4_ext_path *path;
 	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
 	struct ext4_extent *ex;
 	struct ext4_map_blocks map;
 	const struct kunit_ext_test_param *param =
 		(const struct kunit_ext_test_param *)(test->param_value);
 	int blkbits = inode->i_sb->s_blocksize_bits;
 
+	if (param->disable_zeroout)
+		EXT4_SB(inode->i_sb)->s_extent_max_zeroout_kb = 0;
+
+	if (!param->is_unwrit_at_start) {
+		ext4_ext_mark_initialized(EXT_FIRST_EXTENT(eh));
+		down_write(&EXT4_I(inode)->i_data_sem);
+		ext4_es_insert_extent(inode, EXT_DATA_LBLK, EXT_DATA_LEN,
+				      EXT_DATA_PBLK, EXTENT_STATUS_WRITTEN, 0);
+		up_write(&EXT4_I(inode)->i_data_sem);
+	}
+
 	if (param->is_zeroout_test)
 		/*
 		 * Force zeroout by making ext4_ext_insert_extent return ENOSPC
@@ -406,6 +691,7 @@ static void test_split_convert(struct kunit *test)
 					   ext4_ext_insert_extent_stub);
 
 	path = ext4_find_extent(inode, EXT_DATA_LBLK, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
 	ex = path->p_ext;
 	KUNIT_EXPECT_EQ(test, EXT_DATA_LBLK, le32_to_cpu(ex->ee_block));
 	KUNIT_EXPECT_EQ(test, EXT_DATA_LEN, ext4_ext_get_actual_len(ex));
@@ -423,15 +709,21 @@ static void test_split_convert(struct kunit *test)
 	case TEST_SPLIT_CONVERT:
 		path = ext4_split_convert_extents_test(NULL, inode, &map,
 					path, param->split_flags, NULL);
+		if (!IS_ERR(path))
+			ext4_free_ext_path(path);
 		break;
 	case TEST_CREATE_BLOCKS:
+		ext4_free_ext_path(path);
 		ext4_map_create_blocks_helper(test, inode, &map, param->split_flags);
 		break;
 	default:
+		ext4_free_ext_path(path);
 		KUNIT_FAIL(test, "param->type %d not support.", param->type);
+		return;
 	}
 
 	path = ext4_find_extent(inode, EXT_DATA_LBLK, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
 	ex = path->p_ext;
 
 	for (int i = 0; i < param->nr_exp_ext; i++) {
@@ -494,6 +786,8 @@ static void test_split_convert(struct kunit *test)
 		ex = ex + 1;
 	}
 
+	ext4_free_ext_path(path);
+
 	if (!param->is_zeroout_test)
 		return;
 
@@ -512,8 +806,1439 @@ static void test_split_convert(struct kunit *test)
 				    "# corruption in byte range [%lld, %lld)",
 				    off, len);
 	}
+}
+
+/*
+ * Helper to reset an inode's extent tree and extent status cache to empty.
+ */
+static void ext_reset_tree(struct inode *inode)
+{
+	struct ext4_inode_info *ei = EXT4_I(inode);
+
+	down_write(&ei->i_data_sem);
+	ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+	memset(ei->i_data, 0, sizeof(ei->i_data));
+	ext4_ext_tree_init(NULL, inode);
+	up_write(&ei->i_data_sem);
+}
+
+/*
+ * Helper to insert an extent into @inode's extent tree under i_data_sem.
+ */
+static int ext_insert_one(struct inode *inode, ext4_lblk_t lblk,
+			  unsigned short len, ext4_fsblk_t pblk,
+			  bool unwritten, int gb_flags)
+{
+	struct ext4_ext_path *path;
+	struct ext4_extent newex = {};
+
+	newex.ee_block = cpu_to_le32(lblk);
+	newex.ee_len = cpu_to_le16(len);
+	ext4_ext_store_pblock(&newex, pblk);
+	if (unwritten)
+		ext4_ext_mark_unwritten(&newex);
+
+	down_write(&EXT4_I(inode)->i_data_sem);
+	path = ext4_find_extent(inode, lblk, NULL, EXT4_EX_NOCACHE);
+	if (IS_ERR(path)) {
+		up_write(&EXT4_I(inode)->i_data_sem);
+		return PTR_ERR(path);
+	}
+	path = ext4_ext_insert_extent(NULL, inode, path, &newex,
+				      gb_flags | EXT4_EX_NOCACHE);
+	up_write(&EXT4_I(inode)->i_data_sem);
+	if (IS_ERR(path))
+		return PTR_ERR(path);
+
+	ext4_free_ext_path(path);
+	return 0;
+}
+
+/*
+ * Verify 48-bit physical block number store and load helpers for both
+ * leaf extents (ext4_extent) and index entries (ext4_extent_idx).
+ */
+static void test_ext_pblock_48bit(struct kunit *test)
+{
+	struct ext4_extent ex = {};
+	struct ext4_extent_idx idx = {};
+	const ext4_fsblk_t pblk_48 = 0x123456789abcULL;
+	const ext4_fsblk_t pblk_max = 0xffffffffffffULL;
+
+	ext4_ext_store_pblock(&ex, 0);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex), 0ULL);
+
+	ext4_ext_store_pblock(&ex, 0xffffffffULL);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex), 0xffffffffULL);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(ex.ee_start_hi), 0);
+
+	ext4_ext_store_pblock(&ex, pblk_48);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex.ee_start_lo), 0x56789abcU);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(ex.ee_start_hi), 0x1234U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex), pblk_48);
+
+	ext4_ext_store_pblock(&ex, pblk_max);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex), pblk_max);
+
+	ext4_idx_store_pblock(&idx, pblk_48);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(idx.ei_leaf_lo), 0x56789abcU);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(idx.ei_leaf_hi), 0x1234U);
+	KUNIT_EXPECT_EQ(test, ext4_idx_pblock(&idx), pblk_48);
+
+	ext4_idx_store_pblock(&idx, pblk_max);
+	KUNIT_EXPECT_EQ(test, ext4_idx_pblock(&idx), pblk_max);
+}
+
+/*
+ * Verify extent length encoding and unwritten bit manipulation at boundaries:
+ * - EXT_INIT_MAX_LEN (32768 = 0x8000) is the maximum initialized extent length
+ *   and must NOT be treated as unwritten.
+ * - EXT_UNWRITTEN_MAX_LEN (32767 = 0x7fff) is the maximum unwritten extent
+ *   length (stored on disk as 0xffff).
+ */
+static void test_ext_unwritten_len_encoding(struct kunit *test)
+{
+	struct ext4_extent ex = {};
+
+	ex.ee_len = cpu_to_le16(1);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 0);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex), 1);
+
+	ext4_ext_mark_unwritten(&ex);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 1);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex), 1);
+
+	ext4_ext_mark_initialized(&ex);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 0);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex), 1);
+
+	ex.ee_len = cpu_to_le16(EXT_UNWRITTEN_MAX_LEN);
+	ext4_ext_mark_unwritten(&ex);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 1);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex),
+			EXT_UNWRITTEN_MAX_LEN);
+
+	ext4_ext_mark_initialized(&ex);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 0);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex),
+			EXT_UNWRITTEN_MAX_LEN);
+
+	ex.ee_len = cpu_to_le16(EXT_INIT_MAX_LEN);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex), 0);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex), EXT_INIT_MAX_LEN);
+}
+
+/*
+ * Verify ext4_ext_tree_init() initializes the in-inode extent header and
+ * ext4_ext_check_inode() accepts valid empty and populated root headers.
+ */
+static void test_ext_tree_init_and_check_inode(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
+
+	memset(k_ctx.k_ei->i_data, 0xff, sizeof(k_ctx.k_ei->i_data));
+	ext4_ext_tree_init(NULL, inode);
+
+	KUNIT_EXPECT_EQ(test, eh->eh_magic, EXT4_EXT_MAGIC);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_depth), 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_max), 4);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(eh->eh_generation), 0U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_check_inode(inode), 0);
+
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 5, 100, false, 0), 0);
+	KUNIT_EXPECT_EQ(test, ext4_ext_check_inode(inode), 0);
+}
+
+enum ext_check_corrupt_type {
+	CORRUPT_BAD_MAGIC,
+	CORRUPT_BAD_DEPTH,
+	CORRUPT_ZERO_MAX,
+	CORRUPT_EXCESS_MAX,
+	CORRUPT_ENTRIES_GT_MAX,
+	CORRUPT_EMPTY_NONROOT_INDEX,
+	CORRUPT_ZERO_LEN_EXTENT,
+	CORRUPT_LBLK_OVERFLOW,
+	CORRUPT_PBLK_TOO_LOW,
+	CORRUPT_PBLK_TOO_HIGH,
+	CORRUPT_UNSORTED_EXTENTS,
+	CORRUPT_UNSORTED_INDEXES,
+	CORRUPT_NONROOT_LBLK_MISMATCH,
+};
+
+struct ext_check_corrupt_param {
+	const char *desc;
+	enum ext_check_corrupt_type type;
+};
+
+static const struct ext_check_corrupt_param ext_check_corrupt_params[] = {
+	{ "invalid eh_magic", CORRUPT_BAD_MAGIC },
+	{ "mismatched eh_depth", CORRUPT_BAD_DEPTH },
+	{ "eh_max == 0", CORRUPT_ZERO_MAX },
+	{ "eh_max exceeds root capacity", CORRUPT_EXCESS_MAX },
+	{ "eh_entries exceeds eh_max", CORRUPT_ENTRIES_GT_MAX },
+	{ "empty index block at depth > 0", CORRUPT_EMPTY_NONROOT_INDEX },
+	{ "extent with ee_len == 0", CORRUPT_ZERO_LEN_EXTENT },
+	{ "extent logical block 32-bit wrap-around", CORRUPT_LBLK_OVERFLOW },
+	{ "extent physical block <= first_data_block", CORRUPT_PBLK_TOO_LOW },
+	{ "extent physical block past blocks_count", CORRUPT_PBLK_TOO_HIGH },
+	{ "overlapping/unsorted leaf extents", CORRUPT_UNSORTED_EXTENTS },
+	{ "unsorted index entries", CORRUPT_UNSORTED_INDEXES },
+	{ "non-root leaf first ee_block != parent ei_block",
+	  CORRUPT_NONROOT_LBLK_MISMATCH },
+};
+
+static void ext_check_corrupt_get_desc(struct kunit *test, const void *p,
+				       char *desc)
+{
+	const struct ext_check_corrupt_param *param = p;
+
+	strscpy(desc, param->desc, KUNIT_PARAM_DESC_SIZE);
+}
+
+static int ext_check_corrupt_param_init(struct kunit *test)
+{
+	kunit_register_params_array(test, ext_check_corrupt_params,
+				    ARRAY_SIZE(ext_check_corrupt_params),
+				    ext_check_corrupt_get_desc);
+	return 0;
+}
+
+/*
+ * Verify __ext4_ext_check() detects and rejects all classes of corrupt
+ * extent headers, leaf extents, and index entries with -EFSCORRUPTED.
+ */
+static void test_ext_check_corruptions(struct kunit *test)
+{
+	const struct ext_check_corrupt_param *param = test->param_value;
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
+	struct ext4_extent *ex;
+	struct ext4_extent_idx *ix;
+	struct {
+		struct ext4_extent_header eh;
+		struct ext4_extent ex;
+	} nonroot_leaf;
+	int depth = 0;
+	ext4_fsblk_t pblk = 0;
+	ext4_lblk_t lblk = 0;
+	int err;
+
+	ext_reset_tree(inode);
+	eh->eh_entries = cpu_to_le16(1);
+	ex = EXT_FIRST_EXTENT(eh);
+	ex->ee_block = cpu_to_le32(10);
+	ex->ee_len = cpu_to_le16(4);
+	ext4_ext_store_pblock(ex, 100);
+
+	switch (param->type) {
+	case CORRUPT_BAD_MAGIC:
+		eh->eh_magic = cpu_to_le16(0xdead);
+		break;
+	case CORRUPT_BAD_DEPTH:
+		eh->eh_depth = cpu_to_le16(1);
+		depth = 0;
+		break;
+	case CORRUPT_ZERO_MAX:
+		eh->eh_max = 0;
+		break;
+	case CORRUPT_EXCESS_MAX:
+		eh->eh_max = cpu_to_le16(10);
+		break;
+	case CORRUPT_ENTRIES_GT_MAX:
+		eh->eh_entries = cpu_to_le16(5);
+		break;
+	case CORRUPT_EMPTY_NONROOT_INDEX:
+		eh->eh_depth = cpu_to_le16(1);
+		eh->eh_entries = 0;
+		depth = 1;
+		break;
+	case CORRUPT_ZERO_LEN_EXTENT:
+		ex->ee_len = 0;
+		break;
+	case CORRUPT_LBLK_OVERFLOW:
+		ex->ee_block = cpu_to_le32(0xfffffff0U);
+		ex->ee_len = cpu_to_le16(32);
+		break;
+	case CORRUPT_PBLK_TOO_LOW:
+		ext4_ext_store_pblock(ex, 1);
+		break;
+	case CORRUPT_PBLK_TOO_HIGH:
+		ext4_ext_store_pblock(ex, EXT_TOTAL_BLOCKS);
+		break;
+	case CORRUPT_UNSORTED_EXTENTS:
+		eh->eh_entries = cpu_to_le16(2);
+		ex[1].ee_block = cpu_to_le32(12);
+		ex[1].ee_len = cpu_to_le16(4);
+		ext4_ext_store_pblock(&ex[1], 200);
+		break;
+	case CORRUPT_UNSORTED_INDEXES:
+		eh->eh_depth = cpu_to_le16(1);
+		eh->eh_entries = cpu_to_le16(2);
+		depth = 1;
+		ix = EXT_FIRST_INDEX(eh);
+		ix[0].ei_block = cpu_to_le32(20);
+		ext4_idx_store_pblock(&ix[0], 500);
+		ix[1].ei_block = cpu_to_le32(10);
+		ext4_idx_store_pblock(&ix[1], 501);
+		break;
+	case CORRUPT_NONROOT_LBLK_MISMATCH:
+		eh->eh_depth = cpu_to_le16(1);
+		memset(&nonroot_leaf, 0, sizeof(nonroot_leaf));
+		nonroot_leaf.eh.eh_magic = EXT4_EXT_MAGIC;
+		nonroot_leaf.eh.eh_depth = 0;
+		nonroot_leaf.eh.eh_entries = cpu_to_le16(1);
+		nonroot_leaf.eh.eh_max = cpu_to_le16(340);
+		nonroot_leaf.ex.ee_block = cpu_to_le32(10);
+		nonroot_leaf.ex.ee_len = cpu_to_le16(4);
+		ext4_ext_store_pblock(&nonroot_leaf.ex, 100);
+		eh = &nonroot_leaf.eh;
+		depth = 0;
+		pblk = 500;
+		lblk = 20;
+		break;
+	}
+
+	/* Suppress __ext4_error_inode() log spam during expected failure */
+	set_bit(EXT4_FLAGS_SHUTDOWN, &sbi->s_ext4_flags);
+	err = ext4_ext_check_test(inode, eh, depth, pblk, lblk);
+	clear_bit(EXT4_FLAGS_SHUTDOWN, &sbi->s_ext4_flags);
+
+	KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+}
+
+/*
+ * Verify metadata checksum calculation and verification on external extent
+ * tree blocks when EXT4_FEATURE_RO_COMPAT_METADATA_CSUM is enabled.
+ */
+static void test_ext_block_csum_verify(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+	struct buffer_head *bh;
+	struct ext4_extent_header *eh;
+	struct ext4_extent *ex;
+	struct ext4_extent_tail *et;
+	int max_entries;
+	int err;
+
+	ext_reset_tree(inode);
+	ext_inode_hdr(inode)->eh_depth = cpu_to_le16(1);
+	ext4_set_feature_metadata_csum(inode->i_sb);
+	bh = ext4_ext_getblk_stub(inode, EXT_META_BASE_PBLK, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, bh);
+
+	max_entries = (inode->i_sb->s_blocksize -
+		       sizeof(struct ext4_extent_header) -
+		       sizeof(struct ext4_extent_tail)) /
+		      sizeof(struct ext4_extent);
+
+	eh = ext_block_hdr(bh);
+	eh->eh_magic = EXT4_EXT_MAGIC;
+	eh->eh_depth = 0;
+	eh->eh_entries = cpu_to_le16(1);
+	eh->eh_max = cpu_to_le16(max_entries);
+
+	ex = EXT_FIRST_EXTENT(eh);
+	ex->ee_block = cpu_to_le32(10);
+	ex->ee_len = cpu_to_le16(5);
+	ext4_ext_store_pblock(ex, 100);
+
+	ext4_extent_block_csum_set_test(inode, eh);
+	err = ext4_ext_check_test(inode, eh, 0, EXT_META_BASE_PBLK, 10);
+	KUNIT_EXPECT_EQ(test, err, 0);
+
+	/* Flip a bit in the tail checksum and verify -EFSBADCRC */
+	et = find_ext4_extent_tail(eh);
+	et->et_checksum ^= cpu_to_le32(0x1);
+
+	set_bit(EXT4_FLAGS_SHUTDOWN, &sbi->s_ext4_flags);
+	err = ext4_ext_check_test(inode, eh, 0, EXT_META_BASE_PBLK, 10);
+	clear_bit(EXT4_FLAGS_SHUTDOWN, &sbi->s_ext4_flags);
+
+	KUNIT_EXPECT_EQ(test, err, -EFSBADCRC);
+	brelse(bh);
+}
+
+/*
+ * Verify ext4_find_extent() binary search on an empty tree and a multi-extent
+ * root leaf, as well as extent status caching vs EXT4_EX_NOCACHE.
+ */
+static void test_find_extent_root_binsearch(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_ext_path *path;
+	struct extent_status es;
+
+	ext_reset_tree(inode);
+
+	/* Empty tree returns NULL p_ext */
+	path = ext4_find_extent(inode, 10, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+	KUNIT_EXPECT_PTR_EQ(test, path[0].p_ext, NULL);
+	ext4_free_ext_path(path);
+
+	/* Populate 3 separated extents: [10, 15), [20, 25), [30, 35) */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 5, 100, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 20, 5, 200, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 30, 5, 300, false, 0), 0);
+
+	/* Lookup before first extent (lblk 5) -> points to first extent */
+	path = ext4_find_extent(inode, 5, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+	KUNIT_ASSERT_NOT_NULL(test, path[0].p_ext);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(path[0].p_ext->ee_block), 10U);
+	ext4_free_ext_path(path);
+
+	/* Lookup inside first extent (lblk 12) */
+	path = ext4_find_extent(inode, 12, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(path[0].p_ext->ee_block), 10U);
+	ext4_free_ext_path(path);
+
+	/* Lookup in hole between 1st and 2nd extent (lblk 17) -> 1st extent */
+	path = ext4_find_extent(inode, 17, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(path[0].p_ext->ee_block), 10U);
+	ext4_free_ext_path(path);
+
+	/* Lookup exact start of 2nd extent (lblk 20) */
+	path = ext4_find_extent(inode, 20, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(path[0].p_ext->ee_block), 20U);
+	ext4_free_ext_path(path);
+
+	/* Lookup past last extent (lblk 50) -> 3rd extent */
+	path = ext4_find_extent(inode, 50, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(path[0].p_ext->ee_block), 30U);
+	ext4_free_ext_path(path);
+
+	/* EXT4_EX_NOCACHE left i_es_tree empty */
+	KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 10, NULL, &es, NULL),
+			0);
+
+	/* Lookup without EXT4_EX_NOCACHE populates extents and holes in es */
+	down_read(&EXT4_I(inode)->i_data_sem);
+	path = ext4_find_extent(inode, 20, NULL, 0);
+	up_read(&EXT4_I(inode)->i_data_sem);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+	ext4_free_ext_path(path);
+
+	/*
+	 * Verify inter-extent hole [15, 20) and extents are cached in es
+	 * (ext4_cache_extents only caches holes where prev != 0).
+	 */
+	KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 5, NULL, &es, NULL),
+			0);
+	KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 17, NULL, &es, NULL),
+			1);
+	KUNIT_EXPECT_TRUE(test, ext4_es_is_hole(&es));
+	KUNIT_EXPECT_EQ(test, es.es_lblk, 15U);
+	KUNIT_EXPECT_EQ(test, es.es_len, 5U);
+
+	KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 22, NULL, &es, NULL),
+			1);
+	KUNIT_EXPECT_TRUE(test, ext4_es_is_written(&es));
+	KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), 200ULL);
+}
+
+/*
+ * Helper to construct a depth-1 tree with two external leaf blocks:
+ *   Leaf 0 (pblk 500): [10, 15) -> 1000 (written), [20, 25) -> 1100 (written)
+ *   Leaf 1 (pblk 501): [40, 45) -> 2000 (unwritten), [50, 55) -> 2100 (written)
+ */
+static void setup_depth1_two_leaf_tree(struct kunit *test, struct inode *inode)
+{
+	struct ext4_extent_header *root_eh = ext_inode_hdr(inode);
+	struct ext4_extent_idx *idx;
+	struct buffer_head *bh0, *bh1;
+	struct ext4_extent_header *eh0, *eh1;
+	struct ext4_extent *ex;
+	int max_leaf = (inode->i_sb->s_blocksize -
+			sizeof(struct ext4_extent_header)) /
+		       sizeof(struct ext4_extent);
+
+	ext_reset_tree(inode);
+
+	root_eh->eh_depth = cpu_to_le16(1);
+	root_eh->eh_entries = cpu_to_le16(2);
+	root_eh->eh_max = cpu_to_le16(ext4_ext_space_root_idx_test(inode, 0));
+
+	idx = EXT_FIRST_INDEX(root_eh);
+	idx[0].ei_block = cpu_to_le32(10);
+	ext4_idx_store_pblock(&idx[0], EXT_META_BASE_PBLK);
+	idx[1].ei_block = cpu_to_le32(40);
+	ext4_idx_store_pblock(&idx[1], EXT_META_BASE_PBLK + 1);
+
+	bh0 = ext4_ext_getblk_stub(inode, EXT_META_BASE_PBLK, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, bh0);
+	eh0 = ext_block_hdr(bh0);
+	eh0->eh_magic = EXT4_EXT_MAGIC;
+	eh0->eh_depth = 0;
+	eh0->eh_entries = cpu_to_le16(2);
+	eh0->eh_max = cpu_to_le16(max_leaf);
+	ex = EXT_FIRST_EXTENT(eh0);
+	ex[0].ee_block = cpu_to_le32(10);
+	ex[0].ee_len = cpu_to_le16(5);
+	ext4_ext_store_pblock(&ex[0], 1000);
+	ex[1].ee_block = cpu_to_le32(20);
+	ex[1].ee_len = cpu_to_le16(5);
+	ext4_ext_store_pblock(&ex[1], 1100);
+	brelse(bh0);
+
+	bh1 = ext4_ext_getblk_stub(inode, EXT_META_BASE_PBLK + 1, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, bh1);
+	eh1 = ext_block_hdr(bh1);
+	eh1->eh_magic = EXT4_EXT_MAGIC;
+	eh1->eh_depth = 0;
+	eh1->eh_entries = cpu_to_le16(2);
+	eh1->eh_max = cpu_to_le16(max_leaf);
+	ex = EXT_FIRST_EXTENT(eh1);
+	ex[0].ee_block = cpu_to_le32(40);
+	ex[0].ee_len = cpu_to_le16(5);
+	ext4_ext_store_pblock(&ex[0], 2000);
+	ext4_ext_mark_unwritten(&ex[0]);
+	ex[1].ee_block = cpu_to_le32(50);
+	ex[1].ee_len = cpu_to_le16(5);
+	ext4_ext_store_pblock(&ex[1], 2100);
+	brelse(bh1);
+}
+
+/*
+ * Verify multi-level (depth 1) extent lookup and tree navigation helpers:
+ * ext4_ext_next_allocated_block(), ext4_ext_next_leaf_block(),
+ * ext4_ext_search_left(), and ext4_ext_search_right() across leaf boundaries.
+ */
+static void test_find_extent_multilevel_and_navigation(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_ext_path *path;
+	struct ext4_extent ret_ex;
+	ext4_lblk_t logical;
+	ext4_fsblk_t phys;
+	int ret;
+
+	setup_depth1_two_leaf_tree(test, inode);
+
+	/* Lookup first extent [10, 15) in leaf 0 */
+	path = ext4_find_extent(inode, 10, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+	KUNIT_EXPECT_EQ(test, path->p_depth, 1);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(path[1].p_ext->ee_block), 10U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_next_allocated_block(path), 20U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_next_leaf_block_test(path), 40U);
+	ext4_free_ext_path(path);
+
+	/*
+	 * Lookup lblk 30 (in hole between leaf 0 and leaf 1):
+	 * path[1].p_ext points to [20, 25) (last extent in leaf 0).
+	 */
+	path = ext4_find_extent(inode, 30, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(path[1].p_ext->ee_block), 20U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_next_allocated_block(path), 40U);
+
+	/* Search left from lblk 30 finds last block of [20, 25) -> 24 @ 1104 */
+	logical = 30;
+	ret = ext4_ext_search_left_test(inode, path, &logical, &phys);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, logical, 24U);
+	KUNIT_EXPECT_EQ(test, phys, 1104ULL);
+
+	/* Search right from lblk 30 crosses into leaf 1 -> [40, 45) @ 2000 */
+	logical = 30;
+	ret = ext4_ext_search_right_test(inode, path, &logical, &phys, &ret_ex,
+					 EXT4_EX_NOCACHE);
+	KUNIT_EXPECT_EQ(test, ret, 1);
+	KUNIT_EXPECT_EQ(test, logical, 40U);
+	KUNIT_EXPECT_EQ(test, phys, 2000ULL);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ret_ex.ee_block), 40U);
+	ext4_free_ext_path(path);
+
+	/* Lookup last extent [50, 55) in leaf 1 */
+	path = ext4_find_extent(inode, 52, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(path[1].p_ext->ee_block), 50U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_next_allocated_block(path),
+			EXT_MAX_BLOCKS);
+	KUNIT_EXPECT_EQ(test, ext4_ext_next_leaf_block_test(path),
+			EXT_MAX_BLOCKS);
+	ext4_free_ext_path(path);
+}
+
+/*
+ * Verify ext4_ext_precache() walks all leaf blocks of a multi-level extent
+ * tree, caches all extents and holes into the extent status tree, and sets
+ * EXT4_STATE_EXT_PRECACHED.
+ */
+static void test_ext_precache(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct extent_status es;
+	int err;
+
+	setup_depth1_two_leaf_tree(test, inode);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(inode,
+						 EXT4_STATE_EXT_PRECACHED));
+
+	err = ext4_ext_precache(inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_inode_state(inode,
+						EXT4_STATE_EXT_PRECACHED));
+
+	/* Verify cached inter-extent hole [15, 20) and extents from both leaves */
+	KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 17, NULL, &es, NULL),
+			1);
+	KUNIT_EXPECT_TRUE(test, ext4_es_is_hole(&es));
+	KUNIT_EXPECT_EQ(test, es.es_lblk, 15U);
+	KUNIT_EXPECT_EQ(test, es.es_len, 5U);
+
+	KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 10, NULL, &es, NULL),
+			1);
+	KUNIT_EXPECT_TRUE(test, ext4_es_is_written(&es));
+	KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), 1000ULL);
+
+	KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 40, NULL, &es, NULL),
+			1);
+	KUNIT_EXPECT_TRUE(test, ext4_es_is_unwritten(&es));
+	KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), 2000ULL);
+
+	KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 50, NULL, &es, NULL),
+			1);
+	KUNIT_EXPECT_TRUE(test, ext4_es_is_written(&es));
+	KUNIT_EXPECT_EQ(test, ext4_es_pblock(&es), 2100ULL);
+}
+
+/*
+ * Verify ext4_can_extents_be_merged() rules for written/unwritten state,
+ * logical and physical contiguity, and maximum length limits.
+ */
+static void test_can_extents_be_merged(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_extent ex1 = {}, ex2 = {};
+
+	/* Contiguous written extents [10, 15) @ 100 and [15, 20) @ 105 */
+	ex1.ee_block = cpu_to_le32(10);
+	ex1.ee_len = cpu_to_le16(5);
+	ext4_ext_store_pblock(&ex1, 100);
+	ex2.ee_block = cpu_to_le32(15);
+	ex2.ee_len = cpu_to_le16(5);
+	ext4_ext_store_pblock(&ex2, 105);
+	KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+			1);
+
+	/* Mismatched written vs unwritten cannot merge */
+	ext4_ext_mark_unwritten(&ex2);
+	KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+			0);
+
+	/* Both unwritten and contiguous can merge */
+	ext4_ext_mark_unwritten(&ex1);
+	KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+			1);
+
+	/* Non-contiguous physical blocks cannot merge */
+	ext4_ext_mark_initialized(&ex1);
+	ext4_ext_mark_initialized(&ex2);
+	ext4_ext_store_pblock(&ex2, 200);
+	KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+			0);
+
+	/* Non-contiguous logical blocks cannot merge */
+	ext4_ext_store_pblock(&ex2, 105);
+	ex2.ee_block = cpu_to_le32(16);
+	KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+			0);
+
+	/* Written max length boundary (EXT_INIT_MAX_LEN = 32768) */
+	ex1.ee_block = cpu_to_le32(0);
+	ex1.ee_len = cpu_to_le16(32760);
+	ext4_ext_store_pblock(&ex1, 100);
+	ex2.ee_block = cpu_to_le32(32760);
+	ex2.ee_len = cpu_to_le16(8);
+	ext4_ext_store_pblock(&ex2, 100 + 32760);
+	KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+			1);
+	ex2.ee_len = cpu_to_le16(9);
+	KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+			0);
+
+	/* Unwritten max length boundary (EXT_UNWRITTEN_MAX_LEN = 32767) */
+	ex1.ee_len = cpu_to_le16(32760);
+	ext4_ext_mark_unwritten(&ex1);
+	ex2.ee_len = cpu_to_le16(7);
+	ext4_ext_mark_unwritten(&ex2);
+	KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+			1);
+	ex2.ee_len = cpu_to_le16(8);
+	ext4_ext_mark_unwritten(&ex2);
+	KUNIT_EXPECT_EQ(test, ext4_can_extents_be_merged_test(inode, &ex1, &ex2),
+			0);
+}
+
+/*
+ * Verify ext4_ext_insert_extent() appending, prepending, 3-way bridge merging,
+ * and EXT4_GET_BLOCKS_SPLIT_NOMERGE suppression.
+ */
+static void test_insert_extent_merge_and_nomerge(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
+	struct ext4_extent *ex;
+
+	ext_reset_tree(inode);
+
+	/* Insert [10, 15) @ 100 and [20, 25) @ 110 */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 5, 100, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 20, 5, 110, false, 0), 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 2);
+
+	/* Append [15, 18) @ 105 -> merges with [10, 15) into [10, 18) */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 15, 3, 105, false, 0), 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 2);
+	ex = EXT_FIRST_EXTENT(eh);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 8);
+
+	/* Insert [18, 20) @ 108 -> bridges [10, 18) and [20, 25) into [10, 25) */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 18, 2, 108, false, 0), 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 15);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[0]), 100ULL);
+
+	/* Insert contiguous [25, 30) @ 115 with SPLIT_NOMERGE -> stays separate */
+	KUNIT_ASSERT_EQ(test,
+			ext_insert_one(inode, 25, 5, 115, false,
+				       EXT4_GET_BLOCKS_SPLIT_NOMERGE),
+			0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 2);
+}
+
+/*
+ * Verify tree growth (depth 0 -> 1 via ext4_ext_grow_indepth), parent index
+ * update when inserting a new lowest lblk (ext4_ext_correct_indexes), and
+ * tree collapse back into the inode root (depth 1 -> 0 via
+ * ext4_ext_try_to_merge_up) when merging reduces leaf entries to <= 4.
+ */
+static void test_insert_extent_grow_and_merge_up(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
+	struct ext4_extent *ex;
+
+	ext_reset_tree(inode);
+
+	/*
+	 * Insert 4 non-contiguous extents that fill the root leaf (eh_max = 4):
+	 * [10, 12) @ 100, [14, 16) @ 104, [18, 20) @ 108, [22, 24) @ 112.
+	 */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 2, 100, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 14, 2, 104, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 18, 2, 108, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 22, 2, 112, false, 0), 0);
+	KUNIT_EXPECT_EQ(test, ext_depth(inode), 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 4);
+
+	/*
+	 * Insert 5th extent [26, 28) @ 116: triggers ext4_ext_grow_indepth(),
+	 * moving the leaf to a newly allocated external block and increasing
+	 * tree depth to 1.
+	 */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 26, 2, 116, false, 0), 0);
+	KUNIT_EXPECT_EQ(test, ext_depth(inode), 1);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT_FIRST_INDEX(eh)->ei_block), 10U);
+	KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 1U);
+
+	/*
+	 * Prepend [8, 10) @ 98 to [10, 12) @ 100: modifies first extent in the
+	 * leaf and triggers ext4_ext_correct_indexes(), updating root ei_block
+	 * from 10 to 8.
+	 */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 8, 2, 98, false, 0), 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT_FIRST_INDEX(eh)->ei_block), 8U);
+
+	/*
+	 * Bridge two gaps ([12, 14) @ 102 and [16, 18) @ 106): leaf entries
+	 * shrink from 5 -> 4 -> 3. Once <= 4 (max_root), ext4_ext_try_to_merge_up()
+	 * copies the extents back into the inode root and frees the leaf block!
+	 */
+	k_ctx.free_calls = 0;
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 12, 2, 102, false, 0), 0);
+	KUNIT_EXPECT_EQ(test, ext_depth(inode), 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 4);
+	KUNIT_EXPECT_EQ(test, k_ctx.free_calls, 1U);
+
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 16, 2, 106, false, 0), 0);
+	KUNIT_EXPECT_EQ(test, ext_depth(inode), 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+	ex = EXT_FIRST_EXTENT(eh);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 8U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 12);
+}
+
+/*
+ * Verify ext4_ext_remove_space() for middle-of-extent hole punching, full
+ * extent removal with left-scooting, and ext4_ext_truncate() tail removal.
+ */
+static void test_remove_space_tail_and_hole_punch(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
+	struct ext4_extent *ex;
+	int err;
+
+	ext_reset_tree(inode);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 10, 100, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 30, 10, 200, false, 0), 0);
+
+	/*
+	 * Punch hole [12, 17] inside [10, 20): splits [10, 20) into [10, 12)
+	 * and [18, 20), leaving 3 extents total.
+	 */
+	down_write(&EXT4_I(inode)->i_data_sem);
+	err = ext4_ext_remove_space(inode, 12, 17);
+	up_write(&EXT4_I(inode)->i_data_sem);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+	ex = EXT_FIRST_EXTENT(eh);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 2);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[1].ee_block), 18U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[1]), 2);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[1]), 108ULL);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[2].ee_block), 30U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[2]), 10);
+
+	/*
+	 * Punch out the entire middle extent [18, 19]: scoots [30, 40) into
+	 * slot 1 and reduces eh_entries to 2.
+	 */
+	down_write(&EXT4_I(inode)->i_data_sem);
+	err = ext4_ext_remove_space(inode, 18, 19);
+	up_write(&EXT4_I(inode)->i_data_sem);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 2);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[1].ee_block), 30U);
+
+	/*
+	 * Truncate file to 11 blocks (i_size = 11 * 4096): removes [30, 40)
+	 * and trims [10, 12) down to [10, 11).
+	 */
+	inode->i_size = 11ULL << inode->i_sb->s_blocksize_bits;
+	down_write(&EXT4_I(inode)->i_data_sem);
+	err = ext4_ext_truncate(NULL, inode);
+	up_write(&EXT4_I(inode)->i_data_sem);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 1);
+}
+
+/*
+ * Verify ext4_ext_remove_space() on a depth-1 tree removes all leaf extents,
+ * removes empty index entries via ext4_ext_rm_idx(), frees the leaf metadata
+ * blocks, and resets the root header to eh_depth == 0.
+ */
+static void test_remove_space_multilevel_collapse(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
+	int err;
+
+	setup_depth1_two_leaf_tree(test, inode);
+	k_ctx.free_calls = 0;
+	k_ctx.total_freed_blocks = 0;
+
+	down_write(&EXT4_I(inode)->i_data_sem);
+	err = ext4_ext_remove_space(inode, 0, EXT_MAX_BLOCKS - 1);
+	up_write(&EXT4_I(inode)->i_data_sem);
+
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, ext_depth(inode), 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_max), 4);
+	/* 4 data extents (5 blocks each = 20) + 2 leaf metadata blocks = 22 */
+	KUNIT_EXPECT_EQ(test, k_ctx.total_freed_blocks, 22UL);
+}
+
+/*
+ * Verify bigalloc partial cluster handling in ext4_ext_remove_space():
+ * 1. When a cluster is shared with a remaining extent (state == nofree),
+ *    the physical cluster is not freed.
+ * 2. When the entire unaligned extent is removed and has a pending reservation
+ *    in i_pending_tree, the cluster is freed with RERESERVE_CLUSTER and
+ *    ext4_rereserve_cluster() re-increments i_reserved_data_blocks and removes
+ *    the pending reservation.
+ */
+static void test_remove_space_bigalloc_partial_cluster(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+	struct ext4_inode_info *ei = EXT4_I(inode);
+	int err;
+
+	sbi->s_cluster_bits = 2;
+	sbi->s_cluster_ratio = 4;
+
+	/*
+	 * Case 1: [0, 2) @ 100 and [2, 4) @ 102 share physical cluster 25
+	 * (blocks 100..103). Punching [0, 1] must NOT free cluster 25.
+	 */
+	ext_reset_tree(inode);
+	KUNIT_ASSERT_EQ(test,
+			ext_insert_one(inode, 0, 2, 100, false,
+				       EXT4_GET_BLOCKS_SPLIT_NOMERGE),
+			0);
+	KUNIT_ASSERT_EQ(test,
+			ext_insert_one(inode, 2, 2, 102, false,
+				       EXT4_GET_BLOCKS_SPLIT_NOMERGE),
+			0);
+
+	k_ctx.free_calls = 0;
+	k_ctx.total_freed_blocks = 0;
+	down_write(&ei->i_data_sem);
+	err = ext4_ext_remove_space(inode, 0, 1);
+	up_write(&ei->i_data_sem);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	/*
+	 * ext4_remove_blocks() is called with NOFREE_FIRST_CLUSTER |
+	 * NOFREE_LAST_CLUSTER, and partial cluster 25 is marked nofree.
+	 */
+	KUNIT_EXPECT_EQ(test, k_ctx.free_calls, 1U);
+	KUNIT_EXPECT_TRUE(test,
+			  (k_ctx.last_free_flags &
+			   EXT4_FREE_BLOCKS_NOFREE_FIRST_CLUSTER) != 0);
+
+	/*
+	 * Case 2: Single unaligned extent [1, 3) @ 101 in cluster 0 (pclu 25)
+	 * with a pending reservation on cluster 0. Truncating from 1 frees
+	 * cluster 25 with RERESERVE_CLUSTER and calls ext4_rereserve_cluster().
+	 */
+	ext_reset_tree(inode);
+	set_opt(inode->i_sb, DELALLOC);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 1, 2, 101, true, 0), 0);
+
+	down_write(&ei->i_data_sem);
+	/* Insert delayed extent at lblk 0 in already-allocated cluster 0 */
+	ext4_es_insert_delayed_extent(inode, 0, 1, true, false);
+	up_write(&ei->i_data_sem);
+	KUNIT_ASSERT_TRUE(test, ext4_is_pending(inode, 0));
+
+	ei->i_reserved_data_blocks = 0;
+	k_ctx.free_calls = 0;
+	down_write(&ei->i_data_sem);
+	err = ext4_ext_remove_space(inode, 1, EXT_MAX_BLOCKS - 1);
+	up_write(&ei->i_data_sem);
+
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, k_ctx.last_free_pblk, 100ULL);
+	KUNIT_EXPECT_EQ(test, k_ctx.last_free_count, 4UL);
+	KUNIT_EXPECT_TRUE(test,
+			  (k_ctx.last_free_flags &
+			   EXT4_FREE_BLOCKS_RERESERVE_CLUSTER) != 0);
+	KUNIT_EXPECT_EQ(test, ei->i_reserved_data_blocks, 1U);
+	KUNIT_EXPECT_FALSE(test, ext4_is_pending(inode, 0));
+}
+
+/*
+ * Verify ext4_ext_map_blocks() hole lookup, delayed-extent hole truncation
+ * (ext4_ext_determine_insert_hole), and EXT4_GET_BLOCKS_QUERY_LAST_IN_LEAF.
+ */
+static void test_map_blocks_lookup_hole_and_query_last_in_leaf(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_map_blocks map = {};
+	int ret;
+
+	ext_reset_tree(inode);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 5, 100, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 20, 5, 200, false, 0), 0);
+
+	/* Hole lookup at [2, 22) returns 0 and trims m_len to [2, 10) (len 8) */
+	map.m_lblk = 2;
+	map.m_len = 20;
+	map.m_flags = 0;
+	down_read(&EXT4_I(inode)->i_data_sem);
+	ret = ext4_ext_map_blocks(NULL, inode, &map, 0);
+	up_read(&EXT4_I(inode)->i_data_sem);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, map.m_pblk, 0ULL);
+	KUNIT_EXPECT_EQ(test, map.m_len, 8U);
+
+	/*
+	 * Insert a delayed extent at [5, 7) in the es tree: hole lookup at
+	 * [2, 22) must stop at the delayed extent -> [2, 5) (len 3).
+	 */
+	down_write(&EXT4_I(inode)->i_data_sem);
+	ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+	ext4_es_insert_delayed_extent(inode, 5, 2, false, false);
+	up_write(&EXT4_I(inode)->i_data_sem);
+
+	map.m_lblk = 2;
+	map.m_len = 20;
+	map.m_flags = 0;
+	down_read(&EXT4_I(inode)->i_data_sem);
+	ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_EX_NOCACHE);
+	up_read(&EXT4_I(inode)->i_data_sem);
+	KUNIT_EXPECT_EQ(test, ret, 0);
+	KUNIT_EXPECT_EQ(test, map.m_len, 3U);
+
+	/* QUERY_LAST_IN_LEAF on first extent [10, 15) -> flag NOT set */
+	map.m_lblk = 10;
+	map.m_len = 5;
+	map.m_flags = 0;
+	down_read(&EXT4_I(inode)->i_data_sem);
+	ret = ext4_ext_map_blocks(NULL, inode, &map,
+				  EXT4_GET_BLOCKS_QUERY_LAST_IN_LEAF);
+	up_read(&EXT4_I(inode)->i_data_sem);
+	KUNIT_EXPECT_EQ(test, ret, 5);
+	KUNIT_EXPECT_FALSE(test,
+			   (map.m_flags & EXT4_MAP_QUERY_LAST_IN_LEAF) != 0);
+
+	/* QUERY_LAST_IN_LEAF on last extent [20, 25) -> flag IS set */
+	map.m_lblk = 20;
+	map.m_len = 5;
+	map.m_flags = 0;
+	down_read(&EXT4_I(inode)->i_data_sem);
+	ret = ext4_ext_map_blocks(NULL, inode, &map,
+				  EXT4_GET_BLOCKS_QUERY_LAST_IN_LEAF);
+	up_read(&EXT4_I(inode)->i_data_sem);
+	KUNIT_EXPECT_EQ(test, ret, 5);
+	KUNIT_EXPECT_TRUE(test,
+			  (map.m_flags & EXT4_MAP_QUERY_LAST_IN_LEAF) != 0);
+}
+
+/*
+ * Verify ext4_ext_map_blocks() block allocation with overlap clamping
+ * (ext4_ext_check_overlap) and freeing of newly allocated blocks when
+ * ext4_ext_insert_extent() fails with -ENOSPC.
+ */
+static void test_map_blocks_alloc_overlap_and_enospc(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_map_blocks map = {};
+	int ret;
+
+	ext_reset_tree(inode);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 15, 5, 200, false, 0), 0);
+
+	/*
+	 * Request allocation for [10, 20) (len 10): ext4_ext_check_overlap()
+	 * clamps the allocation to [10, 15) (len 5) to avoid overlapping [15, 20).
+	 */
+	k_ctx.alloc_calls = 0;
+	map.m_lblk = 10;
+	map.m_len = 10;
+	map.m_flags = 0;
+	down_write(&EXT4_I(inode)->i_data_sem);
+	ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CREATE);
+	up_write(&EXT4_I(inode)->i_data_sem);
+
+	KUNIT_EXPECT_EQ(test, ret, 5);
+	KUNIT_EXPECT_EQ(test, map.m_len, 5U);
+	KUNIT_EXPECT_EQ(test, map.m_pblk, EXT_ALLOC_BASE_PBLK);
+	KUNIT_EXPECT_TRUE(test,
+			  (map.m_flags & (EXT4_MAP_NEW | EXT4_MAP_MAPPED)) ==
+				  (EXT4_MAP_NEW | EXT4_MAP_MAPPED));
+	KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 1U);
+	KUNIT_EXPECT_EQ(test, k_ctx.last_alloc_len, 5U);
+
+	/*
+	 * Force ext4_ext_insert_extent() to return -ENOSPC and verify that
+	 * ext4_ext_map_blocks() frees the newly allocated blocks.
+	 */
+	kunit_activate_static_stub(test, ext4_ext_insert_extent,
+				   ext4_ext_insert_extent_stub);
+	k_ctx.free_calls = 0;
+	map.m_lblk = 30;
+	map.m_len = 4;
+	map.m_flags = 0;
+	down_write(&EXT4_I(inode)->i_data_sem);
+	ret = ext4_ext_map_blocks(NULL, inode, &map,
+				  EXT4_GET_BLOCKS_CREATE |
+					  EXT4_GET_BLOCKS_DELALLOC_RESERVE);
+	up_write(&EXT4_I(inode)->i_data_sem);
+
+	KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+	KUNIT_EXPECT_EQ(test, k_ctx.free_calls, 1U);
+	KUNIT_EXPECT_EQ(test, k_ctx.last_free_pblk, k_ctx.last_alloc_pblk);
+	KUNIT_EXPECT_EQ(test, k_ctx.last_free_count, 4UL);
+	KUNIT_EXPECT_EQ(test, k_ctx.last_free_flags,
+			EXT4_FREE_BLOCKS_NO_QUOT_UPDATE);
+}
+
+/*
+ * Verify ext4_ext_convert_to_initialized() fastpath block transfers to
+ * abutting left (ex - 1) and right (ex + 1) initialized neighbors without
+ * splitting into new extent entries.
+ */
+static void test_convert_to_initialized_abut_neighbors(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
+	struct ext4_extent *ex;
+	struct ext4_map_blocks map = {};
+	int ret;
+
+	EXT4_SB(inode->i_sb)->s_extent_max_zeroout_kb = 0;
+	ext_reset_tree(inode);
+
+	/*
+	 * Setup: [10, 12) @ 100 (written), [12, 16) @ 102 (unwritten),
+	 * [16, 18) @ 106 (written).
+	 */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 2, 100, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 12, 4, 102, true, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 16, 2, 106, false, 0), 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+	/*
+	 * Convert head of unwritten extent [12, 14): transfers 2 blocks to
+	 * left neighbor [10, 12) -> [10, 14) written, [14, 16) unwritten.
+	 */
+	map.m_lblk = 12;
+	map.m_len = 2;
+	map.m_flags = 0;
+	down_write(&EXT4_I(inode)->i_data_sem);
+	ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CREATE);
+	up_write(&EXT4_I(inode)->i_data_sem);
+	KUNIT_EXPECT_EQ(test, ret, 2);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+	ex = EXT_FIRST_EXTENT(eh);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 4);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex[0]), 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[1].ee_block), 14U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[1]), 2);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex[1]), 1);
+
+	/*
+	 * Convert tail of unwritten extent [15, 16): transfers 1 block to
+	 * right neighbor [16, 18) -> [14, 15) unwritten, [15, 18) written.
+	 */
+	map.m_lblk = 15;
+	map.m_len = 1;
+	map.m_flags = 0;
+	down_write(&EXT4_I(inode)->i_data_sem);
+	ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CREATE);
+	up_write(&EXT4_I(inode)->i_data_sem);
+	KUNIT_EXPECT_EQ(test, ret, 1);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[1].ee_block), 14U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[1]), 1);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex[1]), 1);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[2].ee_block), 15U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[2]), 3);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[2]), 105ULL);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex[2]), 0);
+}
+
+/*
+ * Verify bigalloc get_implied_cluster_alloc() in ext4_ext_map_blocks() when
+ * an existing extent to the left or right already maps the same logical
+ * cluster, avoiding a duplicate physical cluster allocation.
+ */
+static void test_map_blocks_bigalloc_implied_cluster(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+	struct ext4_map_blocks map = {};
+	int ret;
+
+	sbi->s_cluster_bits = 2;
+	sbi->s_cluster_ratio = 4;
+
+	/*
+	 * Case 1: Left extent [8, 9) @ 100 in logical cluster 2 (lblks 8..11).
+	 * Allocating [10, 12) reuses physical cluster 25 (pblks 102..103)
+	 * without calling ext4_mb_new_blocks().
+	 */
+	ext_reset_tree(inode);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 8, 1, 100, false, 0), 0);
+
+	k_ctx.alloc_calls = 0;
+	map.m_lblk = 10;
+	map.m_len = 2;
+	map.m_flags = 0;
+	down_write(&EXT4_I(inode)->i_data_sem);
+	ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CREATE);
+	up_write(&EXT4_I(inode)->i_data_sem);
+
+	KUNIT_EXPECT_EQ(test, ret, 2);
+	KUNIT_EXPECT_EQ(test, map.m_pblk, 102ULL);
+	KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 0U);
+
+	/*
+	 * Case 2: Right extent [10, 12) @ 102 in logical cluster 2 (lblks 8..11).
+	 * Allocating [8, 10) searches right, finds [10, 12), and reuses
+	 * physical cluster 25 (pblks 100..101) without calling ext4_mb_new_blocks().
+	 */
+	ext_reset_tree(inode);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 2, 102, false, 0), 0);
+
+	k_ctx.alloc_calls = 0;
+	map.m_lblk = 8;
+	map.m_len = 2;
+	map.m_flags = 0;
+	down_write(&EXT4_I(inode)->i_data_sem);
+	ret = ext4_ext_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CREATE);
+	up_write(&EXT4_I(inode)->i_data_sem);
+
+	KUNIT_EXPECT_EQ(test, ret, 2);
+	KUNIT_EXPECT_EQ(test, map.m_pblk, 100ULL);
+	KUNIT_EXPECT_EQ(test, k_ctx.alloc_calls, 0U);
+}
+
+/*
+ * Verify ext4_ext_shift_extents() for SHIFT_LEFT (collapse range) with
+ * neighbor merging, SHIFT_RIGHT (insert range), and -EINVAL boundary checks.
+ */
+static void test_ext_shift_extents(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
+	struct ext4_extent *ex;
+	int err;
+
+	ext_reset_tree(inode);
+	/* [10, 15) @ 100 and [20, 25) @ 105 (physically contiguous!) */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 5, 100, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 20, 5, 105, false, 0), 0);
+
+	/* Shifting [20, 25) left by 6 overlaps [10, 15) -> -EINVAL */
+	down_write(&EXT4_I(inode)->i_data_sem);
+	err = ext4_ext_shift_extents_test(inode, NULL, 20, 6, SHIFT_LEFT);
+	up_write(&EXT4_I(inode)->i_data_sem);
+	KUNIT_EXPECT_EQ(test, err, -EINVAL);
+
+	/* Shifting [20, 25) left by 5 merges with [10, 15) into [10, 20) @ 100 */
+	down_write(&EXT4_I(inode)->i_data_sem);
+	err = ext4_ext_shift_extents_test(inode, NULL, 20, 5, SHIFT_LEFT);
+	up_write(&EXT4_I(inode)->i_data_sem);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+	ex = EXT_FIRST_EXTENT(eh);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 10);
+
+	/* Shifting [10, 20) right by 4 moves it to [14, 24) @ 100 */
+	down_write(&EXT4_I(inode)->i_data_sem);
+	err = ext4_ext_shift_extents_test(inode, NULL, 10, 4, SHIFT_RIGHT);
+	up_write(&EXT4_I(inode)->i_data_sem);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 14U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 10);
+
+	/* Shifting right past EXT_MAX_BLOCKS -> -EINVAL */
+	down_write(&EXT4_I(inode)->i_data_sem);
+	err = ext4_ext_shift_extents_test(inode, NULL, 14, EXT_MAX_BLOCKS,
+					  SHIFT_RIGHT);
+	up_write(&EXT4_I(inode)->i_data_sem);
+	KUNIT_EXPECT_EQ(test, err, -EINVAL);
+}
+
+/*
+ * Verify ext4_swap_extents() splits unaligned boundaries on both inodes,
+ * swaps physical block mappings for the requested subrange, and marks
+ * the donor inode's swapped extent unwritten when requested.
+ */
+static void test_swap_extents(struct kunit *test)
+{
+	struct inode *inode1 = &k_ctx.k_ei->vfs_inode;
+	struct inode *inode2;
+	struct ext4_extent_header *eh1, *eh2;
+	struct ext4_extent *ex1, *ex2;
+	int replaced, err = 0;
+
+	k_ctx.k_ei2 = kzalloc_obj(struct ext4_inode_info);
+	KUNIT_ASSERT_NOT_NULL(test, k_ctx.k_ei2);
+	ext_init_mock_inode(k_ctx.k_ei2, inode1->i_sb, 13);
+	inode2 = &k_ctx.k_ei2->vfs_inode;
+
+	ext_reset_tree(inode1);
+	ext_reset_tree(inode2);
+
+	/* inode1: [10, 20) @ 100; inode2: [10, 20) @ 200 */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode1, 10, 10, 100, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode2, 10, 10, 200, false, 0), 0);
+
+	inode_lock(inode1);
+	inode_lock(inode2);
+	down_write(&EXT4_I(inode1)->i_data_sem);
+	down_write(&EXT4_I(inode2)->i_data_sem);
+
+	replaced = ext4_swap_extents(NULL, inode1, inode2, 12, 12, 5, 1, &err);
+
+	up_write(&EXT4_I(inode2)->i_data_sem);
+	up_write(&EXT4_I(inode1)->i_data_sem);
+	inode_unlock(inode2);
+	inode_unlock(inode1);
+
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, replaced, 5);
+
+	eh1 = ext_inode_hdr(inode1);
+	eh2 = ext_inode_hdr(inode2);
+	KUNIT_ASSERT_EQ(test, le16_to_cpu(eh1->eh_entries), 3);
+	KUNIT_ASSERT_EQ(test, le16_to_cpu(eh2->eh_entries), 3);
+
+	ex1 = EXT_FIRST_EXTENT(eh1);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex1[0].ee_block), 10U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex1[0]), 100ULL);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex1[1].ee_block), 12U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex1[1]), 5);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex1[1]), 202ULL);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex1[1]), 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex1[2].ee_block), 17U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex1[2]), 107ULL);
+
+	ex2 = EXT_FIRST_EXTENT(eh2);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex2[0].ee_block), 10U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex2[0]), 200ULL);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex2[1].ee_block), 12U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex2[1]), 5);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex2[1]), 102ULL);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex2[1]), 1);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex2[2].ee_block), 17U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex2[2]), 207ULL);
+}
+
+/*
+ * Verify ext4_clu_mapped() across inline-data inodes, empty trees, clusters
+ * mapped at the start or end of a cluster, and unmapped clusters.
+ */
+static void test_clu_mapped(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+
+	sbi->s_cluster_bits = 2;
+	sbi->s_cluster_ratio = 4;
+
+	ext_reset_tree(inode);
+	/* Empty tree -> unmapped */
+	KUNIT_EXPECT_EQ(test, ext4_clu_mapped(inode, 0), 0);
+
+	/*
+	 * Insert [0, 2) @ 100 (in lclu 0 = lblks 0..3) and
+	 * [6, 8) @ 106 (in lclu 1 = lblks 4..7).
+	 */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 0, 2, 100, false, 0), 0);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 6, 2, 106, false, 0), 0);
+
+	/* Inline-data state returns 0 immediately */
+	ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+	KUNIT_EXPECT_EQ(test, ext4_clu_mapped(inode, 0), 0);
+	ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+
+	/* lclu 0 (lblk 0) is mapped directly by [0, 2) */
+	KUNIT_EXPECT_EQ(test, ext4_clu_mapped(inode, 0), 1);
+	/* lclu 1 (lblk 4) is mapped by next allocated extent [6, 8) */
+	KUNIT_EXPECT_EQ(test, ext4_clu_mapped(inode, 1), 1);
+	/* lclu 2 (lblk 8..11) is unmapped */
+	KUNIT_EXPECT_EQ(test, ext4_clu_mapped(inode, 2), 0);
+}
+
+/*
+ * Verify ext4_ext_index_trans_blocks() and
+ * ext4_ext_calc_credits_for_single_extent() credit calculations.
+ */
+static void test_ext_credits_calculation(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
+	struct ext4_ext_path *path;
+	int credits_with_space, credits_full;
+
+	/* 1 extent vs multiple extents */
+	KUNIT_EXPECT_EQ(test, ext4_ext_index_trans_blocks(inode, 1),
+			EXT4_MAX_EXTENT_DEPTH * 2 + 1);
+	KUNIT_EXPECT_EQ(test, ext4_ext_index_trans_blocks(inode, 2),
+			EXT4_MAX_EXTENT_DEPTH * 3 + 2);
+
+	/* Inline data inode needs only 1 index block credit */
+	ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+	EXT4_I(inode)->i_inline_off = 1;
+	KUNIT_EXPECT_EQ(test, ext4_ext_index_trans_blocks(inode, 2), 1);
+	EXT4_I(inode)->i_inline_off = 0;
+	ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+
+	ext_reset_tree(inode);
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 2, 100, false, 0), 0);
+
+	path = ext4_find_extent(inode, 10, NULL, EXT4_EX_NOCACHE);
+	KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(path));
+	/* Leaf has free space (eh_entries = 1 < eh_max = 4) */
+	credits_with_space =
+		ext4_ext_calc_credits_for_single_extent(inode, 1, path);
+
+	/* Mark leaf full (eh_entries == eh_max) */
+	eh->eh_entries = eh->eh_max;
+	credits_full = ext4_ext_calc_credits_for_single_extent(inode, 1, path);
+	eh->eh_entries = cpu_to_le16(1);
+	ext4_free_ext_path(path);
+
+	KUNIT_EXPECT_LT(test, credits_with_space, credits_full);
+	KUNIT_EXPECT_EQ(test, credits_full,
+			ext4_ext_calc_credits_for_single_extent(inode, 1, NULL));
+}
+
+/*
+ * Verify fast-commit replay extent helpers:
+ * - ext4_ext_replay_update_ex() splits and updates extent state/pblk
+ * - ext4_ext_replay_shrink_inode() merges adjacent compatible extents
+ * - ext4_ext_replay_set_iblocks() counts data and extent tree blocks and
+ *   updates inode->i_blocks.
+ */
+static void test_ext_fast_commit_replay(struct kunit *test)
+{
+	struct inode *inode = &k_ctx.k_ei->vfs_inode;
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
+	struct ext4_extent *ex;
+	int err;
 
-	return;
+	ext_reset_tree(inode);
+	/* Start with unwritten extent [10, 20) @ 100 */
+	KUNIT_ASSERT_EQ(test, ext_insert_one(inode, 10, 10, 100, true, 0), 0);
+
+	/* Replay update converting middle [12, 16) to written @ 102 */
+	err = ext4_ext_replay_update_ex(inode, 12, 4, 0, 102);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+	/* Replay update converting [10, 12) @ 100 and [16, 20) @ 106 to written */
+	KUNIT_ASSERT_EQ(test, ext4_ext_replay_update_ex(inode, 10, 2, 0, 100),
+			0);
+	KUNIT_ASSERT_EQ(test, ext4_ext_replay_update_ex(inode, 16, 4, 0, 106),
+			0);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 3);
+
+	/* Shrink inode merges the 3 contiguous written extents into [10, 20) */
+	ext4_ext_replay_shrink_inode(inode, 25);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_entries), 1);
+	ex = EXT_FIRST_EXTENT(eh);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 10U);
+	KUNIT_EXPECT_EQ(test, ext4_ext_get_actual_len(&ex[0]), 10);
+	KUNIT_EXPECT_EQ(test, ext4_ext_is_unwritten(&ex[0]), 0);
+
+	/* Recalculate i_blocks for the 10-block extent (10 * 8 = 80 sectors) */
+	inode->i_size = 30ULL << inode->i_sb->s_blocksize_bits;
+	inode->i_blocks = 0;
+	err = ext4_ext_replay_set_iblocks(inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, inode->i_blocks,
+			10ULL << (inode->i_sb->s_blocksize_bits - 9));
 }
 
 static const struct kunit_ext_test_param test_split_convert_params[] = {
@@ -1049,6 +2774,31 @@ static struct kunit_case extents_test_cases[] = {
 				   test_convert_initialized_param_init, NULL),
 	KUNIT_CASE_PARAM_WITH_INIT(test_split_convert, kunit_array_gen_params,
 				   test_handle_unwritten_init, NULL),
+	KUNIT_CASE(test_ext_pblock_48bit),
+	KUNIT_CASE(test_ext_unwritten_len_encoding),
+	KUNIT_CASE(test_ext_tree_init_and_check_inode),
+	KUNIT_CASE_PARAM_WITH_INIT(test_ext_check_corruptions,
+				   kunit_array_gen_params,
+				   ext_check_corrupt_param_init, NULL),
+	KUNIT_CASE(test_ext_block_csum_verify),
+	KUNIT_CASE(test_find_extent_root_binsearch),
+	KUNIT_CASE(test_find_extent_multilevel_and_navigation),
+	KUNIT_CASE(test_ext_precache),
+	KUNIT_CASE(test_can_extents_be_merged),
+	KUNIT_CASE(test_insert_extent_merge_and_nomerge),
+	KUNIT_CASE(test_insert_extent_grow_and_merge_up),
+	KUNIT_CASE(test_remove_space_tail_and_hole_punch),
+	KUNIT_CASE(test_remove_space_multilevel_collapse),
+	KUNIT_CASE(test_remove_space_bigalloc_partial_cluster),
+	KUNIT_CASE(test_map_blocks_lookup_hole_and_query_last_in_leaf),
+	KUNIT_CASE(test_map_blocks_alloc_overlap_and_enospc),
+	KUNIT_CASE(test_convert_to_initialized_abut_neighbors),
+	KUNIT_CASE(test_map_blocks_bigalloc_implied_cluster),
+	KUNIT_CASE(test_ext_shift_extents),
+	KUNIT_CASE(test_swap_extents),
+	KUNIT_CASE(test_clu_mapped),
+	KUNIT_CASE(test_ext_credits_calculation),
+	KUNIT_CASE(test_ext_fast_commit_replay),
 	{}
 };
 
diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
index 6c018bcc0118..efbee7aaa95d 100644
--- a/fs/ext4/extents.c
+++ b/fs/ext4/extents.c
@@ -547,6 +547,13 @@ static void ext4_cache_extents(struct inode *inode,
 	}
 }
 
+struct buffer_head *ext4_ext_getblk(struct inode *inode, ext4_fsblk_t pblk,
+				    gfp_t gfp_flags)
+{
+	KUNIT_STATIC_STUB_REDIRECT(ext4_ext_getblk, inode, pblk, gfp_flags);
+	return sb_getblk_gfp(inode->i_sb, pblk, gfp_flags);
+}
+
 static struct buffer_head *
 __read_extent_tree_block(const char *function, unsigned int line,
 			 struct inode *inode, struct ext4_extent_idx *idx,
@@ -561,7 +568,7 @@ __read_extent_tree_block(const char *function, unsigned int line,
 		gfp_flags |= __GFP_NOFAIL;
 
 	pblk = ext4_idx_pblock(idx);
-	bh = sb_getblk_gfp(inode->i_sb, pblk, gfp_flags);
+	bh = ext4_ext_getblk(inode, pblk, gfp_flags);
 	if (unlikely(!bh))
 		return ERR_PTR(-ENOMEM);
 
@@ -1124,7 +1131,7 @@ static int ext4_ext_split(handle_t *handle, struct inode *inode,
 		err = -EFSCORRUPTED;
 		goto cleanup;
 	}
-	bh = sb_getblk_gfp(inode->i_sb, newblock, __GFP_MOVABLE | GFP_NOFS);
+	bh = ext4_ext_getblk(inode, newblock, __GFP_MOVABLE | GFP_NOFS);
 	if (unlikely(!bh)) {
 		err = -ENOMEM;
 		goto cleanup;
@@ -1203,7 +1210,7 @@ static int ext4_ext_split(handle_t *handle, struct inode *inode,
 	while (k--) {
 		oldblock = newblock;
 		newblock = ablocks[--a];
-		bh = sb_getblk(inode->i_sb, newblock);
+		bh = ext4_ext_getblk(inode, newblock, __GFP_MOVABLE | GFP_NOFS);
 		if (unlikely(!bh)) {
 			err = -ENOMEM;
 			goto cleanup;
@@ -1332,7 +1339,7 @@ static int ext4_ext_grow_indepth(handle_t *handle, struct inode *inode,
 	if (newblock == 0)
 		return err;
 
-	bh = sb_getblk_gfp(inode->i_sb, newblock, __GFP_MOVABLE | GFP_NOFS);
+	bh = ext4_ext_getblk(inode, newblock, __GFP_MOVABLE | GFP_NOFS);
 	if (unlikely(!bh))
 		return -ENOMEM;
 	lock_buffer(bh);
@@ -6405,11 +6412,79 @@ struct ext4_ext_path *ext4_split_convert_extents_test(handle_t *handle,
 }
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_split_convert_extents_test);
 
+int ext4_ext_check_test(struct inode *inode, struct ext4_extent_header *eh,
+			int depth, ext4_fsblk_t pblk, ext4_lblk_t lblk)
+{
+	return __ext4_ext_check(__func__, __LINE__, inode, eh, depth, pblk,
+				lblk);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_check_test);
+
+void ext4_extent_block_csum_set_test(struct inode *inode,
+				     struct ext4_extent_header *eh)
+{
+	ext4_extent_block_csum_set(inode, eh);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_extent_block_csum_set_test);
+
+int ext4_can_extents_be_merged_test(struct inode *inode,
+				    struct ext4_extent *ex1,
+				    struct ext4_extent *ex2)
+{
+	return ext4_can_extents_be_merged(inode, ex1, ex2);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_can_extents_be_merged_test);
+
+int ext4_ext_search_left_test(struct inode *inode, struct ext4_ext_path *path,
+			      ext4_lblk_t *logical, ext4_fsblk_t *phys)
+{
+	return ext4_ext_search_left(inode, path, logical, phys);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_search_left_test);
+
+int ext4_ext_search_right_test(struct inode *inode, struct ext4_ext_path *path,
+			       ext4_lblk_t *logical, ext4_fsblk_t *phys,
+			       struct ext4_extent *ret_ex, int flags)
+{
+	return ext4_ext_search_right(inode, path, logical, phys, ret_ex, flags);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_search_right_test);
+
+ext4_lblk_t ext4_ext_next_leaf_block_test(struct ext4_ext_path *path)
+{
+	return ext4_ext_next_leaf_block(path);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_next_leaf_block_test);
+
+int ext4_ext_shift_extents_test(struct inode *inode, handle_t *handle,
+				ext4_lblk_t start, ext4_lblk_t shift,
+				enum SHIFT_DIRECTION direction)
+{
+	return ext4_ext_shift_extents(inode, handle, start, shift, direction);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_shift_extents_test);
+
 EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_ext_dirty);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_getblk);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_zeroout);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_map_create_blocks);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_insert_extent);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_extent);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_issue_zeroout);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_map_query_blocks);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_ext_path);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_tree_init);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_check_inode);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_precache);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_next_allocated_block);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_calc_credits_for_single_extent);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_index_trans_blocks);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_remove_space);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_truncate);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_map_blocks);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_swap_extents);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_clu_mapped);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_replay_update_ex);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_replay_shrink_inode);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_replay_set_iblocks);
 #endif
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 966a760f64e6..289f7fa35090 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -6699,6 +6699,9 @@ int __ext4_mark_inode_dirty(handle_t *handle, struct inode *inode,
 	struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
 	int err;
 
+	KUNIT_STATIC_STUB_REDIRECT(__ext4_mark_inode_dirty, handle, inode,
+				   func, line);
+
 	might_sleep();
 	trace_ext4_mark_inode_dirty(inode, _RET_IP_);
 	err = ext4_reserve_inode_write(handle, inode, &iloc);
@@ -6982,4 +6985,6 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
 #if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_update_reserve_space);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_release_space);
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_mark_inode_dirty);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_map_blocks);
 #endif
diff --git a/fs/ext4/mballoc.c b/fs/ext4/mballoc.c
index 06171a11db12..0f73fb826dbf 100644
--- a/fs/ext4/mballoc.c
+++ b/fs/ext4/mballoc.c
@@ -6239,6 +6239,8 @@ ext4_fsblk_t ext4_mb_new_blocks(handle_t *handle,
 	int retries = 0;
 	u64 seq;
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_mb_new_blocks, handle, ar, errp);
+
 	might_sleep();
 	sb = ar->inode->i_sb;
 	sbi = EXT4_SB(sb);
@@ -6697,6 +6699,9 @@ void ext4_free_blocks(handle_t *handle, struct inode *inode,
 	unsigned int overflow;
 	struct ext4_sb_info *sbi;
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_free_blocks, handle, inode, bh, block,
+				   count, flags);
+
 	sbi = EXT4_SB(sb);
 
 	if (bh) {
@@ -7281,4 +7286,6 @@ EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_block_bitmap_nowait);
 EXPORT_SYMBOL_FOR_EXT4_TEST(mb_set_bits);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_init_inode);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_mark_context);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_new_blocks);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_blocks);
 #endif
-- 
2.56.0.542.g3ef89b7020-goog


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

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-3-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