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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.