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

Add the ext4_orphan_test KUnit test suite in fs/ext4/orphan-test.c (22
test cases, 25 subtests total when CONFIG_QUOTA is enabled; 21 test
cases, 24 subtests otherwise) to comprehensively exercise orphan inode
tracking, orphan file management, and mount-time orphan recovery in
fs/ext4/orphan.c using an in-memory VFS superblock, mock block device,
and deterministic buffer pool without requiring a mounted block device:

- Orphan block geometry and empty-state checks
  (ext4_inodes_per_orphan_block across 1K, 2K, 4K, and 64K block sizes,
  and ext4_orphan_file_empty across disabled feature, zero-block files,
  and multi-block occupancy transitions).
- Superblock linked-list orphan insertion (ext4_orphan_add), covering
  no-journal and bad-inode early exits, I_NEW unlocked insertion and
  deletion, idempotency when already tracked via i_orphan or
  EXT4_STATE_ORPHAN_FILE, LIFO chaining across regular, directory, and
  symlink inodes with superblock CRC32C updates, NEXT_ORPHAN boundary
  checks at s_inodes_count vs. s_inodes_count + 1, and journal/iloc
  error rollbacks without leaking buffer references or leaving stray
  s_orphan entries.
- Superblock linked-list orphan removal (ext4_orphan_del), covering
  untracked inodes, no-journal early exit vs. EXT4_ORPHAN_FS recovery
  mode, head/middle/tail unlinking, NULL-handle in-core-only cleanup,
  and error propagation across superblock and predecessor/target inode
  journal operations.
- Orphan-file insertion and deletion (ext4_orphan_file_add and
  ext4_orphan_file_del), covering EXT4_JTR_ORPHAN_FILE trigger wiring,
  slot claiming and freeing, circular block-ring wrap-around and final
  slot allocation, -ENOSPC fallback to the superblock linked list when
  all orphan blocks are full, non-ENOSPC journal write-access and
  dirty-metadata error propagation without fallback, NULL-handle state
  cleanup, and out-of-range block index warning guards.
- Orphan-file JBD2 frozen-buffer checksum trigger and lifecycle
  management (ext4_orphan_file_block_trigger on live and frozen buffers,
  ext4_init_orphan_info, ext4_orphan_file_block_csum_verify with and
  without metadata_csum, the 512-block EXT4_MAX_ORPHAN_FILE_BLOCKS size
  boundary, partial-load error cleanup on iget/bread/hole/bad-magic/
  bad-checksum failures, and ext4_release_orphan_info).
- End-to-end mount-time orphan recovery (ext4_orphan_cleanup and
  ext4_process_orphan), covering empty-fs, read-only bdev, incompatible
  ROCOMPAT features, and EXT4_ERROR_FS skip guards, unlinked file and
  fast-symlink eviction, linked-inode truncation, combined linked-list
  and orphan-file recovery with corrupt-slot skipping and SB_RDONLY
  restoration, truncate failure handling via ext4_orphan_del(NULL, ...)
  and mid-loop EXT4_ERROR_FS abort, and quota activation/teardown paths
  under CONFIG_QUOTA and CONFIG_QFMT_V2.

Add a KUNIT_STATIC_STUB_REDIRECT hook to ext4_truncate() in
fs/ext4/inode.c for truncate failure injection during orphan recovery,
and export the remaining non-static fs/ext4/orphan.c entry points with
EXPORT_SYMBOL_FOR_EXT4_TEST.

Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
 fs/ext4/Makefile      |    2 +-
 fs/ext4/inode.c       |    2 +
 fs/ext4/orphan-test.c | 2364 +++++++++++++++++++++++++++++++++++++++++
 fs/ext4/orphan.c      |    5 +
 4 files changed, 2372 insertions(+), 1 deletion(-)
 create mode 100644 fs/ext4/orphan-test.c

diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 7d0ae7fd4448..9bad2bff0de9 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -19,7 +19,7 @@ ext4-test-objs				+= inode-test.o mballoc-test.o \
 					   extents_status-test.o xattr-test.o \
 					   namei-test.o inline-test.o \
 					   fast_commit-test.o ialloc-test.o \
