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 07/27] ext4: add KUnit tests for fs/ext4/inline.c
Date: Wed, 11 Nov 2026 23:06:18 -0500 [thread overview]
Message-ID: <20261112040638.1576836-8-tytso@mit.edu> (raw)
In-Reply-To: <20261112040638.1576836-1-tytso@mit.edu>
Add the ext4_inline_test KUnit test suite in fs/ext4/inline-test.c to
comprehensively exercise inline data operations for regular files,
symlinks, and directories in fs/ext4/inline.c without requiring a
mounted block device:
- Inline capacity and discovery (ext4_get_max_inline_size and
ext4_find_inline_data_nolock) across fresh inodes, inodes with zero
extra_isize or 128-byte inode size, inodes with coexisting user
xattrs in ibody, 0-byte and non-empty system.data xattrs, and
corrupted xattr entries (e_name_len overflow and non-zero
e_value_inum).
- Inline file creation, expansion, overwrite, and destruction
(ext4_try_to_write_inline_data, ext4_write_inline_data_end, and
ext4_destroy_inline_data) spanning i_block (< 60 bytes) and the
ibody system.data xattr (>= 60 bytes), short writes with failed-write
truncation, non-uptodate folios, emergency shutdown
(EXT4_FLAGS_SHUTDOWN), concurrent inline flag clearing, and corrupted
inline xattr state detection.
- Inline page reading, symlinks, and iomap mapping
(ext4_readpage_inline on index 0 and index > 0, i_inline_size >
PAGE_SIZE corruption detection, ext4_read_inline_link for short and
long symlinks, and ext4_inline_data_iomap).
- Inline file truncation (ext4_inline_data_truncate) across size
extensions, shrinking within system.data xattr, shrinking into
i_block with tail zeroing, truncating to zero, and clearing the
extent status tree when EXT4_STATE_MAY_INLINE_DATA is unset.
- Inline-to-extent conversion for regular files
(ext4_convert_inline_data, ext4_convert_inline_data_to_extent, and
ext4_da_convert_inline_data_to_extent via
ext4_generic_write_inline_data), including DIOREAD_NOLOCK, DELALLOC,
and journaled data modes, plus error rollback via
ext4_restore_inline_data when block allocation, buffer lookup, or
metadata dirtying fails.
- Inline directory operations (ext4_update_final_de,
ext4_try_create_inline_dir, ext4_get_first_inline_block,
ext4_try_add_inline_entry across i_block and system.data xattr,
duplicate -EEXIST handling, ext4_find_inline_entry,
ext4_read_inline_dir with cookie/iversion revalidation,
ext4_inlinedir_to_tree, ext4_delete_inline_entry, empty_inline_dir,
conversion of full inline directories to extent-mapped directory
blocks with rollback on allocation failure, and corrupted dirent
detection).
Export the tested functions with EXPORT_SYMBOL_FOR_EXT4_TEST and add
KUNIT_STATIC_STUB_REDIRECT hooks for ext4_inline_getblk,
ext4_da_get_block_prep, and ext4_inode_journal_mode.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 2 +-
fs/ext4/ext4.h | 2 +
fs/ext4/ext4_jbd2.c | 3 +
fs/ext4/inline-test.c | 1825 +++++++++++++++++++++++++++++++++++++++++
fs/ext4/inline.c | 30 +-
fs/ext4/inode.c | 4 +
6 files changed, 1864 insertions(+), 2 deletions(-)
create mode 100644 fs/ext4/inline-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 8fe7b512ebb4..5a90bf5b28e3 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -17,7 +17,7 @@ ext4-$(CONFIG_EXT4_FS_SECURITY) += xattr_security.o
ext4-test-objs += inode-test.o mballoc-test.o \
extents-test.o hash-test.o \
extents_status-test.o xattr-test.o \
- namei-test.o
+ namei-test.o inline-test.o
obj-$(CONFIG_EXT4_KUNIT_TESTS) += ext4-test.o
ext4-$(CONFIG_FS_VERITY) += verity.o
ext4-$(CONFIG_FS_ENCRYPTION) += crypto.o
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index b57176b61d80..bcb4eb10d363 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3765,6 +3765,8 @@ extern const struct file_operations ext4_file_operations;
extern loff_t ext4_llseek(struct file *file, loff_t offset, int origin);
/* inline.c */
+extern struct buffer_head *ext4_inline_getblk(struct super_block *sb,
+ ext4_fsblk_t block);
extern int ext4_get_max_inline_size(struct inode *inode);
extern int ext4_find_inline_data_nolock(struct inode *inode);
extern int ext4_destroy_inline_data(handle_t *handle, struct inode *inode);
diff --git a/fs/ext4/ext4_jbd2.c b/fs/ext4/ext4_jbd2.c
index d6ffb6f08606..caba7ac68841 100644
--- a/fs/ext4/ext4_jbd2.c
+++ b/fs/ext4/ext4_jbd2.c
@@ -10,6 +10,8 @@
int ext4_inode_journal_mode(struct inode *inode)
{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_inode_journal_mode, inode);
+
if (EXT4_JOURNAL(inode) == NULL)
return EXT4_INODE_WRITEBACK_DATA_MODE; /* writeback */
/* We do not support data journalling with delayed allocation */
@@ -437,5 +439,6 @@ int __ext4_handle_dirty_metadata(const char *where, unsigned int line,
}
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_journal_mode);
EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_handle_dirty_metadata);
#endif
diff --git a/fs/ext4/inline-test.c b/fs/ext4/inline-test.c
new file mode 100644
index 000000000000..a75f46475c4a
--- /dev/null
+++ b/fs/ext4/inline-test.c
@@ -0,0 +1,1825 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for ext4 inline data operations (fs/ext4/inline.c)
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/backing-dev.h>
+#include <linux/buffer_head.h>
+#include <linux/fs.h>
+#include <linux/iomap.h>
+#include <linux/iversion.h>
+#include <linux/limits.h>
+#include <linux/mm.h>
+#include <linux/pagemap.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "xattr.h"
+#include "truncate.h"
+#include "namei.h"
+
+#define TEST_INODE_SIZE 256
+#define TEST_EXTRA_ISIZE 32
+#define TEST_BLOCK_SIZE 1024
+#define TEST_BLOCK_BITS 10
+#define EXT4_MIN_INLINE_DATA_SIZE ((sizeof(__le32) * EXT4_N_BLOCKS))
+#define EXT4_INLINE_DOTDOT_SIZE 4
+/*
+ * For a 256-byte inode with 32-byte i_extra_isize:
+ * EXT4_GOOD_OLD_INODE_SIZE (128) + 32 = 160 bytes fixed fields
+ * Remaining ibody space = 96 bytes
+ * 4-byte xattr magic + 20-byte system.data entry + 4-byte gap = 28 bytes
+ * Available xattr value space = 68 bytes
+ * Plus 60 bytes in i_block (EXT4_MIN_INLINE_DATA_SIZE) = 128 bytes max inline size.
+ */
+#define TEST_MAX_INLINE_SIZE 128
+
+#define TEST_INO 12
+#define TEST_PARENT_INO 2
+#define TEST_PBLK 100
+
+struct inline_test_ctx {
+ struct super_block sb;
+ struct ext4_sb_info sbi;
+ struct ext4_super_block es;
+ struct block_device bdev;
+ struct address_space bd_mapping;
+ struct buffer_head sb_bh;
+ struct ext4_inode_info ei;
+ struct buffer_head iloc_bh;
+ u8 raw_inode_buf[TEST_BLOCK_SIZE];
+ struct buffer_head data_bh;
+ u8 data_block_buf[PAGE_SIZE];
+ int get_inode_loc_err;
+ int map_blocks_ret;
+ ext4_fsblk_t map_pblk;
+ unsigned int map_flags;
+ bool getblk_fail;
+ int dirty_metadata_err;
+ int dirty_metadata_fail_on_call;
+ int dirty_metadata_calls;
+ int da_get_block_prep_err;
+ int journal_mode;
+};
+
+static struct inline_test_ctx *t_ctx;
+
+static const struct super_operations inline_test_sops = {};
+
+static bool test_release_folio(struct folio *folio, gfp_t wait)
+{
+ return try_to_free_buffers(folio);
+}
+
+static const struct address_space_operations test_inline_aops = {
+ .dirty_folio = noop_dirty_folio,
+ .invalidate_folio = block_invalidate_folio,
+ .release_folio = test_release_folio,
+};
+
+/* --- Static Stubs --- */
+
+static int stub_get_inode_loc(struct inode *inode, struct ext4_iloc *iloc)
+{
+ if (t_ctx->get_inode_loc_err)
+ return t_ctx->get_inode_loc_err;
+
+ memset(iloc, 0, sizeof(*iloc));
+ atomic_inc(&t_ctx->iloc_bh.b_count);
+ iloc->bh = &t_ctx->iloc_bh;
+ iloc->offset = 0;
+ iloc->block_group = 0;
+ return 0;
+}
+
+static int stub_handle_dirty_metadata(const char *where, unsigned int line,
+ handle_t *handle, struct inode *inode,
+ struct buffer_head *bh)
+{
+ t_ctx->dirty_metadata_calls++;
+ if (t_ctx->dirty_metadata_fail_on_call > 0 &&
+ t_ctx->dirty_metadata_calls == t_ctx->dirty_metadata_fail_on_call)
+ return t_ctx->dirty_metadata_err;
+ if (t_ctx->dirty_metadata_fail_on_call == 0 &&
+ t_ctx->dirty_metadata_err)
+ return t_ctx->dirty_metadata_err;
+ set_buffer_dirty(bh);
+ return 0;
+}
+
+static int stub_mark_iloc_dirty(handle_t *handle, struct inode *inode,
+ struct ext4_iloc *iloc)
+{
+ brelse(iloc->bh);
+ return 0;
+}
+
+static int stub_mark_inode_dirty(handle_t *handle, struct inode *inode,
+ const char *func, unsigned int line)
+{
+ return 0;
+}
+
+static int stub_map_blocks(handle_t *handle, struct inode *inode,
+ struct ext4_map_blocks *map, int flags)
+{
+ if (t_ctx->map_blocks_ret < 0)
+ return t_ctx->map_blocks_ret;
+
+ map->m_pblk = t_ctx->map_pblk;
+ map->m_flags = t_ctx->map_flags;
+ if (t_ctx->map_blocks_ret > 0)
+ map->m_len = t_ctx->map_blocks_ret;
+ return t_ctx->map_blocks_ret;
+}
+
+static struct buffer_head *stub_inline_getblk(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ if (t_ctx->getblk_fail)
+ return NULL;
+
+ atomic_inc(&t_ctx->data_bh.b_count);
+ t_ctx->data_bh.b_blocknr = block;
+ return &t_ctx->data_bh;
+}
+
+static int stub_da_get_block_prep(struct inode *inode, sector_t iblock,
+ struct buffer_head *bh, int create)
+{
+ if (t_ctx->da_get_block_prep_err)
+ return t_ctx->da_get_block_prep_err;
+
+ set_buffer_new(bh);
+ set_buffer_delay(bh);
+ bh->b_blocknr = 0;
+ bh->b_size = inode->i_sb->s_blocksize;
+ return 0;
+}
+
+static int stub_inode_journal_mode(struct inode *inode)
+{
+ return t_ctx->journal_mode;
+}
+
+/* --- Helpers --- */
+
+static void *raw_inline_i_block(struct inline_test_ctx *ctx)
+{
+ struct ext4_inode *raw_inode = (struct ext4_inode *)ctx->raw_inode_buf;
+
+ return (void *)raw_inode->i_block;
+}
+
+static void *raw_inline_xattr_pos(struct inline_test_ctx *ctx)
+{
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_inode *raw_inode = (struct ext4_inode *)ctx->raw_inode_buf;
+ struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
+ struct ext4_xattr_entry *entry;
+
+ entry = (struct ext4_xattr_entry *)((void *)raw_inode +
+ ctx->ei.i_inline_off);
+ return (void *)IFIRST(header) + le16_to_cpu(entry->e_value_offs);
+}
+
+static void reset_inode_state(struct inline_test_ctx *ctx, umode_t mode)
+{
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_inode *raw_inode;
+
+ if (!list_empty(&ctx->ei.i_orphan))
+ list_del_init(&ctx->ei.i_orphan);
+
+ truncate_inode_pages(&inode->i_data, 0);
+
+ memset(ctx->raw_inode_buf, 0, sizeof(ctx->raw_inode_buf));
+ memset(ctx->data_block_buf, 0, sizeof(ctx->data_block_buf));
+ memset(ctx->ei.i_data, 0, sizeof(ctx->ei.i_data));
+
+ raw_inode = (struct ext4_inode *)ctx->raw_inode_buf;
+ raw_inode->i_extra_isize = cpu_to_le16(TEST_EXTRA_ISIZE);
+
+ ctx->ei.i_extra_isize = TEST_EXTRA_ISIZE;
+ ctx->ei.i_inline_off = 0;
+ ctx->ei.i_inline_size = 0;
+ ctx->ei.i_flags = 0;
+ ext4_clear_state_flags(&ctx->ei);
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ctx->ei.i_disksize = 0;
+
+ inode->i_mode = mode;
+ inode->i_size = 0;
+ inode->i_blocks = 0;
+ inode_set_iversion(inode, 1);
+ inode_state_set_raw(inode, I_DIRTY);
+}
+
+static int create_inline_file(struct inode *inode, const void *data, size_t len)
+{
+ struct folio *folio = NULL;
+ void *kaddr;
+ int ret;
+
+ inode_lock(inode);
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, len,
+ &folio);
+ if (ret != 1) {
+ inode_unlock(inode);
+ return ret < 0 ? ret : -ENOSPC;
+ }
+
+ if (data && len) {
+ kaddr = kmap_local_folio(folio, 0);
+ memcpy(kaddr, data, len);
+ kunmap_local(kaddr);
+ }
+
+ ret = ext4_write_inline_data_end(inode, 0, len, len, folio);
+ inode_unlock(inode);
+ if (ret < 0)
+ return ret;
+
+ i_size_write(inode, len);
+ EXT4_I(inode)->i_disksize = len;
+ return 0;
+}
+
+static int inline_test_init(struct kunit *test)
+{
+ struct inline_test_ctx *ctx;
+ struct inode *inode;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+ t_ctx = ctx;
+ test->priv = ctx;
+
+ /* Superblock and ext4_sb_info setup */
+ ctx->sb.s_blocksize = TEST_BLOCK_SIZE;
+ ctx->sb.s_blocksize_bits = TEST_BLOCK_BITS;
+ ctx->sb.s_maxbytes = MAX_LFS_FILESIZE;
+ ctx->sb.s_time_gran = 1;
+ ctx->sb.s_bdi = &noop_backing_dev_info;
+ ctx->sb.s_fs_info = &ctx->sbi;
+ ctx->sb.s_op = &inline_test_sops;
+ snprintf(ctx->sb.s_id, sizeof(ctx->sb.s_id), "ext4-inline-test");
+ init_rwsem(&ctx->sb.s_umount);
+
+ ctx->bdev.bd_mapping = &ctx->bd_mapping;
+ bdev_set_flag(&ctx->bdev, BD_READ_ONLY);
+ ctx->sb.s_bdev = &ctx->bdev;
+
+ ctx->es.s_feature_incompat =
+ cpu_to_le32(EXT4_FEATURE_INCOMPAT_INLINE_DATA |
+ EXT4_FEATURE_INCOMPAT_EXTENTS |
+ EXT4_FEATURE_INCOMPAT_FILETYPE);
+ ctx->es.s_first_data_block = cpu_to_le32(1);
+ ctx->es.s_inodes_count = cpu_to_le32(1024);
+ ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ ctx->es.s_inode_size = cpu_to_le16(TEST_INODE_SIZE);
+
+ ctx->sb_bh.b_data = (char *)&ctx->es;
+ ctx->sb_bh.b_size = sizeof(ctx->es);
+ ctx->sb_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->sb_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->sb_bh);
+ ctx->sbi.s_sbh = &ctx->sb_bh;
+
+ ctx->sbi.s_sb = &ctx->sb;
+ ctx->sbi.s_es = &ctx->es;
+ ctx->sbi.s_inode_size = TEST_INODE_SIZE;
+ ctx->sbi.s_want_extra_isize = TEST_EXTRA_ISIZE;
+ ctx->sbi.s_cluster_bits = 0;
+ ctx->sbi.s_cluster_ratio = 1;
+ INIT_LIST_HEAD(&ctx->sbi.s_orphan);
+ mutex_init(&ctx->sbi.s_orphan_lock);
+ set_opt(&ctx->sb, ERRORS_CONT);
+ spin_lock_init(&ctx->sbi.s_bdev_wb_lock);
+ spin_lock_init(&ctx->sbi.s_error_lock);
+ spin_lock_init(&ctx->sbi.s_md_lock);
+ ratelimit_state_init(&ctx->sbi.s_err_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+ ratelimit_state_init(&ctx->sbi.s_warning_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+ ratelimit_state_init(&ctx->sbi.s_msg_ratelimit_state,
+ DEFAULT_RATELIMIT_INTERVAL, 0);
+
+ /* Mock buffer_head for raw inode (iloc.bh) */
+ ctx->iloc_bh.b_data = (char *)ctx->raw_inode_buf;
+ ctx->iloc_bh.b_size = TEST_BLOCK_SIZE;
+ ctx->iloc_bh.b_blocknr = 10;
+ ctx->iloc_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->iloc_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->iloc_bh);
+ set_buffer_mapped(&ctx->iloc_bh);
+
+ /* Mock buffer_head for allocated data block (data_bh) */
+ ctx->data_bh.b_data = (char *)ctx->data_block_buf;
+ ctx->data_bh.b_size = TEST_BLOCK_SIZE;
+ ctx->data_bh.b_blocknr = TEST_PBLK;
+ ctx->data_bh.b_bdev = &ctx->bdev;
+ atomic_set(&ctx->data_bh.b_count, 1);
+ set_buffer_uptodate(&ctx->data_bh);
+ set_buffer_mapped(&ctx->data_bh);
+
+ /* Default stub behavior */
+ ctx->map_blocks_ret = 1;
+ ctx->map_pblk = TEST_PBLK;
+ ctx->map_flags = EXT4_MAP_MAPPED;
+ ctx->journal_mode = EXT4_INODE_WRITEBACK_DATA_MODE;
+
+ /* Initialize mock ext4_inode_info and embedded vfs_inode */
+ inode = &ctx->ei.vfs_inode;
+ inode->i_sb = &ctx->sb;
+ inode->i_ino = TEST_INO;
+ inode->i_blkbits = TEST_BLOCK_BITS;
+ set_nlink(inode, 1);
+ atomic_set(&inode->i_count, 1);
+ spin_lock_init(&inode->i_lock);
+ init_rwsem(&inode->i_rwsem);
+ INIT_LIST_HEAD(&inode->i_io_list);
+ INIT_LIST_HEAD(&inode->i_wb_list);
+ INIT_LIST_HEAD(&inode->i_lru);
+ INIT_HLIST_NODE(&inode->i_hash);
+ inode_state_set_raw(inode, I_DIRTY);
+
+ address_space_init_once(&inode->i_data);
+ inode->i_data.host = inode;
+ inode->i_data.a_ops = &test_inline_aops;
+ mapping_set_gfp_mask(&inode->i_data, GFP_HIGHUSER_MOVABLE);
+ init_rwsem(&inode->i_data.invalidate_lock);
+ inode->i_mapping = &inode->i_data;
+
+ init_rwsem(&ctx->ei.xattr_sem);
+ init_rwsem(&ctx->ei.i_data_sem);
+ spin_lock_init(&ctx->ei.i_raw_lock);
+ spin_lock_init(&ctx->ei.i_block_reservation_lock);
+ INIT_LIST_HEAD(&ctx->ei.i_orphan);
+ ctx->ei.i_prealloc_node = RB_ROOT;
+ rwlock_init(&ctx->ei.i_prealloc_lock);
+ ext4_es_init_tree(&ctx->ei.i_es_tree);
+ rwlock_init(&ctx->ei.i_es_lock);
+ INIT_LIST_HEAD(&ctx->ei.i_es_list);
+ ctx->ei.i_es_all_nr = 0;
+ ctx->ei.i_es_shk_nr = 0;
+
+ reset_inode_state(ctx, S_IFREG | 0644);
+
+ /* Activate static stubs */
+ kunit_activate_static_stub(test, ext4_get_inode_loc,
+ stub_get_inode_loc);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ stub_handle_dirty_metadata);
+ kunit_activate_static_stub(test, ext4_mark_iloc_dirty,
+ stub_mark_iloc_dirty);
+ kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+ stub_mark_inode_dirty);
+ kunit_activate_static_stub(test, ext4_map_blocks,
+ stub_map_blocks);
+ kunit_activate_static_stub(test, ext4_inline_getblk,
+ stub_inline_getblk);
+ kunit_activate_static_stub(test, ext4_da_get_block_prep,
+ stub_da_get_block_prep);
+ kunit_activate_static_stub(test, ext4_inode_journal_mode,
+ stub_inode_journal_mode);
+
+ return 0;
+}
+
+static void inline_test_exit(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+
+ if (current->journal_info)
+ ext4_journal_stop(ext4_journal_current_handle());
+
+ if (ctx) {
+ if (!list_empty(&ctx->ei.i_orphan))
+ list_del_init(&ctx->ei.i_orphan);
+ truncate_inode_pages(&ctx->ei.vfs_inode.i_data, 0);
+ }
+ t_ctx = NULL;
+}
+
+/* =========================================================================
+ * 1. Capacity and Lookup Tests (ext4_get_max_inline_size /
+ * ext4_find_inline_data_nolock)
+ * =========================================================================
+ */
+
+static void test_max_inline_size_variants(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ int ret;
+
+ /* 1. Fresh 256-byte inode with 32-byte extra_isize -> 128 bytes */
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode),
+ TEST_MAX_INLINE_SIZE);
+
+ /* 2. Inode with i_extra_isize == 0 -> 0 */
+ ctx->ei.i_extra_isize = 0;
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode), 0);
+ ctx->ei.i_extra_isize = TEST_EXTRA_ISIZE;
+
+ /* 3. Old 128-byte inode size (no xattr space) -> 0 */
+ ctx->sbi.s_inode_size = EXT4_GOOD_OLD_INODE_SIZE;
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode), 0);
+ ctx->sbi.s_inode_size = TEST_INODE_SIZE;
+
+ /* 4. ext4_get_inode_loc error -> 0 */
+ ctx->get_inode_loc_err = -EIO;
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode), 0);
+ ctx->get_inode_loc_err = 0;
+
+ /* 5. With a user xattr in ibody reducing free space */
+ {
+ struct ext4_xattr_ibody_find is = {
+ .s = { .not_found = -ENODATA, },
+ };
+ struct ext4_xattr_info i = {
+ .name_index = EXT4_XATTR_INDEX_USER,
+ .name = "foo",
+ .value = "12345678",
+ .value_len = 8,
+ };
+
+ ret = ext4_get_inode_loc(inode, &is.iloc);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_xattr_ibody_find(inode, &i, &is);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_xattr_ibody_set(NULL, inode, &i, &is);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ brelse(is.iloc.bh);
+ }
+ /*
+ * "foo" entry takes EXT4_XATTR_LEN(3) = 20 bytes + EXT4_XATTR_SIZE(8) = 8
+ * Total 28 bytes consumed in ibody -> max inline size drops from 128 to 100.
+ */
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode),
+ TEST_MAX_INLINE_SIZE - 28);
+
+ /* 6. When inline data already exists in system.data, its space counts as usable */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ {
+ char buf[80];
+
+ memset(buf, 'A', sizeof(buf));
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ }
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode),
+ TEST_MAX_INLINE_SIZE);
+
+ /*
+ * 7. When ibody free space is smaller than EXT4_XATTR_LEN(strlen("data"))
+ * (20 bytes), get_max_inline_xattr_value_size returns 0 -> max is 0.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ {
+ struct ext4_xattr_ibody_find is = {
+ .s = { .not_found = -ENODATA, },
+ };
+ char val[60];
+ struct ext4_xattr_info i = {
+ .name_index = EXT4_XATTR_INDEX_USER,
+ .name = "bigattr",
+ .value = val,
+ .value_len = sizeof(val),
+ };
+
+ memset(val, 'x', sizeof(val));
+ ret = ext4_get_inode_loc(inode, &is.iloc);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_xattr_ibody_find(inode, &i, &is);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_xattr_ibody_set(NULL, inode, &i, &is);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ brelse(is.iloc.bh);
+ }
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode), 0);
+
+ /*
+ * 8. Corrupted xattr entry in ibody (e_name_len overflows end of inode)
+ * -> get_max_inline_xattr_value_size returns 0 -> total max is 0.
+ */
+ {
+ struct ext4_inode *raw_inode =
+ (struct ext4_inode *)ctx->raw_inode_buf;
+ struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
+ struct ext4_xattr_entry *entry = IFIRST(header);
+
+ entry->e_name_len = 250;
+ KUNIT_EXPECT_EQ(test, ext4_get_max_inline_size(inode), 0);
+ }
+}
+
+static void test_find_inline_data_nolock(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_iloc iloc = { 0 };
+ char buf[90];
+ int ret;
+
+ /* 1. i_extra_isize == 0 -> returns 0 immediately without setting inline state */
+ ctx->ei.i_extra_isize = 0;
+ ret = ext4_get_inode_loc(inode, &iloc);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ brelse(iloc.bh);
+ ctx->ei.i_extra_isize = TEST_EXTRA_ISIZE;
+
+ /* 2. ext4_get_inode_loc error -> propagates error */
+ ctx->get_inode_loc_err = -EIO;
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ ctx->get_inode_loc_err = 0;
+
+ /* 3. Inode without EXT4_STATE_XATTR -> returns 0, inline_size == 0 */
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 0);
+
+ /* 4. Inode with EXT4_STATE_XATTR set, but no system.data xattr -> 0 */
+ ext4_set_inode_state(inode, EXT4_STATE_XATTR);
+ {
+ struct ext4_inode *raw_inode =
+ (struct ext4_inode *)ctx->raw_inode_buf;
+ struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
+
+ header->h_magic = cpu_to_le32(EXT4_XATTR_MAGIC);
+ }
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 0);
+
+ /* 5. Inode with 30-byte inline file (0-byte system.data xattr) */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ memset(buf, 'B', sizeof(buf));
+ ret = create_inline_file(inode, buf, 30);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ctx->ei.i_inline_off = 0;
+ ctx->ei.i_inline_size = 0;
+ ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_NE(test, ctx->ei.i_inline_off, 0);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+
+ /* 6. Inode with 90-byte inline file (30-byte system.data xattr) */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, 90);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ctx->ei.i_inline_off = 0;
+ ctx->ei.i_inline_size = 0;
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 90);
+ KUNIT_EXPECT_GT(test, ctx->ei.i_inline_off,
+ EXT4_GOOD_OLD_INODE_SIZE + TEST_EXTRA_ISIZE);
+
+ /* 7. Corrupted system.data entry with non-zero e_value_inum -> -EFSCORRUPTED */
+ {
+ struct ext4_inode *raw_inode =
+ (struct ext4_inode *)ctx->raw_inode_buf;
+ struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
+ struct ext4_xattr_entry *entry = IFIRST(header);
+
+ entry->e_value_inum = cpu_to_le32(99);
+ ret = ext4_find_inline_data_nolock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+ }
+}
+
+/* =========================================================================
+ * 2. Inline File Write & Destroy Lifecycle Tests
+ * =========================================================================
+ */
+
+static void test_write_and_destroy_inline_data(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct ext4_iloc iloc = { 0 };
+ struct folio *folio = NULL;
+ handle_t *handle;
+ char pattern[100];
+ char verify_buf[100];
+ void *kaddr;
+ int i, ret;
+
+ for (i = 0; i < sizeof(pattern); i++)
+ pattern[i] = (char)('A' + (i % 26));
+
+ /*
+ * Step 1: Write 20 bytes into a brand-new inode (fits in i_block).
+ */
+ inode_lock(inode);
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 20,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, folio);
+ KUNIT_EXPECT_TRUE(test, folio_test_locked(folio));
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size,
+ (u16)EXT4_MIN_INLINE_DATA_SIZE);
+
+ kaddr = kmap_local_folio(folio, 0);
+ memcpy(kaddr, pattern, 20);
+ kunmap_local(kaddr);
+
+ ret = ext4_write_inline_data_end(inode, 0, 20, 20, folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 20);
+ KUNIT_EXPECT_NULL(test, current->journal_info);
+ i_size_write(inode, 20);
+ ctx->ei.i_disksize = 20;
+
+ /* Verify i_block contents */
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_i_block(ctx), pattern, 20);
+
+ /*
+ * Step 2: Expand from 20 bytes to 90 bytes (crosses 60-byte boundary
+ * into ibody system.data xattr via ext4_update_inline_data).
+ */
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 20, 70,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, folio);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 90);
+
+ /* Verify existing first 20 bytes were loaded into the folio by prepare_inline_data */
+ kaddr = kmap_local_folio(folio, 0);
+ KUNIT_EXPECT_MEMEQ(test, kaddr, pattern, 20);
+ memcpy((char *)kaddr + 20, pattern + 20, 70);
+ kunmap_local(kaddr);
+
+ ret = ext4_write_inline_data_end(inode, 20, 70, 70, folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 70);
+ i_size_write(inode, 90);
+ ctx->ei.i_disksize = 90;
+
+ /*
+ * Step 3: Overwrite 15 bytes purely within the xattr region
+ * (pos = 65, len = 15 -> pos >= EXT4_MIN_INLINE_DATA_SIZE).
+ */
+ memset(pattern + 65, 'Z', 15);
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 65, 15,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ kaddr = kmap_local_folio(folio, 0);
+ memcpy((char *)kaddr + 65, pattern + 65, 15);
+ kunmap_local(kaddr);
+ ret = ext4_write_inline_data_end(inode, 65, 15, 15, folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 15);
+
+ /* Verify both i_block (first 60B) and ibody xattr (remaining 30B) */
+ memcpy(verify_buf, raw_inline_i_block(ctx), EXT4_MIN_INLINE_DATA_SIZE);
+ memcpy(verify_buf + EXT4_MIN_INLINE_DATA_SIZE,
+ raw_inline_xattr_pos(ctx), 30);
+ KUNIT_EXPECT_MEMEQ(test, verify_buf, pattern, 90);
+
+ /*
+ * Step 4: Destroy inline data on a regular file -> clears inline state
+ * and initializes extent tree (EXT4_INODE_EXTENTS).
+ */
+ handle = ext4_journal_start(inode, EXT4_HT_INODE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_get_inode_loc(inode, &iloc);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = ext4_destroy_inline_data(handle, inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_off, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 0);
+ KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ brelse(iloc.bh);
+
+ /* Calling destroy again when inline_off == 0 is a no-op returning 0 */
+ ret = ext4_destroy_inline_data(handle, inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ ext4_journal_stop(handle);
+
+ /*
+ * Step 5: Initial creation directly > 60 bytes (75 bytes) on a non-extent
+ * mode inode (e.g. S_IFIFO) -> destroy does NOT set EXT4_INODE_EXTENTS.
+ */
+ reset_inode_state(ctx, S_IFIFO | 0644);
+ ret = create_inline_file(inode, pattern, 75);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 75);
+
+ handle = ext4_journal_start(inode, EXT4_HT_INODE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_destroy_inline_data(handle, inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ ext4_journal_stop(handle);
+}
+
+static void test_write_inline_short_copy_and_edge_cases(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct folio *folio = NULL;
+ char buf[80];
+ void *kaddr;
+ int ret;
+
+ memset(buf, 'M', sizeof(buf));
+ ret = create_inline_file(inode, buf, 20);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /*
+ * 1. Short write (copied < len) when expanding from 20B to 80B:
+ * prepare_inline_data expands xattr to 80B, but only 10 bytes are
+ * copied (pos=20, len=60, copied=10 -> pos+copied=30 > i_size=20).
+ * ext4_write_inline_data_end adds to orphan, updates i_disksize to 30,
+ * and calls ext4_truncate_failed_write -> shrinks inline data back to
+ * 60B (i_block only).
+ */
+ inode_lock(inode);
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 20, 60,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 80);
+
+ kaddr = kmap_local_folio(folio, 0);
+ memcpy((char *)kaddr + 20, "0123456789", 10);
+ kunmap_local(kaddr);
+
+ ret = ext4_write_inline_data_end(inode, 20, 60, 10, folio);
+ KUNIT_EXPECT_EQ(test, ret, 10);
+ KUNIT_EXPECT_EQ(test, i_size_read(inode), 30);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size,
+ (u16)EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->ei.i_orphan));
+ inode_unlock(inode);
+
+ /*
+ * 2. Short write (copied < len) when folio is NOT uptodate ->
+ * copied is forced to 0.
+ */
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 30, 10,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ folio_clear_uptodate(folio);
+ ret = ext4_write_inline_data_end(inode, 30, 10, 5, folio);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, i_size_read(inode), 30);
+ inode_unlock(inode);
+
+ /*
+ * 3. Emergency shutdown flag (EXT4_FLAGS_SHUTDOWN):
+ * 3a. Set before ext4_try_to_write_inline_data -> ext4_journal_start
+ * fails with -EIO.
+ * 3b. Set after ext4_try_to_write_inline_data -> ext4_write_inline_data
+ * returns early without modifying inline data.
+ */
+ set_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 10,
+ &folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ clear_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 10,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ kaddr = kmap_local_folio(folio, 0);
+ memcpy(kaddr, "XXXXXXXXXX", 10);
+ kunmap_local(kaddr);
+ set_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+ ret = ext4_write_inline_data_end(inode, 0, 10, 10, folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 10);
+ /* i_block should still have 'M's because shutdown skipped ext4_write_inline_data */
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_i_block(ctx), buf, 10);
+ clear_bit(EXT4_FLAGS_SHUTDOWN, &ctx->sbi.s_ext4_flags);
+
+ /*
+ * 4. Race condition where EXT4_INODE_INLINE_DATA is cleared before
+ * ext4_write_inline_data_end runs -> skips inline copy and returns 0.
+ */
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 10,
+ &folio);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ret = ext4_write_inline_data_end(inode, 0, 10, 10, folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+
+ /*
+ * 5. Corruption detection in ext4_create_inline_data when system.data
+ * already exists on an inode with i_inline_off == 0 (e.g. corrupted
+ * e_value_inum so ext4_find_inline_data_nolock leaves i_inline_off == 0).
+ */
+ {
+ struct ext4_inode *raw_inode =
+ (struct ext4_inode *)ctx->raw_inode_buf;
+ struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
+ struct ext4_xattr_entry *entry = IFIRST(header);
+
+ entry->e_value_inum = cpu_to_le32(1);
+ ctx->ei.i_inline_off = 0;
+ ctx->ei.i_inline_size = 0;
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0,
+ 10, &folio);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+ }
+
+ /*
+ * 6. Corruption detection in ext4_update_inline_data when inline state
+ * claims inline data exists, but system.data xattr was wiped.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ctx->ei.i_inline_off = 164;
+ ctx->ei.i_inline_size = EXT4_MIN_INLINE_DATA_SIZE;
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 80,
+ &folio);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+}
+
+/* =========================================================================
+ * 3. Reading Inline Data, Symlinks, and iomap Tests
+ * =========================================================================
+ */
+
+static void test_readpage_inline_link_and_iomap(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct folio *folio;
+ struct iomap iomap = { 0 };
+ char content[85];
+ const char *short_link = "/usr/bin/python3";
+ char long_link[85];
+ char *target;
+ void *kaddr;
+ int i, ret;
+
+ for (i = 0; i < sizeof(content); i++)
+ content[i] = (char)('0' + (i % 10));
+
+ /* 1. ext4_readpage_inline and ext4_inline_data_iomap on non-inline inode -> -EAGAIN */
+ folio = __filemap_get_folio(&inode->i_data, 0,
+ FGP_LOCK | FGP_ACCESSED | FGP_CREAT,
+ GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ ret = ext4_readpage_inline(inode, folio);
+ KUNIT_EXPECT_EQ(test, ret, -EAGAIN);
+ folio_unlock(folio);
+ folio_put(folio);
+
+ ret = ext4_inline_data_iomap(inode, &iomap);
+ KUNIT_EXPECT_EQ(test, ret, -EAGAIN);
+
+ /* 2. Create 85-byte inline file and test ext4_readpage_inline on index 0 */
+ truncate_inode_pages(&inode->i_data, 0);
+ ret = create_inline_file(inode, content, sizeof(content));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ truncate_inode_pages(&inode->i_data, 0);
+
+ folio = __filemap_get_folio(&inode->i_data, 0,
+ FGP_LOCK | FGP_ACCESSED | FGP_CREAT,
+ GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ ret = ext4_readpage_inline(inode, folio);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_TRUE(test, folio_test_uptodate(folio));
+ KUNIT_EXPECT_FALSE(test, folio_test_locked(folio));
+
+ kaddr = kmap_local_folio(folio, 0);
+ KUNIT_EXPECT_MEMEQ(test, kaddr, content, sizeof(content));
+ /* Verify zero-padding past inline_size */
+ KUNIT_EXPECT_EQ(test, ((char *)kaddr)[sizeof(content)], 0);
+ KUNIT_EXPECT_EQ(test, ((char *)kaddr)[PAGE_SIZE - 1], 0);
+ kunmap_local(kaddr);
+ folio_put(folio);
+
+ /* 3. ext4_readpage_inline on folio->index > 0 zeros the folio and marks uptodate */
+ folio = __filemap_get_folio(&inode->i_data, 1,
+ FGP_LOCK | FGP_ACCESSED | FGP_CREAT,
+ GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ kaddr = kmap_local_folio(folio, 0);
+ memset(kaddr, 0xff, PAGE_SIZE);
+ kunmap_local(kaddr);
+
+ ret = ext4_readpage_inline(inode, folio);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_TRUE(test, folio_test_uptodate(folio));
+ kaddr = kmap_local_folio(folio, 0);
+ KUNIT_EXPECT_EQ(test, ((unsigned char *)kaddr)[0], 0);
+ kunmap_local(kaddr);
+ folio_put(folio);
+
+ /* 4. ext4_readpage_inline when ext4_get_inode_loc fails -> propagates error */
+ truncate_inode_pages(&inode->i_data, 0);
+ ctx->get_inode_loc_err = -EIO;
+ folio = __filemap_get_folio(&inode->i_data, 0,
+ FGP_LOCK | FGP_ACCESSED | FGP_CREAT,
+ GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ ret = ext4_readpage_inline(inode, folio);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ folio_put(folio);
+ ctx->get_inode_loc_err = 0;
+
+ /*
+ * 5. ext4_readpage_inline when i_inline_size > PAGE_SIZE -> -EFSCORRUPTED.
+ * i_inline_size is a u16, so it can only exceed PAGE_SIZE when
+ * PAGE_SIZE < U16_MAX (e.g. 4KB, 8KB, 16KB, or 32KB base pages).
+ */
+ if (PAGE_SIZE < U16_MAX) {
+ truncate_inode_pages(&inode->i_data, 0);
+ ctx->ei.i_inline_size = U16_MAX;
+ i_size_write(inode, U16_MAX);
+ folio = __filemap_get_folio(&inode->i_data, 0,
+ FGP_LOCK | FGP_ACCESSED | FGP_CREAT,
+ GFP_KERNEL);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(folio));
+ ret = ext4_readpage_inline(inode, folio);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+ folio_put(folio);
+ ctx->ei.i_inline_size = sizeof(content);
+ i_size_write(inode, sizeof(content));
+ }
+
+ /* 6. ext4_inline_data_iomap populates IOMAP_INLINE with inline address/len */
+ memset(&iomap, 0, sizeof(iomap));
+ ret = ext4_inline_data_iomap(inode, &iomap);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_INLINE);
+ KUNIT_EXPECT_EQ(test, iomap.offset, 0ULL);
+ KUNIT_EXPECT_EQ(test, iomap.length, (u64)sizeof(content));
+ KUNIT_EXPECT_EQ(test, iomap.addr,
+ ((u64)ctx->iloc_bh.b_blocknr << TEST_BLOCK_BITS) +
+ offsetof(struct ext4_inode, i_block));
+
+ /* 7. ext4_inline_data_iomap error when ext4_get_inode_loc fails */
+ ctx->get_inode_loc_err = -EIO;
+ ret = ext4_inline_data_iomap(inode, &iomap);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ ctx->get_inode_loc_err = 0;
+
+ /* 8. ext4_read_inline_link: short symlink (in i_block) and long symlink (in xattr) */
+ reset_inode_state(ctx, S_IFLNK | 0777);
+ ret = create_inline_file(inode, short_link, strlen(short_link));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ target = ext4_read_inline_link(inode);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(target));
+ KUNIT_EXPECT_STREQ(test, target, short_link);
+ kfree(target);
+
+ reset_inode_state(ctx, S_IFLNK | 0777);
+ memset(long_link, 'a', sizeof(long_link) - 1);
+ long_link[sizeof(long_link) - 1] = '\0';
+ ret = create_inline_file(inode, long_link, strlen(long_link));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ target = ext4_read_inline_link(inode);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(target));
+ KUNIT_EXPECT_STREQ(test, target, long_link);
+ kfree(target);
+
+ /* ext4_read_inline_link error when ext4_get_inode_loc fails */
+ ctx->get_inode_loc_err = -EIO;
+ target = ext4_read_inline_link(inode);
+ KUNIT_EXPECT_TRUE(test, IS_ERR(target));
+ KUNIT_EXPECT_EQ(test, PTR_ERR(target), -EIO);
+ ctx->get_inode_loc_err = 0;
+}
+
+/* =========================================================================
+ * 4. Inline Truncation Tests (ext4_inline_data_truncate)
+ * =========================================================================
+ */
+
+static void test_inline_data_truncate(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ int has_inline = 1;
+ char buf[100];
+ int i, ret;
+
+ for (i = 0; i < sizeof(buf); i++)
+ buf[i] = (char)('a' + (i % 26));
+
+ /* 1. Non-inline inode -> sets *has_inline = 0 and returns */
+ inode_lock(inode);
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, has_inline, 0);
+
+ /* 2. Create a 100-byte inline file (60B in i_block + 40B in xattr) */
+ ret = create_inline_file(inode, buf, 100);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 100);
+
+ /* 3. Truncate extending i_size (100 -> 120): no-op on inline size */
+ inode_lock(inode);
+ i_size_write(inode, 120);
+ has_inline = 1;
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, has_inline, 1);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 100);
+
+ /* 4. Shrink within xattr space (100 -> 75 bytes) */
+ inode_lock(inode);
+ i_size_write(inode, 75);
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, has_inline, 1);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, 75);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 75);
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_xattr_pos(ctx),
+ buf + EXT4_MIN_INLINE_DATA_SIZE, 15);
+
+ /* 5. Shrink from xattr space into i_block (75 -> 25 bytes): zeros i_block[25..59] */
+ inode_lock(inode);
+ i_size_write(inode, 25);
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size,
+ (u16)EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 25);
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_i_block(ctx), buf, 25);
+ for (i = 25; i < EXT4_MIN_INLINE_DATA_SIZE; i++)
+ KUNIT_EXPECT_EQ(test, ((char *)raw_inline_i_block(ctx))[i], 0);
+
+ /* 6. Shrink to 0 bytes: zeros all of i_block */
+ inode_lock(inode);
+ i_size_write(inode, 0);
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 0);
+ for (i = 0; i < EXT4_MIN_INLINE_DATA_SIZE; i++)
+ KUNIT_EXPECT_EQ(test, ((char *)raw_inline_i_block(ctx))[i], 0);
+
+ /*
+ * 7. When !EXT4_STATE_MAY_INLINE_DATA (being converted),
+ * ext4_inline_data_truncate clears the extent status tree.
+ */
+ ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ inode_lock(inode);
+ ext4_inline_data_truncate(inode, &has_inline);
+ inode_unlock(inode);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->ei.i_orphan));
+}
+
+/* =========================================================================
+ * 5. Inline-to-Extent Conversion Tests for Regular Files
+ * =========================================================================
+ */
+
+static void test_convert_inline_data_and_rollback(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ char buf[80];
+ int i, ret;
+
+ for (i = 0; i < sizeof(buf); i++)
+ buf[i] = (char)('A' + (i % 26));
+
+ /* 1. Non-inline inode -> clears MAY_INLINE_DATA and returns 0 immediately */
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA));
+ ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+
+ /*
+ * 2. Inline inode without EXT4_STATE_MAY_INLINE_DATA -> calls
+ * filemap_flush() and finishes converting inline data if still inline.
+ */
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_MEMEQ(test, ctx->data_block_buf, buf, sizeof(buf));
+
+ /*
+ * 3. Error rollback when ext4_map_blocks fails (-ENOSPC):
+ * ext4_restore_inline_data restores the original 80 bytes of inline data.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->map_blocks_ret = -ENOSPC;
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, sizeof(buf));
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_i_block(ctx), buf,
+ EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_MEMEQ(test, raw_inline_xattr_pos(ctx),
+ buf + EXT4_MIN_INLINE_DATA_SIZE,
+ sizeof(buf) - EXT4_MIN_INLINE_DATA_SIZE);
+ ctx->map_blocks_ret = 1;
+
+ /*
+ * 4. Error rollback when ext4_map_blocks returns unmapped block -> -EIO
+ */
+ ctx->map_flags = 0;
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+ ctx->map_flags = EXT4_MAP_MAPPED;
+
+ /*
+ * 5. Error rollback when ext4_inline_getblk returns NULL -> -ENOMEM
+ */
+ ctx->getblk_fail = true;
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, -ENOMEM);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+ ctx->getblk_fail = false;
+
+ /*
+ * 6. Error rollback when ext4_handle_dirty_metadata fails on data_bh
+ */
+ ctx->dirty_metadata_err = -EIO;
+ ctx->dirty_metadata_fail_on_call = 1;
+ ctx->dirty_metadata_calls = 0;
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(inode));
+ ctx->dirty_metadata_err = 0;
+ ctx->dirty_metadata_fail_on_call = 0;
+
+ /*
+ * 7. Successful conversion via ext4_convert_inline_data():
+ * copies 80 bytes into data_bh, destroys inline data, initializes
+ * extent tree.
+ */
+ memset(ctx->data_block_buf, 0, sizeof(ctx->data_block_buf));
+ ret = ext4_convert_inline_data(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_MEMEQ(test, ctx->data_block_buf, buf, sizeof(buf));
+}
+
+static void test_write_triggers_inline_to_extent_conversion(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *inode = &ctx->ei.vfs_inode;
+ struct folio *folio = NULL;
+ char buf[50];
+ int ret;
+
+ memset(buf, 'K', sizeof(buf));
+
+ /*
+ * 1. Standard write (ext4_try_to_write_inline_data) exceeding
+ * TEST_MAX_INLINE_SIZE (pos=0, len=200) converts inline data via
+ * ext4_convert_inline_data_to_extent -> returns 0 so caller proceeds
+ * with normal block write.
+ */
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ inode_lock(inode);
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_TRUE(test, ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+
+ /*
+ * 2. Conversion with DIOREAD_NOLOCK + DELALLOC enabled and journal_data
+ * mode (exercises ext4_should_dioread_nolock and ext4_should_journal_data
+ * branches in ext4_convert_inline_data_to_extent).
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ set_opt(&ctx->sb, DIOREAD_NOLOCK);
+ set_opt(&ctx->sb, DELALLOC);
+ ctx->journal_mode = EXT4_INODE_JOURNAL_DATA_MODE;
+
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+
+ clear_opt(&ctx->sb, DIOREAD_NOLOCK);
+ clear_opt(&ctx->sb, DELALLOC);
+ ctx->journal_mode = EXT4_INODE_WRITEBACK_DATA_MODE;
+
+ /*
+ * 3. Error paths in ext4_convert_inline_data_to_extent when
+ * ext4_get_block fails during ext4_block_write_begin (-EIO and -ENOSPC):
+ * cleans up orphan and propagates error.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ctx->map_blocks_ret = -EIO;
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -EIO);
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->ei.i_orphan));
+ ctx->map_blocks_ret = 1;
+
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ ctx->map_blocks_ret = -ENOSPC;
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(inode));
+ KUNIT_EXPECT_TRUE(test, list_empty(&ctx->ei.i_orphan));
+ ctx->map_blocks_ret = 1;
+
+ /*
+ * 4. Delayed allocation conversion (da = true in
+ * ext4_generic_write_inline_data):
+ * 4a. Fresh inode without inline data yet -> clears MAY_INLINE_DATA
+ * and returns 0.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ folio = NULL;
+ ret = ext4_generic_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio, true);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA));
+
+ /*
+ * 4b. Existing inline data -> ext4_da_convert_inline_data_to_extent
+ * loads inline bytes into folio, marks buffer delayed, and clears
+ * EXT4_STATE_MAY_INLINE_DATA while keeping inline data until
+ * writepages.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ truncate_inode_pages(&inode->i_data, 0);
+
+ folio = NULL;
+ ret = ext4_generic_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio, true);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA));
+
+ /*
+ * 4c. Error in ext4_da_get_block_prep during DA conversion ->
+ * propagates error and unlocks folio.
+ */
+ reset_inode_state(ctx, S_IFREG | 0644);
+ ret = create_inline_file(inode, buf, sizeof(buf));
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ truncate_inode_pages(&inode->i_data, 0);
+
+ ctx->da_get_block_prep_err = -ENOSPC;
+ inode_lock(inode);
+ folio = NULL;
+ ret = ext4_generic_write_inline_data(&inode->i_data, inode, 0, 200,
+ &folio, true);
+ inode_unlock(inode);
+ KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+ ctx->da_get_block_prep_err = 0;
+}
+
+/* =========================================================================
+ * 6. Inline Directory Operations Tests
+ * =========================================================================
+ */
+
+struct test_dir_ctx {
+ struct dir_context ctx;
+ int count;
+ char names[16][32];
+ u64 inos[16];
+ int max_emit;
+ struct inode *mutate_inode_on_second;
+};
+
+static bool test_filldir_actor(struct dir_context *dctx, const char *name,
+ int namelen, loff_t offset, u64 ino,
+ unsigned int d_type)
+{
+ struct test_dir_ctx *tctx =
+ container_of(dctx, struct test_dir_ctx, ctx);
+
+ if (tctx->max_emit > 0 && tctx->count >= tctx->max_emit)
+ return false;
+
+ if (tctx->count < 16) {
+ int copy_len = min(namelen, 31);
+
+ memcpy(tctx->names[tctx->count], name, copy_len);
+ tctx->names[tctx->count][copy_len] = '\0';
+ tctx->inos[tctx->count] = ino;
+ }
+ tctx->count++;
+
+ if (tctx->mutate_inode_on_second && tctx->count == 2) {
+ inode_inc_iversion(tctx->mutate_inode_on_second);
+ tctx->mutate_inode_on_second = NULL;
+ }
+ return true;
+}
+
+struct inline_dir_test_env {
+ struct inode parent;
+ struct inode child;
+ struct qstr qname;
+ struct ext4_filename fname;
+ struct dir_private_info info;
+ struct file filp;
+ struct test_dir_ctx dctx;
+ struct ext4_dir_entry_2 bogus_de;
+};
+
+static struct inline_dir_test_env *
+alloc_dir_test_env(struct kunit *test, struct inline_test_ctx *ctx)
+{
+ struct inline_dir_test_env *env;
+
+ env = kunit_kzalloc(test, sizeof(*env), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, env);
+ env->parent.i_sb = &ctx->sb;
+ env->parent.i_ino = TEST_PARENT_INO;
+ env->parent.i_mode = S_IFDIR | 0755;
+ return env;
+}
+
+static void init_mock_fname_and_child(struct inline_dir_test_env *env,
+ struct super_block *sb,
+ const char *name, ino_t ino,
+ umode_t mode)
+{
+ memset(&env->fname, 0, sizeof(env->fname));
+ memset(&env->qname, 0, sizeof(env->qname));
+ memset(&env->child, 0, sizeof(env->child));
+
+ env->child.i_sb = sb;
+ env->child.i_ino = ino;
+ env->child.i_mode = mode;
+
+ env->qname.name = (const unsigned char *)name;
+ env->qname.len = strlen(name);
+ env->fname.usr_fname = &env->qname;
+ env->fname.disk_name.name = (unsigned char *)name;
+ env->fname.disk_name.len = strlen(name);
+}
+
+static void test_update_final_de(struct kunit *test)
+{
+ u8 buf[64];
+ struct ext4_dir_entry_2 *de1, *de2;
+
+ memset(buf, 0, sizeof(buf));
+
+ /* 1. old_size == 0 -> sets de1->rec_len to new_size (32) */
+ de1 = (struct ext4_dir_entry_2 *)buf;
+ ext4_update_final_de(buf, 0, 32);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(de1->rec_len), 32);
+
+ /* 2. Two entries in 32 bytes (12 + 20), expand to 56 bytes -> last entry becomes 44 */
+ de1->rec_len = cpu_to_le16(12);
+ de2 = (struct ext4_dir_entry_2 *)(buf + 12);
+ de2->rec_len = cpu_to_le16(20);
+ ext4_update_final_de(buf, 32, 56);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(de1->rec_len), 12);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(de2->rec_len), 44);
+}
+
+static void test_inline_dir_lifecycle_and_operations(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *dir = &ctx->ei.vfs_inode;
+ struct inline_dir_test_env *env;
+ struct ext4_dir_entry_2 *de = NULL;
+ struct buffer_head *bh = NULL;
+ handle_t *handle;
+ int has_inline_data = 1;
+ int ret;
+
+ env = alloc_dir_test_env(test, ctx);
+ reset_inode_state(ctx, S_IFDIR | 0755);
+
+ /*
+ * 1. Create inline directory via ext4_try_create_inline_dir.
+ */
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_create_inline_dir(handle, &env->parent, dir);
+ ext4_journal_stop(handle);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(dir));
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size,
+ (u16)EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_EQ(test, i_size_read(dir),
+ (loff_t)EXT4_MIN_INLINE_DATA_SIZE);
+
+ /* Newly created inline dir is empty */
+ KUNIT_EXPECT_TRUE(test, empty_inline_dir(dir, &has_inline_data));
+ KUNIT_EXPECT_EQ(test, has_inline_data, 1);
+
+ /*
+ * 2. Verify ext4_get_first_inline_block returns the inline buffer
+ * starting at parent_de (whose inode field holds the parent ino).
+ */
+ bh = ext4_get_first_inline_block(dir, &de, &ret);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_PARENT_INO);
+ brelse(bh);
+
+ /*
+ * 3. Add first entry "file1" (fits in i_block alongside parent ino).
+ * Return value 1 means added inline without block conversion.
+ */
+ init_mock_fname_and_child(env, &ctx->sb, "file1", 101, S_IFREG | 0644);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size,
+ (u16)EXT4_MIN_INLINE_DATA_SIZE);
+ KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
+
+ /*
+ * 4. Adding duplicate "file1" returns -EEXIST.
+ */
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, -EEXIST);
+
+ /*
+ * 5. Add more entries ("file2_longer_name", "file3_longer_name",
+ * "file4_in_xattr") to overflow i_block (56B) and trigger expansion
+ * into ibody system.data xattr via ext4_update_inline_dir (132B).
+ */
+ init_mock_fname_and_child(env, &ctx->sb, "file2_longer_name", 102,
+ S_IFREG | 0644);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+
+ init_mock_fname_and_child(env, &ctx->sb, "file3_longer_name", 103,
+ S_IFDIR | 0755);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+ KUNIT_EXPECT_EQ(test, ctx->ei.i_inline_size, TEST_MAX_INLINE_SIZE);
+
+ init_mock_fname_and_child(env, &ctx->sb, "file4_in_xattr", 104,
+ S_IFREG | 0644);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 1);
+
+ /*
+ * 6. Lookup entries in both i_block ("file1") and xattr ("file4_in_xattr"),
+ * and verify missing entry returns NULL.
+ */
+ init_mock_fname_and_child(env, &ctx->sb, "file1", 0, 0);
+ bh = ext4_find_inline_entry(dir, &env->fname, &de, &has_inline_data);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, has_inline_data, 1);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), 101U);
+ brelse(bh);
+
+ init_mock_fname_and_child(env, &ctx->sb, "file4_in_xattr", 0, 0);
+ bh = ext4_find_inline_entry(dir, &env->fname, &de, &has_inline_data);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), 104U);
+ brelse(bh);
+
+ init_mock_fname_and_child(env, &ctx->sb, "nonexistent", 0, 0);
+ bh = ext4_find_inline_entry(dir, &env->fname, &de, &has_inline_data);
+ KUNIT_EXPECT_NULL(test, bh);
+ KUNIT_EXPECT_EQ(test, has_inline_data, 1);
+
+ /*
+ * 7. Test ext4_read_inline_dir (full iteration, partial iteration,
+ * and iversion change re-validation).
+ */
+ env->filp.f_inode = dir;
+ env->filp.private_data = &env->info;
+ env->dctx.ctx.actor = test_filldir_actor;
+ env->dctx.ctx.pos = 0;
+
+ ret = ext4_read_inline_dir(&env->filp, &env->dctx.ctx,
+ &has_inline_data);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, has_inline_data, 1);
+ /*
+ * Expect ".", "..", "file1", "file2_longer_name",
+ * "file3_longer_name", "file4_in_xattr"
+ */
+ KUNIT_EXPECT_EQ(test, env->dctx.count, 6);
+ KUNIT_EXPECT_STREQ(test, env->dctx.names[0], ".");
+ KUNIT_EXPECT_STREQ(test, env->dctx.names[1], "..");
+ KUNIT_EXPECT_STREQ(test, env->dctx.names[2], "file1");
+ KUNIT_EXPECT_STREQ(test, env->dctx.names[5], "file4_in_xattr");
+ /*
+ * extra_size = (dotdot_size - EXT4_INLINE_DOTDOT_SIZE) +
+ * TEST_MAX_INLINE_SIZE = (24 - 4) + 128 = 148
+ */
+ KUNIT_EXPECT_EQ(test, env->dctx.ctx.pos,
+ (loff_t)(TEST_MAX_INLINE_SIZE + 20));
+
+ /* Calling again at EOF (pos == inline_size) emits nothing */
+ env->dctx.count = 0;
+ ret = ext4_read_inline_dir(&env->filp, &env->dctx.ctx,
+ &has_inline_data);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, env->dctx.count, 0);
+
+ /* Mutating inode iversion during iteration triggers offset revalidation */
+ memset(&env->dctx, 0, sizeof(env->dctx));
+ env->dctx.ctx.actor = test_filldir_actor;
+ env->dctx.ctx.pos = 0;
+ env->dctx.mutate_inode_on_second = dir;
+ ret = ext4_read_inline_dir(&env->filp, &env->dctx.ctx,
+ &has_inline_data);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, env->dctx.count, 6);
+
+ /*
+ * 8. Test ext4_inlinedir_to_tree (htree rbtree population).
+ */
+ {
+ struct dir_private_info *info;
+ struct dx_hash_info hinfo = {
+ .hash_version = DX_HASH_HALF_MD4,
+ };
+
+ memset(&env->filp, 0, sizeof(env->filp));
+ env->filp.f_inode = dir;
+ info = kzalloc_obj(*info, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, info);
+ env->filp.private_data = info;
+ ret = ext4_inlinedir_to_tree(&env->filp, dir, 0, &hinfo,
+ 0, 0, &has_inline_data);
+ KUNIT_EXPECT_GT(test, ret, 0);
+ KUNIT_EXPECT_EQ(test, has_inline_data, 1);
+
+ /* With start_hash = 0xffffffffU, entries with smaller hash are skipped */
+ ext4_htree_free_dir_info(info);
+ info = kzalloc_obj(*info, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, info);
+ env->filp.private_data = info;
+ ret = ext4_inlinedir_to_tree(&env->filp, dir, 0, &hinfo,
+ 0xffffffffU, 0xffffffffU,
+ &has_inline_data);
+ KUNIT_EXPECT_GE(test, ret, 0);
+ ext4_htree_free_dir_info(info);
+ }
+
+ /*
+ * 9. Delete entries in both i_block and xattr regions and verify
+ * empty_inline_dir behavior when only xattr has an entry vs completely empty.
+ */
+ {
+ static const char * const del_names[] = {
+ "file1",
+ "file2_longer_name",
+ "file3_longer_name",
+ };
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(del_names); i++) {
+ init_mock_fname_and_child(env, &ctx->sb, del_names[i],
+ 0, 0);
+ bh = ext4_find_inline_entry(dir, &env->fname, &de,
+ &has_inline_data);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_delete_inline_entry(handle, dir, de, bh,
+ &has_inline_data);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ brelse(bh);
+ }
+ }
+
+ /* i_block now has no live entries, but "file4_in_xattr" is still in xattr */
+ KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
+
+ /* Delete "file4_in_xattr" -> directory becomes completely empty */
+ init_mock_fname_and_child(env, &ctx->sb, "file4_in_xattr", 0, 0);
+ bh = ext4_find_inline_entry(dir, &env->fname, &de, &has_inline_data);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+
+ /* Deleting an address outside inline buffers returns -ENOENT */
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_delete_inline_entry(handle, dir, &env->bogus_de, bh,
+ &has_inline_data);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, -ENOENT);
+
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_delete_inline_entry(handle, dir, de, bh, &has_inline_data);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ brelse(bh);
+
+ KUNIT_EXPECT_TRUE(test, empty_inline_dir(dir, &has_inline_data));
+}
+
+static void test_inline_dir_conversion_and_corruptions(struct kunit *test)
+{
+ struct inline_test_ctx *ctx = test->priv;
+ struct inode *dir = &ctx->ei.vfs_inode;
+ struct inline_dir_test_env *env;
+ struct ext4_dir_entry_2 *de;
+ handle_t *handle;
+ int has_inline_data = 1;
+ int i, ret;
+
+ env = alloc_dir_test_env(test, ctx);
+ reset_inode_state(ctx, S_IFDIR | 0755);
+
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_create_inline_dir(handle, &env->parent, dir);
+ ext4_journal_stop(handle);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /* Fill the inline directory up to its 128-byte limit */
+ for (i = 0; i < 4; i++) {
+ char name[24];
+
+ snprintf(name, sizeof(name), "entry_long_name_%d", i);
+ init_mock_fname_and_child(env, &ctx->sb, name, 200 + i,
+ S_IFREG | 0644);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir,
+ &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_ASSERT_EQ(test, ret, 1);
+ }
+
+ /*
+ * 1. When full, adding another entry triggers ext4_convert_inline_data_nolock.
+ * Test rollback first when ext4_map_blocks fails (-ENOSPC).
+ */
+ init_mock_fname_and_child(env, &ctx->sb, "overflow_entry_name", 250,
+ S_IFREG | 0644);
+ ctx->map_blocks_ret = -ENOSPC;
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, -ENOSPC);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(dir));
+ ctx->map_blocks_ret = 1;
+
+ /*
+ * 2. Successful conversion of full inline directory to a normal
+ * 1024-byte directory block -> ext4_try_add_inline_entry returns 0
+ * and data_block_buf starts with "." and ".." followed by copied entries.
+ */
+ memset(ctx->data_block_buf, 0, sizeof(ctx->data_block_buf));
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_add_inline_entry(handle, &env->fname, dir, &env->child);
+ ext4_journal_stop(handle);
+ KUNIT_EXPECT_EQ(test, ret, 0);
+ KUNIT_EXPECT_FALSE(test, ext4_has_inline_data(dir));
+ KUNIT_EXPECT_EQ(test, i_size_read(dir), (loff_t)TEST_BLOCK_SIZE);
+
+ /* Verify "." and ".." headers in converted block */
+ de = (struct ext4_dir_entry_2 *)ctx->data_block_buf;
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_INO);
+ KUNIT_EXPECT_EQ(test, de->name_len, 1);
+ KUNIT_EXPECT_EQ(test, de->name[0], '.');
+ de = ext4_next_entry(de, TEST_BLOCK_SIZE);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_PARENT_INO);
+ KUNIT_EXPECT_EQ(test, de->name_len, 2);
+ KUNIT_EXPECT_MEMEQ(test, de->name, "..", 2);
+
+ /*
+ * 3. Non-inline directory behavior on inline dir functions:
+ * sets *has_inline_data = 0 and returns early.
+ */
+ has_inline_data = 1;
+ KUNIT_EXPECT_TRUE(test, empty_inline_dir(dir, &has_inline_data));
+ KUNIT_EXPECT_EQ(test, has_inline_data, 0);
+
+ /*
+ * 4. Corrupted inline directory checks:
+ * 4a. Parent ino ("..") == 0 -> empty_inline_dir returns false.
+ * 4b. Zero rec_len dirent -> empty_inline_dir returns false,
+ * ext4_inlinedir_to_tree returns -EFSCORRUPTED, and
+ * ext4_convert_inline_data_nolock aborts via ext4_check_all_de.
+ */
+ reset_inode_state(ctx, S_IFDIR | 0755);
+ handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ret = ext4_try_create_inline_dir(handle, &env->parent, dir);
+ ext4_journal_stop(handle);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ /* Corrupt parent ino to 0 */
+ *(__le32 *)raw_inline_i_block(ctx) = cpu_to_le32(0);
+ KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
+ *(__le32 *)raw_inline_i_block(ctx) = cpu_to_le32(TEST_PARENT_INO);
+
+ /* Corrupt first dirent rec_len to 0 */
+ de = (struct ext4_dir_entry_2 *)((char *)raw_inline_i_block(ctx) +
+ EXT4_INLINE_DOTDOT_SIZE);
+ de->rec_len = cpu_to_le16(0);
+ KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
+
+ /* ext4_convert_inline_data on corrupted inline dir fails in ext4_check_all_de */
+ ret = ext4_convert_inline_data(dir);
+ KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
+ KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(dir));
+}
+
+static struct kunit_case ext4_inline_test_cases[] = {
+ KUNIT_CASE(test_max_inline_size_variants),
+ KUNIT_CASE(test_find_inline_data_nolock),
+ KUNIT_CASE(test_write_and_destroy_inline_data),
+ KUNIT_CASE(test_write_inline_short_copy_and_edge_cases),
+ KUNIT_CASE(test_readpage_inline_link_and_iomap),
+ KUNIT_CASE(test_inline_data_truncate),
+ KUNIT_CASE(test_convert_inline_data_and_rollback),
+ KUNIT_CASE(test_write_triggers_inline_to_extent_conversion),
+ KUNIT_CASE(test_update_final_de),
+ KUNIT_CASE(test_inline_dir_lifecycle_and_operations),
+ KUNIT_CASE(test_inline_dir_conversion_and_corruptions),
+ {}
+};
+
+static struct kunit_suite ext4_inline_test_suite = {
+ .name = "ext4_inline_test",
+ .init = inline_test_init,
+ .exit = inline_test_exit,
+ .test_cases = ext4_inline_test_cases,
+};
+
+kunit_test_suites(&ext4_inline_test_suite);
+
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 0e662a9f7574..97cc2c61ce25 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -9,6 +9,7 @@
#include <linux/namei.h>
#include <linux/iversion.h>
#include <linux/sched/mm.h>
+#include <kunit/static_stub.h>
#include "ext4_jbd2.h"
#include "ext4.h"
@@ -1096,6 +1097,13 @@ static void ext4_restore_inline_data(handle_t *handle, struct inode *inode,
ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
}
+struct buffer_head *ext4_inline_getblk(struct super_block *sb,
+ ext4_fsblk_t block)
+{
+ KUNIT_STATIC_STUB_REDIRECT(ext4_inline_getblk, sb, block);
+ return sb_getblk(sb, block);
+}
+
static int ext4_convert_inline_data_nolock(handle_t *handle,
struct inode *inode,
struct ext4_iloc *iloc)
@@ -1144,7 +1152,7 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
goto out_restore;
}
- data_bh = sb_getblk(inode->i_sb, map.m_pblk);
+ data_bh = ext4_inline_getblk(inode->i_sb, map.m_pblk);
if (!data_bh) {
error = -ENOMEM;
goto out_restore;
@@ -2018,5 +2026,25 @@ int ext4_convert_inline_data(struct inode *inode)
}
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inline_getblk);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_max_inline_size);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_inline_data_nolock);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_readpage_inline);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_generic_write_inline_data);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_try_to_write_inline_data);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_write_inline_data_end);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_update_final_de);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_try_add_inline_entry);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inlinedir_to_tree);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_inline_dir);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_inline_link);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_first_inline_block);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_try_create_inline_dir);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_find_inline_entry);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_delete_inline_entry);
+EXPORT_SYMBOL_FOR_EXT4_TEST(empty_inline_dir);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_destroy_inline_data);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inline_data_iomap);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inline_data_truncate);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_convert_inline_data);
#endif
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index c8a06c455b3f..105c731b95b1 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -2030,6 +2030,9 @@ int ext4_da_get_block_prep(struct inode *inode, sector_t iblock,
sector_t invalid_block = ~((sector_t) 0xffff);
int ret = 0;
+ KUNIT_STATIC_STUB_REDIRECT(ext4_da_get_block_prep, inode, iblock, bh,
+ create);
+
BUG_ON(create == 0);
BUG_ON(bh->b_size != inode->i_sb->s_blocksize);
@@ -6999,6 +7002,7 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
}
#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_get_block_prep);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_update_reserve_space);
EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_release_space);
EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_mark_inode_dirty);
--
2.56.0.542.g3ef89b7020-goog
next prev 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 ` Theodore Ts'o [this message]
2026-11-12 4:29 ` [PATCH 07/27] ext4: add KUnit tests for fs/ext4/inline.c sashiko-bot
2026-11-12 4:06 ` [PATCH 08/27] ext4: zero s_fc_replay_state in ext4_fc_replay_cleanup() Theodore Ts'o
2026-11-12 4:16 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 09/27] ext4: add KUnit tests for fs/ext4/fast_commit.c Theodore Ts'o
2026-11-12 4:28 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 10/27] ext4: expand KUnit tests for fs/ext4/inode.c Theodore Ts'o
2026-11-12 4:36 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 11/27] ext4: add KUnit tests for fs/ext4/ialloc.c Theodore Ts'o
2026-11-12 4:25 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 12/27] ext4: add KUnit tests for fs/ext4/balloc.c Theodore Ts'o
2026-11-12 4:16 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 13/27] ext4: add KUnit tests for fs/ext4/orphan.c Theodore Ts'o
2026-11-12 4:18 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 14/27] ext4: add KUnit tests for fs/ext4/block_validity.c Theodore Ts'o
2026-11-12 4:21 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 15/27] ext4: check multi-block overlap on left traversal in add_system_zone() Theodore Ts'o
2026-11-12 4:10 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 16/27] ext4: enable full feature coverage in fs/ext4/.kunitconfig Theodore Ts'o
2026-11-12 4:24 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 17/27] ext4: add KUnit tests for fs/ext4/super.c Theodore Ts'o
2026-11-12 4:27 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 18/27] ext4: add KUnit tests for fs/ext4/sysfs.c Theodore Ts'o
2026-11-12 4:32 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 19/27] ext4: add KUnit tests for fs/ext4/ioctl.c Theodore Ts'o
2026-11-12 4:35 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 20/27] ext4: add KUnit tests for fs/ext4/resize.c Theodore Ts'o
2026-11-12 4:34 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 21/27] ext4: add KUnit tests for fs/ext4/migrate.c Theodore Ts'o
2026-11-12 4:37 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 22/27] ext4: add KUnit tests for fs/ext4/indirect.c Theodore Ts'o
2026-11-12 4:46 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 23/27] ext4: expand KUnit tests in fs/ext4/extents-test.c for file range operations Theodore Ts'o
2026-11-12 4:34 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 24/27] ext4: add KUnit tests for fs/ext4/fsmap.c Theodore Ts'o
2026-11-12 4:30 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 25/27] ext4: add KUnit tests for fs/ext4/move_extent.c Theodore Ts'o
2026-11-12 4:36 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 26/27] ext4: add KUnit tests for fs/ext4/mmp.c Theodore Ts'o
2026-11-12 4:29 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 27/27] ext4: add KUnit tests for fs/ext4/dir.c Theodore Ts'o
2026-11-12 4:41 ` sashiko-bot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261112040638.1576836-8-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;
as well as URLs for NNTP newsgroup(s).