-					   balloc-test.o
+					   balloc-test.o orphan-test.o
 obj-$(CONFIG_EXT4_KUNIT_TESTS)		+= ext4-test.o
 ext4-$(CONFIG_FS_VERITY)		+= verity.o
 ext4-$(CONFIG_FS_ENCRYPTION)		+= crypto.o
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 98444d8ddb40..92f20d7606ea 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4686,6 +4686,8 @@ int ext4_truncate(struct inode *inode)
 	int err = 0, err2;
 	handle_t *handle;
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_truncate, inode);
+
 	/*
 	 * There is a possibility that we're either freeing the inode
 	 * or it's a completely new inode. In those cases we might not
diff --git a/fs/ext4/orphan-test.c b/fs/ext4/orphan-test.c
new file mode 100644
index 000000000000..f93057df1d31
--- /dev/null
+++ b/fs/ext4/orphan-test.c
@@ -0,0 +1,2364 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/orphan.c
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/crc32c.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/iversion.h>
+#include <linux/jbd2.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/part_stat.h>
+#include <linux/percpu.h>
+#include <linux/quotaops.h>
+#include <linux/rcupdate.h>
+#include <linux/slab.h>
+#include <linux/stat.h>
+#include <linux/timer.h>
+#include <linux/workqueue.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "ext4_extents.h"
+#include "xattr.h"
+
+/*
+ * Mock filesystem geometry:
+ *  - 2 block groups, 1024-byte blocks (254 orphan entries per orphan block)
+ *  - 64 inodes per group (128 total inodes), 256-byte inodes
+ *  - s_first_ino = 11, s_orphan_file_inum = 12
+ */
+#define ORPHAN_TEST_NGROUPS		2U
+#define ORPHAN_TEST_BLK_SIZE		1024U
+#define ORPHAN_TEST_BLK_BITS		10U
+#define ORPHAN_TEST_BLOCKS_PER_GRP	1024U
+#define ORPHAN_TEST_FIRST_DATA_BLK	1U
+#define ORPHAN_TEST_TOTAL_BLKS		(ORPHAN_TEST_FIRST_DATA_BLK + \
+					 ORPHAN_TEST_NGROUPS * \
+					 ORPHAN_TEST_BLOCKS_PER_GRP)
+#define ORPHAN_TEST_INODE_SIZE		256U
+#define ORPHAN_TEST_EXTRA_ISIZE		32U
+#define ORPHAN_TEST_INODES_PER_BLK	(ORPHAN_TEST_BLK_SIZE / \
+					 ORPHAN_TEST_INODE_SIZE)
+#define ORPHAN_TEST_INODES_PER_GRP	64U
+#define ORPHAN_TEST_ITB_PER_GRP		(ORPHAN_TEST_INODES_PER_GRP / \
+					 ORPHAN_TEST_INODES_PER_BLK)
+#define ORPHAN_TEST_TOTAL_INODES	(ORPHAN_TEST_NGROUPS * \
+					 ORPHAN_TEST_INODES_PER_GRP)
+#define ORPHAN_TEST_FIRST_INO		11U
+#define ORPHAN_TEST_ORPHAN_INO		12U
+#define ORPHAN_TEST_CSUM_SEED		0x4d7a19c5U
+#define ORPHAN_TEST_OF_CSUM_SEED	0x91e23b77U
+#define ORPHAN_TEST_INODES_PER_OB	254U
+
+#define ORPHAN_TEST_GDESC_BLK		2ULL
+#define ORPHAN_TEST_OB_BASE_BLK		500ULL
+#define ORPHAN_TEST_MAX_OB_BLKS		4U
+#define ORPHAN_TEST_MAX_SLOTS		16U
+
+struct orphan_test_ibitmap {
+	struct buffer_head bh;
+	char buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+};
+
+struct orphan_test_ob_blk {
+	bool present;
+	int bread_err;
+	bool return_hole;
+	struct buffer_head bh;
+	char buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+};
+
+struct orphan_test_inode_slot {
+	bool in_use;
+	unsigned long ino;
+	bool handoff_to_iget;
+	struct inode *inode;
+	struct buffer_head iloc_bh;
+	char raw_buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+};
+
+struct orphan_test_ctx {
+	struct super_block *sb;
+	struct ext4_sb_info sbi;
+	struct ext4_super_block es;
+	struct blockgroup_lock bgl;
+	struct gendisk bdev_disk;
+	struct block_device bdev;
+	struct request_queue bdev_queue;
+	struct inode bd_inode;
+	struct buffer_head sb_bh;
+	struct buffer_head gdesc_bh;
+	char gdesc_buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+
+	struct ext4_group_desc gdps[ORPHAN_TEST_NGROUPS];
+	struct ext4_group_info grp_infos[ORPHAN_TEST_NGROUPS];
+	struct ext4_group_info *grp_info_ptrs[16];
+	struct ext4_group_info **grp_info_slice[1];
+	struct orphan_test_ibitmap ibitmaps[ORPHAN_TEST_NGROUPS];
+
+	journal_t mock_journal;
+	handle_t mock_handle;
+
+	struct orphan_test_ob_blk ob_pool[ORPHAN_TEST_MAX_OB_BLKS];
+	struct ext4_orphan_block static_binfo[ORPHAN_TEST_MAX_OB_BLKS];
+
+	/* Shared read-only block used for 512-block max-size boundary test */
+	bool use_boundary_bh;
+	struct buffer_head boundary_bh;
+	char boundary_buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+	char frozen_buf[ORPHAN_TEST_BLK_SIZE] __aligned(8);
+#ifdef CONFIG_QUOTA
+	char qf_name[16];
+#endif
+
+	struct orphan_test_inode_slot slots[ORPHAN_TEST_MAX_SLOTS];
+	bool orphan_info_allocated;
+	bool enable_real_evict;
+
+	/* Fault injection controls */
+	int fail_write_access_err;
+	int fail_write_access_on_call;
+	int fail_dirty_metadata_err;
+	int fail_dirty_metadata_on_call;
+	int fail_get_iloc_err;
+	int fail_get_iloc_on_call;
+	int fail_mark_iloc_err;
+	int fail_mark_iloc_on_call;
+	int fail_mark_inode_dirty_err;
+	int fail_iget_err;
+	unsigned long fail_iget_ino;
+	int stub_truncate_err;
+
+	/* Call tracking & verification state */
+	unsigned int write_access_calls;
+	enum ext4_journal_trigger_type last_trigger_type;
+	unsigned int dirty_metadata_calls;
+	unsigned int get_iloc_calls;
+	unsigned int mark_iloc_calls;
+	unsigned int iget_calls;
+	unsigned int truncate_calls;
+	bool saw_rw_during_cleanup;
+
+	unsigned long freed_inos[ORPHAN_TEST_MAX_SLOTS];
+	unsigned int nr_freed_inos;
+	ext4_fsblk_t last_freed_blk;
+	unsigned long total_freed_blks;
+};
+
+static inline ext4_fsblk_t orphan_test_ibitmap_blk(ext4_group_t g)
+{
+	return ORPHAN_TEST_FIRST_DATA_BLK +
+	       (ext4_fsblk_t)g * ORPHAN_TEST_BLOCKS_PER_GRP + 3ULL;
+}
+
+static inline ext4_fsblk_t orphan_test_bbitmap_blk(ext4_group_t g)
+{
+	return ORPHAN_TEST_FIRST_DATA_BLK +
+	       (ext4_fsblk_t)g * ORPHAN_TEST_BLOCKS_PER_GRP + 2ULL;
+}
+
+static inline ext4_fsblk_t orphan_test_itable_blk(ext4_group_t g)
+{
+	return ORPHAN_TEST_FIRST_DATA_BLK +
+	       (ext4_fsblk_t)g * ORPHAN_TEST_BLOCKS_PER_GRP + 4ULL;
+}
+
+static void orphan_test_sb_upd_work(struct work_struct *work)
+{
+}
+
+static void orphan_test_err_timer(struct timer_list *t)
+{
+}
+
+static void orphan_test_init_ei_fields(struct ext4_inode_info *ei)
+{
+	inode_set_iversion(&ei->vfs_inode, 1);
+	ei->i_flags = 0;
+	ext4_clear_state_flags(ei);
+	spin_lock_init(&ei->i_raw_lock);
+	ei->i_prealloc_node = RB_ROOT;
+	atomic_set(&ei->i_prealloc_active, 0);
+	rwlock_init(&ei->i_prealloc_lock);
+	ext4_es_init_tree(&ei->i_es_tree);
+	rwlock_init(&ei->i_es_lock);
+	INIT_LIST_HEAD(&ei->i_es_list);
+	ei->i_es_all_nr = 0;
+	ei->i_es_shk_nr = 0;
+	ei->i_es_shrink_lblk = 0;
+	ei->i_es_seq = 0;
+	ei->i_reserved_data_blocks = 0;
+	spin_lock_init(&ei->i_block_reservation_lock);
+	ext4_init_pending_tree(&ei->i_pending_tree);
+#ifdef CONFIG_QUOTA
+	ei->i_reserved_quota = 0;
+	memset(&ei->i_dquot, 0, sizeof(ei->i_dquot));
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+	ei->i_crypt_info = NULL;
+#endif
+	ei->jinode = NULL;
+	init_rwsem(&ei->xattr_sem);
+	INIT_LIST_HEAD(&ei->i_rsv_conversion_list);
+	spin_lock_init(&ei->i_completed_io_lock);
+	ei->i_sync_tid = 0;
+	ei->i_datasync_tid = 0;
+	INIT_LIST_HEAD(&ei->i_orphan);
+	ext4_fc_init_inode(&ei->vfs_inode);
+	spin_lock_init(&ei->i_fc_lock);
+	ei->i_metadata_bhs = NULL;
+	ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+	ei->i_extra_isize = ORPHAN_TEST_EXTRA_ISIZE;
+	ei->i_csum_seed = ORPHAN_TEST_OF_CSUM_SEED;
+	ei->i_last_alloc_group = ~0U;
+}
+
+static struct inode *orphan_test_alloc_inode(struct super_block *sb)
+{
+	struct ext4_inode_info *ei;
+
+	ei = kzalloc_obj(*ei);
+	if (!ei)
+		return NULL;
+
+	init_rwsem(&ei->i_data_sem);
+	inode_init_once(&ei->vfs_inode);
+	orphan_test_init_ei_fields(ei);
+	return &ei->vfs_inode;
+}
+
+static void orphan_test_evict_inode(struct inode *inode)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (ctx && ctx->enable_real_evict) {
+		ext4_evict_inode(inode);
+		return;
+	}
+	truncate_inode_pages_final(&inode->i_data);
+	ext4_clear_inode(inode);
+}
+
+static void orphan_test_destroy_inode(struct inode *inode)
+{
+	kfree(ext4_i_metadata_bhs(inode));
+	EXT4_I(inode)->i_metadata_bhs = NULL;
+}
+
+static void orphan_test_free_inode_mem(struct inode *inode)
+{
+	kfree(EXT4_I(inode));
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **orphan_test_get_dquots(struct inode *inode)
+{
+	return EXT4_I(inode)->i_dquot;
+}
+
+static qsize_t *orphan_test_get_reserved_space(struct inode *inode)
+{
+	return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations orphan_test_qops = {
+	.get_reserved_space = orphan_test_get_reserved_space,
+	.alloc_dquot	    = dquot_alloc,
+	.destroy_dquot	    = dquot_destroy,
+	.get_next_id	    = dquot_get_next_id,
+};
+#endif
+
+static const struct super_operations orphan_test_sops = {
+	.alloc_inode	= orphan_test_alloc_inode,
+	.evict_inode	= orphan_test_evict_inode,
+	.destroy_inode	= orphan_test_destroy_inode,
+	.free_inode	= orphan_test_free_inode_mem,
+	.drop_inode	= inode_just_drop,
+#ifdef CONFIG_QUOTA
+	.get_dquots	= orphan_test_get_dquots,
+#endif
+};
+
+static int orphan_test_init_fs_context(struct fs_context *fc)
+{
+	return 0;
+}
+
+static struct file_system_type orphan_test_fs_type = {
+	.name		 = "ext4_orphan_kunit",
+	.init_fs_context = orphan_test_init_fs_context,
+	.kill_sb	 = kill_anon_super,
+};
+
+/* --- Slot & Orphan Block Helpers --- */
+
+static struct orphan_test_inode_slot *
+orphan_test_find_slot(struct orphan_test_ctx *ctx, unsigned long ino)
+{
+	unsigned int i;
+
+	for (i = 0; i < ORPHAN_TEST_MAX_SLOTS; i++) {
+		if (ctx->slots[i].in_use && ctx->slots[i].ino == ino)
+			return &ctx->slots[i];
+	}
+	return NULL;
+}
+
+static void orphan_test_set_ibitmap_bit(struct orphan_test_ctx *ctx,
+					unsigned long ino, bool set)
+{
+	ext4_group_t g = (ino - 1) / ORPHAN_TEST_INODES_PER_GRP;
+	unsigned int bit = (ino - 1) % ORPHAN_TEST_INODES_PER_GRP;
+
+	if (g >= ORPHAN_TEST_NGROUPS)
+		return;
+	if (set)
+		ext4_set_bit(bit, ctx->ibitmaps[g].buf);
+	else
+		ext4_clear_bit(bit, ctx->ibitmaps[g].buf);
+}
+
+static struct orphan_test_inode_slot *
+orphan_test_create_inode(struct kunit *test, struct orphan_test_ctx *ctx,
+			 unsigned long ino, umode_t mode, unsigned int nlink)
+{
+	struct orphan_test_inode_slot *slot = NULL;
+	struct ext4_extent_header *eh;
+	struct inode *inode;
+	unsigned int i;
+
+	for (i = 0; i < ORPHAN_TEST_MAX_SLOTS; i++) {
+		if (!ctx->slots[i].in_use) {
+			slot = &ctx->slots[i];
+			break;
+		}
+	}
+	KUNIT_ASSERT_NOT_NULL(test, slot);
+
+	inode = new_inode(ctx->sb);
+	KUNIT_ASSERT_NOT_NULL(test, inode);
+
+	memset(slot, 0, sizeof(*slot));
+	slot->in_use = true;
+	slot->ino = ino;
+	slot->inode = inode;
+	slot->iloc_bh.b_data = slot->raw_buf;
+	slot->iloc_bh.b_size = ORPHAN_TEST_BLK_SIZE;
+	slot->iloc_bh.b_bdev = &ctx->bdev;
+	slot->iloc_bh.b_blocknr = orphan_test_itable_blk(0) + i;
+	atomic_set(&slot->iloc_bh.b_count, 1);
+	set_buffer_uptodate(&slot->iloc_bh);
+
+	inode->i_ino = ino;
+	inode->i_mode = mode;
+	inode->i_blkbits = ctx->sb->s_blocksize_bits;
+	inode->i_flags |= S_NOQUOTA;
+	set_nlink(inode, nlink);
+	EXT4_I(inode)->i_block_group = (ino - 1) / ORPHAN_TEST_INODES_PER_GRP;
+
+	if (S_ISREG(mode) || S_ISDIR(mode)) {
+		ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+		eh = ext_inode_hdr(inode);
+		memset(eh, 0, sizeof(EXT4_I(inode)->i_data));
+		eh->eh_magic = EXT4_EXT_MAGIC;
+		eh->eh_entries = 0;
+		eh->eh_max = cpu_to_le16((sizeof(EXT4_I(inode)->i_data) -
+					  sizeof(*eh)) /
+					 sizeof(struct ext4_extent));
+		eh->eh_depth = 0;
+	}
+
+	orphan_test_set_ibitmap_bit(ctx, ino, true);
+	return slot;
+}
+
+static void orphan_test_add_single_extent(struct inode *inode,
+					  ext4_lblk_t lblk, ext4_fsblk_t pblk,
+					  unsigned short len)
+{
+	struct ext4_extent_header *eh = ext_inode_hdr(inode);
+	struct ext4_extent *ex = EXT_FIRST_EXTENT(eh);
+
+	eh->eh_entries = cpu_to_le16(1);
+	ex->ee_block = cpu_to_le32(lblk);
+	ex->ee_len = cpu_to_le16(len);
+	ext4_ext_store_pblock(ex, pblk);
+	inode->i_blocks = (blkcnt_t)len << (inode->i_blkbits - 9);
+}
+
+static __u32 orphan_test_calc_ob_csum(struct super_block *sb, __u32 seed,
+				      ext4_fsblk_t blocknr, const void *data)
+{
+	__le64 dsk_block_nr = cpu_to_le64(blocknr);
+	size_t payload_bytes = sb->s_blocksize -
+			       sizeof(struct ext4_orphan_block_tail);
+	__u32 csum;
+
+	csum = crc32c(seed, &dsk_block_nr, sizeof(dsk_block_nr));
+	return crc32c(csum, data, payload_bytes);
+}
+
+static void orphan_test_seal_ob_bh(struct super_block *sb, __u32 seed,
+				   struct buffer_head *bh, __u32 magic)
+{
+	struct ext4_orphan_block_tail *ot;
+	__u32 csum;
+
+	ot = (struct ext4_orphan_block_tail *)(bh->b_data + sb->s_blocksize -
+					       sizeof(*ot));
+	ot->ob_magic = cpu_to_le32(magic);
+	csum = orphan_test_calc_ob_csum(sb, seed, bh->b_blocknr, bh->b_data);
+	ot->ob_checksum = cpu_to_le32(csum);
+}
+
+static void orphan_test_setup_static_orphan_file(struct orphan_test_ctx *ctx,
+						 int num_blocks)
+{
+	struct super_block *sb = ctx->sb;
+	struct ext4_orphan_info *oi = &ctx->sbi.s_orphan_info;
+	int i;
+
+	ext4_set_feature_orphan_file(sb);
+	oi->of_blocks = num_blocks;
+	oi->of_csum_seed = ORPHAN_TEST_OF_CSUM_SEED;
+	oi->of_binfo = ctx->static_binfo;
+	ctx->orphan_info_allocated = false;
+
+	for (i = 0; i < num_blocks; i++) {
+		struct orphan_test_ob_blk *oblk = &ctx->ob_pool[i];
+
+		memset(oblk, 0, sizeof(*oblk));
+		oblk->present = true;
+		oblk->bh.b_data = oblk->buf;
+		oblk->bh.b_size = sb->s_blocksize;
+		oblk->bh.b_bdev = &ctx->bdev;
+		oblk->bh.b_blocknr = ORPHAN_TEST_OB_BASE_BLK + i;
+		atomic_set(&oblk->bh.b_count, 1);
+		set_buffer_uptodate(&oblk->bh);
+		orphan_test_seal_ob_bh(sb, oi->of_csum_seed, &oblk->bh,
+				       EXT4_ORPHAN_BLOCK_MAGIC);
+
+		ctx->static_binfo[i].ob_bh = &oblk->bh;
+		atomic_set(&ctx->static_binfo[i].ob_free_entries,
+			   ORPHAN_TEST_INODES_PER_OB);
+	}
+}
+
+static void orphan_test_reset_faults(struct orphan_test_ctx *ctx)
+{
+	ctx->fail_write_access_err = 0;
+	ctx->fail_write_access_on_call = 0;
+	ctx->fail_dirty_metadata_err = 0;
+	ctx->fail_dirty_metadata_on_call = 0;
+	ctx->fail_get_iloc_err = 0;
+	ctx->fail_get_iloc_on_call = 0;
+	ctx->fail_mark_iloc_err = 0;
+	ctx->fail_mark_iloc_on_call = 0;
+	ctx->fail_mark_inode_dirty_err = 0;
+	ctx->fail_iget_err = 0;
+	ctx->fail_iget_ino = 0;
+	ctx->stub_truncate_err = 0;
+
+	ctx->write_access_calls = 0;
+	ctx->last_trigger_type = EXT4_JTR_NONE;
+	ctx->dirty_metadata_calls = 0;
+	ctx->get_iloc_calls = 0;
+	ctx->mark_iloc_calls = 0;
+	ctx->iget_calls = 0;
+	ctx->truncate_calls = 0;
+	ctx->saw_rw_during_cleanup = false;
+	ctx->nr_freed_inos = 0;
+	ctx->last_freed_blk = 0;
+	ctx->total_freed_blks = 0;
+	ctx->sbi.s_mount_state &= ~EXT4_ERROR_FS;
+}
+
+static bool orphan_test_was_ino_freed(const struct orphan_test_ctx *ctx,
+				      unsigned long ino)
+{
+	unsigned int i;
+
+	for (i = 0; i < ctx->nr_freed_inos; i++) {
+		if (ctx->freed_inos[i] == ino)
+			return true;
+	}
+	return false;
+}
+
+static int orphan_test_call_add(handle_t *handle, struct inode *inode)
+{
+	int err;
+
+	inode_lock(inode);
+	err = ext4_orphan_add(handle, inode);
+	inode_unlock(inode);
+	return err;
+}
+
+static int orphan_test_call_del(handle_t *handle, struct inode *inode)
+{
+	int err;
+
+	inode_lock(inode);
+	err = ext4_orphan_del(handle, inode);
+	inode_unlock(inode);
+	return err;
+}
+
+/* --- KUnit Static Stubs --- */
+
+static int
+orphan_test_get_write_access_stub(const char *where, unsigned int line,
+				  handle_t *handle, struct super_block *sb,
+				  struct buffer_head *bh,
+				  enum ext4_journal_trigger_type trigger_type)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx)
+		return 0;
+
+	ctx->write_access_calls++;
+	ctx->last_trigger_type = trigger_type;
+	if (ctx->fail_write_access_err &&
+	    (!ctx->fail_write_access_on_call ||
+	     ctx->write_access_calls == (unsigned int)ctx->fail_write_access_on_call))
+		return ctx->fail_write_access_err;
+	return 0;
+}
+
+static int orphan_test_dirty_metadata_stub(const char *where, unsigned int line,
+					   handle_t *handle,
+					   struct inode *inode,
+					   struct buffer_head *bh)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx)
+		return 0;
+
+	ctx->dirty_metadata_calls++;
+	if (ctx->fail_dirty_metadata_err &&
+	    (!ctx->fail_dirty_metadata_on_call ||
+	     ctx->dirty_metadata_calls == (unsigned int)ctx->fail_dirty_metadata_on_call))
+		return ctx->fail_dirty_metadata_err;
+
+	if (bh) {
+		set_buffer_uptodate(bh);
+		clear_buffer_dirty(bh);
+	}
+	return 0;
+}
+
+static int orphan_test_get_inode_loc_stub(struct inode *inode,
+					  struct ext4_iloc *iloc)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+	struct orphan_test_inode_slot *slot;
+
+	if (!ctx)
+		return -EIO;
+
+	ctx->get_iloc_calls++;
+	if (ctx->fail_get_iloc_err &&
+	    (!ctx->fail_get_iloc_on_call ||
+	     ctx->get_iloc_calls == (unsigned int)ctx->fail_get_iloc_on_call))
+		return ctx->fail_get_iloc_err;
+
+	slot = orphan_test_find_slot(ctx, inode->i_ino);
+	if (!slot)
+		return -ENOENT;
+
+	get_bh(&slot->iloc_bh);
+	iloc->bh = &slot->iloc_bh;
+	iloc->offset = 0;
+	iloc->block_group = EXT4_I(inode)->i_block_group;
+	return 0;
+}
+
+static int orphan_test_mark_iloc_dirty_stub(handle_t *handle,
+					    struct inode *inode,
+					    struct ext4_iloc *iloc)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+	int err = 0;
+
+	if (ctx) {
+		ctx->mark_iloc_calls++;
+		if (ctx->fail_mark_iloc_err &&
+		    (!ctx->fail_mark_iloc_on_call ||
+		     ctx->mark_iloc_calls == (unsigned int)ctx->fail_mark_iloc_on_call))
+			err = ctx->fail_mark_iloc_err;
+	}
+
+	/* Real ext4_mark_iloc_dirty() always drops iloc->bh */
+	brelse(iloc->bh);
+	iloc->bh = NULL;
+	return err;
+}
+
+static int orphan_test_mark_inode_dirty_stub(handle_t *handle,
+					     struct inode *inode,
+					     const char *func,
+					     unsigned int line)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx)
+		return 0;
+	return ctx->fail_mark_inode_dirty_err;
+}
+
+static struct inode *orphan_test_iget_stub(struct super_block *sb,
+					   unsigned long ino,
+					   ext4_iget_flags flags,
+					   const char *function,
+					   unsigned int line)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+	struct orphan_test_inode_slot *slot;
+	struct inode *inode;
+
+	if (!ctx)
+		return ERR_PTR(-EIO);
+
+	ctx->iget_calls++;
+	if (ctx->fail_iget_err &&
+	    (!ctx->fail_iget_ino || ctx->fail_iget_ino == ino))
+		return ERR_PTR(ctx->fail_iget_err);
+
+	slot = orphan_test_find_slot(ctx, ino);
+	if (!slot || !slot->inode)
+		return ERR_PTR(-ENOENT);
+
+	inode = slot->inode;
+	if (slot->handoff_to_iget)
+		slot->inode = NULL;
+	else
+		ihold(inode);
+	return inode;
+}
+
+static struct buffer_head *orphan_test_getblk_stub(handle_t *handle,
+						   struct inode *inode,
+						   ext4_lblk_t block,
+						   int map_flags)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+	struct orphan_test_ob_blk *oblk;
+
+	if (!ctx)
+		return ERR_PTR(-EIO);
+
+	if (ctx->use_boundary_bh) {
+		get_bh(&ctx->boundary_bh);
+		return &ctx->boundary_bh;
+	}
+
+	if (block >= ORPHAN_TEST_MAX_OB_BLKS)
+		return ERR_PTR(-EFSCORRUPTED);
+
+	oblk = &ctx->ob_pool[block];
+	if (oblk->bread_err)
+		return ERR_PTR(oblk->bread_err);
+	if (oblk->return_hole || !oblk->present)
+		return NULL;
+
+	get_bh(&oblk->bh);
+	return &oblk->bh;
+}
+
+static struct ext4_group_desc *
+orphan_test_get_group_desc_stub(struct super_block *sb,
+				ext4_group_t block_group,
+				struct buffer_head **bh)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx || block_group >= ORPHAN_TEST_NGROUPS)
+		return NULL;
+	if (bh)
+		*bh = &ctx->gdesc_bh;
+	return &ctx->gdps[block_group];
+}
+
+static struct buffer_head *orphan_test_sb_getblk_stub(struct super_block *sb,
+						      ext4_fsblk_t block)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+	ext4_group_t g;
+
+	if (!ctx)
+		return NULL;
+
+	for (g = 0; g < ORPHAN_TEST_NGROUPS; g++) {
+		if (block == orphan_test_ibitmap_blk(g)) {
+			get_bh(&ctx->ibitmaps[g].bh);
+			return &ctx->ibitmaps[g].bh;
+		}
+	}
+	return NULL;
+}
+
+static void orphan_test_free_inode_stub(handle_t *handle, struct inode *inode)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (ctx) {
+		if (!sb_rdonly(inode->i_sb))
+			ctx->saw_rw_during_cleanup = true;
+		if (ctx->nr_freed_inos < ORPHAN_TEST_MAX_SLOTS)
+			ctx->freed_inos[ctx->nr_freed_inos++] = inode->i_ino;
+		orphan_test_set_ibitmap_bit(ctx, inode->i_ino, false);
+	}
+	ext4_clear_inode(inode);
+}
+
+static void orphan_test_free_blocks_stub(handle_t *handle, struct inode *inode,
+					 struct buffer_head *bh,
+					 ext4_fsblk_t block,
+					 unsigned long count, int flags)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (bh)
+		brelse(bh);
+	if (!ctx)
+		return;
+	ctx->last_freed_blk = block;
+	ctx->total_freed_blks += count;
+}
+
+static int orphan_test_truncate_stub(struct inode *inode)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct orphan_test_ctx *ctx = test ? test->priv : NULL;
+
+	if (!ctx)
+		return 0;
+
+	ctx->truncate_calls++;
+	if (!sb_rdonly(inode->i_sb))
+		ctx->saw_rw_during_cleanup = true;
+	return ctx->stub_truncate_err;
+}
+
+/* --- Fixture Setup & Teardown --- */
+
+static void orphan_test_cleanup(struct orphan_test_ctx *ctx)
+{
+	handle_t *handle;
+	unsigned int i;
+
+	ctx->enable_real_evict = false;
+	ctx->sbi.s_journal = NULL;
+	ctx->sbi.s_mount_state &= ~EXT4_ORPHAN_FS;
+
+	while ((handle = ext4_journal_current_handle()) != NULL)
+		ext4_journal_stop(handle);
+
+	if (ctx->orphan_info_allocated) {
+		ext4_release_orphan_info(ctx->sb);
+		ctx->orphan_info_allocated = false;
+	}
+	memset(&ctx->sbi.s_orphan_info, 0, sizeof(ctx->sbi.s_orphan_info));
+	INIT_LIST_HEAD(&ctx->sbi.s_orphan);
+
+	for (i = 0; i < ORPHAN_TEST_MAX_SLOTS; i++) {
+		struct inode *inode = ctx->slots[i].inode;
+
+		if (!inode)
+			continue;
+		INIT_LIST_HEAD(&EXT4_I(inode)->i_orphan);
+		ext4_clear_inode_state(inode, EXT4_STATE_ORPHAN_FILE);
+		spin_lock(&inode->i_lock);
+		inode_state_clear(inode, I_DIRTY_TIME | I_NEW);
+		spin_unlock(&inode->i_lock);
+		set_nlink(inode, 1);
+		iput(inode);
+		ctx->slots[i].inode = NULL;
+	}
+
+	deactivate_super(ctx->sb);
+	rcu_barrier();
+
+	cancel_work_sync(&ctx->sbi.s_sb_upd_work);
+	timer_delete_sync(&ctx->sbi.s_err_report);
+	free_percpu(ctx->bdev.bd_stats);
+	kfree(ctx);
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(orphan_test_cleanup_action, orphan_test_cleanup,
+			    struct orphan_test_ctx *);
+
+static int orphan_test_init(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx;
+	struct fs_context *fc;
+	struct super_block *sb;
+	struct ext4_sb_info *sbi;
+	struct inode *root_inode;
+	ext4_group_t g;
+	unsigned int bit;
+	int err;
+
+	ctx = kzalloc_obj(*ctx);
+	KUNIT_ASSERT_NOT_NULL(test, ctx);
+	test->priv = ctx;
+
+	ctx->bdev.bd_stats = alloc_percpu(struct disk_stats);
+	if (!ctx->bdev.bd_stats) {
+		kfree(ctx);
+		KUNIT_FAIL_AND_ABORT(test, "failed to alloc bd_stats");
+	}
+
+	fc = fs_context_for_mount(&orphan_test_fs_type, 0);
+	if (IS_ERR(fc)) {
+		err = PTR_ERR(fc);
+		free_percpu(ctx->bdev.bd_stats);
+		kfree(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+	sb = sget_fc(fc, NULL, set_anon_super_fc);
+	put_fs_context(fc);
+	if (IS_ERR(sb)) {
+		err = PTR_ERR(sb);
+		free_percpu(ctx->bdev.bd_stats);
+		kfree(ctx);
+		KUNIT_ASSERT_EQ(test, err, 0);
+	}
+	ctx->sb = sb;
+	sbi = &ctx->sbi;
+
+	sbi->s_sb = sb;
+	sb->s_fs_info = sbi;
+	sb->s_op = &orphan_test_sops;
+#ifdef CONFIG_QUOTA
+	sb->dq_op = &orphan_test_qops;
+#endif
+#ifdef CONFIG_FS_ENCRYPTION
+	sb->s_cop = &ext4_cryptops;
+#endif
+	sb->s_flags |= SB_ACTIVE;
+	sb->s_blocksize = ORPHAN_TEST_BLK_SIZE;
+	sb->s_blocksize_bits = ORPHAN_TEST_BLK_BITS;
+	sb->s_maxbytes = MAX_LFS_FILESIZE;
+	sb->s_time_gran = 1;
+	sb->s_xattr = ext4_xattr_handlers;
+	strscpy(sb->s_id, "ext4_orphan_kunit", sizeof(sb->s_id));
+
+	inode_init_once(&ctx->bd_inode);
+	ctx->bd_inode.i_sb = sb;
+	ctx->bd_inode.i_blkbits = ORPHAN_TEST_BLK_BITS;
+	ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data;
+	ctx->bd_inode.i_data.host = &ctx->bd_inode;
+	ctx->bdev_queue.limits.logical_block_size = 512;
+	ctx->bdev_queue.limits.dma_alignment = 511;
+	ctx->bdev_disk.part0 = &ctx->bdev;
+	ctx->bdev_disk.queue = &ctx->bdev_queue;
+	ctx->bdev.bd_disk = &ctx->bdev_disk;
+	ctx->bdev.bd_queue = &ctx->bdev_queue;
+	ctx->bdev.bd_mapping = &ctx->bd_inode.i_data;
+	/* Writable bdev by default so ext4_orphan_cleanup can proceed */
+	bdev_clear_flag(&ctx->bdev, BD_READ_ONLY);
+	sb->s_bdev = &ctx->bdev;
+
+	bgl_lock_init(&ctx->bgl);
+	sbi->s_blockgroup_lock = &ctx->bgl;
+
+	sbi->s_es = &ctx->es;
+	ctx->es.s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+	ctx->es.s_first_data_block = cpu_to_le32(ORPHAN_TEST_FIRST_DATA_BLK);
+	ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+	ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_LINUX);
+	ctx->es.s_inodes_count = cpu_to_le32(ORPHAN_TEST_TOTAL_INODES);
+	ctx->es.s_inodes_per_group = cpu_to_le32(ORPHAN_TEST_INODES_PER_GRP);
+	ctx->es.s_blocks_per_group = cpu_to_le32(ORPHAN_TEST_BLOCKS_PER_GRP);
+	ctx->es.s_clusters_per_group = cpu_to_le32(ORPHAN_TEST_BLOCKS_PER_GRP);
+	ctx->es.s_first_ino = cpu_to_le32(ORPHAN_TEST_FIRST_INO);
+	ctx->es.s_orphan_file_inum = cpu_to_le32(ORPHAN_TEST_ORPHAN_INO);
+	ctx->es.s_inode_size = cpu_to_le16(ORPHAN_TEST_INODE_SIZE);
+	ctx->es.s_min_extra_isize = cpu_to_le16(ORPHAN_TEST_EXTRA_ISIZE);
+	ctx->es.s_want_extra_isize = cpu_to_le16(ORPHAN_TEST_EXTRA_ISIZE);
+	ctx->es.s_checksum_type = EXT4_CRC32C_CHKSUM;
+	ext4_blocks_count_set(&ctx->es, ORPHAN_TEST_TOTAL_BLKS);
+
+	ext4_set_feature_sparse_super(sb);
+	ext4_set_feature_extents(sb);
+	ext4_set_feature_extra_isize(sb);
+	ext4_set_feature_metadata_csum(sb);
+	ext4_set_feature_64bit(sb);
+
+	/* Keep sb_bh unmapped so ext4_commit_super() skips bh_submit I/O */
+	ctx->sb_bh.b_data = (char *)&ctx->es;
+	ctx->sb_bh.b_size = sizeof(ctx->es);
+	ctx->sb_bh.b_bdev = &ctx->bdev;
+	atomic_set(&ctx->sb_bh.b_count, 1);
+	set_buffer_uptodate(&ctx->sb_bh);
+	sbi->s_sbh = &ctx->sb_bh;
+	ext4_superblock_csum_set(sb);
+
+	ctx->gdesc_bh.b_data = ctx->gdesc_buf;
+	ctx->gdesc_bh.b_size = ORPHAN_TEST_BLK_SIZE;
+	ctx->gdesc_bh.b_blocknr = ORPHAN_TEST_GDESC_BLK;
+	ctx->gdesc_bh.b_bdev = &ctx->bdev;
+	atomic_set(&ctx->gdesc_bh.b_count, 1);
+	set_buffer_uptodate(&ctx->gdesc_bh);
+
+	sbi->s_inode_size = ORPHAN_TEST_INODE_SIZE;
+	sbi->s_inodes_per_block = ORPHAN_TEST_INODES_PER_BLK;
+	sbi->s_inodes_per_group = ORPHAN_TEST_INODES_PER_GRP;
+	sbi->s_itb_per_group = ORPHAN_TEST_ITB_PER_GRP;
+	sbi->s_first_ino = ORPHAN_TEST_FIRST_INO;
+	sbi->s_want_extra_isize = ORPHAN_TEST_EXTRA_ISIZE;
+	sbi->s_blocks_per_group = ORPHAN_TEST_BLOCKS_PER_GRP;
+	sbi->s_clusters_per_group = ORPHAN_TEST_BLOCKS_PER_GRP;
+	sbi->s_cluster_ratio = 1;
+	sbi->s_cluster_bits = 0;
+	sbi->s_groups_count = ORPHAN_TEST_NGROUPS;
+	sbi->s_gdb_count = 1;
+	sbi->s_desc_size = EXT4_MIN_DESC_SIZE_64BIT;
+	sbi->s_desc_per_block = ORPHAN_TEST_BLK_SIZE / EXT4_MIN_DESC_SIZE_64BIT;
+	sbi->s_desc_per_block_bits = ilog2(sbi->s_desc_per_block);
+	sbi->s_bitmap_maxbytes = 0x1ff7ffff000ULL;
+	sbi->s_csum_seed = ORPHAN_TEST_CSUM_SEED;
+
+	mutex_init(&sbi->s_orphan_lock);
+	INIT_LIST_HEAD(&sbi->s_orphan);
+	sbi->s_journal = &ctx->mock_journal;
+	sbi->s_journal_triggers[EXT4_JTR_ORPHAN_FILE].sb = sb;
+	sbi->s_journal_triggers[EXT4_JTR_ORPHAN_FILE].tr_triggers.t_frozen =
+		ext4_orphan_file_block_trigger;
+
+	set_opt(sb, ERRORS_CONT);
+	set_opt(sb, ORDERED_DATA);
+	spin_lock_init(&sbi->s_bdev_wb_lock);
+	spin_lock_init(&sbi->s_error_lock);
+	spin_lock_init(&sbi->s_md_lock);
+	INIT_WORK(&sbi->s_sb_upd_work, orphan_test_sb_upd_work);
+	timer_setup(&sbi->s_err_report, orphan_test_err_timer, 0);
+	mutex_init(&sbi->s_fc_lock);
+	INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_MAIN]);
+	INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_STAGING]);
+	INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_MAIN]);
+	INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_STAGING]);
+
+	ratelimit_state_init(&sbi->s_err_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+	ratelimit_state_init(&sbi->s_warning_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+	ratelimit_state_init(&sbi->s_msg_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+
+	for (g = 0; g < ORPHAN_TEST_NGROUPS; g++) {
+		struct orphan_test_ibitmap *ibm = &ctx->ibitmaps[g];
+
+		init_rwsem(&ctx->grp_infos[g].alloc_sem);
+		ctx->grp_infos[g].bb_state = 0;
+		ctx->grp_info_ptrs[g] = &ctx->grp_infos[g];
+
+		ext4_block_bitmap_set(sb, &ctx->gdps[g],
+				      orphan_test_bbitmap_blk(g));
+		ext4_inode_bitmap_set(sb, &ctx->gdps[g],
+				      orphan_test_ibitmap_blk(g));
+		ext4_inode_table_set(sb, &ctx->gdps[g],
+				     orphan_test_itable_blk(g));
+
+		ibm->bh.b_data = ibm->buf;
+		ibm->bh.b_size = ORPHAN_TEST_BLK_SIZE;
+		ibm->bh.b_bdev = &ctx->bdev;
+		ibm->bh.b_blocknr = orphan_test_ibitmap_blk(g);
+		atomic_set(&ibm->bh.b_count, 1);
+		if (g == 0) {
+			for (bit = 0; bit < ORPHAN_TEST_FIRST_INO - 1; bit++)
+				ext4_set_bit(bit, ibm->buf);
+		}
+		ext4_mark_bitmap_end(ORPHAN_TEST_INODES_PER_GRP,
+				     ORPHAN_TEST_BLK_SIZE * 8, ibm->buf);
+		set_buffer_uptodate(&ibm->bh);
+		set_bitmap_uptodate(&ibm->bh);
+		set_buffer_verified(&ibm->bh);
+	}
+	ctx->grp_info_slice[0] = ctx->grp_info_ptrs;
+	rcu_assign_pointer(sbi->s_group_info, ctx->grp_info_slice);
+
+	root_inode = new_inode(sb);
+	if (!root_inode) {
+		deactivate_locked_super(sb);
+		rcu_barrier();
+		free_percpu(ctx->bdev.bd_stats);
+		kfree(ctx);
+		KUNIT_FAIL_AND_ABORT(test, "failed to alloc root_inode");
+	}
+	root_inode->i_ino = EXT4_ROOT_INO;
+	root_inode->i_mode = S_IFDIR | 0755;
+	root_inode->i_op = &simple_dir_inode_operations;
+	set_nlink(root_inode, 2);
+	sb->s_root = d_make_root(root_inode);
+	if (!sb->s_root) {
+		deactivate_locked_super(sb);
+		rcu_barrier();
+		free_percpu(ctx->bdev.bd_stats);
+		kfree(ctx);
+		KUNIT_FAIL_AND_ABORT(test, "failed to create s_root");
+	}
+
+	up_write(&sb->s_umount);
+
+	kunit_activate_static_stub(test, __ext4_journal_get_write_access,
+				   orphan_test_get_write_access_stub);
+	kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+				   orphan_test_dirty_metadata_stub);
+	kunit_activate_static_stub(test, ext4_get_inode_loc,
+				   orphan_test_get_inode_loc_stub);
+	kunit_activate_static_stub(test, ext4_mark_iloc_dirty,
+				   orphan_test_mark_iloc_dirty_stub);
+	kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+				   orphan_test_mark_inode_dirty_stub);
+	kunit_activate_static_stub(test, __ext4_iget,
+				   orphan_test_iget_stub);
+	kunit_activate_static_stub(test, ext4_getblk,
+				   orphan_test_getblk_stub);
+	kunit_activate_static_stub(test, ext4_get_group_desc,
+				   orphan_test_get_group_desc_stub);
+	kunit_activate_static_stub(test, ext4_sb_getblk,
+				   orphan_test_sb_getblk_stub);
+	kunit_activate_static_stub(test, ext4_free_inode,
+				   orphan_test_free_inode_stub);
+	kunit_activate_static_stub(test, ext4_free_blocks,
+				   orphan_test_free_blocks_stub);
+
+	err = kunit_add_action_or_reset(test, orphan_test_cleanup_action, ctx);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	ctx->enable_real_evict = true;
+	return 0;
+}
+
+/* =========================================================================
+ * 1. Geometry & Empty-State Helpers
+ * =========================================================================
+ */
+
+struct orphan_blksize_param {
+	const char *desc;
+	unsigned long blocksize;
+	unsigned char blocksize_bits;
+	int expected_inodes_per_ob;
+};
+
+static const struct orphan_blksize_param orphan_blksize_params[] = {
+	{ "1K_blocksize",  1024,  10, 254   },
+	{ "2K_blocksize",  2048,  11, 510   },
+	{ "4K_blocksize",  4096,  12, 1022  },
+	{ "64K_blocksize", 65536, 16, 16382 },
+};
+
+KUNIT_ARRAY_PARAM_DESC(orphan_blksize, orphan_blksize_params, desc);
+
+static void test_inodes_per_orphan_block_param(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	const struct orphan_blksize_param *p = test->param_value;
+
+	ctx->sb->s_blocksize = p->blocksize;
+	ctx->sb->s_blocksize_bits = p->blocksize_bits;
+
+	KUNIT_EXPECT_EQ(test, ext4_inodes_per_orphan_block(ctx->sb),
+			p->expected_inodes_per_ob);
+}
+
+static void test_orphan_file_empty(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+
+	/* 1. Feature disabled returns 1 regardless of s_orphan_info */
+	ext4_clear_feature_orphan_file(sb);
+	KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+
+	/* 2. Feature enabled with 0 blocks returns 1 */
+	ext4_set_feature_orphan_file(sb);
+	ctx->sbi.s_orphan_info.of_blocks = 0;
+	KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+
+	/* 3. Multi-block orphan file with all slots free returns 1 */
+	orphan_test_setup_static_orphan_file(ctx, 3);
+	KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+
+	/* 4. Occupied entry in a non-first block makes file non-empty (0) */
+	atomic_set(&ctx->static_binfo[2].ob_free_entries,
+		   ORPHAN_TEST_INODES_PER_OB - 1);
+	KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 0);
+
+	/* 5. Restoring free count returns 1 again */
+	atomic_set(&ctx->static_binfo[2].ob_free_entries,
+		   ORPHAN_TEST_INODES_PER_OB);
+	KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+}
+
+/* =========================================================================
+ * 2. Superblock Linked-List ext4_orphan_add()
+ * =========================================================================
+ */
+
+static void test_orphan_add_early_exits_and_idempotency(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct orphan_test_inode_slot *s1, *s2, *s3, *s4;
+	int err;
+
+	s1 = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+	s2 = orphan_test_create_inode(test, ctx, 16, S_IFREG | 0644, 0);
+	s3 = orphan_test_create_inode(test, ctx, 17, S_IFREG | 0644, 0);
+	s4 = orphan_test_create_inode(test, ctx, 18, S_IFREG | 0644, 0);
+
+	/* 1. !sbi->s_journal -> no-op returning 0 */
+	ctx->sbi.s_journal = NULL;
+	err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+	KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+	ctx->sbi.s_journal = &ctx->mock_journal;
+
+	/* 2. is_bad_inode(inode) -> no-op returning 0 */
+	make_bad_inode(s2->inode);
+	err = orphan_test_call_add(&ctx->mock_handle, s2->inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s2->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+
+	/* 3. Idempotent when already on sbi->s_orphan list */
+	err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+
+	orphan_test_reset_faults(ctx);
+	err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+	KUNIT_EXPECT_EQ(test, ctx->dirty_metadata_calls, 0U);
+
+	/* 4. Idempotent when already tracked via EXT4_STATE_ORPHAN_FILE */
+	ext4_set_inode_state(s3->inode, EXT4_STATE_ORPHAN_FILE);
+	EXT4_I(s3->inode)->i_orphan_idx = 0;
+	err = orphan_test_call_add(&ctx->mock_handle, s3->inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+
+	/* 5. I_NEW inode can be added & deleted without holding i_rwsem */
+	spin_lock(&s4->inode->i_lock);
+	inode_state_set(s4->inode, I_NEW);
+	spin_unlock(&s4->inode->i_lock);
+
+	err = ext4_orphan_add(&ctx->mock_handle, s4->inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 18U);
+
+	err = ext4_orphan_del(&ctx->mock_handle, s4->inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+
+	spin_lock(&s4->inode->i_lock);
+	inode_state_clear(s4->inode, I_NEW);
+	spin_unlock(&s4->inode->i_lock);
+}
+
+static void test_orphan_add_list_chaining_and_csum(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct orphan_test_inode_slot *s1, *s2, *s3;
+	__le32 csum_before;
+	int err;
+
+	s1 = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+	s2 = orphan_test_create_inode(test, ctx, 20, S_IFDIR | 0755, 0);
+	s3 = orphan_test_create_inode(test, ctx, 25, S_IFLNK | 0777, 0);
+
+	/* Corrupt s_checksum first so we verify ext4_superblock_csum_set() */
+	ctx->es.s_checksum = 0;
+	csum_before = ctx->es.s_checksum;
+
+	err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s1->inode), 0U);
+	KUNIT_EXPECT_NE(test, ctx->es.s_checksum, csum_before);
+	KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+	err = orphan_test_call_add(&ctx->mock_handle, s2->inode);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 20U);
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s2->inode), 15U);
+
+	err = orphan_test_call_add(&ctx->mock_handle, s3->inode);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 25U);
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s3->inode), 20U);
+
+	/* In-memory list head order must match LIFO insertion: s3 -> s2 -> s1 */
+	KUNIT_EXPECT_PTR_EQ(test, ctx->sbi.s_orphan.next,
+			    &EXT4_I(s3->inode)->i_orphan);
+	KUNIT_EXPECT_PTR_EQ(test, EXT4_I(s3->inode)->i_orphan.next,
+			    &EXT4_I(s2->inode)->i_orphan);
+	KUNIT_EXPECT_PTR_EQ(test, EXT4_I(s2->inode)->i_orphan.next,
+			    &EXT4_I(s1->inode)->i_orphan);
+}
+
+static void test_orphan_add_next_orphan_bounds(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct orphan_test_inode_slot *s_valid, *s_oob;
+	int err;
+
+	s_valid = orphan_test_create_inode(test, ctx, 18, S_IFREG | 0644, 0);
+	s_oob = orphan_test_create_inode(test, ctx, 19, S_IFREG | 0644, 0);
+
+	/*
+	 * 1. NEXT_ORPHAN == s_inodes_count (valid upper boundary):
+	 *    inode is treated as already on-disk; skip on-disk modification,
+	 *    link into in-memory s_orphan, and brelse(iloc.bh).
+	 */
+	NEXT_ORPHAN(s_valid->inode) = ORPHAN_TEST_TOTAL_INODES;
+	ctx->es.s_last_orphan = cpu_to_le32(15);
+	orphan_test_reset_faults(ctx);
+
+	err = orphan_test_call_add(&ctx->mock_handle, s_valid->inode);
+
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_FALSE(test, list_empty(&EXT4_I(s_valid->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s_valid->inode),
+			ORPHAN_TEST_TOTAL_INODES);
+	KUNIT_EXPECT_EQ(test, ctx->dirty_metadata_calls, 0U);
+	KUNIT_EXPECT_EQ(test, ctx->mark_iloc_calls, 0U);
+	KUNIT_EXPECT_EQ(test, atomic_read(&s_valid->iloc_bh.b_count), 1);
+
+	/*
+	 * 2. NEXT_ORPHAN == s_inodes_count + 1 (invalid / garbage):
+	 *    must overwrite NEXT_ORPHAN with old s_last_orphan (15), update
+	 *    s_last_orphan to 19, and dirty both sb and iloc.
+	 */
+	NEXT_ORPHAN(s_oob->inode) = ORPHAN_TEST_TOTAL_INODES + 1;
+	orphan_test_reset_faults(ctx);
+
+	err = orphan_test_call_add(&ctx->mock_handle, s_oob->inode);
+
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_FALSE(test, list_empty(&EXT4_I(s_oob->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 19U);
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s_oob->inode), 15U);
+	KUNIT_EXPECT_EQ(test, ctx->dirty_metadata_calls, 1U);
+	KUNIT_EXPECT_EQ(test, ctx->mark_iloc_calls, 1U);
+	KUNIT_EXPECT_EQ(test, atomic_read(&s_oob->iloc_bh.b_count), 1);
+}
+
+static void test_orphan_add_list_error_paths(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct orphan_test_inode_slot *s1, *s2;
+	int err;
+
+	s1 = orphan_test_create_inode(test, ctx, 21, S_IFREG | 0644, 0);
+	s2 = orphan_test_create_inode(test, ctx, 22, S_IFREG | 0644, 0);
+
+	/* 1. ext4_journal_get_write_access(s_sbh) fails */
+	orphan_test_reset_faults(ctx);
+	ctx->fail_write_access_err = -EIO;
+	ctx->fail_write_access_on_call = 1;
+	err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+	KUNIT_EXPECT_NE(test, ctx->sbi.s_mount_state & EXT4_ERROR_FS, 0);
+
+	/* 2. ext4_reserve_inode_write (ext4_get_inode_loc) fails */
+	orphan_test_reset_faults(ctx);
+	ctx->fail_get_iloc_err = -EIO;
+	err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+	/*
+	 * 3. ext4_handle_dirty_metadata(s_sbh) fails while ext4_mark_iloc_dirty
+	 *    succeeds: must still consume iloc.bh and unlink from s_orphan!
+	 */
+	orphan_test_reset_faults(ctx);
+	ctx->fail_dirty_metadata_err = -EIO;
+	err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_EQ(test, ctx->mark_iloc_calls, 1U);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+	/*
+	 * 4. ext4_handle_dirty_metadata(s_sbh) succeeds while
+	 *    ext4_mark_iloc_dirty fails: err = rc and rollback from s_orphan.
+	 */
+	orphan_test_reset_faults(ctx);
+	ctx->es.s_last_orphan = 0;
+	ctx->fail_mark_iloc_err = -ENOSPC;
+	err = orphan_test_call_add(&ctx->mock_handle, s2->inode);
+	KUNIT_EXPECT_EQ(test, err, -ENOSPC);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s2->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, atomic_read(&s2->iloc_bh.b_count), 1);
+}
+
+/* =========================================================================
+ * 3. Superblock Linked-List ext4_orphan_del()
+ * =========================================================================
+ */
+
+static void test_orphan_del_early_exits(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct orphan_test_inode_slot *s1, *s_untracked;
+	int err;
+
+	s1 = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+	s_untracked = orphan_test_create_inode(test, ctx, 16, S_IFREG | 0644, 0);
+
+	/* 1. Untracked inode is a fast no-op */
+	orphan_test_reset_faults(ctx);
+	err = orphan_test_call_del(&ctx->mock_handle, s_untracked->inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, ctx->get_iloc_calls, 0U);
+
+	/* Add s1 to orphan list */
+	err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+	KUNIT_ASSERT_EQ(test, err, 0);
+
+	/* 2. !s_journal && !(s_mount_state & EXT4_ORPHAN_FS) -> early return 0 */
+	ctx->sbi.s_journal = NULL;
+	ctx->sbi.s_mount_state &= ~EXT4_ORPHAN_FS;
+	err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_FALSE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+
+	/* 3. !s_journal WITH EXT4_ORPHAN_FS proceeds with full deletion */
+	ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS;
+	err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+}
+
+static void test_orphan_del_list_positions_and_null_handle(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct orphan_test_inode_slot *s1, *s2, *s3;
+	int err;
+
+	s1 = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+	s2 = orphan_test_create_inode(test, ctx, 20, S_IFREG | 0644, 0);
+	s3 = orphan_test_create_inode(test, ctx, 25, S_IFREG | 0644, 0);
+
+	/* Build chain: s_last_orphan -> 25 (s3) -> 20 (s2) -> 15 (s1) -> 0 */
+	KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+			0);
+	KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s2->inode),
+			0);
+	KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s3->inode),
+			0);
+
+	/* 1. Delete middle entry (s2 = 20): s3->NEXT_ORPHAN becomes 15 */
+	err = orphan_test_call_del(&ctx->mock_handle, s2->inode);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s2->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s2->inode), 0U);
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s3->inode), 15U);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 25U);
+
+	/* 2. Delete tail entry (s1 = 15): s3->NEXT_ORPHAN becomes 0 */
+	err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s1->inode), 0U);
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s3->inode), 0U);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 25U);
+
+	/* 3. Delete head entry (s3 = 25): s_last_orphan becomes 0 */
+	err = orphan_test_call_del(&ctx->mock_handle, s3->inode);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test, list_empty(&ctx->sbi.s_orphan));
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s3->inode), 0U);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+
+	/* 4. handle == NULL removes in-core entry without on-disk updates */
+	KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+			0);
+	orphan_test_reset_faults(ctx);
+
+	err = orphan_test_call_del(NULL, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+	KUNIT_EXPECT_EQ(test, ctx->get_iloc_calls, 0U);
+	KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+}
+
+static void test_orphan_del_list_error_paths(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct orphan_test_inode_slot *s1, *s2;
+	int err;
+
+	s1 = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+	s2 = orphan_test_create_inode(test, ctx, 20, S_IFREG | 0644, 0);
+
+	/* 1. Initial ext4_reserve_inode_write fails -> still unlinks in-core */
+	KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+			0);
+	orphan_test_reset_faults(ctx);
+	ctx->fail_get_iloc_err = -EIO;
+	ctx->fail_get_iloc_on_call = 1;
+	err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+	/* 2. Head delete: ext4_journal_get_write_access(s_sbh) fails */
+	ctx->es.s_last_orphan = 0;
+	NEXT_ORPHAN(s1->inode) = 0;
+	orphan_test_reset_faults(ctx);
+	KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+			0);
+	orphan_test_reset_faults(ctx);
+	/* Call 1 is reserve_inode_write(s1); call 2 is get_write_access(s_sbh) */
+	ctx->fail_write_access_err = -EIO;
+	ctx->fail_write_access_on_call = 2;
+	err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+	KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+	/* 3. Head delete: ext4_handle_dirty_metadata(s_sbh) fails */
+	ctx->es.s_last_orphan = 0;
+	NEXT_ORPHAN(s1->inode) = 0;
+	orphan_test_reset_faults(ctx);
+	KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+			0);
+	orphan_test_reset_faults(ctx);
+	ctx->fail_dirty_metadata_err = -EIO;
+	ctx->fail_dirty_metadata_on_call = 1;
+	err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+
+	/* 4. Non-head delete: ext4_reserve_inode_write(i_prev) fails */
+	ctx->es.s_last_orphan = 0;
+	NEXT_ORPHAN(s1->inode) = 0;
+	NEXT_ORPHAN(s2->inode) = 0;
+	orphan_test_reset_faults(ctx);
+	KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s1->inode),
+			0);
+	KUNIT_ASSERT_EQ(test, orphan_test_call_add(&ctx->mock_handle, s2->inode),
+			0);
+
+	orphan_test_reset_faults(ctx);
+	/* Call 1 is s1 iloc; call 2 is i_prev (s2) iloc */
+	ctx->fail_get_iloc_err = -EIO;
+	ctx->fail_get_iloc_on_call = 2;
+	err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s2->inode), 15U);
+	KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+	KUNIT_EXPECT_EQ(test, atomic_read(&s2->iloc_bh.b_count), 1);
+
+	/* 5. Non-head delete: ext4_mark_iloc_dirty(i_prev) fails */
+	list_add(&EXT4_I(s1->inode)->i_orphan, &EXT4_I(s2->inode)->i_orphan);
+	orphan_test_reset_faults(ctx);
+	ctx->fail_mark_iloc_err = -EIO;
+	ctx->fail_mark_iloc_on_call = 1;
+	err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_EQ(test, atomic_read(&s1->iloc_bh.b_count), 1);
+	KUNIT_EXPECT_EQ(test, atomic_read(&s2->iloc_bh.b_count), 1);
+
+	/* 6. Final ext4_mark_iloc_dirty(inode) fails on head delete */
+	orphan_test_reset_faults(ctx);
+	ctx->fail_mark_iloc_err = -ENOSPC;
+	ctx->fail_mark_iloc_on_call = 1;
+	err = orphan_test_call_del(&ctx->mock_handle, s2->inode);
+	KUNIT_EXPECT_EQ(test, err, -ENOSPC);
+	KUNIT_EXPECT_EQ(test, NEXT_ORPHAN(s2->inode), 0U);
+	KUNIT_EXPECT_EQ(test, atomic_read(&s2->iloc_bh.b_count), 1);
+}
+
+/* =========================================================================
+ * 4. Orphan-File ext4_orphan_add() & ext4_orphan_del()
+ * =========================================================================
+ */
+
+static void test_orphan_file_add_and_del_basic(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct orphan_test_inode_slot *s1;
+	unsigned int blk, off;
+	__le32 *bdata;
+	int err;
+
+	orphan_test_setup_static_orphan_file(ctx, 2);
+	s1 = orphan_test_create_inode(test, ctx, 24, S_IFREG | 0644, 0);
+
+	err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+	KUNIT_EXPECT_TRUE(test, ext4_inode_orphan_tracked(s1->inode));
+	KUNIT_EXPECT_EQ(test, ctx->last_trigger_type, EXT4_JTR_ORPHAN_FILE);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+	KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 0);
+
+	blk = EXT4_I(s1->inode)->i_orphan_idx / ORPHAN_TEST_INODES_PER_OB;
+	off = EXT4_I(s1->inode)->i_orphan_idx % ORPHAN_TEST_INODES_PER_OB;
+	KUNIT_ASSERT_LT(test, blk, 2U);
+	KUNIT_EXPECT_EQ(test, off, 0U);
+	bdata = (__le32 *)ctx->static_binfo[blk].ob_bh->b_data;
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata[off]), 24U);
+	KUNIT_EXPECT_EQ(test,
+			atomic_read(&ctx->static_binfo[blk].ob_free_entries),
+			(int)(ORPHAN_TEST_INODES_PER_OB - 1));
+
+	/* Delete from orphan file and verify slot & state restoration */
+	orphan_test_reset_faults(ctx);
+	err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+	KUNIT_EXPECT_FALSE(test, ext4_inode_orphan_tracked(s1->inode));
+	KUNIT_EXPECT_EQ(test, ctx->last_trigger_type, EXT4_JTR_ORPHAN_FILE);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata[off]), 0U);
+	KUNIT_EXPECT_EQ(test,
+			atomic_read(&ctx->static_binfo[blk].ob_free_entries),
+			(int)ORPHAN_TEST_INODES_PER_OB);
+	KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+}
+
+static void test_orphan_file_add_block_wraparound_and_last_slot(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct orphan_test_inode_slot *s1, *s2;
+	__le32 *bdata;
+	unsigned int j;
+	int b, err;
+
+	orphan_test_setup_static_orphan_file(ctx, 3);
+	s1 = orphan_test_create_inode(test, ctx, 30, S_IFREG | 0644, 0);
+	s2 = orphan_test_create_inode(test, ctx, 31, S_IFREG | 0644, 0);
+
+	/*
+	 * Mark blocks 0 and 1 full (0 free), and leave ONLY the very last slot
+	 * (ORPHAN_TEST_INODES_PER_OB - 1 = 253) free in block 2. Then test a
+	 * second add when only block 0 has 1 slot free, guaranteeing
+	 * wrap-around across the circular block ring regardless of CPU id.
+	 */
+	atomic_set(&ctx->static_binfo[0].ob_free_entries, 0);
+	atomic_set(&ctx->static_binfo[1].ob_free_entries, 0);
+	atomic_set(&ctx->static_binfo[2].ob_free_entries, 1);
+	bdata = (__le32 *)ctx->static_binfo[2].ob_bh->b_data;
+	for (j = 0; j < ORPHAN_TEST_INODES_PER_OB - 1; j++)
+		bdata[j] = cpu_to_le32(100 + j);
+	bdata[ORPHAN_TEST_INODES_PER_OB - 1] = 0;
+
+	err = orphan_test_call_add(&ctx->mock_handle, s1->inode);
+
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, EXT4_I(s1->inode)->i_orphan_idx,
+			2 * ORPHAN_TEST_INODES_PER_OB +
+			(ORPHAN_TEST_INODES_PER_OB - 1));
+	KUNIT_EXPECT_EQ(test,
+			le32_to_cpu(bdata[ORPHAN_TEST_INODES_PER_OB - 1]), 30U);
+	KUNIT_EXPECT_EQ(test,
+			atomic_read(&ctx->static_binfo[2].ob_free_entries), 0);
+
+	/* Now make only block 0 slot (ORPHAN_TEST_INODES_PER_OB - 1) free */
+	for (b = 0; b < 3; b++)
+		atomic_set(&ctx->static_binfo[b].ob_free_entries, 0);
+	atomic_set(&ctx->static_binfo[0].ob_free_entries, 1);
+	bdata = (__le32 *)ctx->static_binfo[0].ob_bh->b_data;
+	for (j = 0; j < ORPHAN_TEST_INODES_PER_OB - 1; j++)
+		bdata[j] = cpu_to_le32(100 + j);
+	bdata[ORPHAN_TEST_INODES_PER_OB - 1] = 0;
+
+	err = orphan_test_call_add(&ctx->mock_handle, s2->inode);
+
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, EXT4_I(s2->inode)->i_orphan_idx,
+			ORPHAN_TEST_INODES_PER_OB - 1);
+	KUNIT_EXPECT_EQ(test,
+			le32_to_cpu(bdata[ORPHAN_TEST_INODES_PER_OB - 1]), 31U);
+}
+
+static void test_orphan_file_add_enospc_fallback_and_write_err(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct orphan_test_inode_slot *s_fb, *s_err, *s_dirty_err;
+	int err;
+
+	orphan_test_setup_static_orphan_file(ctx, 2);
+	s_fb = orphan_test_create_inode(test, ctx, 32, S_IFREG | 0644, 0);
+	s_err = orphan_test_create_inode(test, ctx, 33, S_IFREG | 0644, 0);
+	s_dirty_err = orphan_test_create_inode(test, ctx, 34, S_IFREG | 0644, 0);
+
+	/*
+	 * 1. Full orphan file (all ob_free_entries == 0):
+	 *    ext4_orphan_file_add() returns -ENOSPC, and ext4_orphan_add()
+	 *    falls back to the superblock linked list!
+	 */
+	atomic_set(&ctx->static_binfo[0].ob_free_entries, 0);
+	atomic_set(&ctx->static_binfo[1].ob_free_entries, 0);
+
+	err = orphan_test_call_add(&ctx->mock_handle, s_fb->inode);
+
+	KUNIT_ASSERT_EQ(test, err, 0);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(s_fb->inode,
+						 EXT4_STATE_ORPHAN_FILE));
+	KUNIT_EXPECT_FALSE(test, list_empty(&EXT4_I(s_fb->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 32U);
+
+	/*
+	 * 2. Non-ENOSPC error (-EIO from ext4_journal_get_write_access):
+	 *    must restore ob_free_entries via atomic_inc and return -EIO
+	 *    directly WITHOUT falling back to the superblock linked list!
+	 */
+	atomic_set(&ctx->static_binfo[0].ob_free_entries, 5);
+	atomic_set(&ctx->static_binfo[1].ob_free_entries, 0);
+	orphan_test_reset_faults(ctx);
+	ctx->fail_write_access_err = -EIO;
+	ctx->fail_write_access_on_call = 1;
+
+	err = orphan_test_call_add(&ctx->mock_handle, s_err->inode);
+
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_EQ(test,
+			atomic_read(&ctx->static_binfo[0].ob_free_entries), 5);
+	KUNIT_EXPECT_FALSE(test, ext4_inode_orphan_tracked(s_err->inode));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 32U);
+
+	/*
+	 * 3. Non-ENOSPC error (-EIO from ext4_handle_dirty_metadata):
+	 *    propagates -EIO directly without falling back to s_last_orphan.
+	 */
+	orphan_test_reset_faults(ctx);
+	ctx->fail_dirty_metadata_err = -EIO;
+	ctx->fail_dirty_metadata_on_call = 1;
+
+	err = orphan_test_call_add(&ctx->mock_handle, s_dirty_err->inode);
+
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_inode_state(s_dirty_err->inode,
+						EXT4_STATE_ORPHAN_FILE));
+	KUNIT_EXPECT_PTR_EQ(test, ctx->sbi.s_orphan.next,
+			    &EXT4_I(s_fb->inode)->i_orphan);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 32U);
+}
+
+static void test_orphan_file_del_edge_and_error_paths(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct orphan_test_inode_slot *s1;
+	__le32 *bdata;
+	int err;
+
+	orphan_test_setup_static_orphan_file(ctx, 2);
+	s1 = orphan_test_create_inode(test, ctx, 35, S_IFREG | 0644, 0);
+	bdata = (__le32 *)ctx->static_binfo[1].ob_bh->b_data;
+
+	/* 1. handle == NULL clears in-core state without touching orphan block */
+	bdata[4] = cpu_to_le32(35);
+	atomic_set(&ctx->static_binfo[1].ob_free_entries,
+		   ORPHAN_TEST_INODES_PER_OB - 1);
+	EXT4_I(s1->inode)->i_orphan_idx = 1 * ORPHAN_TEST_INODES_PER_OB + 4;
+	ext4_set_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE);
+
+	err = orphan_test_call_del(NULL, s1->inode);
+
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(s1->inode)->i_orphan));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata[4]), 35U);
+	KUNIT_EXPECT_EQ(test,
+			atomic_read(&ctx->static_binfo[1].ob_free_entries),
+			(int)(ORPHAN_TEST_INODES_PER_OB - 1));
+
+	/* 2. Out-of-range blk >= oi->of_blocks hits WARN_ON_ONCE guard safely */
+	EXT4_I(s1->inode)->i_orphan_idx = 2 * ORPHAN_TEST_INODES_PER_OB;
+	ext4_set_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE);
+	orphan_test_reset_faults(ctx);
+
+	kunit_warning_suppress(test) {
+		err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+	}
+
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+	KUNIT_EXPECT_EQ(test, ctx->write_access_calls, 0U);
+
+	/* 3. ext4_journal_get_write_access error leaves bdata[off] intact */
+	EXT4_I(s1->inode)->i_orphan_idx = 1 * ORPHAN_TEST_INODES_PER_OB + 4;
+	ext4_set_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE);
+	orphan_test_reset_faults(ctx);
+	ctx->fail_write_access_err = -EIO;
+
+	err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata[4]), 35U);
+	KUNIT_EXPECT_EQ(test,
+			atomic_read(&ctx->static_binfo[1].ob_free_entries),
+			(int)(ORPHAN_TEST_INODES_PER_OB - 1));
+
+	/* 4. ext4_handle_dirty_metadata error propagates after clearing slot */
+	EXT4_I(s1->inode)->i_orphan_idx = 1 * ORPHAN_TEST_INODES_PER_OB + 4;
+	ext4_set_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE);
+	orphan_test_reset_faults(ctx);
+	ctx->fail_dirty_metadata_err = -ENOSPC;
+
+	err = orphan_test_call_del(&ctx->mock_handle, s1->inode);
+
+	KUNIT_EXPECT_EQ(test, err, -ENOSPC);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(s1->inode, EXT4_STATE_ORPHAN_FILE));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata[4]), 0U);
+	KUNIT_EXPECT_EQ(test,
+			atomic_read(&ctx->static_binfo[1].ob_free_entries),
+			(int)ORPHAN_TEST_INODES_PER_OB);
+}
+
+/* =========================================================================
+ * 5. Trigger, Init & Release of Orphan File Info
+ * =========================================================================
+ */
+
+static void test_orphan_file_block_trigger_and_frozen_data(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct jbd2_buffer_trigger_type *triggers =
+		&ctx->sbi.s_journal_triggers[EXT4_JTR_ORPHAN_FILE].tr_triggers;
+	struct buffer_head *bh;
+	struct ext4_orphan_block_tail *ot;
+	__le32 *bdata, *fdata;
+	__u32 expected_csum;
+
+	orphan_test_setup_static_orphan_file(ctx, 1);
+	bh = ctx->static_binfo[0].ob_bh;
+	ot = (struct ext4_orphan_block_tail *)(bh->b_data + sb->s_blocksize -
+					       sizeof(*ot));
+	bdata = (__le32 *)bh->b_data;
+	bdata[0] = cpu_to_le32(42);
+	ot->ob_checksum = 0;
+
+	/* 1. Trigger with data == bh->b_data computes valid block checksum */
+	ext4_orphan_file_block_trigger(triggers, bh, bh->b_data, bh->b_size);
+	expected_csum = orphan_test_calc_ob_csum(sb, ORPHAN_TEST_OF_CSUM_SEED,
+						 bh->b_blocknr, bh->b_data);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ot->ob_checksum), expected_csum);
+
+	/* 2. Trigger with separate JBD2 frozen buffer hashes @data, not bh */
+	memset(ctx->frozen_buf, 0, sizeof(ctx->frozen_buf));
+	fdata = (__le32 *)ctx->frozen_buf;
+	fdata[0] = cpu_to_le32(99);
+	fdata[10] = cpu_to_le32(105);
+
+	ext4_orphan_file_block_trigger(triggers, bh, ctx->frozen_buf,
+				       bh->b_size);
+	expected_csum = orphan_test_calc_ob_csum(sb, ORPHAN_TEST_OF_CSUM_SEED,
+						 bh->b_blocknr,
+						 ctx->frozen_buf);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ot->ob_checksum), expected_csum);
+}
+
+static void test_init_and_release_orphan_info_lifecycle(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_orphan_info *oi = &ctx->sbi.s_orphan_info;
+	struct orphan_test_inode_slot *of_slot;
+	struct ext4_orphan_block_tail *ot;
+	__le32 *bdata;
+	int i, err;
+
+	/* 1. Feature disabled: init and release are clean no-ops */
+	ext4_clear_feature_orphan_file(sb);
+	err = ext4_init_orphan_info(sb);
+	KUNIT_EXPECT_EQ(test, err, 0);
+	KUNIT_EXPECT_EQ(test, oi->of_blocks, 0);
+	KUNIT_EXPECT_NULL(test, oi->of_binfo);
+	ext4_release_orphan_info(sb);
+
+	/* 2. Valid 2-block orphan file load and release */
+	ext4_set_feature_orphan_file(sb);
+	of_slot = orphan_test_create_inode(test, ctx, ORPHAN_TEST_ORPHAN_INO,
+					   S_IFREG | 0600, 1);
+	of_slot->inode->i_size = 2 * ORPHAN_TEST_BLK_SIZE;
+	EXT4_I(of_slot->inode)->i_csum_seed = ORPHAN_TEST_OF_CSUM_SEED;
+
+	for (i = 0; i < 2; i++) {
+		struct orphan_test_ob_blk *oblk = &ctx->ob_pool[i];
+
+		memset(oblk, 0, sizeof(*oblk));
+		oblk->present = true;
+		oblk->bh.b_data = oblk->buf;
+		oblk->bh.b_size = sb->s_blocksize;
+		oblk->bh.b_bdev = &ctx->bdev;
+		oblk->bh.b_blocknr = ORPHAN_TEST_OB_BASE_BLK + i;
+		atomic_set(&oblk->bh.b_count, 1);
+		set_buffer_uptodate(&oblk->bh);
+	}
+	/* Populate 3 active entries in block 0, 0 entries in block 1 */
+	bdata = (__le32 *)ctx->ob_pool[0].buf;
+	bdata[0] = cpu_to_le32(20);
+	bdata[5] = cpu_to_le32(21);
+	bdata[253] = cpu_to_le32(22);
+	orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED,
+			       &ctx->ob_pool[0].bh, EXT4_ORPHAN_BLOCK_MAGIC);
+	orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED,
+			       &ctx->ob_pool[1].bh, EXT4_ORPHAN_BLOCK_MAGIC);
+
+	err = ext4_init_orphan_info(sb);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	ctx->orphan_info_allocated = true;
+
+	KUNIT_EXPECT_EQ(test, oi->of_blocks, 2);
+	KUNIT_EXPECT_EQ(test, oi->of_csum_seed, ORPHAN_TEST_OF_CSUM_SEED);
+	KUNIT_ASSERT_NOT_NULL(test, oi->of_binfo);
+	KUNIT_EXPECT_EQ(test, atomic_read(&oi->of_binfo[0].ob_free_entries),
+			(int)(ORPHAN_TEST_INODES_PER_OB - 3));
+	KUNIT_EXPECT_EQ(test, atomic_read(&oi->of_binfo[1].ob_free_entries),
+			(int)ORPHAN_TEST_INODES_PER_OB);
+	KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 0);
+	KUNIT_EXPECT_EQ(test, atomic_read(&of_slot->inode->i_count), 1);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 2);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[1].bh.b_count), 2);
+
+	/* Release drops every block bh reference and frees of_binfo */
+	ext4_release_orphan_info(sb);
+	ctx->orphan_info_allocated = false;
+	memset(oi, 0, sizeof(*oi));
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 1);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[1].bh.b_count), 1);
+
+	/*
+	 * 3. With metadata_csum disabled, ext4_orphan_file_block_csum_verify()
+	 *    returns 1 even if ob_checksum does not match.
+	 */
+	ext4_clear_feature_metadata_csum(sb);
+	of_slot->inode->i_size = ORPHAN_TEST_BLK_SIZE;
+	ot = (struct ext4_orphan_block_tail *)(ctx->ob_pool[0].buf +
+					       sb->s_blocksize - sizeof(*ot));
+	ot->ob_checksum = cpu_to_le32(0xdeadbeefU);
+
+	err = ext4_init_orphan_info(sb);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	ctx->orphan_info_allocated = true;
+
+	ext4_release_orphan_info(sb);
+	ctx->orphan_info_allocated = false;
+	memset(oi, 0, sizeof(*oi));
+}
+
+static void test_init_orphan_info_max_blocks_boundary(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_orphan_info *oi = &ctx->sbi.s_orphan_info;
+	struct orphan_test_inode_slot *of_slot;
+	int err;
+
+	ext4_set_feature_orphan_file(sb);
+	of_slot = orphan_test_create_inode(test, ctx, ORPHAN_TEST_ORPHAN_INO,
+					   S_IFREG | 0600, 1);
+	EXT4_I(of_slot->inode)->i_csum_seed = ORPHAN_TEST_OF_CSUM_SEED;
+
+	/* 1. Size 1 byte over 512 blocks (EXT4_MAX_ORPHAN_FILE_BLOCKS) fails */
+	of_slot->inode->i_size = (512LL << ORPHAN_TEST_BLK_BITS) + 1;
+	err = ext4_init_orphan_info(sb);
+	KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED);
+	KUNIT_EXPECT_EQ(test, atomic_read(&of_slot->inode->i_count), 1);
+
+	/* 2. Exact 512-block limit succeeds and releases all 512 bh refs */
+	memset(ctx->boundary_buf, 0, sizeof(ctx->boundary_buf));
+	ctx->boundary_bh.b_data = ctx->boundary_buf;
+	ctx->boundary_bh.b_size = sb->s_blocksize;
+	ctx->boundary_bh.b_bdev = &ctx->bdev;
+	ctx->boundary_bh.b_blocknr = ORPHAN_TEST_OB_BASE_BLK;
+	atomic_set(&ctx->boundary_bh.b_count, 1);
+	set_buffer_uptodate(&ctx->boundary_bh);
+	orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED,
+			       &ctx->boundary_bh, EXT4_ORPHAN_BLOCK_MAGIC);
+
+	ctx->use_boundary_bh = true;
+	of_slot->inode->i_size = 512LL << ORPHAN_TEST_BLK_BITS;
+
+	err = ext4_init_orphan_info(sb);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	ctx->orphan_info_allocated = true;
+	KUNIT_EXPECT_EQ(test, oi->of_blocks, 512);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->boundary_bh.b_count), 513);
+
+	ext4_release_orphan_info(sb);
+	ctx->orphan_info_allocated = false;
+	memset(oi, 0, sizeof(*oi));
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->boundary_bh.b_count), 1);
+}
+
+static void test_init_orphan_info_error_paths_and_cleanup(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_orphan_info *oi = &ctx->sbi.s_orphan_info;
+	struct orphan_test_inode_slot *of_slot;
+	struct ext4_orphan_block_tail *ot;
+	int i, err;
+
+	ext4_set_feature_orphan_file(sb);
+
+	/* 1. ext4_iget failure returns error immediately */
+	ctx->fail_iget_err = -ENOENT;
+	err = ext4_init_orphan_info(sb);
+	KUNIT_EXPECT_EQ(test, err, -ENOENT);
+	orphan_test_reset_faults(ctx);
+
+	/* Prepare 2-block orphan inode where block 0 loads cleanly */
+	of_slot = orphan_test_create_inode(test, ctx, ORPHAN_TEST_ORPHAN_INO,
+					   S_IFREG | 0600, 1);
+	of_slot->inode->i_size = 2 * ORPHAN_TEST_BLK_SIZE;
+	EXT4_I(of_slot->inode)->i_csum_seed = ORPHAN_TEST_OF_CSUM_SEED;
+
+	for (i = 0; i < 2; i++) {
+		struct orphan_test_ob_blk *oblk = &ctx->ob_pool[i];
+
+		memset(oblk, 0, sizeof(*oblk));
+		oblk->present = true;
+		oblk->bh.b_data = oblk->buf;
+		oblk->bh.b_size = sb->s_blocksize;
+		oblk->bh.b_bdev = &ctx->bdev;
+		oblk->bh.b_blocknr = ORPHAN_TEST_OB_BASE_BLK + i;
+		atomic_set(&oblk->bh.b_count, 1);
+		set_buffer_uptodate(&oblk->bh);
+		orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED, &oblk->bh,
+				       EXT4_ORPHAN_BLOCK_MAGIC);
+	}
+
+	/* 2. Block 1 ext4_bread returns ERR_PTR(-EIO) -> block 0 brelse'd */
+	ctx->ob_pool[1].bread_err = -EIO;
+	err = ext4_init_orphan_info(sb);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 1);
+	KUNIT_EXPECT_EQ(test, atomic_read(&of_slot->inode->i_count), 1);
+	memset(oi, 0, sizeof(*oi));
+	ctx->ob_pool[1].bread_err = 0;
+
+	/* 3. Block 1 ext4_bread returns NULL (unmapped hole) -> -EIO */
+	ctx->ob_pool[1].return_hole = true;
+	err = ext4_init_orphan_info(sb);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 1);
+	memset(oi, 0, sizeof(*oi));
+	ctx->ob_pool[1].return_hole = false;
+
+	/* 4. Block 1 has invalid ob_magic -> ext4_error + -EIO + brelse */
+	orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED,
+			       &ctx->ob_pool[1].bh, 0xbad0bad0U);
+	orphan_test_reset_faults(ctx);
+	err = ext4_init_orphan_info(sb);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_NE(test, ctx->sbi.s_mount_state & EXT4_ERROR_FS, 0);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 1);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[1].bh.b_count), 1);
+	memset(oi, 0, sizeof(*oi));
+
+	/* 5. Block 1 has valid magic but corrupted ob_checksum -> -EIO */
+	orphan_test_seal_ob_bh(sb, ORPHAN_TEST_OF_CSUM_SEED,
+			       &ctx->ob_pool[1].bh, EXT4_ORPHAN_BLOCK_MAGIC);
+	ot = (struct ext4_orphan_block_tail *)(ctx->ob_pool[1].buf +
+					       sb->s_blocksize - sizeof(*ot));
+	ot->ob_checksum = cpu_to_le32(le32_to_cpu(ot->ob_checksum) ^ 0x1U);
+	orphan_test_reset_faults(ctx);
+
+	err = ext4_init_orphan_info(sb);
+	KUNIT_EXPECT_EQ(test, err, -EIO);
+	KUNIT_EXPECT_NE(test, ctx->sbi.s_mount_state & EXT4_ERROR_FS, 0);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[0].bh.b_count), 1);
+	KUNIT_EXPECT_EQ(test, atomic_read(&ctx->ob_pool[1].bh.b_count), 1);
+	memset(oi, 0, sizeof(*oi));
+}
+
+/* =========================================================================
+ * 6. End-to-End Orphan Recovery: ext4_orphan_cleanup() & ext4_process_orphan()
+ * =========================================================================
+ */
+
+static void test_orphan_cleanup_skip_guards(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	__le32 saved_ro_compat;
+
+	/* 1. Empty s_last_orphan and empty orphan file -> fast return */
+	ctx->es.s_last_orphan = 0;
+	ext4_orphan_cleanup(sb, &ctx->es);
+	KUNIT_EXPECT_EQ(test, ctx->iget_calls, 0U);
+
+	/* 2. Read-only block device skips orphan cleanup */
+	ctx->es.s_last_orphan = cpu_to_le32(15);
+	bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+	ext4_orphan_cleanup(sb, &ctx->es);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+	KUNIT_EXPECT_EQ(test, ctx->iget_calls, 0U);
+	bdev_clear_flag(&ctx->bdev, BD_READ_ONLY);
+
+	/* 3. Unknown ROCOMPAT feature fails ext4_feature_set_ok(sb, 0) */
+	saved_ro_compat = ctx->es.s_feature_ro_compat;
+	ctx->es.s_feature_ro_compat |= cpu_to_le32(0x80000000U);
+	ext4_orphan_cleanup(sb, &ctx->es);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+	KUNIT_EXPECT_EQ(test, ctx->iget_calls, 0U);
+	ctx->es.s_feature_ro_compat = saved_ro_compat;
+
+	/* 4a. EXT4_ERROR_FS on SB_RDONLY mount preserves s_last_orphan */
+	ctx->sbi.s_mount_state |= EXT4_ERROR_FS;
+	sb->s_flags |= SB_RDONLY;
+	ext4_orphan_cleanup(sb, &ctx->es);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 15U);
+	sb->s_flags &= ~SB_RDONLY;
+
+	/* 4b. EXT4_ERROR_FS on R/W mount clears s_last_orphan to 0 */
+	ext4_orphan_cleanup(sb, &ctx->es);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+	ctx->sbi.s_mount_state &= ~EXT4_ERROR_FS;
+}
+
+static void
+test_orphan_cleanup_linked_list_unlinked_and_truncated(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct orphan_test_inode_slot *s_unlinked, *s_trunc, *s_symlink;
+
+	/*
+	 * Match ext4_fill_super() no-journal recovery state:
+	 * s_journal = NULL, s_mount_state |= EXT4_ORPHAN_FS.
+	 */
+	ctx->sbi.s_journal = NULL;
+	ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS;
+	set_opt(sb, DEBUG);
+
+	/*
+	 * Build on-disk orphan chain:
+	 * s_last_orphan -> 15 (unlinked reg w/ extent) -> 16 (linked truncate
+	 * w/ extent) -> 17 (unlinked fast symlink) -> 0
+	 */
+	s_unlinked = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+	s_unlinked->handoff_to_iget = true;
+	orphan_test_add_single_extent(s_unlinked->inode, 0, 200ULL, 2);
+	NEXT_ORPHAN(s_unlinked->inode) = 16;
+
+	s_trunc = orphan_test_create_inode(test, ctx, 16, S_IFREG | 0644, 1);
+	s_trunc->handoff_to_iget = true;
+	s_trunc->inode->i_size = 0;
+	EXT4_I(s_trunc->inode)->i_disksize = 2 * ORPHAN_TEST_BLK_SIZE;
+	orphan_test_add_single_extent(s_trunc->inode, 0, 300ULL, 2);
+	NEXT_ORPHAN(s_trunc->inode) = 17;
+
+	s_symlink = orphan_test_create_inode(test, ctx, 17, S_IFLNK | 0777, 0);
+	s_symlink->handoff_to_iget = true;
+	s_symlink->inode->i_size = 8;
+	memcpy(EXT4_I(s_symlink->inode)->i_data, "target12", 8);
+	NEXT_ORPHAN(s_symlink->inode) = 0;
+
+	ctx->es.s_last_orphan = cpu_to_le32(15);
+
+	ext4_orphan_cleanup(sb, &ctx->es);
+
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+	KUNIT_EXPECT_TRUE(test, list_empty(&ctx->sbi.s_orphan));
+	KUNIT_EXPECT_EQ(test, ctx->nr_freed_inos, 2U);
+	KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 15));
+	KUNIT_EXPECT_FALSE(test, orphan_test_was_ino_freed(ctx, 16));
+	KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 17));
+	/* Both s_unlinked (2 blks) and s_trunc (2 blks) freed their extents */
+	KUNIT_EXPECT_EQ(test, ctx->total_freed_blks, 4UL);
+}
+
+static void
+test_orphan_cleanup_orphan_file_and_rdonly_restore(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct orphan_test_inode_slot *s_list, *s_of_del, *s_of_trunc, *s_of_only;
+	__le32 *bdata0, *bdata1;
+
+	ctx->sbi.s_journal = NULL;
+	ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS;
+	sb->s_flags |= SB_RDONLY;
+
+	orphan_test_setup_static_orphan_file(ctx, 2);
+	bdata0 = (__le32 *)ctx->static_binfo[0].ob_bh->b_data;
+	bdata1 = (__le32 *)ctx->static_binfo[1].ob_bh->b_data;
+
+	/* 1. Linked-list unlinked orphan (ino 15) */
+	s_list = orphan_test_create_inode(test, ctx, 15, S_IFREG | 0644, 0);
+	s_list->handoff_to_iget = true;
+	NEXT_ORPHAN(s_list->inode) = 0;
+	ctx->es.s_last_orphan = cpu_to_le32(15);
+
+	/*
+	 * 2. Orphan file entries:
+	 *    - Block 0 slot 2: valid unlinked orphan (ino 16)
+	 *    - Block 0 slot 10: unallocated ino 99 -> ext4_orphan_get() fails,
+	 *      loop must continue to remaining entries!
+	 *    - Block 1 slot 5: valid linked truncate orphan (ino 18)
+	 */
+	s_of_del = orphan_test_create_inode(test, ctx, 16, S_IFREG | 0644, 0);
+	s_of_del->handoff_to_iget = true;
+	bdata0[2] = cpu_to_le32(16);
+	bdata0[10] = cpu_to_le32(99);
+	atomic_set(&ctx->static_binfo[0].ob_free_entries,
+		   ORPHAN_TEST_INODES_PER_OB - 2);
+
+	s_of_trunc = orphan_test_create_inode(test, ctx, 18, S_IFREG | 0644, 1);
+	s_of_trunc->handoff_to_iget = true;
+	s_of_trunc->inode->i_size = 0;
+	orphan_test_add_single_extent(s_of_trunc->inode, 0, 400ULL, 1);
+	bdata1[5] = cpu_to_le32(18);
+	atomic_set(&ctx->static_binfo[1].ob_free_entries,
+		   ORPHAN_TEST_INODES_PER_OB - 1);
+
+	ext4_orphan_cleanup(sb, &ctx->es);
+
+	/* SB_RDONLY was cleared during cleanup and restored upon return */
+	KUNIT_EXPECT_TRUE(test, ctx->saw_rw_during_cleanup);
+	KUNIT_EXPECT_NE(test, sb->s_flags & SB_RDONLY, 0UL);
+
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+	KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 15));
+	KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 16));
+	KUNIT_EXPECT_FALSE(test, orphan_test_was_ino_freed(ctx, 18));
+	KUNIT_EXPECT_EQ(test, ctx->total_freed_blks, 1UL);
+
+	/* Valid orphan-file slots were cleared; bad slot 10 was skipped */
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata0[2]), 0U);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata0[10]), 99U);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata1[5]), 0U);
+	KUNIT_EXPECT_EQ(test,
+			atomic_read(&ctx->static_binfo[0].ob_free_entries),
+			(int)(ORPHAN_TEST_INODES_PER_OB - 1));
+	KUNIT_EXPECT_EQ(test,
+			atomic_read(&ctx->static_binfo[1].ob_free_entries),
+			(int)ORPHAN_TEST_INODES_PER_OB);
+
+	/*
+	 * 3. s_last_orphan == 0 with non-empty orphan file:
+	 *    must NOT early-return at (!es->s_last_orphan && orphan_file_empty).
+	 */
+	bdata0[10] = 0;
+	atomic_set(&ctx->static_binfo[0].ob_free_entries,
+		   ORPHAN_TEST_INODES_PER_OB);
+	s_of_only = orphan_test_create_inode(test, ctx, 19, S_IFREG | 0644, 0);
+	s_of_only->handoff_to_iget = true;
+	bdata1[8] = cpu_to_le32(19);
+	atomic_set(&ctx->static_binfo[1].ob_free_entries,
+		   ORPHAN_TEST_INODES_PER_OB - 1);
+	orphan_test_reset_faults(ctx);
+
+	ext4_orphan_cleanup(sb, &ctx->es);
+
+	KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 19));
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(bdata1[8]), 0U);
+	KUNIT_EXPECT_EQ(test, ext4_orphan_file_empty(sb), 1);
+}
+
+static void
+test_orphan_cleanup_list_errors_and_truncate_fail(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct orphan_test_inode_slot *s1, *s2;
+
+	ctx->sbi.s_journal = NULL;
+	ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS;
+
+	/*
+	 * 1. Reserved / invalid orphan ino (< EXT4_FIRST_INO) on s_last_orphan:
+	 *    ext4_orphan_get() returns -EFSCORRUPTED, loop clears s_last_orphan
+	 *    and breaks cleanly.
+	 */
+	ctx->es.s_last_orphan = cpu_to_le32(5);
+	ext4_orphan_cleanup(sb, &ctx->es);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+
+	/*
+	 * 2. ext4_truncate() fails (-EIO) on linked orphan s1 (20), which
+	 *    points to s2 (21):
+	 *    - ext4_process_orphan() removes s1 from in-core s_orphan via
+	 *      ext4_orphan_del(NULL, s1) and sets EXT4_ERROR_FS via
+	 *      ext4_std_error().
+	 *    - Next loop iteration detects EXT4_ERROR_FS, clears s_last_orphan,
+	 *      and aborts recovery before touching s2 (21).
+	 */
+	orphan_test_reset_faults(ctx);
+	kunit_activate_static_stub(test, ext4_truncate,
+				   orphan_test_truncate_stub);
+	ctx->stub_truncate_err = -EIO;
+
+	s1 = orphan_test_create_inode(test, ctx, 20, S_IFREG | 0644, 1);
+	s1->handoff_to_iget = true;
+	NEXT_ORPHAN(s1->inode) = 21;
+
+	s2 = orphan_test_create_inode(test, ctx, 21, S_IFREG | 0644, 0);
+	s2->handoff_to_iget = true;
+	NEXT_ORPHAN(s2->inode) = 0;
+
+	ctx->es.s_last_orphan = cpu_to_le32(20);
+
+	ext4_orphan_cleanup(sb, &ctx->es);
+	kunit_deactivate_static_stub(test, ext4_truncate);
+
+	KUNIT_EXPECT_EQ(test, ctx->truncate_calls, 1U);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+	KUNIT_EXPECT_TRUE(test, list_empty(&ctx->sbi.s_orphan));
+	KUNIT_EXPECT_FALSE(test, orphan_test_was_ino_freed(ctx, 21));
+	KUNIT_EXPECT_NOT_NULL(test, s2->inode);
+}
+
+#ifdef CONFIG_QUOTA
+static void test_orphan_cleanup_quota_paths(struct kunit *test)
+{
+	struct orphan_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct orphan_test_inode_slot *s1, *s2;
+
+	ctx->sbi.s_journal = NULL;
+	ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS;
+	sb->s_flags |= SB_RDONLY;
+	strscpy(ctx->qf_name, "aquota.user", sizeof(ctx->qf_name));
+
+	/*
+	 * 1. Exercise journaled quota mount hook (ext4_quota_on_mount) and,
+	 *    when CONFIG_QFMT_V2 is also enabled, ext4_enable_quotas() success
+	 *    (s_usr_quota_inum == 0 -> quota_update = 1) on SB_RDONLY orphan
+	 *    recovery. Hold s_umount to satisfy lockdep in s_qf_names RCU
+	 *    dereference (matching mount-time ext4_fill_super context).
+	 */
+	rcu_assign_pointer(ctx->sbi.s_qf_names[USRQUOTA], ctx->qf_name);
+#if IS_ENABLED(CONFIG_QFMT_V2)
+	ext4_set_feature_quota(sb);
+	ctx->es.s_usr_quota_inum = 0;
+#endif
+
+	s1 = orphan_test_create_inode(test, ctx, 28, S_IFREG | 0644, 0);
+	s1->handoff_to_iget = true;
+	NEXT_ORPHAN(s1->inode) = 0;
+	ctx->es.s_last_orphan = cpu_to_le32(28);
+
+	down_write(&sb->s_umount);
+	ext4_orphan_cleanup(sb, &ctx->es);
+	rcu_assign_pointer(ctx->sbi.s_qf_names[USRQUOTA], NULL);
+	up_write(&sb->s_umount);
+
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+	KUNIT_EXPECT_TRUE(test, orphan_test_was_ino_freed(ctx, 28));
+	KUNIT_EXPECT_NE(test, sb->s_flags & SB_RDONLY, 0UL);
+
+#if IS_ENABLED(CONFIG_QFMT_V2)
+	/*
+	 * 2. Exercise ext4_enable_quotas() failure path: missing quota inode 3
+	 *    triggers ext4_error() (setting EXT4_ERROR_FS), logs the quota
+	 *    turn-on error, aborts s_last_orphan recovery while clearing
+	 *    s_last_orphan to 0, and still restores SB_RDONLY.
+	 */
+	orphan_test_reset_faults(ctx);
+	ctx->es.s_usr_quota_inum = cpu_to_le32(EXT4_USR_QUOTA_INO);
+	s2 = orphan_test_create_inode(test, ctx, 29, S_IFREG | 0644, 0);
+	s2->handoff_to_iget = true;
+	NEXT_ORPHAN(s2->inode) = 0;
+	ctx->es.s_last_orphan = cpu_to_le32(29);
+
+	down_write(&sb->s_umount);
+	ext4_orphan_cleanup(sb, &ctx->es);
+	up_write(&sb->s_umount);
+
+	KUNIT_EXPECT_NE(test, ctx->sbi.s_mount_state & EXT4_ERROR_FS, 0);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(ctx->es.s_last_orphan), 0U);
+	KUNIT_EXPECT_FALSE(test, orphan_test_was_ino_freed(ctx, 29));
+	KUNIT_EXPECT_NE(test, sb->s_flags & SB_RDONLY, 0UL);
+#else
+	(void)s2;
+#endif
+}
+#endif
+
+static struct kunit_case orphan_test_cases[] = {
+	KUNIT_CASE_PARAM(test_inodes_per_orphan_block_param,
+			 orphan_blksize_gen_params),
+	KUNIT_CASE(test_orphan_file_empty),
+	KUNIT_CASE(test_orphan_add_early_exits_and_idempotency),
+	KUNIT_CASE(test_orphan_add_list_chaining_and_csum),
+	KUNIT_CASE(test_orphan_add_next_orphan_bounds),
+	KUNIT_CASE(test_orphan_add_list_error_paths),
+	KUNIT_CASE(test_orphan_del_early_exits),
+	KUNIT_CASE(test_orphan_del_list_positions_and_null_handle),
+	KUNIT_CASE(test_orphan_del_list_error_paths),
+	KUNIT_CASE(test_orphan_file_add_and_del_basic),
+	KUNIT_CASE(test_orphan_file_add_block_wraparound_and_last_slot),
+	KUNIT_CASE(test_orphan_file_add_enospc_fallback_and_write_err),
+	KUNIT_CASE(test_orphan_file_del_edge_and_error_paths),
+	KUNIT_CASE(test_orphan_file_block_trigger_and_frozen_data),
+	KUNIT_CASE(test_init_and_release_orphan_info_lifecycle),
+	KUNIT_CASE(test_init_orphan_info_max_blocks_boundary),
+	KUNIT_CASE(test_init_orphan_info_error_paths_and_cleanup),
+	KUNIT_CASE(test_orphan_cleanup_skip_guards),
+	KUNIT_CASE(test_orphan_cleanup_linked_list_unlinked_and_truncated),
+	KUNIT_CASE(test_orphan_cleanup_orphan_file_and_rdonly_restore),
+	KUNIT_CASE(test_orphan_cleanup_list_errors_and_truncate_fail),
+#ifdef CONFIG_QUOTA
+	KUNIT_CASE(test_orphan_cleanup_quota_paths),
+#endif
+	{}
+};
+
+static struct kunit_suite orphan_test_suite = {
+	.name = "ext4_orphan_test",
+	.init = orphan_test_init,
+	.test_cases = orphan_test_cases,
+};
+
+kunit_test_suites(&orphan_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 orphan inode handling");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/orphan.c b/fs/ext4/orphan.c
index 03538d498811..a12022913ec6 100644
--- a/fs/ext4/orphan.c
+++ b/fs/ext4/orphan.c
@@ -511,6 +511,7 @@ void ext4_orphan_cleanup(struct super_block *sb, struct ext4_super_block *es)
 #endif
 	sb->s_flags = s_flags; /* Restore SB_RDONLY status */
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_orphan_cleanup);
 
 void ext4_release_orphan_info(struct super_block *sb)
 {
@@ -523,6 +524,7 @@ void ext4_release_orphan_info(struct super_block *sb)
 		brelse(oi->of_binfo[i].ob_bh);
 	kvfree(oi->of_binfo);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_release_orphan_info);
 
 static struct ext4_orphan_block_tail *ext4_orphan_block_tail(
 						struct super_block *sb,
@@ -570,6 +572,7 @@ void ext4_orphan_file_block_trigger(struct jbd2_buffer_trigger_type *triggers,
 	ot = ext4_orphan_block_tail(sb, bh);
 	ot->ob_checksum = cpu_to_le32(csum);
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_orphan_file_block_trigger);
 
 int ext4_init_orphan_info(struct super_block *sb)
 {
@@ -652,6 +655,7 @@ int ext4_init_orphan_info(struct super_block *sb)
 	iput(inode);
 	return ret;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_init_orphan_info);
 
 int ext4_orphan_file_empty(struct super_block *sb)
 {
@@ -667,3 +671,4 @@ int ext4_orphan_file_empty(struct super_block *sb)
 			return 0;
 	return 1;
 }
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_orphan_file_empty);
-- 
2.56.0.542.g3ef89b7020-goog


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

Thread overview: 55+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-12  4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
2026-11-12  4:06 ` [PATCH 01/27] ext4: add KUinit tests for fs/ext4/extents_status.c Theodore Ts'o
2026-11-12  4:18   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 02/27] ext4: add KUnit tests for fs/ext4/extents.c Theodore Ts'o
2026-11-12  4:28   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 03/27] ext4: add KUnit tests for ext4_ext_split() leaf and index splitting Theodore Ts'o
2026-11-12  4:15   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 04/27] ext4: add KUnit tests for fs/ext4/xattr.c Theodore Ts'o
2026-11-12  4:26   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 05/27] ext4: add KUnit tests for fs/ext4/namei.c Theodore Ts'o
2026-11-12  4:21   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 06/27] ext4: expand KUnit tests for fs/ext4/mballoc.c Theodore Ts'o
2026-11-12  4:26   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 07/27] ext4: add KUnit tests for fs/ext4/inline.c Theodore Ts'o
2026-11-12  4:29   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 08/27] ext4: zero s_fc_replay_state in ext4_fc_replay_cleanup() Theodore Ts'o
2026-11-12  4:16   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 09/27] ext4: add KUnit tests for fs/ext4/fast_commit.c Theodore Ts'o
2026-11-12  4:28   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 10/27] ext4: expand KUnit tests for fs/ext4/inode.c Theodore Ts'o
2026-11-12  4:36   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 11/27] ext4: add KUnit tests for fs/ext4/ialloc.c Theodore Ts'o
2026-11-12  4:25   ` sashiko-bot
2026-11-12  4:06 ` [PATCH 12/27] ext4: add KUnit tests for fs/ext4/balloc.c Theodore Ts'o
2026-11-12  4:16   ` sashiko-bot
2026-11-12  4:06 ` Theodore Ts'o [this message]
2026-11-12  4:18   ` [PATCH 13/27] ext4: add KUnit tests for fs/ext4/orphan.c 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-14-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