From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=18.9.28.11 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1794456427; b=zJvlAqLUNIcQ5QwqgqSsph08+E6cq9DNDF190uvq+7FcG2BOrDHs9A7TCda9fochhUcC HGFPhWZYRi561daOUqh0ASXRhm+FW2CRrsmD1GTgVLYnhaK6awap56A5HljEA3K0NEF7v p3ekssPhxTQndRdeSka92qb5KgaVU273r8BWMcoyBXa0asWScVxRBPTFyhhvMFHIGd0ex kXn/zGnitU6SUljUk5X6g/4rvIEshoqZsr/2Y3bJ52wskNR7P4z/8A/1wmGs9QXFfZ2Om HtOQR6Obdyz11EqTlOMMhT2eMw2e3bEbKfXEIhMeUXUUdXBClaW+6b9/QLj1/wCkemQ== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1794456427; h=DMARC-Filter:Received:DKIM-Signature:Received:From:To:Cc:Subject: Date:Message-ID:X-Mailer:In-Reply-To:References:MIME-Version: Content-Transfer-Encoding; bh=/tdojRXdaGh8BJBsoHlNTXQQHYVoxPQcahAX2FB9Nik=; b=jw70qWPZrsmd0BsC2SCOXd4eNaPAMEIePr12IvWkzRCabhONE8mjCmL6+sVND2+7ERd9 gYIjGCjS0ttbvEqeDtPLWZTXVDwPr+3Wq3UjHxi2W4V1LN1MCmjZlKQ4vqr4mUpGpxs/D 1XWOg4AyqS5FoU4QYgFfnLnflvo/ifvEXkeqp6VMGsQ4hi15FKwIoNgmNZOThKrxRcfnK pkcId+gvkG4IxS2ndE35HA9t9U2BF+vg1doX24nsTwpU+NUz+yJPeYcTa6LbtLCYVKbAj pzvEJdyEY6kAp/JftgPqGgqtp92OWUufR1+v0GXOnuDdyarbkLR6KQ6iQRMjjeQpyBA== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass header.from=mit.edu; spf=pass smtp.mailfrom=mit.edu; dkim=pass header.d=mit.edu header.i=@mit.edu header.a=rsa-sha256 header.s=outgoing header.b=cuPTLT9K; arc=none smtp.remote-ip=18.9.28.11 Received: from outgoing.mit.edu (outgoing-auth-1.mit.edu [18.9.28.11]) (using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A4F1F606A338 for ; Thu, 12 Nov 2026 04:07:02 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.2 smtp.subspace.kernel.org A4F1F606A338 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=mit.edu Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=mit.edu Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=mit.edu header.i=@mit.edu header.a=rsa-sha256 header.s=outgoing header.b=cuPTLT9K Received: from trampoline.thunk.org (pool-173-48-111-191.bstnma.fios.verizon.net [173.48.111.191]) (authenticated bits=0) (User authenticated as tytso@ATHENA.MIT.EDU) by outgoing.mit.edu (8.14.7/8.12.4) with ESMTP id 6AC46rHs016827 (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Wed, 11 Nov 2026 23:06:53 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=mit.edu; s=outgoing; t=1794456414; bh=/tdojRXdaGh8BJBsoHlNTXQQHYVoxPQcahAX2FB9Nik=; h=From:Subject:Date:Message-ID:MIME-Version; b=cuPTLT9K/M6ICWgfYP3qDLpEQIfkpRB5zf0lkBH5NGrRxx+DYSUT3OE0QXsyWpfYp IpsG4noua+XUnDb6WsWkMwle8LhkdMgCPpf5rPF/jFgJiQVwtpEJmvHcMlOjuTRaqj O/jmrX12Vk++lbQfvfeFZLEQAIFGNDDt1IPqd0W2Izcno+TbbbYgGsqQbfihdjz45M aJBH2YFRcAvJ0L/r8EkbraAJw3yLPCc1m9kWMMuK/ZAggPJKKE5JRPmVTZv4m6dDm+ 7UWDV6uo9Zcp89Zz8PzAvDyMacvJ7aB+dbzcwva8464N5FdGn1CNkYxPbfluyMQnkL FYNTigXGgeZKw== Received: by trampoline.thunk.org (Postfix, from userid 15806) id 5EBB82E014F; Wed, 11 Nov 2026 23:06:51 -0500 (EST) From: "Theodore Ts'o" To: Ext4 Developers List Cc: "Theodore Ts'o" Subject: [PATCH 10/27] ext4: expand KUnit tests for fs/ext4/inode.c Date: Wed, 11 Nov 2026 23:06:21 -0500 Message-ID: <20261112040638.1576836-11-tytso@mit.edu> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20261112040638.1576836-1-tytso@mit.edu> References: <20261112040638.1576836-1-tytso@mit.edu> X-Mailing-List: linux-ext4@vger.kernel.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Expand the ext4_inode_test KUnit test suite in fs/ext4/inode-test.c from 1 parameterized test (16 timestamp decoding subtests) to 26 test cases (41 subtests total) to comprehensively exercise fs/ext4/inode.c using an in-memory VFS superblock, mock block device, and deterministic block pool without requiring a mounted block device: - Extra timestamp encoding and 64-bit round-trips across all 16 boundary vectors, 256-byte inodes with 34-bit seconds + nanoseconds, and 128-byte old inodes storing signed 32-bit seconds with zeroed nanoseconds. - Raw inode CRC32C checksum computation and verification (ext4_inode_csum_set and ext4_inode_csum_verify) across legacy non-csum filesystems, 128-byte inodes (16-bit lo CRC), 256-byte inodes with i_extra_isize = 0 vs >= 4 (32-bit CRC), and bit-flip corruptions. - Inode block count encoding and decoding (ext4_inode_blocks_set and ext4_inode_blocks) across 32-bit 512B sectors, 48-bit huge_file sectors, and EXT4_HUGE_FILE_FL filesystem block units. - VFS-to-raw inode serialization and writeback paths (ext4_fill_raw_inode, ext4_reserve_inode_write, ext4_mark_iloc_dirty, __ext4_mark_inode_dirty, ext4_dirty_inode, ext4_inode_attach_jinode, ext4_write_inode, and ext4_sync_inode_metadata), including inline data preservation, project IDs, lazytime neighbor timestamp flushes, emergency read-only -EROFS handling, and buffer I/O error propagation. - Dynamic extra-isize expansion (__ext4_expand_extra_isize, ext4_try_to_expand_extra_isize, and ext4_expand_extra_isize), including corrupted/invalid size guards, EXT4_STATE_NO_EXPAND, xattr_sem contention, and shifting in-body extended attributes forward. - On-disk inode location lookup (__ext4_get_inode_loc, ext4_get_inode_loc, ext4_get_inode_loc_noio, and ext4_get_fc_inode_loc), including zero-I/O uninitialized/unused inode table optimization, neighbor bitmap occupancy checks, readahead window calculation, and read -EIO handling. - Inode loading and validation (__ext4_iget) across EXT4_IGET_NORMAL, EXT4_IGET_SPECIAL, EXT4_IGET_HANDLE, EXT4_IGET_EA_INODE, EXT4_IGET_NOWAIT, and EXT4_IGET_BAD, special device rdev encodings, fast symlinks, and corruption rejections (bad CRC, unlinked ino, bad mode, reserved ino < first_ino, bad extra_isize, conflicting flags, oversize indirect i_size, bad i_file_acl, and bad extent header). - Inode eviction (ext4_evict_inode) across live inodes, unlinked regular files (truncating data extents, setting i_dtime, and freeing the inode), and unlinked fast symlinks. - Fast symlink classification, ext4_can_truncate, inode flag propagation (ext4_set_inode_flags), address_space_operations selection (ext4_set_aops), folio order caps (ext4_set_inode_mapping_order), project ID lookup (ext4_get_projid), DIO alignment checks (ext4_dio_alignment), and STATX_BTIME / STATX_DIOALIGN / STATX_WRITE_ATOMIC / STATX_ATTR_* reporting in ext4_getattr and ext4_file_getattr. - Delayed allocation space accounting (ext4_da_reserve_space, ext4_da_update_reserve_space, ext4_da_release_space, and ext4_nonda_switch) and bigalloc shared-cluster pending reservations (ext4_clu_alloc_state, ext4_insert_delayed_blocks, and ext4_da_map_blocks). - Logical-to-physical block mapping (ext4_map_blocks, ext4_get_block, ext4_get_block_unwritten, ext4_da_get_block_prep, ext4_getblk, ext4_bread, ext4_bread_batch, and ext4_walk_page_buffers) across ES hits, extent allocation, unwritten extents, zeroout, and batch reads. - iomap mapping callbacks (ext4_iomap_begin, ext4_iomap_ops, and ext4_iomap_report_ops, including bigalloc multi-block IOMAP_ATOMIC writes), buffered write_begin/write_end (ext4_da_aops and ext4_aops), bmap, invalidate_folio, release_folio, and ext4_page_mkwrite. - Transaction credit helpers (ext4_meta_trans_blocks, ext4_chunk_trans_extent, and ext4_chunk_trans_blocks), ext4_break_layouts, ext4_update_disksize_before_punch, partial-block and EOF zeroing (ext4_zero_partial_blocks, ext4_block_zero_eof, and ext4_truncate_page_cache_block_range), ext4_punch_hole, ext4_truncate, and ext4_setattr. Add thin KUNIT_STATIC_STUB_REDIRECT wrappers for buffer-cache lookup and inode freeing (ext4_sb_getblk, ext4_sb_find_get_block, ext4_sb_getblk_unmovable, ext4_read_bh_nowait, ext4_read_bh_lock, ext4_sb_breadahead_unmovable, and ext4_free_inode) and export the tested entry points and static-helper wrappers with EXPORT_SYMBOL_FOR_EXT4_TEST. Signed-off-by: Theodore Ts'o --- fs/ext4/ext4.h | 29 + fs/ext4/ext4_jbd2.c | 2 + fs/ext4/ialloc.c | 3 + fs/ext4/inode-test.c | 3449 +++++++++++++++++++++++++++++++++++++++++- fs/ext4/inode.c | 149 +- fs/ext4/page-io.c | 1 + fs/ext4/super.c | 19 +- 7 files changed, 3640 insertions(+), 12 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index afa2db3b6d57..c4f685704031 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3258,6 +3258,35 @@ extern void ext4_da_update_reserve_space(struct inode *inode, int used, int quota_claim); extern int ext4_issue_zeroout(struct inode *inode, ext4_lblk_t lblk, ext4_fsblk_t pblk, ext4_lblk_t len); +struct buffer_head *ext4_sb_getblk(struct super_block *sb, ext4_fsblk_t block); +struct buffer_head *ext4_sb_find_get_block(struct super_block *sb, + ext4_fsblk_t block); +struct buffer_head *ext4_sb_getblk_unmovable(struct super_block *sb, + ext4_fsblk_t block); +#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS) +int ext4_inode_csum_verify_test(struct inode *inode, struct ext4_inode *raw, + struct ext4_inode_info *ei); +int ext4_inode_blocks_set_test(struct ext4_inode *raw_inode, + struct ext4_inode_info *ei); +blkcnt_t ext4_inode_blocks_test(struct ext4_inode *raw_inode, + struct ext4_inode_info *ei); +int ext4_fill_raw_inode_test(struct inode *inode, + struct ext4_inode *raw_inode); +int ext4_expand_extra_isize_test(struct inode *inode, + unsigned int new_extra_isize, + struct ext4_iloc *iloc, + handle_t *handle, int *no_expand); +int ext4_try_to_expand_extra_isize_test(struct inode *inode, + unsigned int new_extra_isize, + struct ext4_iloc *iloc, + handle_t *handle); +int ext4_da_reserve_space_test(struct inode *inode, int nr_resv); +int ext4_clu_alloc_state_test(struct inode *inode, ext4_lblk_t lblk); +int ext4_insert_delayed_blocks_test(struct inode *inode, ext4_lblk_t lblk, + ext4_lblk_t len); +int ext4_da_map_blocks_test(struct inode *inode, struct ext4_map_blocks *map); +int ext4_nonda_switch_test(struct super_block *sb); +#endif static inline bool is_special_ino(struct super_block *sb, unsigned long ino) { diff --git a/fs/ext4/ext4_jbd2.c b/fs/ext4/ext4_jbd2.c index caba7ac68841..f18a01e6fd13 100644 --- a/fs/ext4/ext4_jbd2.c +++ b/fs/ext4/ext4_jbd2.c @@ -125,6 +125,7 @@ handle_t *__ext4_journal_start_sb(struct inode *inode, return jbd2__journal_start(journal, blocks, rsv_blocks, revoke_creds, GFP_NOFS, type, line); } +EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_journal_start_sb); int __ext4_journal_stop(const char *where, unsigned int line, handle_t *handle) { @@ -152,6 +153,7 @@ int __ext4_journal_stop(const char *where, unsigned int line, handle_t *handle) __ext4_std_error(sb, where, line, err); return err; } +EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_journal_stop); handle_t *__ext4_journal_start_reserved(handle_t *handle, unsigned int line, int type) diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c index 25b387f3ce85..02546b828836 100644 --- a/fs/ext4/ialloc.c +++ b/fs/ext4/ialloc.c @@ -250,6 +250,8 @@ void ext4_free_inode(handle_t *handle, struct inode *inode) int fatal = 0, err, count, cleared; struct ext4_group_info *grp; + KUNIT_STATIC_STUB_REDIRECT(ext4_free_inode, handle, inode); + if (!sb) { printk(KERN_ERR "EXT4-fs: %s:%d: inode on " "nonexistent device\n", __func__, __LINE__); @@ -363,6 +365,7 @@ void ext4_free_inode(handle_t *handle, struct inode *inode) brelse(bitmap_bh); ext4_std_error(sb, fatal); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_inode); struct orlov_stats { __u64 free_clusters; diff --git a/fs/ext4/inode-test.c b/fs/ext4/inode-test.c index 749af7ad4e09..e8d19c1d4270 100644 --- a/fs/ext4/inode-test.c +++ b/fs/ext4/inode-test.c @@ -1,14 +1,32 @@ // SPDX-License-Identifier: GPL-2.0 /* - * KUnit test of ext4 inode that verify the seconds part of [a/c/m] - * timestamps in ext4 inode structs are decoded correctly. + * KUnit tests for fs/ext4/inode.c */ #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include #include +#include #include "ext4.h" +#include "ext4_jbd2.h" +#include "ext4_extents.h" +#include "xattr.h" +#include "truncate.h" /* * For constructing the nonnegative timestamp lower bound value. @@ -233,7 +251,6 @@ static time64_t get_32bit_time(const struct timestamp_expectation * const test) return UPPER_MSB_0; } - /* * Test data is derived from the table in the Inode Timestamps section of * Documentation/filesystems/ext4/inodes.rst. @@ -267,8 +284,3432 @@ static void inode_test_xtimestamp_decoding(struct kunit *test) test_param->extra_bits); } +/* ------------------------------------------------------------------------- + * Mock filesystem, block pool, and VFS inode fixture for fs/ext4/inode.c + * ------------------------------------------------------------------------- + */ + +#define INODE_TEST_BLK_SIZE 4096 +#define INODE_TEST_BLK_BITS 12 +#define INODE_TEST_TOTAL_BLKS 32768 +#define INODE_TEST_INODE_SIZE 256 +#define INODE_TEST_EXTRA_ISIZE 32 +#define INODE_TEST_INODES_PER_BLK (INODE_TEST_BLK_SIZE / INODE_TEST_INODE_SIZE) +#define INODE_TEST_INODES_PER_GRP 2048 +#define INODE_TEST_FIRST_INO 11 +#define INODE_TEST_PRIMARY_INO 12 +#define INODE_TEST_CSUM_SEED 0x5a3c9e71U + +#define INODE_TEST_IBITMAP_BLK 10 +#define INODE_TEST_ITABLE_BLK0 20 +#define INODE_TEST_ITABLE_BLK1 21 +#define INODE_TEST_META_BLK0 50 +#define INODE_TEST_META_BLK1 51 +#define INODE_TEST_DATA_BLK0 100 +#define INODE_TEST_DATA_NBLKS 8 +#define INODE_TEST_POOL_SIZE 16 +#define INODE_TEST_MAX_TRACKED_INODES 32 + +struct inode_test_blk { + bool in_use; + bool in_cache; + ext4_fsblk_t blocknr; + struct buffer_head bh; + char cache_buf[INODE_TEST_BLK_SIZE] __aligned(8); + char disk_buf[INODE_TEST_BLK_SIZE] __aligned(8); +}; + +struct inode_test_ctx { + struct super_block *sb; + struct ext4_sb_info sbi; + struct ext4_super_block es; + struct ext4_group_desc gdp; + struct block_device bdev; + struct request_queue bdev_queue; + struct inode bd_inode; + struct buffer_head sb_bh; + struct dentry root_dentry; + + struct ext4_inode_info ei; + struct ext4_inode_info scratch_ei; + union { + struct ext4_inode raw; + u8 bytes[INODE_TEST_INODE_SIZE]; + } scratch_raw; + struct inode *tracked_inodes[INODE_TEST_MAX_TRACKED_INODES]; + unsigned int nr_tracked_inodes; + + struct inode_test_blk pool[INODE_TEST_POOL_SIZE]; + + bool fail_group_desc; + bool fail_getblk; + bool fail_read_eio; + int fail_zeroout_err; + int fail_alloc_err; + int fail_orphan_err; + + ext4_fsblk_t next_alloc_pblk; + unsigned int alloc_calls; + unsigned int free_calls; + ext4_fsblk_t last_free_pblk; + unsigned long last_free_count; + unsigned long total_freed_blocks; + unsigned int zeroout_calls; + ext4_lblk_t last_zeroout_lblk; + ext4_fsblk_t last_zeroout_pblk; + ext4_lblk_t last_zeroout_len; + unsigned int read_nowait_calls; + unsigned int read_lock_calls; + unsigned int readahead_calls; + ext4_fsblk_t first_readahead_blk; + ext4_fsblk_t last_readahead_blk; + unsigned int orphan_add_calls; + unsigned int orphan_del_calls; + unsigned int free_inode_calls; +}; + +static unsigned long inode_test_shrinker_count(struct shrinker *shrink, + struct shrink_control *sc) +{ + return 0; +} + +static unsigned long inode_test_shrinker_scan(struct shrinker *shrink, + struct shrink_control *sc) +{ + return SHRINK_STOP; +} + +static void inode_test_init_ei_fields(struct ext4_inode_info *ei) +{ + inode_set_iversion(&ei->vfs_inode, 1); + ei->i_flags = 0; + ext4_clear_state_flags(ei); + spin_lock_init(&ei->i_raw_lock); + ei->i_prealloc_node = RB_ROOT; + atomic_set(&ei->i_prealloc_active, 0); + rwlock_init(&ei->i_prealloc_lock); + ext4_es_init_tree(&ei->i_es_tree); + rwlock_init(&ei->i_es_lock); + INIT_LIST_HEAD(&ei->i_es_list); + ei->i_es_all_nr = 0; + ei->i_es_shk_nr = 0; + ei->i_es_shrink_lblk = 0; + ei->i_es_seq = 0; + ei->i_reserved_data_blocks = 0; + spin_lock_init(&ei->i_block_reservation_lock); + ext4_init_pending_tree(&ei->i_pending_tree); +#ifdef CONFIG_QUOTA + ei->i_reserved_quota = 0; + memset(&ei->i_dquot, 0, sizeof(ei->i_dquot)); +#endif + ei->jinode = NULL; + init_rwsem(&ei->xattr_sem); + INIT_LIST_HEAD(&ei->i_rsv_conversion_list); + spin_lock_init(&ei->i_completed_io_lock); + ei->i_sync_tid = 0; + ei->i_datasync_tid = 0; + INIT_WORK(&ei->i_rsv_conversion_work, ext4_end_io_rsv_work); + INIT_LIST_HEAD(&ei->i_orphan); + ext4_fc_init_inode(&ei->vfs_inode); + spin_lock_init(&ei->i_fc_lock); + ei->i_metadata_bhs = NULL; + ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID); + ei->i_extra_isize = INODE_TEST_EXTRA_ISIZE; + ei->i_csum_seed = INODE_TEST_CSUM_SEED; +} + +static void inode_test_cleanup_ext4_trees(struct inode *inode) +{ + struct ext4_inode_info *ei = EXT4_I(inode); + struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb); + struct rb_node *node; + + down_write(&ei->i_data_sem); + ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS); + while ((node = rb_first(&ei->i_pending_tree.root)) != NULL) { + struct pending_reservation *pr; + + pr = rb_entry(node, struct pending_reservation, rb_node); + ext4_remove_pending(inode, EXT4_C2B(sbi, pr->lclu)); + } + up_write(&ei->i_data_sem); +} + +static struct inode *inode_test_alloc_inode(struct super_block *sb) +{ + struct ext4_inode_info *ei; + + ei = kzalloc_obj(*ei); + if (!ei) + return NULL; + + init_rwsem(&ei->i_data_sem); + inode_init_once(&ei->vfs_inode); + inode_test_init_ei_fields(ei); + return &ei->vfs_inode; +} + +static void inode_test_destroy_inode(struct inode *inode) +{ + inode_test_cleanup_ext4_trees(inode); + kfree(ext4_i_metadata_bhs(inode)); + EXT4_I(inode)->i_metadata_bhs = NULL; +} + +static void inode_test_free_inode(struct inode *inode) +{ + kfree(EXT4_I(inode)); +} + +static const struct super_operations inode_test_sops = { + .alloc_inode = inode_test_alloc_inode, + .evict_inode = ext4_evict_inode, + .destroy_inode = inode_test_destroy_inode, + .free_inode = inode_test_free_inode, + .drop_inode = inode_just_drop, +}; + +#ifdef CONFIG_QUOTA +static qsize_t *inode_test_get_reserved_space(struct inode *inode) +{ + return &EXT4_I(inode)->i_reserved_quota; +} + +static const struct dquot_operations inode_test_qops = { + .get_reserved_space = inode_test_get_reserved_space, +}; +#endif + +static int inode_test_init_fs_context(struct fs_context *fc) +{ + return 0; +} + +static struct file_system_type inode_test_fs_type = { + .name = "ext4_inode_kunit", + .init_fs_context = inode_test_init_fs_context, + .kill_sb = kill_anon_super, +}; + +static struct inode_test_blk *inode_test_find_blk(struct inode_test_ctx *ctx, + ext4_fsblk_t blocknr) +{ + int i; + + for (i = 0; i < INODE_TEST_POOL_SIZE; i++) { + if (ctx->pool[i].in_use && ctx->pool[i].blocknr == blocknr) + return &ctx->pool[i]; + } + return NULL; +} + +static struct inode_test_blk *inode_test_find_blk_by_bh( + struct inode_test_ctx *ctx, struct buffer_head *bh) +{ + int i; + + for (i = 0; i < INODE_TEST_POOL_SIZE; i++) { + if (ctx->pool[i].in_use && &ctx->pool[i].bh == bh) + return &ctx->pool[i]; + } + return NULL; +} + +static struct inode_test_blk *inode_test_setup_blk(struct inode_test_ctx *ctx, + int idx, + ext4_fsblk_t blocknr, + bool in_cache, + bool uptodate) +{ + struct inode_test_blk *slot = &ctx->pool[idx]; + + memset(slot, 0, sizeof(*slot)); + slot->in_use = true; + slot->in_cache = in_cache; + slot->blocknr = blocknr; + slot->bh.b_bdev = &ctx->bdev; + slot->bh.b_blocknr = blocknr; + slot->bh.b_size = INODE_TEST_BLK_SIZE; + slot->bh.b_data = slot->cache_buf; + atomic_set(&slot->bh.b_count, 1); + if (uptodate) + set_buffer_uptodate(&slot->bh); + return slot; +} + +/* --- Static Stubs --- */ + +static struct ext4_group_desc *inode_test_get_group_desc_stub( + struct super_block *sb, ext4_group_t block_group, + struct buffer_head **bh) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + + if (!ctx || ctx->fail_group_desc || block_group != 0) + return NULL; + if (bh) + *bh = NULL; + return &ctx->gdp; +} + +static struct buffer_head *inode_test_sb_getblk_stub(struct super_block *sb, + ext4_fsblk_t block) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + struct inode_test_blk *slot; + + if (!ctx || ctx->fail_getblk) + return NULL; + slot = inode_test_find_blk(ctx, block); + if (!slot) + return NULL; + slot->in_cache = true; + get_bh(&slot->bh); + return &slot->bh; +} + +static struct buffer_head *inode_test_sb_find_get_block_stub( + struct super_block *sb, ext4_fsblk_t block) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + struct inode_test_blk *slot; + + if (!ctx) + return NULL; + slot = inode_test_find_blk(ctx, block); + if (!slot || !slot->in_cache) + return NULL; + get_bh(&slot->bh); + return &slot->bh; +} + +static struct buffer_head *inode_test_sb_getblk_unmovable_stub( + struct super_block *sb, ext4_fsblk_t block) +{ + return inode_test_sb_getblk_stub(sb, block); +} + +static struct buffer_head *inode_test_ext_getblk_stub(struct inode *inode, + ext4_fsblk_t pblk, + gfp_t gfp_flags) +{ + struct buffer_head *bh; + + bh = inode_test_sb_getblk_stub(inode->i_sb, pblk); + if (bh) { + set_buffer_uptodate(bh); + set_buffer_verified(bh); + } + return bh; +} + +static void inode_test_read_bh_nowait_stub(struct buffer_head *bh, + blk_opf_t op_flags, + bio_end_io_t end_io, + bool simu_fail) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + struct inode_test_blk *slot; + + if (!ctx) { + unlock_buffer(bh); + return; + } + ctx->read_nowait_calls++; + if (ctx->fail_read_eio || simu_fail) { + clear_buffer_uptodate(bh); + unlock_buffer(bh); + return; + } + slot = inode_test_find_blk_by_bh(ctx, bh); + if (!slot) + slot = inode_test_find_blk(ctx, bh->b_blocknr); + if (slot) { + memcpy(bh->b_data, slot->disk_buf, bh->b_size); + set_buffer_uptodate(bh); + } else { + clear_buffer_uptodate(bh); + } + unlock_buffer(bh); +} + +static int inode_test_read_bh_lock_stub(struct buffer_head *bh, + blk_opf_t op_flags, bool wait) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + struct inode_test_blk *slot; + + if (!ctx) + return -EIO; + ctx->read_lock_calls++; + lock_buffer(bh); + if (ctx->fail_read_eio) { + clear_buffer_uptodate(bh); + unlock_buffer(bh); + return wait ? -EIO : 0; + } + slot = inode_test_find_blk_by_bh(ctx, bh); + if (!slot) + slot = inode_test_find_blk(ctx, bh->b_blocknr); + if (!slot) { + clear_buffer_uptodate(bh); + unlock_buffer(bh); + return wait ? -EIO : 0; + } + memcpy(bh->b_data, slot->disk_buf, bh->b_size); + set_buffer_uptodate(bh); + unlock_buffer(bh); + return 0; +} + +static void inode_test_breadahead_stub(struct super_block *sb, sector_t block) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + + if (!ctx) + return; + if (ctx->readahead_calls == 0) + ctx->first_readahead_blk = block; + ctx->last_readahead_blk = block; + ctx->readahead_calls++; +} + +static ext4_fsblk_t inode_test_mb_new_blocks_stub( + handle_t *handle, struct ext4_allocation_request *ar, int *errp) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + ext4_fsblk_t pblk; + + if (!ctx) { + *errp = -EIO; + return 0; + } + if (ctx->fail_alloc_err) { + *errp = ctx->fail_alloc_err; + return 0; + } + ctx->alloc_calls++; + pblk = ctx->next_alloc_pblk; + ctx->next_alloc_pblk += ar->len; + *errp = 0; + return pblk; +} + +static void inode_test_free_blocks_stub(handle_t *handle, struct inode *inode, + struct buffer_head *bh, + ext4_fsblk_t block, + unsigned long count, int flags) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + + if (bh && (flags & EXT4_FREE_BLOCKS_FORGET)) + brelse(bh); + if (!ctx) + return; + ctx->free_calls++; + ctx->last_free_pblk = block; + ctx->last_free_count = count; + ctx->total_freed_blocks += count; +} + +static int inode_test_issue_zeroout_stub(struct inode *inode, ext4_lblk_t lblk, + ext4_fsblk_t pblk, ext4_lblk_t len) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + + if (!ctx) + return -EIO; + ctx->zeroout_calls++; + ctx->last_zeroout_lblk = lblk; + ctx->last_zeroout_pblk = pblk; + ctx->last_zeroout_len = len; + return ctx->fail_zeroout_err; +} + +static int inode_test_dirty_metadata_stub(const char *where, unsigned int line, + handle_t *handle, + struct inode *inode, + struct buffer_head *bh) +{ + if (bh) { + set_buffer_uptodate(bh); + set_buffer_dirty(bh); + } + return 0; +} + +static int inode_test_ext_dirty_stub(const char *where, unsigned int line, + handle_t *handle, struct inode *inode, + struct ext4_ext_path *path) +{ + if (path && path->p_bh) { + ext4_extent_block_csum_set_test(inode, + ext_block_hdr(path->p_bh)); + set_buffer_verified(path->p_bh); + } + return 0; +} + +static int inode_test_orphan_add_stub(handle_t *handle, struct inode *inode) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + + if (!ctx) + return -EIO; + if (ctx->fail_orphan_err) + return ctx->fail_orphan_err; + ctx->orphan_add_calls++; + return 0; +} + +static int inode_test_orphan_del_stub(handle_t *handle, struct inode *inode) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + + if (!ctx) + return -EIO; + ctx->orphan_del_calls++; + return 0; +} + +static void inode_test_free_inode_stub(handle_t *handle, struct inode *inode) +{ + struct kunit *test = kunit_get_current_test(); + struct inode_test_ctx *ctx = test ? test->priv : NULL; + + if (ctx) + ctx->free_inode_calls++; + ext4_clear_inode(inode); +} + +/* --- Fixture Setup & Cleanup --- */ + +static void inode_test_track_iget_inode(struct kunit *test, + struct inode_test_ctx *ctx, + struct inode *inode) +{ + KUNIT_ASSERT_LT(test, ctx->nr_tracked_inodes, + (unsigned int)INODE_TEST_MAX_TRACKED_INODES); + ctx->tracked_inodes[ctx->nr_tracked_inodes++] = inode; +} + +static void inode_test_put_tracked_inode(struct inode_test_ctx *ctx, + struct inode *inode) +{ + unsigned int i; + + for (i = 0; i < ctx->nr_tracked_inodes; i++) { + if (ctx->tracked_inodes[i] == inode) { + ctx->tracked_inodes[i] = + ctx->tracked_inodes[--ctx->nr_tracked_inodes]; + ctx->tracked_inodes[ctx->nr_tracked_inodes] = NULL; + iput(inode); + return; + } + } + iput(inode); +} + +static void inode_test_reset_mock_inode(struct inode_test_ctx *ctx, umode_t mode) +{ + struct ext4_inode_info *ei = &ctx->ei; + struct inode *inode = &ei->vfs_inode; + struct mapping_metadata_bhs *mmb = ext4_i_metadata_bhs(inode); + + truncate_inode_pages_final(&inode->i_data); + inode_io_list_del(inode); + ext4_fc_del(inode); + if (mmb) + mmb_invalidate(mmb); + inode_test_cleanup_ext4_trees(inode); + if (ei->jinode) { + jbd2_free_inode(ei->jinode); + ei->jinode = NULL; + } + + inode_state_assign_raw(inode, 0); + inode->i_ino = INODE_TEST_PRIMARY_INO; + inode->i_mode = mode; + inode->i_blkbits = ctx->sb->s_blocksize_bits; + inode->i_flags = S_NOQUOTA; + set_nlink(inode, 1); + i_uid_write(inode, 0); + i_gid_write(inode, 0); + inode->i_size = 0; + inode->i_blocks = 0; + inode->i_generation = 1; + inode->i_rdev = 0; + ei->i_disksize = 0; + ei->i_flags = 0; + ei->i_file_acl = 0; + ei->i_dtime = 0; + ei->i_inline_off = 0; + ei->i_inline_size = 0; + ei->i_extra_isize = INODE_TEST_EXTRA_ISIZE; + ei->i_reserved_data_blocks = 0; + ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID); + ext4_clear_state_flags(ei); + memset(ei->i_data, 0, sizeof(ei->i_data)); + + if (S_ISREG(mode) || S_ISDIR(mode)) { + ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS); + ext4_ext_tree_init(NULL, inode); + } + if (S_ISREG(mode)) + ext4_set_aops(inode); +} + +static void inode_test_cleanup(void *data) +{ + struct inode_test_ctx *ctx = data; + struct inode *inode = &ctx->ei.vfs_inode; + struct mapping_metadata_bhs *mmb; + unsigned int i; + + ctx->sbi.s_journal = NULL; + + for (i = 0; i < ctx->nr_tracked_inodes; i++) { + struct inode *ti = ctx->tracked_inodes[i]; + + if (ti) { + spin_lock(&ti->i_lock); + inode_state_clear(ti, I_DIRTY_TIME); + spin_unlock(&ti->i_lock); + iput(ti); + ctx->tracked_inodes[i] = NULL; + } + } + ctx->nr_tracked_inodes = 0; + + truncate_inode_pages_final(&inode->i_data); + inode_io_list_del(inode); + ext4_fc_del(inode); + mmb = ext4_i_metadata_bhs(inode); + if (mmb) + mmb_invalidate(mmb); + inode_test_cleanup_ext4_trees(inode); + if (ctx->ei.jinode) { + jbd2_free_inode(ctx->ei.jinode); + ctx->ei.jinode = NULL; + } + + deactivate_super(ctx->sb); + rcu_barrier(); + + ext4_es_unregister_shrinker(&ctx->sbi); + percpu_counter_destroy(&ctx->sbi.s_dirtyclusters_counter); + percpu_counter_destroy(&ctx->sbi.s_freeclusters_counter); + + kfree(mmb); + kfree(ctx); +} + +static struct inode_test_ctx *inode_test_setup(struct kunit *test) +{ + struct inode_test_ctx *ctx; + struct fs_context *fc; + struct super_block *sb; + struct ext4_sb_info *sbi; + struct inode *inode; + int err, i; + + ctx = kzalloc_obj(*ctx); + KUNIT_ASSERT_NOT_NULL(test, ctx); + test->priv = ctx; + + fc = fs_context_for_mount(&inode_test_fs_type, 0); + if (IS_ERR(fc)) { + err = PTR_ERR(fc); + kfree(ctx); + KUNIT_ASSERT_EQ(test, err, 0); + } + sb = sget_fc(fc, NULL, set_anon_super_fc); + put_fs_context(fc); + if (IS_ERR(sb)) { + err = PTR_ERR(sb); + kfree(ctx); + KUNIT_ASSERT_EQ(test, err, 0); + } + ctx->sb = sb; + sbi = &ctx->sbi; + + sbi->s_sb = sb; + sb->s_fs_info = sbi; + sb->s_op = &inode_test_sops; +#ifdef CONFIG_QUOTA + sb->dq_op = &inode_test_qops; +#endif + sb->s_flags |= SB_ACTIVE; + sb->s_blocksize = INODE_TEST_BLK_SIZE; + sb->s_blocksize_bits = INODE_TEST_BLK_BITS; + sb->s_maxbytes = MAX_LFS_FILESIZE; + sb->s_time_gran = 1; + sb->s_xattr = ext4_xattr_handlers; + strscpy(sb->s_id, "ext4_inode_kunit", sizeof(sb->s_id)); + + inode_init_once(&ctx->bd_inode); + ctx->bd_inode.i_sb = sb; + ctx->bd_inode.i_blkbits = INODE_TEST_BLK_BITS; + ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data; + ctx->bd_inode.i_data.host = &ctx->bd_inode; + ctx->bdev_queue.limits.logical_block_size = 512; + ctx->bdev_queue.limits.dma_alignment = 511; + ctx->bdev.bd_queue = &ctx->bdev_queue; + ctx->bdev.bd_mapping = &ctx->bd_inode.i_data; + bdev_set_flag(&ctx->bdev, BD_READ_ONLY); + sb->s_bdev = &ctx->bdev; + + sbi->s_es = &ctx->es; + ctx->es.s_magic = cpu_to_le16(EXT4_SUPER_MAGIC); + ctx->es.s_first_data_block = cpu_to_le32(1); + ctx->es.s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV); + ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_LINUX); + ctx->es.s_inodes_count = cpu_to_le32(INODE_TEST_INODES_PER_GRP); + ctx->es.s_inodes_per_group = cpu_to_le32(INODE_TEST_INODES_PER_GRP); + ctx->es.s_first_ino = cpu_to_le32(INODE_TEST_FIRST_INO); + ctx->es.s_inode_size = cpu_to_le16(INODE_TEST_INODE_SIZE); + ctx->es.s_min_extra_isize = cpu_to_le16(INODE_TEST_EXTRA_ISIZE); + ctx->es.s_want_extra_isize = cpu_to_le16(INODE_TEST_EXTRA_ISIZE); + ctx->es.s_checksum_type = EXT4_CRC32C_CHKSUM; + ext4_blocks_count_set(&ctx->es, INODE_TEST_TOTAL_BLKS); + ext4_set_feature_extents(sb); + ext4_set_feature_extra_isize(sb); + ext4_set_feature_metadata_csum(sb); + ext4_set_feature_64bit(sb); + ext4_set_feature_huge_file(sb); + ext4_set_feature_large_file(sb); + ext4_set_feature_dir_index(sb); + + ctx->sb_bh.b_data = (char *)&ctx->es; + ctx->sb_bh.b_size = sizeof(ctx->es); + ctx->sb_bh.b_bdev = &ctx->bdev; + atomic_set(&ctx->sb_bh.b_count, 1); + set_buffer_uptodate(&ctx->sb_bh); + sbi->s_sbh = &ctx->sb_bh; + + sbi->s_inode_size = INODE_TEST_INODE_SIZE; + sbi->s_inodes_per_block = INODE_TEST_INODES_PER_BLK; + sbi->s_inodes_per_group = INODE_TEST_INODES_PER_GRP; + sbi->s_first_ino = INODE_TEST_FIRST_INO; + sbi->s_want_extra_isize = INODE_TEST_EXTRA_ISIZE; + sbi->s_blocks_per_group = INODE_TEST_TOTAL_BLKS; + sbi->s_clusters_per_group = INODE_TEST_TOTAL_BLKS; + sbi->s_cluster_ratio = 1; + sbi->s_cluster_bits = 0; + sbi->s_groups_count = 1; + sbi->s_gdb_count = 1; + sbi->s_desc_size = EXT4_MIN_DESC_SIZE_64BIT; + sbi->s_desc_per_block = INODE_TEST_BLK_SIZE / EXT4_MIN_DESC_SIZE_64BIT; + sbi->s_bitmap_maxbytes = 0x1ff7ffff000ULL; + sbi->s_extent_max_zeroout_kb = 32; + sbi->s_csum_seed = INODE_TEST_CSUM_SEED; + + ext4_inode_bitmap_set(sb, &ctx->gdp, INODE_TEST_IBITMAP_BLK); + ext4_inode_table_set(sb, &ctx->gdp, INODE_TEST_ITABLE_BLK0); + + set_opt(sb, ERRORS_CONT); + set_opt(sb, ORDERED_DATA); + set_opt(sb, DELALLOC); + set_opt(sb, XATTR_USER); + spin_lock_init(&sbi->s_bdev_wb_lock); + spin_lock_init(&sbi->s_error_lock); + spin_lock_init(&sbi->s_md_lock); + mutex_init(&sbi->s_fc_lock); + INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_MAIN]); + INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_STAGING]); + INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_MAIN]); + INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_STAGING]); + + ratelimit_state_init(&sbi->s_err_ratelimit_state, + DEFAULT_RATELIMIT_INTERVAL, 0); + ratelimit_state_init(&sbi->s_warning_ratelimit_state, + DEFAULT_RATELIMIT_INTERVAL, 0); + ratelimit_state_init(&sbi->s_msg_ratelimit_state, + DEFAULT_RATELIMIT_INTERVAL, 0); + + err = percpu_counter_init(&sbi->s_freeclusters_counter, 4096, + GFP_KERNEL); + if (err) { + deactivate_locked_super(sb); + kfree(ctx); + KUNIT_ASSERT_EQ(test, err, 0); + } + err = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0, + GFP_KERNEL); + if (err) { + percpu_counter_destroy(&sbi->s_freeclusters_counter); + deactivate_locked_super(sb); + kfree(ctx); + KUNIT_ASSERT_EQ(test, err, 0); + } + err = ext4_es_register_shrinker(sbi); + if (err) { + percpu_counter_destroy(&sbi->s_dirtyclusters_counter); + percpu_counter_destroy(&sbi->s_freeclusters_counter); + deactivate_locked_super(sb); + kfree(ctx); + KUNIT_ASSERT_EQ(test, err, 0); + } + sbi->s_es_shrinker->count_objects = inode_test_shrinker_count; + sbi->s_es_shrinker->scan_objects = inode_test_shrinker_scan; + + /* Initialize mock block pool */ + inode_test_setup_blk(ctx, 0, INODE_TEST_IBITMAP_BLK, true, true); + inode_test_setup_blk(ctx, 1, INODE_TEST_ITABLE_BLK0, true, true); + inode_test_setup_blk(ctx, 2, INODE_TEST_ITABLE_BLK1, true, true); + inode_test_setup_blk(ctx, 3, INODE_TEST_META_BLK0, true, true); + inode_test_setup_blk(ctx, 4, INODE_TEST_META_BLK1, true, true); + for (i = 0; i < INODE_TEST_DATA_NBLKS; i++) + inode_test_setup_blk(ctx, 5 + i, INODE_TEST_DATA_BLK0 + i, + true, true); + ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0; + + /* Initialize standalone mock inode */ + inode = &ctx->ei.vfs_inode; + init_rwsem(&ctx->ei.i_data_sem); + inode_init_once(inode); + inode->i_sb = sb; + inode->i_mapping = &inode->i_data; + inode->i_data.host = inode; + mapping_set_gfp_mask(&inode->i_data, GFP_HIGHUSER_MOVABLE); + init_rwsem(&inode->i_rwsem); + init_rwsem(&inode->i_data.invalidate_lock); + spin_lock_init(&inode->i_lock); + inode->__i_nlink = 1; + atomic_set(&inode->i_count, 1); + inode_test_init_ei_fields(&ctx->ei); + + ctx->root_dentry.d_sb = sb; + ctx->root_dentry.d_inode = inode; + + up_write(&sb->s_umount); + + kunit_activate_static_stub(test, ext4_get_group_desc, + inode_test_get_group_desc_stub); + kunit_activate_static_stub(test, ext4_sb_getblk, + inode_test_sb_getblk_stub); + kunit_activate_static_stub(test, ext4_sb_find_get_block, + inode_test_sb_find_get_block_stub); + kunit_activate_static_stub(test, ext4_sb_getblk_unmovable, + inode_test_sb_getblk_unmovable_stub); + kunit_activate_static_stub(test, ext4_ext_getblk, + inode_test_ext_getblk_stub); + kunit_activate_static_stub(test, ext4_read_bh_nowait, + inode_test_read_bh_nowait_stub); + kunit_activate_static_stub(test, ext4_read_bh_lock, + inode_test_read_bh_lock_stub); + kunit_activate_static_stub(test, ext4_sb_breadahead_unmovable, + inode_test_breadahead_stub); + kunit_activate_static_stub(test, ext4_mb_new_blocks, + inode_test_mb_new_blocks_stub); + kunit_activate_static_stub(test, ext4_free_blocks, + inode_test_free_blocks_stub); + kunit_activate_static_stub(test, ext4_issue_zeroout, + inode_test_issue_zeroout_stub); + kunit_activate_static_stub(test, __ext4_handle_dirty_metadata, + inode_test_dirty_metadata_stub); + kunit_activate_static_stub(test, __ext4_ext_dirty, + inode_test_ext_dirty_stub); + kunit_activate_static_stub(test, ext4_orphan_add, + inode_test_orphan_add_stub); + kunit_activate_static_stub(test, ext4_orphan_del, + inode_test_orphan_del_stub); + kunit_activate_static_stub(test, ext4_free_inode, + inode_test_free_inode_stub); + + err = kunit_add_action_or_reset(test, inode_test_cleanup, ctx); + KUNIT_ASSERT_EQ(test, err, 0); + + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + + return ctx; +} + +static struct ext4_inode *inode_test_raw_slot(struct inode_test_ctx *ctx, + unsigned long ino, bool disk) +{ + unsigned int grp_off = (ino - 1) % INODE_TEST_INODES_PER_GRP; + ext4_fsblk_t blk = INODE_TEST_ITABLE_BLK0 + + (grp_off / INODE_TEST_INODES_PER_BLK); + unsigned int byte_off = (grp_off % INODE_TEST_INODES_PER_BLK) * + INODE_TEST_INODE_SIZE; + struct inode_test_blk *slot = inode_test_find_blk(ctx, blk); + char *base = disk ? slot->disk_buf : slot->cache_buf; + + return (struct ext4_inode *)(base + byte_off); +} + +static void inode_test_stamp_raw_csum(struct inode_test_ctx *ctx, + unsigned long ino, + struct ext4_inode *raw) +{ + struct ext4_inode_info *tmp_ei = &ctx->scratch_ei; + struct inode *tmp_inode = &tmp_ei->vfs_inode; + __le32 inum = cpu_to_le32(ino); + __le32 gen = raw->i_generation; + u32 csum; + + memset(tmp_ei, 0, sizeof(*tmp_ei)); + tmp_inode->i_sb = ctx->sb; + tmp_inode->i_ino = ino; + tmp_ei->i_extra_isize = le16_to_cpu(raw->i_extra_isize); + csum = ext4_chksum(ctx->sbi.s_csum_seed, (u8 *)&inum, sizeof(inum)); + tmp_ei->i_csum_seed = ext4_chksum(csum, (u8 *)&gen, sizeof(gen)); + ext4_inode_csum_set(tmp_inode, raw, tmp_ei); +} + +static void inode_test_prepare_valid_raw_inode(struct inode_test_ctx *ctx, + unsigned long ino, umode_t mode) +{ + struct inode_test_blk *slot; + struct ext4_inode *raw; + struct ext4_extent_header *eh; + unsigned int grp_off = (ino - 1) % INODE_TEST_INODES_PER_GRP; + ext4_fsblk_t blk = INODE_TEST_ITABLE_BLK0 + + (grp_off / INODE_TEST_INODES_PER_BLK); + + slot = inode_test_find_blk(ctx, blk); + clear_buffer_uptodate(&slot->bh); + raw = inode_test_raw_slot(ctx, ino, true); + memset(raw, 0, INODE_TEST_INODE_SIZE); + + raw->i_mode = cpu_to_le16(mode); + raw->i_links_count = cpu_to_le16(1); + raw->i_extra_isize = cpu_to_le16(INODE_TEST_EXTRA_ISIZE); + raw->i_generation = cpu_to_le32(7); + if (S_ISREG(mode) || S_ISDIR(mode)) { + raw->i_flags = cpu_to_le32(EXT4_EXTENTS_FL); + eh = (struct ext4_extent_header *)raw->i_block; + eh->eh_magic = EXT4_EXT_MAGIC; + eh->eh_entries = 0; + eh->eh_max = cpu_to_le16(4); + eh->eh_depth = 0; + } + inode_test_stamp_raw_csum(ctx, ino, raw); +} + +/* ------------------------------------------------------------------------- + * 1. Timestamp Encoding & Macro Round-Trip Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_xtimestamp_encoding_roundtrip(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct ext4_inode_info *ei = &ctx->ei; + struct inode *inode = &ei->vfs_inode; + struct ext4_inode *raw = &ctx->scratch_raw.raw; + struct timespec64 ts_in, ts_out; + size_t i; + + for (i = 0; i < ARRAY_SIZE(test_data); i++) { + __le32 encoded_extra = ext4_encode_extra_time(test_data[i].expected); + __le32 encoded_base = cpu_to_le32((s32)test_data[i].expected.tv_sec); + + KUNIT_EXPECT_EQ_MSG(test, le32_to_cpu(encoded_extra), + test_data[i].extra_bits, + "%s", test_data[i].test_case_name); + ts_out = ext4_decode_extra_time(encoded_base, encoded_extra); + KUNIT_EXPECT_EQ(test, ts_out.tv_sec, + test_data[i].expected.tv_sec); + KUNIT_EXPECT_EQ(test, ts_out.tv_nsec, + test_data[i].expected.tv_nsec); + } + + /* 256-byte inode with extra_isize = 32: full 34-bit + nsec round-trip */ + ts_in.tv_sec = 0x240001234LL; + ts_in.tv_nsec = 987654321L; + inode_set_atime_to_ts(inode, ts_in); + ts_in.tv_sec = 0x190005678LL; + ts_in.tv_nsec = 123456789L; + inode_set_mtime_to_ts(inode, ts_in); + ts_in.tv_sec = 0x310009abcLL; + ts_in.tv_nsec = 555666777L; + inode_set_ctime_to_ts(inode, ts_in); + ei->i_crtime.tv_sec = 0x22000def0LL; + ei->i_crtime.tv_nsec = 444333222L; + + EXT4_INODE_SET_ATIME(inode, raw); + EXT4_INODE_SET_MTIME(inode, raw); + EXT4_INODE_SET_CTIME(inode, raw); + EXT4_EINODE_SET_XTIME(i_crtime, ei, raw); + + inode_set_atime(inode, 0, 0); + inode_set_mtime(inode, 0, 0); + inode_set_ctime(inode, 0, 0); + ei->i_crtime = (struct timespec64){}; + + EXT4_INODE_GET_ATIME(inode, raw); + EXT4_INODE_GET_MTIME(inode, raw); + EXT4_INODE_GET_CTIME(inode, raw); + EXT4_EINODE_GET_XTIME(i_crtime, ei, raw); + + KUNIT_EXPECT_EQ(test, inode_get_atime_sec(inode), 0x240001234LL); + KUNIT_EXPECT_EQ(test, inode_get_atime_nsec(inode), 987654321L); + KUNIT_EXPECT_EQ(test, inode_get_mtime_sec(inode), 0x190005678LL); + KUNIT_EXPECT_EQ(test, inode_get_mtime_nsec(inode), 123456789L); + KUNIT_EXPECT_EQ(test, inode_get_ctime_sec(inode), 0x310009abcLL); + KUNIT_EXPECT_EQ(test, inode_get_ctime_nsec(inode), 555666777L); + KUNIT_EXPECT_EQ(test, ctx->ei.i_crtime.tv_sec, 0x22000def0LL); + KUNIT_EXPECT_EQ(test, ctx->ei.i_crtime.tv_nsec, 444333222L); + + /* 128-byte old inode: 32-bit signed seconds only, extra ignored, nsec=0 */ + ctx->sbi.s_inode_size = EXT4_GOOD_OLD_INODE_SIZE; + ctx->ei.i_extra_isize = 0; + memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE); + + inode_set_mtime(inode, S32_MAX, 500); + inode_set_atime(inode, S32_MIN, 700); + EXT4_INODE_SET_MTIME(inode, raw); + EXT4_INODE_SET_ATIME(inode, raw); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_mtime_extra), 0U); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_atime_extra), 0U); + + inode_set_mtime(inode, 0, 999); + inode_set_atime(inode, 0, 999); + EXT4_INODE_GET_MTIME(inode, raw); + EXT4_INODE_GET_ATIME(inode, raw); + + KUNIT_EXPECT_EQ(test, inode_get_mtime_sec(inode), (time64_t)S32_MAX); + KUNIT_EXPECT_EQ(test, inode_get_mtime_nsec(inode), 0L); + KUNIT_EXPECT_EQ(test, inode_get_atime_sec(inode), (time64_t)S32_MIN); + KUNIT_EXPECT_EQ(test, inode_get_atime_nsec(inode), 0L); + + ctx->sbi.s_inode_size = INODE_TEST_INODE_SIZE; + ctx->ei.i_extra_isize = INODE_TEST_EXTRA_ISIZE; +} + +/* ------------------------------------------------------------------------- + * 2. Inode Checksum Calculation & Verification Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_csum_set_and_verify(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct ext4_inode *raw = &ctx->scratch_raw.raw; + + raw->i_mode = cpu_to_le16(S_IFREG | 0644); + raw->i_links_count = cpu_to_le16(1); + raw->i_extra_isize = cpu_to_le16(INODE_TEST_EXTRA_ISIZE); + + /* Full 32-bit checksum (256B inode, extra_isize = 32) */ + ext4_inode_csum_set(inode, raw, &ctx->ei); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 1); + + /* Bit flip in low 16 bits fails */ + raw->i_checksum_lo ^= cpu_to_le16(0x0001); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 0); + raw->i_checksum_lo ^= cpu_to_le16(0x0001); + + /* Bit flip in high 16 bits fails */ + raw->i_checksum_hi ^= cpu_to_le16(0x8000); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 0); + raw->i_checksum_hi ^= cpu_to_le16(0x8000); + + /* Bit flip in raw inode body and in-body xattr tail (byte 200) fails */ + raw->i_mode ^= cpu_to_le16(0x0002); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 0); + raw->i_mode ^= cpu_to_le16(0x0002); + ctx->scratch_raw.bytes[200] ^= 0x01; + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 0); + ctx->scratch_raw.bytes[200] ^= 0x01; + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 1); + + /* Non-Linux creator OS bypasses checksum verification */ + ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_HURD); + raw->i_checksum_lo ^= cpu_to_le16(0xffff); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 1); + ctx->es.s_creator_os = cpu_to_le32(EXT4_OS_LINUX); + + /* Disabled metadata_csum feature bypasses checksum verification */ + ext4_clear_feature_metadata_csum(ctx->sb); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 1); + ext4_set_feature_metadata_csum(ctx->sb); + + /* 256B inode with extra_isize = 0 (excludes i_checksum_hi): 16-bit */ + memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE); + ctx->ei.i_extra_isize = 0; + raw->i_extra_isize = 0; + raw->i_checksum_hi = cpu_to_le16(0xbeef); + ext4_inode_csum_set(inode, raw, &ctx->ei); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_checksum_hi), 0xbeef); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 1); + raw->i_checksum_lo ^= cpu_to_le16(0x0010); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 0); + + /* 256B inode with extra_isize = 4 (includes i_checksum_hi): 32-bit */ + memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE); + ctx->ei.i_extra_isize = 4; + raw->i_extra_isize = cpu_to_le16(4); + ext4_inode_csum_set(inode, raw, &ctx->ei); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 1); + raw->i_checksum_hi ^= cpu_to_le16(0x0001); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 0); + + /* 128B old inode: 16-bit checksum in i_checksum_lo only */ + memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE); + ctx->sbi.s_inode_size = EXT4_GOOD_OLD_INODE_SIZE; + ctx->ei.i_extra_isize = 0; + ext4_inode_csum_set(inode, raw, &ctx->ei); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 1); + raw->i_checksum_lo ^= cpu_to_le16(0x0100); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 0); +} + +/* ------------------------------------------------------------------------- + * 3. i_blocks Encoding & Decoding Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_blocks_encode_decode(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct ext4_inode *raw = &ctx->scratch_raw.raw; + blkcnt_t large_sectors; + int err; + + /* 32-bit sector count clears EXT4_INODE_HUGE_FILE */ + ext4_set_inode_flag(inode, EXT4_INODE_HUGE_FILE); + inode->i_blocks = 0x12345678ULL; + err = ext4_inode_blocks_set_test(raw, &ctx->ei); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_FALSE(test, + ext4_test_inode_flag(inode, EXT4_INODE_HUGE_FILE)); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_blocks_lo), 0x12345678U); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_blocks_high), 0); + KUNIT_EXPECT_EQ(test, ext4_inode_blocks_test(raw, &ctx->ei), + (blkcnt_t)0x12345678ULL); + + /* > 32-bit without huge_file feature fails with -EFSCORRUPTED */ + ext4_clear_feature_huge_file(ctx->sb); + inode->i_blocks = 0x100000000ULL; + err = ext4_inode_blocks_set_test(raw, &ctx->ei); + KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED); + ext4_set_feature_huge_file(ctx->sb); + + /* 48-bit sector count with huge_file feature (still sector units) */ + ext4_set_inode_flag(inode, EXT4_INODE_HUGE_FILE); + inode->i_blocks = 0xabcd12345678ULL; + err = ext4_inode_blocks_set_test(raw, &ctx->ei); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_FALSE(test, + ext4_test_inode_flag(inode, EXT4_INODE_HUGE_FILE)); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_blocks_lo), 0x12345678U); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_blocks_high), 0xabcd); + KUNIT_EXPECT_EQ(test, ext4_inode_blocks_test(raw, &ctx->ei), + (blkcnt_t)0xabcd12345678ULL); + + /* > 48-bit sector count scales by fs-block units and sets HUGE_FILE */ + large_sectors = 0x0002000000000008ULL; /* multiple of 8 (4K / 512B) */ + inode->i_blocks = large_sectors; + err = ext4_inode_blocks_set_test(raw, &ctx->ei); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_TRUE(test, + ext4_test_inode_flag(inode, EXT4_INODE_HUGE_FILE)); + KUNIT_EXPECT_EQ(test, ext4_inode_blocks_test(raw, &ctx->ei), + large_sectors); +} + +/* ------------------------------------------------------------------------- + * 4. Raw Inode Serialization (ext4_fill_raw_inode) Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_fill_raw_inode(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct ext4_inode *raw = &ctx->scratch_raw.raw; + dev_t small_dev, large_dev; + int err; + + /* 32-bit uid/gid, 48-bit file_acl, 64-bit disksize & iversion, projid */ + ext4_set_feature_project(ctx->sb); + i_uid_write(inode, 0x12345678U); + i_gid_write(inode, 0x9abcdef0U); + ctx->ei.i_projid = make_kprojid(&init_user_ns, 0x55aa1122U); + ctx->ei.i_file_acl = 0x000344556677ULL; + ctx->ei.i_disksize = 0x0000000511223344LL; + inode_set_iversion_queried(inode, 0x000000078899aabbULL); + + err = ext4_fill_raw_inode_test(inode, raw); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_low), 0x5678); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_high), 0x1234); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_gid_low), 0xdef0); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_gid_high), 0x9abc); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_projid), 0x55aa1122U); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_file_acl_lo), 0x44556677U); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_file_acl_high), 0x0003); + KUNIT_EXPECT_EQ(test, ext4_isize(ctx->sb, raw), 0x0000000511223344LL); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_disk_version), 0x8899aabbU); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_version_hi), 0x00000007U); + KUNIT_EXPECT_EQ(test, ext4_inode_csum_verify_test(inode, raw, &ctx->ei), + 1); + + /* Non-default projid without PROJECT feature returns -EFSCORRUPTED */ + ext4_clear_feature_project(ctx->sb); + err = ext4_fill_raw_inode_test(inode, raw); + KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED); + ctx->ei.i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID); + + /* NO_UID32 masks uid/gid high bits; HURD_COMPAT skips file_acl_high */ + memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE); + raw->osd2.hurd2.h_i_mode_high = cpu_to_le16(0x7777); + set_opt(ctx->sb, NO_UID32); + set_opt2(ctx->sb, HURD_COMPAT); + err = ext4_fill_raw_inode_test(inode, raw); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_high), 0); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_gid_high), 0); + KUNIT_EXPECT_EQ(test, raw->osd2.hurd2.h_i_mode_high, + cpu_to_le16(0x7777)); + clear_opt(ctx->sb, NO_UID32); + clear_opt2(ctx->sb, HURD_COMPAT); + + /* Deleted inode (i_dtime != 0) clears uid_high/gid_high unless orphan */ + ctx->ei.i_dtime = 100; + err = ext4_fill_raw_inode_test(inode, raw); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_high), 0); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_gid_high), 0); + + ext4_set_inode_state(inode, EXT4_STATE_ORPHAN_FILE); + err = ext4_fill_raw_inode_test(inode, raw); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_high), 0x1234); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_gid_high), 0x9abc); + ext4_clear_inode_state(inode, EXT4_STATE_ORPHAN_FILE); + ctx->ei.i_dtime = 0; + + /* EA_INODE stores raw iversion without shifting out flag bit */ + ext4_set_inode_flag(inode, EXT4_INODE_EA_INODE); + inode_set_iversion_raw(inode, 0x00000002cafebabeULL); + err = ext4_fill_raw_inode_test(inode, raw); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_disk_version), 0xcafebabeU); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_version_hi), 0x00000002U); + ext4_clear_inode_flag(inode, EXT4_INODE_EA_INODE); + + /* Inline-data inode preserves raw_inode->i_block contents */ + memset(ctx->scratch_raw.bytes, 0, INODE_TEST_INODE_SIZE); + raw->i_block[0] = cpu_to_le32(0xdeadbeefU); + ctx->ei.i_data[0] = cpu_to_le32(0x11111111U); + ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS); + ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA); + ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA); + ctx->ei.i_inline_off = 164; + err = ext4_fill_raw_inode_test(inode, raw); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_block[0]), 0xdeadbeefU); + ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA); + ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA); + ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS); + ctx->ei.i_inline_off = 0; + + /* Special device inode encodes old vs new rdev into i_block[0]/[1] */ + inode->i_mode = S_IFCHR | 0600; + small_dev = MKDEV(8, 3); + inode->i_rdev = small_dev; + err = ext4_fill_raw_inode_test(inode, raw); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, old_decode_dev(le32_to_cpu(raw->i_block[0])), + small_dev); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_block[1]), 0U); + + large_dev = MKDEV(512, 1024); + inode->i_rdev = large_dev; + err = ext4_fill_raw_inode_test(inode, raw); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_block[0]), 0U); + KUNIT_EXPECT_EQ(test, new_decode_dev(le32_to_cpu(raw->i_block[1])), + large_dev); +} + +/* ------------------------------------------------------------------------- + * 5. Inode Dirtying, LARGE_FILE Promotion, and Lazytime Neighbor Flush + * ------------------------------------------------------------------------- + */ + +static void inode_test_mark_dirty_and_update(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct inode_test_blk *itable = inode_test_find_blk(ctx, + INODE_TEST_ITABLE_BLK0); + struct ext4_inode *raw; + struct ext4_inode *neighbor_raw; + struct inode *neighbor; + struct ext4_iloc iloc = {}; + handle_t *handle; + int ref_before, err; + + /* + * 1. EXT4_STATE_NEW + LARGE_FILE feature promotion in + * ext4_reserve_inode_write() / ext4_mark_iloc_dirty(). + */ + ext4_clear_feature_large_file(ctx->sb); + ext4_set_inode_state(inode, EXT4_STATE_NEW); + ctx->ei.i_disksize = 0x100000000LL; + inode->i_size = ctx->ei.i_disksize; + raw = inode_test_raw_slot(ctx, INODE_TEST_PRIMARY_INO, false); + memset(raw, 0xff, INODE_TEST_INODE_SIZE); + + handle = ext4_journal_start(inode, EXT4_HT_INODE, 2); + KUNIT_ASSERT_FALSE(test, IS_ERR(handle)); + err = ext4_reserve_inode_write(handle, inode, &iloc); + KUNIT_ASSERT_EQ(test, err, 0); + err = ext4_mark_iloc_dirty(handle, inode, &iloc); + ext4_journal_stop(handle); + + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_FALSE(test, ext4_test_inode_state(inode, EXT4_STATE_NEW)); + KUNIT_EXPECT_TRUE(test, ext4_has_feature_large_file(ctx->sb)); + KUNIT_EXPECT_EQ(test, ext4_isize(ctx->sb, raw), 0x100000000LL); + KUNIT_EXPECT_TRUE(test, + !!(inode_state_read_once(inode) & I_METADATA_WRITEBACK)); + + /* + * 2. SB_LAZYTIME opportunistic neighbor timestamp flush + automatic + * extra_isize expansion via __ext4_mark_inode_dirty(). + */ + inode_test_prepare_valid_raw_inode(ctx, 13, S_IFREG | 0644); + memcpy(itable->cache_buf, itable->disk_buf, INODE_TEST_BLK_SIZE); + set_buffer_uptodate(&itable->bh); + + neighbor = ext4_iget(ctx->sb, 13, EXT4_IGET_NORMAL); + KUNIT_ASSERT_FALSE(test, IS_ERR(neighbor)); + inode_test_track_iget_inode(test, ctx, neighbor); + + inode_set_mtime(neighbor, 0x55667788LL, 1234); + spin_lock(&neighbor->i_lock); + inode_state_set(neighbor, I_DIRTY_TIME); + spin_unlock(&neighbor->i_lock); + + ctx->sb->s_flags |= SB_LAZYTIME; + ctx->ei.i_extra_isize = 16; + ctx->sbi.s_want_extra_isize = 32; + + handle = ext4_journal_start(inode, EXT4_HT_INODE, 2); + KUNIT_ASSERT_FALSE(test, IS_ERR(handle)); + err = __ext4_mark_inode_dirty(handle, inode, __func__, __LINE__); + ext4_journal_stop(handle); + ctx->sb->s_flags &= ~SB_LAZYTIME; + + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, ctx->ei.i_extra_isize, (u16)32); + KUNIT_EXPECT_FALSE(test, + !!(inode_state_read_once(neighbor) & I_DIRTY_TIME)); + neighbor_raw = inode_test_raw_slot(ctx, 13, false); + KUNIT_EXPECT_EQ(test, le32_to_cpu(neighbor_raw->i_mtime), 0x55667788U); + KUNIT_EXPECT_EQ(test, + ext4_inode_csum_verify_test(neighbor, neighbor_raw, + EXT4_I(neighbor)), 1); + + /* 3. ext4_dirty_inode() updates raw inode on I_DIRTY_SYNC */ + inode_set_ctime(inode, 0x12340000LL, 0); + ext4_dirty_inode(inode, I_DIRTY_SYNC); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_ctime), 0x12340000U); + + /* 4. Emergency RO state returns -EROFS and drops iloc.bh cleanly */ + err = ext4_get_inode_loc(inode, &iloc); + KUNIT_ASSERT_EQ(test, err, 0); + ref_before = atomic_read(&itable->bh.b_count); + set_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags); + err = ext4_mark_iloc_dirty(NULL, inode, &iloc); + clear_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags); + KUNIT_EXPECT_EQ(test, err, -EROFS); + KUNIT_EXPECT_EQ(test, atomic_read(&itable->bh.b_count), ref_before - 1); +} + +/* ------------------------------------------------------------------------- + * 6. Extra Isize Expansion Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_expand_extra_isize(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct ext4_iloc iloc = {}; + struct ext4_inode *raw; + struct ext4_xattr_ibody_header *ihdr; + struct ext4_xattr_entry *entry; + int no_expand = 0; + int err; + + err = ext4_get_inode_loc(inode, &iloc); + KUNIT_ASSERT_EQ(test, err, 0); + raw = ext4_raw_inode(&iloc); + + /* Corrupted current i_extra_isize (unaligned or oversize) */ + ctx->ei.i_extra_isize = 13; + err = ext4_expand_extra_isize_test(inode, 32, &iloc, NULL, &no_expand); + KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED); + + ctx->ei.i_extra_isize = 200; + err = ext4_expand_extra_isize_test(inode, 204, &iloc, NULL, &no_expand); + KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED); + + /* Invalid target new_extra_isize */ + ctx->ei.i_extra_isize = 16; + KUNIT_EXPECT_EQ(test, + ext4_expand_extra_isize_test(inode, 12, &iloc, NULL, + &no_expand), -EINVAL); + KUNIT_EXPECT_EQ(test, + ext4_expand_extra_isize_test(inode, 200, &iloc, NULL, + &no_expand), -EINVAL); + + /* Clean expansion without xattrs zeroes newly covered extra space */ + memset((u8 *)raw + EXT4_GOOD_OLD_INODE_SIZE + 16, 0xaa, 32); + err = ext4_expand_extra_isize_test(inode, 32, &iloc, NULL, &no_expand); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, ctx->ei.i_extra_isize, (u16)32); + KUNIT_EXPECT_EQ(test, *((u8 *)raw + EXT4_GOOD_OLD_INODE_SIZE + 16), 0); + KUNIT_EXPECT_EQ(test, *((u8 *)raw + EXT4_GOOD_OLD_INODE_SIZE + 31), 0); + KUNIT_EXPECT_EQ(test, *((u8 *)raw + EXT4_GOOD_OLD_INODE_SIZE + 32), + 0xaa); + + /* Guard checks in ext4_try_to_expand_extra_isize() */ + ctx->ei.i_extra_isize = 16; + ext4_set_inode_state(inode, EXT4_STATE_NO_EXPAND); + KUNIT_EXPECT_EQ(test, + ext4_try_to_expand_extra_isize_test(inode, 32, &iloc, + NULL), -EOVERFLOW); + ext4_clear_inode_state(inode, EXT4_STATE_NO_EXPAND); + + ctx->sb->s_flags &= ~SB_ACTIVE; + KUNIT_EXPECT_EQ(test, + ext4_try_to_expand_extra_isize_test(inode, 32, &iloc, + NULL), -EBUSY); + ctx->sb->s_flags |= SB_ACTIVE; + + down_read(&ctx->ei.xattr_sem); + err = ext4_try_to_expand_extra_isize_test(inode, 32, &iloc, NULL); + up_read(&ctx->ei.xattr_sem); + KUNIT_EXPECT_EQ(test, err, -EBUSY); + + /* Expansion with valid in-body xattr shifts entry forward cleanly */ + memset(raw, 0, INODE_TEST_INODE_SIZE); + ctx->ei.i_extra_isize = 16; + raw->i_extra_isize = cpu_to_le16(16); + ext4_set_inode_state(inode, EXT4_STATE_XATTR); + ihdr = IHDR(inode, raw); + ihdr->h_magic = cpu_to_le32(EXT4_XATTR_MAGIC); + entry = IFIRST(ihdr); + entry->e_name_len = 1; + entry->e_name_index = EXT4_XATTR_INDEX_USER; + entry->e_name[0] = 'k'; + entry->e_value_size = cpu_to_le32(4); + entry->e_value_offs = cpu_to_le16(INODE_TEST_INODE_SIZE - + EXT4_GOOD_OLD_INODE_SIZE - 16 - + sizeof(__u32) - 4); + memcpy((char *)IFIRST(ihdr) + le16_to_cpu(entry->e_value_offs), + "val!", 4); + inode_test_stamp_raw_csum(ctx, inode->i_ino, raw); + + /* ext4_expand_extra_isize() locks xattr_sem and consumes iloc.bh */ + err = ext4_expand_extra_isize(inode, 32, &iloc); + + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, ctx->ei.i_extra_isize, (u16)32); + ihdr = IHDR(inode, raw); + KUNIT_EXPECT_EQ(test, le32_to_cpu(ihdr->h_magic), + (u32)EXT4_XATTR_MAGIC); +} + +/* ------------------------------------------------------------------------- + * 7. Inode Location Lookup (get_inode_loc / noio / fc_inode_loc) Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_get_inode_loc_variants(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct inode_test_blk *itable = inode_test_find_blk(ctx, + INODE_TEST_ITABLE_BLK0); + struct inode_test_blk *ibitmap = inode_test_find_blk(ctx, + INODE_TEST_IBITMAP_BLK); + struct ext4_iloc iloc = {}; + struct ext4_inode *raw; + int err; + + /* Out-of-range inode numbers */ + inode->i_ino = 1; + KUNIT_EXPECT_EQ(test, ext4_get_inode_loc(inode, &iloc), -EFSCORRUPTED); + KUNIT_EXPECT_EQ(test, ext4_get_inode_loc_noio(inode, &iloc), + -EFSCORRUPTED); + KUNIT_EXPECT_EQ(test, ext4_get_fc_inode_loc(ctx->sb, 99999, &iloc), + -EFSCORRUPTED); + inode->i_ino = INODE_TEST_PRIMARY_INO; + + /* Missing group descriptor -> -EIO */ + ctx->fail_group_desc = true; + KUNIT_EXPECT_EQ(test, ext4_get_inode_loc(inode, &iloc), -EIO); + KUNIT_EXPECT_EQ(test, ext4_get_inode_loc_noio(inode, &iloc), -EIO); + ctx->fail_group_desc = false; + + /* Invalid inode table block in group descriptor -> -EFSCORRUPTED */ + ext4_inode_table_set(ctx->sb, &ctx->gdp, 1); + KUNIT_EXPECT_EQ(test, ext4_get_inode_loc(inode, &iloc), -EFSCORRUPTED); + KUNIT_EXPECT_EQ(test, ext4_get_inode_loc_noio(inode, &iloc), + -EFSCORRUPTED); + ext4_inode_table_set(ctx->sb, &ctx->gdp, INODE_TEST_ITABLE_BLK0); + + /* noio variant: uncached -> -EAGAIN, non-uptodate -> -EAGAIN, hit -> 0 */ + itable->in_cache = false; + KUNIT_EXPECT_EQ(test, ext4_get_inode_loc_noio(inode, &iloc), -EAGAIN); + itable->in_cache = true; + clear_buffer_uptodate(&itable->bh); + KUNIT_EXPECT_EQ(test, ext4_get_inode_loc_noio(inode, &iloc), -EAGAIN); + KUNIT_EXPECT_EQ(test, atomic_read(&itable->bh.b_count), 1); + + set_buffer_uptodate(&itable->bh); + err = ext4_get_inode_loc_noio(inode, &iloc); + KUNIT_ASSERT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, iloc.offset, + (unsigned long)((INODE_TEST_PRIMARY_INO - 1) * + INODE_TEST_INODE_SIZE)); + brelse(iloc.bh); + + /* + * Skip-I/O optimization: non-uptodate itable block + uptodate inode + * bitmap with ONLY primary inode bit set -> zero I/O reads! + */ + clear_buffer_uptodate(&itable->bh); + memset(itable->disk_buf, 0x77, INODE_TEST_BLK_SIZE); + memset(ibitmap->cache_buf, 0, INODE_TEST_BLK_SIZE); + ext4_set_bit(INODE_TEST_PRIMARY_INO - 1, ibitmap->cache_buf); + set_buffer_uptodate(&ibitmap->bh); + ctx->read_nowait_calls = 0; + i_uid_write(inode, 0x4242); + + err = ext4_get_inode_loc(inode, &iloc); + KUNIT_ASSERT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 0U); + raw = ext4_raw_inode(&iloc); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_low), 0x4242); + /* Neighbor slot was zeroed rather than read from 0x77 disk_buf */ + KUNIT_EXPECT_EQ(test, (u8)itable->cache_buf[0], 0); + brelse(iloc.bh); + + /* + * Neighbor inode bit also set in bitmap -> cannot skip I/O; performs + * disk read and triggers inode table readahead window calculation. + */ + clear_buffer_uptodate(&itable->bh); + ext4_set_bit(INODE_TEST_PRIMARY_INO, ibitmap->cache_buf); + ctx->sbi.s_inode_readahead_blks = 4; + ctx->read_nowait_calls = 0; + ctx->readahead_calls = 0; + + err = ext4_get_inode_loc(inode, &iloc); + KUNIT_ASSERT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, ctx->read_nowait_calls, 1U); + KUNIT_EXPECT_GT(test, ctx->readahead_calls, 0U); + KUNIT_EXPECT_EQ(test, ctx->first_readahead_blk, + (ext4_fsblk_t)INODE_TEST_ITABLE_BLK0); + KUNIT_EXPECT_EQ(test, (u8)itable->cache_buf[0], 0x77); + brelse(iloc.bh); + ctx->sbi.s_inode_readahead_blks = 0; + + /* Disk read -EIO propagates cleanly with balanced bh refcount */ + clear_buffer_uptodate(&itable->bh); + ctx->fail_read_eio = true; + err = ext4_get_fc_inode_loc(ctx->sb, INODE_TEST_PRIMARY_INO, &iloc); + ctx->fail_read_eio = false; + KUNIT_EXPECT_EQ(test, err, -EIO); + KUNIT_EXPECT_EQ(test, atomic_read(&itable->bh.b_count), 1); +} + +/* ------------------------------------------------------------------------- + * 8. __ext4_iget() Valid Modes, Caching, & Special Inode Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_iget_validation_and_modes(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct ext4_inode *raw; + struct inode *inode, *cached; + + /* Illegal & special ino checks before VFS lookup */ + KUNIT_EXPECT_EQ(test, + PTR_ERR(ext4_iget(ctx->sb, 0, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + KUNIT_EXPECT_EQ(test, + PTR_ERR(ext4_iget(ctx->sb, 0, EXT4_IGET_HANDLE)), + -ESTALE); + KUNIT_EXPECT_EQ(test, + PTR_ERR(ext4_iget(ctx->sb, 8, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + KUNIT_EXPECT_EQ(test, + PTR_ERR(ext4_iget(ctx->sb, 99999, EXT4_IGET_HANDLE)), + -ESTALE); + + /* EXT4_IGET_NOWAIT returns -ENOENT when inode is not cached */ + KUNIT_EXPECT_EQ(test, + PTR_ERR(ext4_iget(ctx->sb, 14, EXT4_IGET_NOWAIT)), + -ENOENT); + + /* Valid regular file inode load & cached NOWAIT / check_igot_inode */ + ext4_set_feature_project(ctx->sb); + inode_test_prepare_valid_raw_inode(ctx, 14, S_IFREG | 0640); + raw = inode_test_raw_slot(ctx, 14, true); + raw->i_uid_low = cpu_to_le16(0x2222); + raw->i_uid_high = cpu_to_le16(0x1111); + raw->i_gid_low = cpu_to_le16(0x4444); + raw->i_gid_high = cpu_to_le16(0x3333); + raw->i_projid = cpu_to_le32(77); + raw->i_size_lo = cpu_to_le32(8192); + raw->i_disk_version = cpu_to_le32(9); + raw->i_version_hi = cpu_to_le32(2); + inode_test_stamp_raw_csum(ctx, 14, raw); + + inode = ext4_iget(ctx->sb, 14, EXT4_IGET_NORMAL); + KUNIT_ASSERT_FALSE(test, IS_ERR(inode)); + inode_test_track_iget_inode(test, ctx, inode); + KUNIT_EXPECT_EQ(test, i_uid_read(inode), 0x11112222U); + KUNIT_EXPECT_EQ(test, i_gid_read(inode), 0x33334444U); + KUNIT_EXPECT_EQ(test, from_kprojid(&init_user_ns, EXT4_I(inode)->i_projid), + 77U); + KUNIT_EXPECT_EQ(test, i_size_read(inode), 8192LL); + KUNIT_EXPECT_EQ(test, inode_peek_iversion(inode), + (0x2ULL << 32) | 9ULL); + + cached = ext4_iget(ctx->sb, 14, EXT4_IGET_NOWAIT); + KUNIT_ASSERT_PTR_EQ(test, cached, inode); + iput(cached); + + /* Cached non-EA inode rejected when EXT4_IGET_EA_INODE requested */ + cached = ext4_iget(ctx->sb, 14, EXT4_IGET_EA_INODE); + KUNIT_EXPECT_EQ(test, PTR_ERR(cached), -EFSCORRUPTED); + + /* Valid fast symlink sets cached i_link */ + inode_test_prepare_valid_raw_inode(ctx, 15, S_IFLNK | 0777); + raw = inode_test_raw_slot(ctx, 15, true); + raw->i_flags = 0; + raw->i_size_lo = cpu_to_le32(11); + memcpy(raw->i_block, "target/path", 11); + inode_test_stamp_raw_csum(ctx, 15, raw); + + inode = ext4_iget(ctx->sb, 15, EXT4_IGET_NORMAL); + KUNIT_ASSERT_FALSE(test, IS_ERR(inode)); + inode_test_track_iget_inode(test, ctx, inode); + KUNIT_EXPECT_STREQ(test, inode->i_link, "target/path"); + + /* Valid character & block device special inodes (old & new rdev) */ + inode_test_prepare_valid_raw_inode(ctx, 16, S_IFCHR | 0600); + raw = inode_test_raw_slot(ctx, 16, true); + raw->i_block[0] = cpu_to_le32(old_encode_dev(MKDEV(4, 64))); + inode_test_stamp_raw_csum(ctx, 16, raw); + + inode = ext4_iget(ctx->sb, 16, EXT4_IGET_NORMAL); + KUNIT_ASSERT_FALSE(test, IS_ERR(inode)); + inode_test_track_iget_inode(test, ctx, inode); + KUNIT_EXPECT_EQ(test, inode->i_rdev, MKDEV(4, 64)); + + inode_test_prepare_valid_raw_inode(ctx, 17, S_IFBLK | 0600); + raw = inode_test_raw_slot(ctx, 17, true); + raw->i_block[0] = 0; + raw->i_block[1] = cpu_to_le32(new_encode_dev(MKDEV(300, 700))); + inode_test_stamp_raw_csum(ctx, 17, raw); + + inode = ext4_iget(ctx->sb, 17, EXT4_IGET_NORMAL); + KUNIT_ASSERT_FALSE(test, IS_ERR(inode)); + inode_test_track_iget_inode(test, ctx, inode); + KUNIT_EXPECT_EQ(test, inode->i_rdev, MKDEV(300, 700)); + + /* Uninitialized EXT4_BOOT_LOADER_INO requires EXT4_IGET_BAD */ + inode_test_prepare_valid_raw_inode(ctx, EXT4_BOOT_LOADER_INO, 0); + raw = inode_test_raw_slot(ctx, EXT4_BOOT_LOADER_INO, true); + raw->i_links_count = 0; + inode_test_stamp_raw_csum(ctx, EXT4_BOOT_LOADER_INO, raw); + + inode = ext4_iget(ctx->sb, EXT4_BOOT_LOADER_INO, EXT4_IGET_SPECIAL); + KUNIT_EXPECT_EQ(test, PTR_ERR(inode), -EFSCORRUPTED); + + inode_test_prepare_valid_raw_inode(ctx, EXT4_BOOT_LOADER_INO, 0); + raw = inode_test_raw_slot(ctx, EXT4_BOOT_LOADER_INO, true); + raw->i_links_count = 0; + inode_test_stamp_raw_csum(ctx, EXT4_BOOT_LOADER_INO, raw); + + inode = ext4_iget(ctx->sb, EXT4_BOOT_LOADER_INO, + EXT4_IGET_SPECIAL | EXT4_IGET_BAD); + KUNIT_ASSERT_FALSE(test, IS_ERR(inode)); + inode_test_track_iget_inode(test, ctx, inode); + KUNIT_EXPECT_TRUE(test, is_bad_inode(inode)); + cached = ext4_iget(ctx->sb, EXT4_BOOT_LOADER_INO, EXT4_IGET_SPECIAL); + KUNIT_EXPECT_EQ(test, PTR_ERR(cached), -EFSCORRUPTED); +} + +/* ------------------------------------------------------------------------- + * 9. __ext4_iget() On-Disk Corruption Rejection Matrix + * ------------------------------------------------------------------------- + */ + +static void inode_test_iget_corruptions(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct ext4_inode *raw; + struct inode *inode; + + /* 1. Unallocated inode with EXT4_IGET_HANDLE -> -ESTALE */ + inode_test_prepare_valid_raw_inode(ctx, 18, 0); + raw = inode_test_raw_slot(ctx, 18, true); + raw->i_links_count = 0; + inode_test_stamp_raw_csum(ctx, 18, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 18, EXT4_IGET_HANDLE)), + -ESTALE); + + /* 2. Unaligned i_extra_isize -> -EFSCORRUPTED */ + inode_test_prepare_valid_raw_inode(ctx, 19, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 19, true); + raw->i_extra_isize = cpu_to_le16(5); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 19, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + + /* 3. Oversize i_extra_isize -> -EFSCORRUPTED */ + inode_test_prepare_valid_raw_inode(ctx, 20, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 20, true); + raw->i_extra_isize = cpu_to_le16(132); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 20, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + + /* 4. Bad checksum -> -EFSBADCRC (and EXT4_FC_REPLAY bypasses it) */ + inode_test_prepare_valid_raw_inode(ctx, 21, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 21, true); + raw->i_checksum_lo ^= cpu_to_le16(0x00ff); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 21, EXT4_IGET_NORMAL)), + -EFSBADCRC); + + inode_test_prepare_valid_raw_inode(ctx, 21, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 21, true); + raw->i_checksum_lo ^= cpu_to_le16(0x00ff); + ctx->sbi.s_mount_state |= EXT4_FC_REPLAY; + inode = ext4_iget(ctx->sb, 21, EXT4_IGET_NORMAL); + ctx->sbi.s_mount_state &= ~EXT4_FC_REPLAY; + KUNIT_ASSERT_FALSE(test, IS_ERR(inode)); + inode_test_track_iget_inode(test, ctx, inode); + + /* 5. Deleted inode (nlink=0) -> -ESTALE normally, allowed in ORPHAN_FS */ + inode_test_prepare_valid_raw_inode(ctx, 22, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 22, true); + raw->i_links_count = 0; + inode_test_stamp_raw_csum(ctx, 22, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 22, EXT4_IGET_NORMAL)), + -ESTALE); + + inode_test_prepare_valid_raw_inode(ctx, 22, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 22, true); + raw->i_links_count = 0; + inode_test_stamp_raw_csum(ctx, 22, raw); + ctx->sbi.s_mount_state |= EXT4_ORPHAN_FS; + inode = ext4_iget(ctx->sb, 22, EXT4_IGET_NORMAL); + ctx->sbi.s_mount_state &= ~EXT4_ORPHAN_FS; + KUNIT_ASSERT_FALSE(test, IS_ERR(inode)); + inode_test_put_tracked_inode(ctx, inode); + + /* 6. Unallocated special inode -> -EFSCORRUPTED */ + inode_test_prepare_valid_raw_inode(ctx, 7, S_IFREG | 0600); + raw = inode_test_raw_slot(ctx, 7, true); + raw->i_links_count = 0; + inode_test_stamp_raw_csum(ctx, 7, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 7, EXT4_IGET_SPECIAL)), + -EFSCORRUPTED); + + /* 7. Conflicting INLINE_DATA | EXTENTS flags -> -EFSCORRUPTED */ + inode_test_prepare_valid_raw_inode(ctx, 23, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 23, true); + raw->i_flags = cpu_to_le32(EXT4_INLINE_DATA_FL | EXT4_EXTENTS_FL); + inode_test_stamp_raw_csum(ctx, 23, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 23, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + + /* 8. Indirect file i_size exceeding s_bitmap_maxbytes -> -EFSCORRUPTED */ + inode_test_prepare_valid_raw_inode(ctx, 24, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 24, true); + raw->i_flags = 0; + memset(raw->i_block, 0, sizeof(raw->i_block)); + raw->i_size_high = cpu_to_le32(0x1000); + inode_test_stamp_raw_csum(ctx, 24, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 24, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + + /* 9. Directory with INDEX flag when dir_index disabled -> -EFSCORRUPTED */ + ext4_clear_feature_dir_index(ctx->sb); + inode_test_prepare_valid_raw_inode(ctx, 25, S_IFDIR | 0755); + raw = inode_test_raw_slot(ctx, 25, true); + raw->i_flags |= cpu_to_le32(EXT4_INDEX_FL); + inode_test_stamp_raw_csum(ctx, 25, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 25, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + ext4_set_feature_dir_index(ctx->sb); + + /* 10. Out-of-range i_file_acl block -> -EFSCORRUPTED */ + inode_test_prepare_valid_raw_inode(ctx, 26, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 26, true); + raw->i_file_acl_lo = cpu_to_le32(INODE_TEST_TOTAL_BLKS + 100); + inode_test_stamp_raw_csum(ctx, 26, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 26, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + + /* 11. Corrupted extent header magic -> -EFSCORRUPTED */ + inode_test_prepare_valid_raw_inode(ctx, 27, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 27, true); + raw->i_block[0] = 0; + inode_test_stamp_raw_csum(ctx, 27, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 27, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + + /* 12. Symlink with IMMUTABLE flag & fast symlink with embedded NUL */ + inode_test_prepare_valid_raw_inode(ctx, 28, S_IFLNK | 0777); + raw = inode_test_raw_slot(ctx, 28, true); + raw->i_flags = cpu_to_le32(EXT4_IMMUTABLE_FL); + raw->i_size_lo = cpu_to_le32(3); + memcpy(raw->i_block, "abc", 3); + inode_test_stamp_raw_csum(ctx, 28, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 28, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + + inode_test_prepare_valid_raw_inode(ctx, 28, S_IFLNK | 0777); + raw = inode_test_raw_slot(ctx, 28, true); + raw->i_flags = 0; + raw->i_size_lo = cpu_to_le32(5); + memcpy(raw->i_block, "ab\0cd", 5); + inode_test_stamp_raw_csum(ctx, 28, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 28, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + + /* 13. Bogus i_mode (0) with nlink > 0 & CASEFOLD without feature */ + inode_test_prepare_valid_raw_inode(ctx, 29, 0); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 29, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + + inode_test_prepare_valid_raw_inode(ctx, 30, S_IFDIR | 0755); + raw = inode_test_raw_slot(ctx, 30, true); + raw->i_flags |= cpu_to_le32(EXT4_CASEFOLD_FL); + inode_test_stamp_raw_csum(ctx, 30, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 30, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); + + /* 14. EA_INODE opened without EXT4_IGET_EA_INODE: -ESTALE vs -EFSCORRUPTED */ + inode_test_prepare_valid_raw_inode(ctx, 31, S_IFREG | 0600); + raw = inode_test_raw_slot(ctx, 31, true); + raw->i_flags |= cpu_to_le32(EXT4_EA_INODE_FL); + inode_test_stamp_raw_csum(ctx, 31, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 31, EXT4_IGET_HANDLE)), + -ESTALE); + + inode_test_prepare_valid_raw_inode(ctx, 31, S_IFREG | 0600); + raw = inode_test_raw_slot(ctx, 31, true); + raw->i_flags |= cpu_to_le32(EXT4_EA_INODE_FL); + inode_test_stamp_raw_csum(ctx, 31, raw); + KUNIT_EXPECT_EQ(test, PTR_ERR(ext4_iget(ctx->sb, 31, EXT4_IGET_NORMAL)), + -EFSCORRUPTED); +} + +/* ------------------------------------------------------------------------- + * 10. Fast Symlink Classification & ext4_can_truncate() Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_fast_symlink_and_can_truncate(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + + /* Fast symlink classification with/without external xattr & EA_INODE */ + inode_test_reset_mock_inode(ctx, S_IFLNK | 0777); + inode->i_size = 20; + inode->i_blocks = 0; + KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 1); + + /* External xattr block accounts for 8 512B sectors when !EA_INODE */ + ctx->ei.i_file_acl = INODE_TEST_META_BLK0; + inode->i_blocks = 8; + KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 1); + inode->i_blocks = 16; + KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 0); + + /* With EA_INODE feature, fast symlink depends on 0 < i_size < 60 */ + ext4_set_feature_ea_inode(ctx->sb); + inode->i_size = 0; + KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 0); + inode->i_size = 20; + KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 1); + inode->i_size = EXT4_N_BLOCKS * 4; + KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 0); + ext4_clear_feature_ea_inode(ctx->sb); + + /* Inline-data symlink is not classified as a legacy fast symlink */ + inode->i_size = 20; + inode->i_blocks = 8; + ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA); + ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA); + ctx->ei.i_inline_off = 164; + KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(inode), 0); + + /* ext4_can_truncate() across file types and fast vs slow symlinks */ + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 1); + + inode_test_reset_mock_inode(ctx, S_IFDIR | 0755); + KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 1); + + inode_test_reset_mock_inode(ctx, S_IFIFO | 0600); + KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 0); + + inode_test_reset_mock_inode(ctx, S_IFCHR | 0600); + KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 0); + + inode_test_reset_mock_inode(ctx, S_IFLNK | 0777); + inode->i_blocks = 0; + KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 0); + inode->i_blocks = 8; + KUNIT_EXPECT_EQ(test, ext4_can_truncate(inode), 1); +} + +/* ------------------------------------------------------------------------- + * 11. VFS Inode Flags, Address Space Ops, and Folio Order Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_flags_aops_mapping_order(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + const struct address_space_operations *da_aops, *nodelalloc_aops; + const struct address_space_operations *jdata_aops; + + /* 1. ext4_set_inode_flags() flag translation & S_DAX policy */ + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + ctx->ei.i_flags = EXT4_SYNC_FL | EXT4_APPEND_FL | EXT4_IMMUTABLE_FL | + EXT4_NOATIME_FL | EXT4_DIRSYNC_FL | EXT4_VERITY_FL; + ext4_set_inode_flags(inode, true); + KUNIT_EXPECT_EQ(test, + inode->i_flags & (S_SYNC | S_APPEND | S_IMMUTABLE | + S_NOATIME | S_DIRSYNC | S_VERITY), + (unsigned int)(S_SYNC | S_APPEND | S_IMMUTABLE | + S_NOATIME | S_DIRSYNC | S_VERITY)); + + ctx->ei.i_flags = 0; + ext4_set_inode_flags(inode, false); + KUNIT_EXPECT_EQ(test, + inode->i_flags & (S_SYNC | S_APPEND | S_IMMUTABLE | + S_NOATIME | S_DIRSYNC), 0U); + + /* S_DAX enabled when BDEV_IS_DAX + DAX_ALWAYS on regular extent file */ + set_bit(EXT4_FLAGS_BDEV_IS_DAX, &ctx->sbi.s_ext4_flags); + set_opt(ctx->sb, DAX_ALWAYS); + ext4_set_inode_flags(inode, true); + if (IS_ENABLED(CONFIG_FS_DAX)) + KUNIT_EXPECT_TRUE(test, IS_DAX(inode)); + + /* DAX_NEVER and JOURNAL_DATA both suppress S_DAX */ + clear_opt(ctx->sb, DAX_ALWAYS); + set_opt2(ctx->sb, DAX_NEVER); + inode->i_flags &= ~S_DAX; + ext4_set_inode_flags(inode, true); + KUNIT_EXPECT_FALSE(test, IS_DAX(inode)); + clear_opt2(ctx->sb, DAX_NEVER); + + clear_opt(ctx->sb, DELALLOC); + ctx->ei.i_flags = EXT4_DAX_FL | EXT4_JOURNAL_DATA_FL; + ctx->sbi.s_journal = (journal_t *)1; + ext4_set_inode_flags(inode, true); + ctx->sbi.s_journal = NULL; + set_opt(ctx->sb, DELALLOC); + KUNIT_EXPECT_FALSE(test, IS_DAX(inode)); + clear_bit(EXT4_FLAGS_BDEV_IS_DAX, &ctx->sbi.s_ext4_flags); + + /* 2. ext4_set_aops() dispatch across delalloc, ordered, journalled */ + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + set_opt(ctx->sb, DELALLOC); + ext4_set_aops(inode); + da_aops = inode->i_mapping->a_ops; + KUNIT_EXPECT_NOT_NULL(test, da_aops); + + clear_opt(ctx->sb, DELALLOC); + ext4_set_aops(inode); + nodelalloc_aops = inode->i_mapping->a_ops; + KUNIT_EXPECT_PTR_NE(test, da_aops, nodelalloc_aops); + + ext4_set_inode_flag(inode, EXT4_INODE_JOURNAL_DATA); + ctx->sbi.s_journal = (journal_t *)1; + ext4_set_aops(inode); + ctx->sbi.s_journal = NULL; + jdata_aops = inode->i_mapping->a_ops; + KUNIT_EXPECT_PTR_NE(test, jdata_aops, da_aops); + KUNIT_EXPECT_PTR_NE(test, jdata_aops, nodelalloc_aops); + set_opt(ctx->sb, DELALLOC); + + /* 3. ext4_set_inode_mapping_order() clamping */ + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + ctx->sbi.s_min_folio_order = 0; + ctx->sbi.s_max_folio_order = 3; + ext4_set_inode_mapping_order(inode); + if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE)) { + KUNIT_EXPECT_EQ(test, + mapping_min_folio_order(inode->i_mapping), 0U); + KUNIT_EXPECT_EQ(test, + mapping_max_folio_order(inode->i_mapping), 3U); + + ctx->sbi.s_min_folio_order = 1; + ext4_set_inode_flag(inode, EXT4_INODE_JOURNAL_DATA); + ext4_set_inode_mapping_order(inode); + KUNIT_EXPECT_EQ(test, + mapping_max_folio_order(inode->i_mapping), 1U); + } + ctx->sbi.s_min_folio_order = 0; + ctx->sbi.s_max_folio_order = 0; +} + +/* ------------------------------------------------------------------------- + * 12. Project ID, DIO Alignment, and getattr / file_getattr Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_projid_dio_and_getattr(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct path path = { .dentry = &ctx->root_dentry }; + struct kstat stat = {}; + kprojid_t projid = {}; + int err; + + /* ext4_get_projid(): -EOPNOTSUPP without feature, value with feature */ + ctx->ei.i_projid = make_kprojid(&init_user_ns, 314); + ext4_clear_feature_project(ctx->sb); + KUNIT_EXPECT_EQ(test, ext4_get_projid(inode, &projid), -EOPNOTSUPP); + ext4_set_feature_project(ctx->sb); + KUNIT_EXPECT_EQ(test, ext4_get_projid(inode, &projid), 0); + KUNIT_EXPECT_EQ(test, from_kprojid(&init_user_ns, projid), 314U); + + /* ext4_dio_alignment(): 1 default, blocksize encrypted, 0 jdata/inline */ + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + KUNIT_EXPECT_EQ(test, ext4_dio_alignment(inode), 1U); + + inode->i_flags |= S_ENCRYPTED; + KUNIT_EXPECT_EQ(test, ext4_dio_alignment(inode), + (u32)INODE_TEST_BLK_SIZE); + inode->i_flags &= ~S_ENCRYPTED; + + clear_opt(ctx->sb, DELALLOC); + ext4_set_inode_flag(inode, EXT4_INODE_JOURNAL_DATA); + ctx->sbi.s_journal = (journal_t *)1; + KUNIT_EXPECT_EQ(test, ext4_dio_alignment(inode), 0U); + ctx->sbi.s_journal = NULL; + ext4_clear_inode_flag(inode, EXT4_INODE_JOURNAL_DATA); + set_opt(ctx->sb, DELALLOC); + + ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA); + ctx->ei.i_inline_off = 164; + KUNIT_EXPECT_EQ(test, ext4_dio_alignment(inode), 0U); + ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA); + ctx->ei.i_inline_off = 0; + + /* ext4_getattr() omits delalloc; ext4_file_getattr() includes it */ + inode->i_size = 8192; + inode->i_blocks = 16; + ctx->ei.i_reserved_data_blocks = 2; + ctx->ei.i_crtime.tv_sec = 0x56781234LL; + ctx->ei.i_crtime.tv_nsec = 999L; + ctx->ei.i_flags |= EXT4_APPEND_FL | EXT4_IMMUTABLE_FL | EXT4_NODUMP_FL; + ctx->sbi.s_awu_min = 4096; + ctx->sbi.s_awu_max = 16384; + + err = ext4_getattr(&nop_mnt_idmap, &path, &stat, + STATX_BASIC_STATS | STATX_BTIME | + STATX_DIOALIGN | STATX_WRITE_ATOMIC, 0); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, stat.blocks, 16ULL); + + memset(&stat, 0, sizeof(stat)); + err = ext4_file_getattr(&nop_mnt_idmap, &path, &stat, + STATX_BASIC_STATS | STATX_BTIME | + STATX_DIOALIGN | STATX_WRITE_ATOMIC, 0); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_TRUE(test, !!(stat.result_mask & STATX_BTIME)); + KUNIT_EXPECT_EQ(test, stat.btime.tv_sec, 0x56781234LL); + KUNIT_EXPECT_EQ(test, stat.btime.tv_nsec, 999L); + /* 16 on-disk sectors + 2 reserved 4K blocks (16 sectors) = 32 sectors */ + KUNIT_EXPECT_EQ(test, stat.blocks, 32ULL); + KUNIT_EXPECT_EQ(test, stat.attributes & (STATX_ATTR_APPEND | + STATX_ATTR_IMMUTABLE | + STATX_ATTR_NODUMP), + (u64)(STATX_ATTR_APPEND | STATX_ATTR_IMMUTABLE | + STATX_ATTR_NODUMP)); + KUNIT_EXPECT_EQ(test, stat.dio_mem_align, 512U); + KUNIT_EXPECT_EQ(test, stat.dio_offset_align, 512U); + KUNIT_EXPECT_EQ(test, stat.atomic_write_unit_min, 4096U); + KUNIT_EXPECT_EQ(test, stat.atomic_write_unit_max, 16384U); + + /* Inline-data file reports simulated 512B sectors from i_size */ + ctx->ei.i_reserved_data_blocks = 0; + inode->i_blocks = 0; + inode->i_size = 1200; + ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS); + ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA); + ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA); + ctx->ei.i_inline_off = 164; + memset(&stat, 0, sizeof(stat)); + err = ext4_file_getattr(&nop_mnt_idmap, &path, &stat, + STATX_BASIC_STATS, 0); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, stat.blocks, 3ULL); + ctx->sbi.s_awu_min = 0; + ctx->sbi.s_awu_max = 0; +} + +/* ------------------------------------------------------------------------- + * 13. Delayed Allocation Reservation, Claim, Release & Nonda Switch Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_da_reserve_and_release(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + int err; + + percpu_counter_set(&ctx->sbi.s_freeclusters_counter, 100); + percpu_counter_set(&ctx->sbi.s_dirtyclusters_counter, 0); + + /* Reserve 5 clusters */ + err = ext4_da_reserve_space_test(inode, 5); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 5U); + KUNIT_EXPECT_EQ(test, + percpu_counter_sum(&ctx->sbi.s_dirtyclusters_counter), + 5LL); + + /* Claim 2 clusters via ext4_da_update_reserve_space() */ + ext4_da_update_reserve_space(inode, 2, 1); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 3U); + KUNIT_EXPECT_EQ(test, + percpu_counter_sum(&ctx->sbi.s_dirtyclusters_counter), + 3LL); + + /* Release 2 clusters via ext4_da_release_space() */ + ext4_da_release_space(inode, 0); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 3U); + ext4_da_release_space(inode, 2); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U); + + /* Underflow in update_reserve_space clamps to 0 cleanly */ + kunit_warning_suppress(test) { + ext4_da_update_reserve_space(inode, 4, 0); + } + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 0U); + + /* Underflow in release_space clamps to 0 cleanly */ + err = ext4_da_reserve_space_test(inode, 1); + KUNIT_EXPECT_EQ(test, err, 0); + kunit_warning_suppress(test) { + ext4_da_release_space(inode, 3); + } + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 0U); + + /* Exhausted free clusters -> -ENOSPC & ext4_nonda_switch() */ + KUNIT_EXPECT_EQ(test, ext4_nonda_switch_test(ctx->sb), 0); + percpu_counter_set(&ctx->sbi.s_freeclusters_counter, 2); + percpu_counter_set(&ctx->sbi.s_dirtyclusters_counter, 4); + KUNIT_EXPECT_EQ(test, ext4_nonda_switch_test(ctx->sb), 1); + + percpu_counter_set(&ctx->sbi.s_freeclusters_counter, 0); + percpu_counter_set(&ctx->sbi.s_dirtyclusters_counter, 0); + KUNIT_EXPECT_EQ(test, ext4_da_reserve_space_test(inode, 1), -ENOSPC); +} + +/* ------------------------------------------------------------------------- + * 14. Bigalloc Delayed Allocation & Cluster Sharing Accounting Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_enable_bigalloc(struct inode_test_ctx *ctx, + unsigned int cluster_ratio) +{ + ext4_set_feature_bigalloc(ctx->sb); + ctx->sbi.s_cluster_ratio = cluster_ratio; + ctx->sbi.s_cluster_bits = ilog2(cluster_ratio); + ctx->sbi.s_clusters_per_group = + ctx->sbi.s_blocks_per_group / cluster_ratio; +} + +static void inode_test_insert_leaf_extent(struct inode *inode, ext4_lblk_t lblk, + u16 len, ext4_fsblk_t pblk, + bool unwritten) +{ + struct ext4_extent_header *eh = ext_inode_hdr(inode); + unsigned int idx = le16_to_cpu(eh->eh_entries); + struct ext4_extent *ex = EXT_FIRST_EXTENT(eh) + idx; + + ex->ee_block = cpu_to_le32(lblk); + ex->ee_len = cpu_to_le16(len); + if (unwritten) + ext4_ext_mark_unwritten(ex); + ext4_ext_store_pblock(ex, pblk); + eh->eh_entries = cpu_to_le16(idx + 1); + ext4_es_remove_extent(inode, lblk, len); +} + +static void inode_test_da_bigalloc_clusters(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct ext4_map_blocks map = {}; + struct buffer_head bh = {}; + int state_unalloc, state_delayed, state_mapped; + int err; + + inode_test_enable_bigalloc(ctx, 4); + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + /* Pre-populate written extent at lblk 4 (lclu 1) -> pblk 104 */ + inode_test_insert_leaf_extent(inode, 4, 1, INODE_TEST_DATA_BLK0 + 4, + false); + /* Pre-populate unwritten extent at lblk 8 (lclu 2) -> pblk 108 */ + inode_test_insert_leaf_extent(inode, 8, 2, 108, true); + + down_write(&ctx->ei.i_data_sem); + state_unalloc = ext4_clu_alloc_state_test(inode, 0); + state_mapped = ext4_clu_alloc_state_test(inode, 5); + up_write(&ctx->ei.i_data_sem); + KUNIT_EXPECT_EQ(test, state_unalloc, 0); + KUNIT_EXPECT_EQ(test, state_mapped, 2); + + /* 1. First block in unallocated cluster 0 reserves 1 cluster */ + memset(&bh, 0, sizeof(bh)); + bh.b_size = INODE_TEST_BLK_SIZE; + err = ext4_da_get_block_prep(inode, 0, &bh, 1); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_TRUE(test, buffer_delay(&bh)); + KUNIT_EXPECT_TRUE(test, buffer_new(&bh)); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U); + + down_write(&ctx->ei.i_data_sem); + state_delayed = ext4_clu_alloc_state_test(inode, 2); + up_write(&ctx->ei.i_data_sem); + KUNIT_EXPECT_EQ(test, state_delayed, 1); + + /* 2. Sibling block in cluster 0 shares existing delalloc reservation */ + memset(&bh, 0, sizeof(bh)); + bh.b_size = INODE_TEST_BLK_SIZE; + err = ext4_da_get_block_prep(inode, 1, &bh, 1); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_TRUE(test, buffer_delay(&bh)); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U); + + /* Multi-block ext4_da_map_blocks() across [lblk 2..3] in cluster 0 */ + map = (struct ext4_map_blocks){ .m_lblk = 2, .m_len = 2 }; + err = ext4_da_map_blocks_test(inode, &map); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, map.m_len, 2U); + KUNIT_EXPECT_TRUE(test, !!(map.m_flags & EXT4_MAP_DELAYED)); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U); + + map = (struct ext4_map_blocks){ .m_lblk = 0, .m_len = 4 }; + err = ext4_da_map_blocks_test(inode, &map); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, map.m_len, 4U); + KUNIT_EXPECT_TRUE(test, !!(map.m_flags & EXT4_MAP_DELAYED)); + + /* + * 3. Delayed block in cluster 1 (sharing written lblk 4) reserves 0 new + * clusters and records a pending reservation on cluster 1! + */ + memset(&bh, 0, sizeof(bh)); + bh.b_size = INODE_TEST_BLK_SIZE; + err = ext4_da_get_block_prep(inode, 5, &bh, 1); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_TRUE(test, buffer_delay(&bh)); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U); + KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 4)); + + /* 4. Already-written block 4 maps directly without BH_Delay */ + memset(&bh, 0, sizeof(bh)); + bh.b_size = INODE_TEST_BLK_SIZE; + err = ext4_da_get_block_prep(inode, 4, &bh, 1); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_TRUE(test, buffer_mapped(&bh)); + KUNIT_EXPECT_FALSE(test, buffer_delay(&bh)); + KUNIT_EXPECT_EQ(test, (ext4_fsblk_t)bh.b_blocknr, + (ext4_fsblk_t)(INODE_TEST_DATA_BLK0 + 4)); + + /* + * 5. Unwritten block 8 sets BH_Unwritten | BH_New | BH_Mapped; + * delayed sibling hole block 10 in the same cluster 2 records a + * pending reservation without reserving an extra cluster. + */ + memset(&bh, 0, sizeof(bh)); + bh.b_size = INODE_TEST_BLK_SIZE; + err = ext4_da_get_block_prep(inode, 8, &bh, 1); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_TRUE(test, buffer_unwritten(&bh)); + KUNIT_EXPECT_TRUE(test, buffer_new(&bh)); + KUNIT_EXPECT_TRUE(test, buffer_mapped(&bh)); + KUNIT_EXPECT_EQ(test, (ext4_fsblk_t)bh.b_blocknr, 108ULL); + KUNIT_EXPECT_FALSE(test, ext4_is_pending(inode, 8)); + + memset(&bh, 0, sizeof(bh)); + bh.b_size = INODE_TEST_BLK_SIZE; + err = ext4_da_get_block_prep(inode, 10, &bh, 1); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_TRUE(test, buffer_delay(&bh)); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U); + KUNIT_EXPECT_TRUE(test, ext4_is_pending(inode, 8)); + + /* Removing ES extents releases the 1 reserved cluster via da_release */ + down_write(&ctx->ei.i_data_sem); + ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS); + up_write(&ctx->ei.i_data_sem); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 0U); +} + +/* ------------------------------------------------------------------------- + * 15. ext4_map_blocks() Lookup, Caching, Allocation, Zeroout & Validity Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_map_blocks_paths(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct ext4_map_blocks map = {}; + struct extent_status es = {}; + handle_t *handle; + int ret; + + /* 1. Out-of-range logical block -> -EFSCORRUPTED */ + map.m_lblk = EXT_MAX_BLOCKS; + map.m_len = 1; + KUNIT_EXPECT_EQ(test, ext4_map_blocks(NULL, inode, &map, 0), + -EFSCORRUPTED); + + /* 2. CACHED_NOWAIT miss returns 0 without querying extent tree */ + inode_test_insert_leaf_extent(inode, 0, 2, INODE_TEST_DATA_BLK0, false); + map = (struct ext4_map_blocks){ .m_lblk = 0, .m_len = 2 }; + ret = ext4_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CACHED_NOWAIT); + KUNIT_EXPECT_EQ(test, ret, 0); + + /* 3. ES miss queries extent tree, caches written extent; NOWAIT hits */ + map = (struct ext4_map_blocks){ .m_lblk = 0, .m_len = 2 }; + ret = ext4_map_blocks(NULL, inode, &map, 0); + KUNIT_EXPECT_EQ(test, ret, 2); + KUNIT_EXPECT_EQ(test, map.m_pblk, (ext4_fsblk_t)INODE_TEST_DATA_BLK0); + KUNIT_EXPECT_TRUE(test, !!(map.m_flags & EXT4_MAP_MAPPED)); + + map = (struct ext4_map_blocks){ .m_lblk = 1, .m_len = 1 }; + ret = ext4_map_blocks(NULL, inode, &map, EXT4_GET_BLOCKS_CACHED_NOWAIT); + KUNIT_EXPECT_EQ(test, ret, 1); + KUNIT_EXPECT_EQ(test, map.m_pblk, + (ext4_fsblk_t)(INODE_TEST_DATA_BLK0 + 1)); + + /* 4. ES delayed & hole hits */ + down_write(&ctx->ei.i_data_sem); + KUNIT_EXPECT_EQ(test, ext4_insert_delayed_blocks_test(inode, 4, 2), 0); + ext4_es_insert_extent(inode, 6, 2, ~0, EXTENT_STATUS_HOLE, 0); + up_write(&ctx->ei.i_data_sem); + + map = (struct ext4_map_blocks){ .m_lblk = 4, .m_len = 2 }; + ret = ext4_map_blocks(NULL, inode, &map, 0); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_TRUE(test, !!(map.m_flags & EXT4_MAP_DELAYED)); + + map = (struct ext4_map_blocks){ .m_lblk = 6, .m_len = 2 }; + ret = ext4_map_blocks(NULL, inode, &map, 0); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, map.m_flags, 0U); + + /* 5. Block allocation + EXT4_GET_BLOCKS_CREATE_ZERO & zeroout error */ + ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0 + 2; + ctx->zeroout_calls = 0; + handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, 4); + KUNIT_ASSERT_FALSE(test, IS_ERR(handle)); + map = (struct ext4_map_blocks){ .m_lblk = 10, .m_len = 2 }; + ret = ext4_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE_ZERO); + ext4_journal_stop(handle); + + KUNIT_EXPECT_EQ(test, ret, 2); + KUNIT_EXPECT_EQ(test, map.m_pblk, + (ext4_fsblk_t)(INODE_TEST_DATA_BLK0 + 2)); + KUNIT_EXPECT_TRUE(test, !!(map.m_flags & EXT4_MAP_NEW)); + KUNIT_EXPECT_EQ(test, ctx->zeroout_calls, 1U); + KUNIT_EXPECT_EQ(test, ctx->last_zeroout_lblk, 10U); + KUNIT_EXPECT_EQ(test, ctx->last_zeroout_len, 2U); + + /* Failed zeroout returns error BEFORE caching written extent in ES */ + ctx->fail_zeroout_err = -EIO; + handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, 4); + KUNIT_ASSERT_FALSE(test, IS_ERR(handle)); + map = (struct ext4_map_blocks){ .m_lblk = 12, .m_len = 1 }; + ret = ext4_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE_ZERO); + ext4_journal_stop(handle); + ctx->fail_zeroout_err = 0; + + KUNIT_EXPECT_EQ(test, ret, -EIO); + KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 12, NULL, &es, NULL), + 1); + KUNIT_EXPECT_FALSE(test, ext4_es_is_written(&es)); + KUNIT_EXPECT_TRUE(test, ext4_es_is_hole(&es)); + + /* 6. check_block_validity() rejects out-of-bounds pblk in extent */ + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + inode_test_insert_leaf_extent(inode, 0, 1, INODE_TEST_TOTAL_BLKS + 50, + false); + map = (struct ext4_map_blocks){ .m_lblk = 0, .m_len = 1 }; + KUNIT_EXPECT_EQ(test, ext4_map_blocks(NULL, inode, &map, 0), + -EFSCORRUPTED); +} + +/* ------------------------------------------------------------------------- + * 16. ext4_get_block, getblk, bread, bread_batch & walk_page_buffers Tests + * ------------------------------------------------------------------------- + */ + +static int inode_test_walk_cb(handle_t *handle, struct inode *inode, + struct buffer_head *bh) +{ + int *visited = (int *)bh->b_private; + + (*visited)++; + if (*visited < 0) + return -EINVAL; + return 0; +} + +static void inode_test_get_block_bread_and_walk_buffers(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct inode_test_blk *dblk0 = inode_test_find_blk(ctx, + INODE_TEST_DATA_BLK0); + struct inode_test_blk *dblk1 = inode_test_find_blk(ctx, + INODE_TEST_DATA_BLK0 + 1); + struct buffer_head bh = {}; + struct buffer_head *res_bh; + struct buffer_head *batch[3] = {}; + struct buffer_head ring[4] = {}; + int visited[4] = { 0, 0, 0, 0 }; + int partial = 0; + handle_t *handle; + int i, err; + + /* 1. ext4_get_block() inline -ERANGE, hole lookup, mapped & create */ + ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS); + ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA); + ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA); + ctx->ei.i_inline_off = 164; + bh.b_size = INODE_TEST_BLK_SIZE; + KUNIT_EXPECT_EQ(test, ext4_get_block(inode, 0, &bh, 0), -ERANGE); + ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA); + ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA); + ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS); + ctx->ei.i_inline_off = 0; + + memset(&bh, 0, sizeof(bh)); + bh.b_size = INODE_TEST_BLK_SIZE; + KUNIT_EXPECT_EQ(test, ext4_get_block(inode, 0, &bh, 0), 0); + KUNIT_EXPECT_FALSE(test, buffer_mapped(&bh)); + + ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0; + handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, 4); + KUNIT_ASSERT_FALSE(test, IS_ERR(handle)); + err = ext4_get_block_unwritten(inode, 0, &bh, 1); + ext4_journal_stop(handle); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_TRUE(test, buffer_mapped(&bh)); + KUNIT_EXPECT_TRUE(test, buffer_unwritten(&bh)); + KUNIT_EXPECT_TRUE(test, buffer_new(&bh)); + + /* 2. ext4_getblk() hole -> NULL, CREATE new block zeroes & dirties bh */ + res_bh = ext4_getblk(NULL, inode, 5, 0); + KUNIT_EXPECT_NULL(test, res_bh); + + clear_buffer_uptodate(&dblk1->bh); + memset(dblk1->cache_buf, 0xcd, INODE_TEST_BLK_SIZE); + ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0 + 1; + handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS, 4); + KUNIT_ASSERT_FALSE(test, IS_ERR(handle)); + res_bh = ext4_getblk(handle, inode, 1, EXT4_GET_BLOCKS_CREATE); + ext4_journal_stop(handle); + KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(res_bh)); + KUNIT_EXPECT_TRUE(test, buffer_uptodate(res_bh)); + KUNIT_EXPECT_EQ(test, (u8)dblk1->cache_buf[0], 0); + brelse(res_bh); + + /* 3. ext4_bread() disk read & -EIO error cleanup */ + inode_test_reset_mock_inode(ctx, S_IFDIR | 0755); + inode_test_insert_leaf_extent(inode, 0, 1, INODE_TEST_DATA_BLK0, false); + inode_test_insert_leaf_extent(inode, 2, 1, INODE_TEST_DATA_BLK0 + 1, + false); + clear_buffer_uptodate(&dblk0->bh); + memset(dblk0->disk_buf, 0x3c, INODE_TEST_BLK_SIZE); + ctx->read_lock_calls = 0; + + res_bh = ext4_bread(NULL, inode, 0, 0); + KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(res_bh)); + KUNIT_EXPECT_EQ(test, ctx->read_lock_calls, 1U); + KUNIT_EXPECT_EQ(test, (u8)res_bh->b_data[0], 0x3c); + brelse(res_bh); + + clear_buffer_uptodate(&dblk0->bh); + ctx->fail_read_eio = true; + res_bh = ext4_bread(NULL, inode, 0, 0); + ctx->fail_read_eio = false; + KUNIT_EXPECT_EQ(test, PTR_ERR(res_bh), -EIO); + KUNIT_EXPECT_EQ(test, atomic_read(&dblk0->bh.b_count), 1); + + /* 4. ext4_bread_batch() across [mapped, hole, mapped] & -EIO cleanup */ + clear_buffer_uptodate(&dblk0->bh); + clear_buffer_uptodate(&dblk1->bh); + memset(dblk1->disk_buf, 0x99, INODE_TEST_BLK_SIZE); + err = ext4_bread_batch(inode, 0, 3, true, batch); + KUNIT_ASSERT_EQ(test, err, 0); + KUNIT_EXPECT_NOT_NULL(test, batch[0]); + KUNIT_EXPECT_NULL(test, batch[1]); + KUNIT_EXPECT_NOT_NULL(test, batch[2]); + KUNIT_EXPECT_EQ(test, (u8)batch[2]->b_data[0], 0x99); + for (i = 0; i < 3; i++) + brelse(batch[i]); + + memset(batch, 0, sizeof(batch)); + clear_buffer_uptodate(&dblk0->bh); + ctx->fail_read_eio = true; + err = ext4_bread_batch(inode, 0, 3, true, batch); + ctx->fail_read_eio = false; + KUNIT_EXPECT_EQ(test, err, -EIO); + KUNIT_EXPECT_EQ(test, atomic_read(&dblk0->bh.b_count), 1); + KUNIT_EXPECT_EQ(test, atomic_read(&dblk1->bh.b_count), 1); + + /* 5. ext4_walk_page_buffers() sub-page range, partial & error stop */ + for (i = 0; i < 4; i++) { + ring[i].b_size = 1024; + ring[i].b_this_page = &ring[(i + 1) % 4]; + ring[i].b_private = &visited[i]; + if (i != 3) + set_buffer_uptodate(&ring[i]); + } + err = ext4_walk_page_buffers(NULL, inode, &ring[0], 1024, 3072, + &partial, inode_test_walk_cb); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, visited[0], 0); + KUNIT_EXPECT_EQ(test, visited[1], 1); + KUNIT_EXPECT_EQ(test, visited[2], 1); + KUNIT_EXPECT_EQ(test, visited[3], 0); + KUNIT_EXPECT_EQ(test, partial, 1); + + set_buffer_uptodate(&ring[3]); + partial = 0; + err = ext4_walk_page_buffers(NULL, inode, &ring[0], 1024, 3072, + &partial, inode_test_walk_cb); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, partial, 0); + + visited[1] = -2; + err = ext4_walk_page_buffers(NULL, inode, &ring[0], 1024, 3072, + NULL, inode_test_walk_cb); + KUNIT_EXPECT_EQ(test, err, -EINVAL); + KUNIT_EXPECT_EQ(test, visited[2], 2); +} + +/* ------------------------------------------------------------------------- + * 17. ext4_iomap_begin, ext4_iomap_report_ops, and Atomic Writes Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_iomap_and_atomic_writes(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct iomap iomap = {}; + struct iomap srcmap = {}; + struct iomap_iter iter = {}; + int err; + + /* 1. Offset past EXT4_MAX_LOGICAL_BLOCK -> -EINVAL */ + err = ext4_iomap_begin(inode, + ((loff_t)EXT4_MAX_LOGICAL_BLOCK + 1) << 12, + 4096, 0, &iomap, &srcmap); + KUNIT_EXPECT_EQ(test, err, -EINVAL); + + /* 2. Inline-data: iomap_begin warns & returns -ERANGE; report works */ + ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA); + ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS); + ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA); + ctx->ei.i_inline_off = 164; + ctx->ei.i_inline_size = 60; + inode->i_size = 24; + + kunit_warning_suppress(test) { + err = ext4_iomap_begin(inode, 0, 24, 0, &iomap, &srcmap); + } + KUNIT_EXPECT_EQ(test, err, -ERANGE); + + iter = (struct iomap_iter){ + .inode = inode, .pos = 0, .len = 24, .flags = IOMAP_REPORT, + }; + err = ext4_iomap_report_ops.iomap_next(&iter, &iter.iomap, + &iter.srcmap); + KUNIT_EXPECT_GT(test, err, 0); + KUNIT_EXPECT_EQ(test, iter.iomap.type, (u16)IOMAP_INLINE); + + iter = (struct iomap_iter){ + .inode = inode, .pos = 60, .len = 24, .flags = IOMAP_REPORT, + }; + err = ext4_iomap_report_ops.iomap_next(&iter, &iter.iomap, + &iter.srcmap); + KUNIT_EXPECT_EQ(test, err, -ENOENT); + + /* 3. Indirect report past s_bitmap_maxbytes returns IOMAP_HOLE */ + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS); + memset(ctx->ei.i_data, 0, sizeof(ctx->ei.i_data)); + iter = (struct iomap_iter){ + .inode = inode, + .pos = ctx->sbi.s_bitmap_maxbytes, + .len = 4096, + .flags = IOMAP_REPORT, + }; + err = ext4_iomap_report_ops.iomap_next(&iter, &iter.iomap, + &iter.srcmap); + KUNIT_EXPECT_GT(test, err, 0); + KUNIT_EXPECT_EQ(test, iter.iomap.type, (u16)IOMAP_HOLE); + + /* Indirect DIO write to hole inside i_size returns -ENOTBLK */ + inode->i_size = 8192; + err = ext4_iomap_begin(inode, 0, 4096, IOMAP_WRITE | IOMAP_DIRECT, + &iomap, &srcmap); + KUNIT_EXPECT_EQ(test, err, -ENOTBLK); + + /* 4. Read mappings (MAPPED, UNWRITTEN, DELALLOC, HOLE, DAX offset) */ + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + inode->i_size = 16384; + ctx->sbi.s_dax_part_off = 0x200000ULL; + down_write(&ctx->ei.i_data_sem); + ext4_es_insert_extent(inode, 0, 1, INODE_TEST_DATA_BLK0, + EXTENT_STATUS_WRITTEN, 0); + ext4_es_insert_extent(inode, 1, 1, INODE_TEST_DATA_BLK0 + 1, + EXTENT_STATUS_UNWRITTEN, 0); + KUNIT_EXPECT_EQ(test, ext4_insert_delayed_blocks_test(inode, 2, 1), 0); + ext4_es_insert_extent(inode, 3, 1, ~0, EXTENT_STATUS_HOLE, 0); + up_write(&ctx->ei.i_data_sem); + + KUNIT_EXPECT_EQ(test, + ext4_iomap_begin(inode, 0, 4096, IOMAP_DAX, &iomap, + &srcmap), 0); + KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_MAPPED); + KUNIT_EXPECT_EQ(test, iomap.addr, + ((u64)INODE_TEST_DATA_BLK0 << 12) + + (IS_ENABLED(CONFIG_FS_DAX) ? 0x200000ULL : 0ULL)); + ctx->sbi.s_dax_part_off = 0; + + KUNIT_EXPECT_EQ(test, + ext4_iomap_begin(inode, 4096, 4096, 0, &iomap, &srcmap), + 0); + KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_UNWRITTEN); + + KUNIT_EXPECT_EQ(test, + ext4_iomap_begin(inode, 8192, 4096, 0, &iomap, &srcmap), + 0); + KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_DELALLOC); + + KUNIT_EXPECT_EQ(test, + ext4_iomap_begin(inode, 12288, 4096, 0, &iomap, + &srcmap), 0); + KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_HOLE); + + iter = (struct iomap_iter){ + .inode = inode, .pos = 0, .len = 4096, .flags = 0, + }; + err = ext4_iomap_ops.iomap_next(&iter, &iter.iomap, &iter.srcmap); + KUNIT_EXPECT_GT(test, err, 0); + KUNIT_EXPECT_EQ(test, iter.iomap.type, (u16)IOMAP_MAPPED); + KUNIT_EXPECT_EQ(test, iter.iomap.addr, + (u64)INODE_TEST_DATA_BLK0 << 12); + + /* 5. DIO write: NOWAIT -> -EAGAIN, inside i_size -> UNWRITTEN */ + KUNIT_EXPECT_EQ(test, + ext4_iomap_begin(inode, 12288, 4096, + IOMAP_WRITE | IOMAP_DIRECT | + IOMAP_NOWAIT, &iomap, &srcmap), + -EAGAIN); + + ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0 + 3; + KUNIT_EXPECT_EQ(test, + ext4_iomap_begin(inode, 12288, 4096, + IOMAP_WRITE | IOMAP_DIRECT, &iomap, + &srcmap), 0); + KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_UNWRITTEN); + KUNIT_EXPECT_TRUE(test, !!(iomap.flags & IOMAP_F_NEW)); + + /* 6. Bigalloc multi-block atomic write across mixed [written + hole] */ + inode_test_enable_bigalloc(ctx, 4); + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + inode->i_size = 16384; + /* Pre-populate lblk 0..1 as written at pblk 100..101; lblk 2..3 hole */ + inode_test_insert_leaf_extent(inode, 0, 2, INODE_TEST_DATA_BLK0, false); + ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0 + 2; + ctx->zeroout_calls = 0; + + err = ext4_iomap_begin(inode, 0, 16384, + IOMAP_WRITE | IOMAP_DIRECT | IOMAP_ATOMIC, + &iomap, &srcmap); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, iomap.type, (u16)IOMAP_MAPPED); + KUNIT_EXPECT_EQ(test, iomap.length, 16384ULL); + KUNIT_EXPECT_EQ(test, iomap.addr, (u64)INODE_TEST_DATA_BLK0 << 12); + KUNIT_EXPECT_TRUE(test, !!(iomap.flags & IOMAP_F_ATOMIC_BIO)); + KUNIT_EXPECT_GT(test, ctx->zeroout_calls, 0U); +} + +/* ------------------------------------------------------------------------- + * 18. Transaction Credits, Layout Breaking & Disksize Update Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_trans_credits_and_disksize(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + int ext_credits, ind_credits, chunk_ext, chunk_jdata; + int err; + + /* 1. ext4_meta_trans_blocks() & ext4_chunk_trans_*() */ + ctx->sbi.s_groups_count = 4; + ctx->sbi.s_gdb_count = 2; + ext_credits = ext4_meta_trans_blocks(inode, 8, 2, 1); + KUNIT_EXPECT_GT(test, ext_credits, 0); + + ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS); + ind_credits = ext4_meta_trans_blocks(inode, 8, 2, 1); + KUNIT_EXPECT_GT(test, ind_credits, 0); + ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS); + ctx->sbi.s_groups_count = 1; + ctx->sbi.s_gdb_count = 1; + + chunk_ext = ext4_chunk_trans_extent(inode, 5); + KUNIT_EXPECT_EQ(test, ext4_chunk_trans_blocks(inode, 5), chunk_ext); + + clear_opt(ctx->sb, DELALLOC); + ext4_set_inode_flag(inode, EXT4_INODE_JOURNAL_DATA); + ctx->sbi.s_journal = (journal_t *)1; + chunk_jdata = ext4_chunk_trans_extent(inode, 5); + ctx->sbi.s_journal = NULL; + ext4_clear_inode_flag(inode, EXT4_INODE_JOURNAL_DATA); + set_opt(ctx->sb, DELALLOC); + KUNIT_EXPECT_EQ(test, chunk_jdata, chunk_ext + 5); + + /* 2. ext4_break_layouts() requires invalidate_lock held */ + kunit_warning_suppress(test) { + err = ext4_break_layouts(inode); + } + KUNIT_EXPECT_EQ(test, err, -EINVAL); + + filemap_invalidate_lock(inode->i_mapping); + err = ext4_break_layouts(inode); + filemap_invalidate_unlock(inode->i_mapping); + KUNIT_EXPECT_EQ(test, err, 0); + + /* 3. ext4_update_disksize_before_punch() */ + inode->i_size = 12000; + ctx->ei.i_disksize = 4096; + + inode_lock(inode); + /* Punch past i_size is a no-op */ + KUNIT_EXPECT_EQ(test, + ext4_update_disksize_before_punch(inode, 16384, 4096), + 0); + KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 4096LL); + + /* Punch within [0, 8192) advances i_disksize to 8192 */ + KUNIT_EXPECT_EQ(test, + ext4_update_disksize_before_punch(inode, 4096, 4096), + 0); + KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 8192LL); + + /* Punch extending past i_size clamps i_disksize to i_size (12000) */ + KUNIT_EXPECT_EQ(test, + ext4_update_disksize_before_punch(inode, 8192, 8192), + 0); + inode_unlock(inode); + KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 12000LL); +} + +/* ------------------------------------------------------------------------- + * 19. Partial Block Zeroing, EOF Zeroing & Pagecache Truncate Range Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_zero_partial_and_eof(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct inode_test_blk *dblk0 = inode_test_find_blk(ctx, + INODE_TEST_DATA_BLK0); + struct inode_test_blk *dblk1 = inode_test_find_blk(ctx, + INODE_TEST_DATA_BLK0 + 1); + struct folio *folio; + unsigned int partial_zeroed = 0; + u8 *kaddr; + int err; + + /* + * Map lblk 0 -> pblk 100 (written), leave lblk 1 as a hole, and map + * lblk 2 -> pblk 101 (written). + */ + inode->i_size = 12288; + ctx->ei.i_disksize = 12288; + inode_test_insert_leaf_extent(inode, 0, 1, INODE_TEST_DATA_BLK0, false); + inode_test_insert_leaf_extent(inode, 2, 1, INODE_TEST_DATA_BLK0 + 1, + false); + memset(dblk0->disk_buf, 0x5a, INODE_TEST_BLK_SIZE); + memset(dblk1->disk_buf, 0xa5, INODE_TEST_BLK_SIZE); + + /* 1. Single-block interior zeroing [512, 1536) sets START | END */ + err = ext4_zero_partial_blocks(inode, 512, 1024, &partial_zeroed); + KUNIT_ASSERT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, partial_zeroed, + (unsigned int)(EXT4_PARTIAL_ZERO_START | + EXT4_PARTIAL_ZERO_END)); + + folio = filemap_get_folio(inode->i_mapping, 0); + KUNIT_ASSERT_FALSE(test, IS_ERR(folio)); + kaddr = kmap_local_folio(folio, 0); + KUNIT_EXPECT_EQ(test, kaddr[511], 0x5a); + KUNIT_EXPECT_EQ(test, kaddr[512], 0x00); + KUNIT_EXPECT_EQ(test, kaddr[1535], 0x00); + KUNIT_EXPECT_EQ(test, kaddr[1536], 0x5a); + kunmap_local(kaddr); + folio_put(folio); + + /* 2. Multi-block [3000, 5000): lblk 0 mapped, lblk 1 hole -> START */ + partial_zeroed = 0; + err = ext4_zero_partial_blocks(inode, 3000, 2000, &partial_zeroed); + KUNIT_ASSERT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, partial_zeroed, + (unsigned int)EXT4_PARTIAL_ZERO_START); + + /* Zero EOF tail [8500, 12288) in lblk 2 -> pblk 101 */ + KUNIT_EXPECT_EQ(test, ext4_block_zero_eof(inode, 8192, 12288), 0); + KUNIT_EXPECT_EQ(test, ext4_block_zero_eof(inode, 9000, 8500), 0); + + err = ext4_block_zero_eof(inode, 8500, 12288); + KUNIT_ASSERT_EQ(test, err, 0); + + folio = filemap_get_folio(inode->i_mapping, 8192 >> PAGE_SHIFT); + KUNIT_ASSERT_FALSE(test, IS_ERR(folio)); + kaddr = kmap_local_folio(folio, offset_in_folio(folio, 8192)); + KUNIT_EXPECT_EQ(test, kaddr[307], 0xa5); + KUNIT_EXPECT_EQ(test, kaddr[308], 0x00); + KUNIT_EXPECT_EQ(test, kaddr[4095], 0x00); + kunmap_local(kaddr); + folio_put(folio); + + /* 3. ext4_truncate_page_cache_block_range() evicts full folio 0 */ + err = ext4_truncate_page_cache_block_range(inode, 0, + INODE_TEST_BLK_SIZE); + KUNIT_EXPECT_EQ(test, err, 0); + err = ext4_truncate_page_cache_block_range(inode, 0, + max_t(loff_t, PAGE_SIZE, INODE_TEST_BLK_SIZE)); + KUNIT_EXPECT_EQ(test, err, 0); + folio = filemap_get_folio(inode->i_mapping, 0); + KUNIT_EXPECT_TRUE(test, IS_ERR(folio)); + if (!IS_ERR(folio)) + folio_put(folio); +} + +/* ------------------------------------------------------------------------- + * 20. ext4_punch_hole() & ext4_truncate() End-to-End Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_punch_hole_and_truncate(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct file *dummy_file; + struct extent_status es = {}; + int err; + + dummy_file = kunit_kzalloc(test, sizeof(*dummy_file), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, dummy_file); + dummy_file->f_inode = inode; + dummy_file->f_mapping = inode->i_mapping; + + /* Set up 4-block written extent [lblk 0..3 -> pblk 100..103] */ + inode->i_size = 4 * INODE_TEST_BLK_SIZE; + ctx->ei.i_disksize = inode->i_size; + inode->i_blocks = 32; + inode_test_insert_leaf_extent(inode, 0, 4, INODE_TEST_DATA_BLK0, false); + down_write(&ctx->ei.i_data_sem); + ext4_es_insert_extent(inode, 0, 4, INODE_TEST_DATA_BLK0, + EXTENT_STATUS_WRITTEN, 0); + up_write(&ctx->ei.i_data_sem); + + /* 1. Punch past i_size is a no-op */ + inode_lock(inode); + err = ext4_punch_hole(dummy_file, 5 * INODE_TEST_BLK_SIZE, + INODE_TEST_BLK_SIZE); + inode_unlock(inode); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 0UL); + + /* 2. Punch middle 2 blocks [4096, 12288) -> frees pblk 101..102 */ + inode_lock(inode); + err = ext4_punch_hole(dummy_file, INODE_TEST_BLK_SIZE, + 2 * INODE_TEST_BLK_SIZE); + inode_unlock(inode); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 2UL); + KUNIT_EXPECT_EQ(test, ext4_es_lookup_extent(inode, 1, NULL, &es, NULL), + 1); + KUNIT_EXPECT_TRUE(test, ext4_es_is_hole(&es)); + KUNIT_EXPECT_EQ(test, es.es_len, 2U); + + /* 3. Truncate down to 1 block (4096B) -> frees trailing pblk 103 */ + ctx->total_freed_blocks = 0; + ctx->orphan_add_calls = 0; + ctx->orphan_del_calls = 0; + inode->i_size = INODE_TEST_BLK_SIZE; + inode_lock(inode); + err = ext4_truncate(inode); + inode_unlock(inode); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, ctx->orphan_add_calls, 1U); + KUNIT_EXPECT_EQ(test, ctx->orphan_del_calls, 1U); + KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 1UL); + KUNIT_EXPECT_EQ(test, ctx->last_free_pblk, + (ext4_fsblk_t)(INODE_TEST_DATA_BLK0 + 3)); + + /* 4. Truncate to 0 sets EXT4_STATE_DA_ALLOC_CLOSE & frees pblk 100 */ + ctx->total_freed_blocks = 0; + inode->i_size = 0; + inode_lock(inode); + err = ext4_truncate(inode); + inode_unlock(inode); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_TRUE(test, + ext4_test_inode_state(inode, + EXT4_STATE_DA_ALLOC_CLOSE)); + KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 1UL); + KUNIT_EXPECT_EQ(test, ctx->last_free_pblk, + (ext4_fsblk_t)INODE_TEST_DATA_BLK0); +} + +/* ------------------------------------------------------------------------- + * 21. ext4_inode_attach_jinode, ext4_write_inode & ext4_sync_inode_metadata + * ------------------------------------------------------------------------- + */ + +static void inode_test_write_and_sync_metadata(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct inode_test_blk *itable = inode_test_find_blk(ctx, + INODE_TEST_ITABLE_BLK0); + struct writeback_control wbc = { .sync_mode = WB_SYNC_ALL }; + struct ext4_inode *raw; + int err; + + /* 1. ext4_inode_attach_jinode(): no-op without journal, allocs with */ + KUNIT_EXPECT_EQ(test, ext4_inode_attach_jinode(inode), 0); + KUNIT_EXPECT_NULL(test, ctx->ei.jinode); + + ctx->sbi.s_journal = (journal_t *)1; + err = ext4_inode_attach_jinode(inode); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_NOT_NULL(test, ctx->ei.jinode); + /* Idempotent when jinode is already attached */ + KUNIT_EXPECT_EQ(test, ext4_inode_attach_jinode(inode), 0); + ctx->sbi.s_journal = NULL; + if (ctx->ei.jinode) { + jbd2_free_inode(ctx->ei.jinode); + ctx->ei.jinode = NULL; + } + + /* 2. ext4_write_inode(): 0 in nojournal, -EROFS in emergency state */ + KUNIT_EXPECT_EQ(test, ext4_write_inode(inode, &wbc), 0); + set_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags); + KUNIT_EXPECT_EQ(test, ext4_write_inode(inode, &wbc), -EROFS); + clear_bit(EXT4_FLAGS_EMERGENCY_RO, &ctx->sbi.s_ext4_flags); + + /* 3. ext4_sync_inode_metadata(): warns & fails if journal present */ + ctx->sbi.s_journal = (journal_t *)1; + kunit_warning_suppress(test) { + err = ext4_sync_inode_metadata(inode, &wbc); + } + ctx->sbi.s_journal = NULL; + KUNIT_EXPECT_EQ(test, err, -EFSCORRUPTED); + + /* Clean nojournal metadata sync flushes dirty raw inode & returns 0 */ + i_uid_write(inode, 0x7788); + ext4_dirty_inode(inode, I_DIRTY_SYNC); + clear_buffer_mapped(&itable->bh); + clear_buffer_write_io_error(&itable->bh); + err = ext4_sync_inode_metadata(inode, &wbc); + KUNIT_EXPECT_EQ(test, err, 0); + raw = inode_test_raw_slot(ctx, INODE_TEST_PRIMARY_INO, false); + KUNIT_EXPECT_EQ(test, le16_to_cpu(raw->i_uid_low), 0x7788); + + /* Simulated buffer write error returns -EIO */ + set_buffer_write_io_error(&itable->bh); + err = ext4_sync_inode_metadata(inode, &wbc); + clear_buffer_write_io_error(&itable->bh); + KUNIT_EXPECT_EQ(test, err, -EIO); +} + +/* ------------------------------------------------------------------------- + * 22. ext4_evict_inode() Live, Unlinked Regular & Fast Symlink Eviction Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_evict_inode_paths(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct ext4_inode *raw; + struct inode *ig; + + /* 1. Live inode (i_nlink == 1): evicts cleanly without freeing inode */ + inode_test_prepare_valid_raw_inode(ctx, 24, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 24, true); + raw->i_size_lo = cpu_to_le32(4096); + inode_test_stamp_raw_csum(ctx, 24, raw); + ig = ext4_iget(ctx->sb, 24, EXT4_IGET_NORMAL); + KUNIT_ASSERT_FALSE(test, IS_ERR(ig)); + ctx->free_inode_calls = 0; + ctx->total_freed_blocks = 0; + iput(ig); + KUNIT_EXPECT_EQ(test, ctx->free_inode_calls, 0U); + KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 0UL); + + /* + * 2. Unlinked regular file (i_nlink == 0) with 2 mapped blocks: + * truncates data blocks, sets i_dtime, updates raw inode, and calls + * ext4_free_inode(). + */ + inode_test_prepare_valid_raw_inode(ctx, 25, S_IFREG | 0644); + raw = inode_test_raw_slot(ctx, 25, true); + raw->i_size_lo = cpu_to_le32(8192); + inode_test_stamp_raw_csum(ctx, 25, raw); + ig = ext4_iget(ctx->sb, 25, EXT4_IGET_NORMAL); + KUNIT_ASSERT_FALSE(test, IS_ERR(ig)); + inode_test_insert_leaf_extent(ig, 0, 2, INODE_TEST_DATA_BLK0, false); + ig->i_blocks = 16; + clear_nlink(ig); + ctx->free_inode_calls = 0; + ctx->total_freed_blocks = 0; + iput(ig); + KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 2UL); + KUNIT_EXPECT_EQ(test, ctx->free_inode_calls, 1U); + raw = inode_test_raw_slot(ctx, 25, false); + KUNIT_EXPECT_NE(test, le32_to_cpu(raw->i_dtime), 0U); + KUNIT_EXPECT_EQ(test, le32_to_cpu(raw->i_size_lo), 0U); + + /* + * 3. Unlinked fast symlink (i_nlink == 0): clears inline i_data target + * without treating bytes as block pointers and frees the inode. + */ + inode_test_prepare_valid_raw_inode(ctx, 26, S_IFLNK | 0777); + raw = inode_test_raw_slot(ctx, 26, true); + raw->i_flags = 0; + raw->i_size_lo = cpu_to_le32(11); + memcpy(raw->i_block, "target/path", 11); + inode_test_stamp_raw_csum(ctx, 26, raw); + ig = ext4_iget(ctx->sb, 26, EXT4_IGET_NORMAL); + KUNIT_ASSERT_FALSE(test, IS_ERR(ig)); + KUNIT_EXPECT_EQ(test, ext4_inode_is_fast_symlink(ig), 1); + clear_nlink(ig); + ctx->free_inode_calls = 0; + ctx->total_freed_blocks = 0; + iput(ig); + KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 0UL); + KUNIT_EXPECT_EQ(test, ctx->free_inode_calls, 1U); +} + +/* ------------------------------------------------------------------------- + * 23. Buffered Write (ext4_da_aops & ext4_aops) and Folio Operations Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_buffered_write_and_folio_ops(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct address_space *mapping = inode->i_mapping; + struct folio *folio = NULL; + void *fsdata = NULL; + u8 *kaddr; + int ret; + + /* + * 1. Delayed-allocation buffered write to hole [0, 128): + * reserves 1 delalloc block and updates i_size while deferring + * i_disksize update until writeback. + */ + inode_lock(inode); + ret = mapping->a_ops->write_begin(NULL, mapping, 0, 128, &folio, + &fsdata); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_ASSERT_NOT_NULL(test, folio); + kaddr = kmap_local_folio(folio, 0); + memset(kaddr, 0xab, 128); + kunmap_local(kaddr); + ret = mapping->a_ops->write_end(NULL, mapping, 0, 128, 128, folio, + fsdata); + inode_unlock(inode); + KUNIT_EXPECT_EQ(test, ret, 128); + KUNIT_EXPECT_EQ(test, inode->i_size, 128LL); + KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 0LL); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U); + + /* + * 2. Delayed-allocation write into an already-written block past i_size + * (placed on folio index 1 regardless of PAGE_SIZE) updates both + * i_size and i_disksize immediately. + */ + { + loff_t wpos = max_t(loff_t, PAGE_SIZE, INODE_TEST_BLK_SIZE); + ext4_lblk_t wlblk = wpos >> INODE_TEST_BLK_BITS; + + inode_test_insert_leaf_extent(inode, wlblk, 1, + INODE_TEST_DATA_BLK0 + 1, false); + inode_lock(inode); + ret = mapping->a_ops->write_begin(NULL, mapping, wpos, 64, + &folio, &fsdata); + KUNIT_ASSERT_EQ(test, ret, 0); + kaddr = kmap_local_folio(folio, offset_in_folio(folio, wpos)); + memset(kaddr, 0xcd, 64); + kunmap_local(kaddr); + ret = mapping->a_ops->write_end(NULL, mapping, wpos, 64, 64, + folio, fsdata); + inode_unlock(inode); + KUNIT_EXPECT_EQ(test, ret, 64); + KUNIT_EXPECT_EQ(test, inode->i_size, wpos + 64); + KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, wpos + 64); + + /* Full folio invalidation detaches buffers cleanly */ + folio = filemap_lock_folio(mapping, wpos >> PAGE_SHIFT); + KUNIT_ASSERT_FALSE(test, IS_ERR(folio)); + KUNIT_EXPECT_NOT_NULL(test, folio_buffers(folio)); + mapping->a_ops->invalidate_folio(folio, 0, folio_size(folio)); + KUNIT_EXPECT_NULL(test, folio_buffers(folio)); + folio_unlock(folio); + folio_put(folio); + } + + /* 3. Non-delalloc buffered write (ext4_aops) allocates pblk directly */ + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + clear_opt(ctx->sb, DELALLOC); + ext4_set_aops(inode); + ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0; + ctx->alloc_calls = 0; + + inode_lock(inode); + ret = mapping->a_ops->write_begin(NULL, mapping, 0, 256, &folio, + &fsdata); + KUNIT_ASSERT_EQ(test, ret, 0); + kaddr = kmap_local_folio(folio, 0); + memset(kaddr, 0xef, 256); + kunmap_local(kaddr); + ret = mapping->a_ops->write_end(NULL, mapping, 0, 256, 256, folio, + fsdata); + inode_unlock(inode); + KUNIT_EXPECT_EQ(test, ret, 256); + KUNIT_EXPECT_EQ(test, ctx->alloc_calls, 1U); + KUNIT_EXPECT_EQ(test, inode->i_size, 256LL); + KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 256LL); + + /* 4. a_ops->bmap, partial invalidate_folio & release_folio */ + KUNIT_EXPECT_EQ(test, (ext4_fsblk_t)mapping->a_ops->bmap(mapping, 0), + (ext4_fsblk_t)INODE_TEST_DATA_BLK0); + KUNIT_EXPECT_EQ(test, (ext4_fsblk_t)mapping->a_ops->bmap(mapping, 5), + 0ULL); + + folio = filemap_lock_folio(mapping, 0); + KUNIT_ASSERT_FALSE(test, IS_ERR(folio)); + folio_set_checked(folio); + KUNIT_EXPECT_FALSE(test, mapping->a_ops->release_folio(folio, + GFP_KERNEL)); + folio_clear_checked(folio); + /* Dirty buffer still pins folio buffers */ + KUNIT_EXPECT_FALSE(test, mapping->a_ops->release_folio(folio, + GFP_KERNEL)); + /* Partial invalidation preserves buffer ring */ + mapping->a_ops->invalidate_folio(folio, 128, 128); + KUNIT_EXPECT_NOT_NULL(test, folio_buffers(folio)); + + clear_buffer_dirty(folio_buffers(folio)); + folio_clear_dirty_for_io(folio); + KUNIT_EXPECT_TRUE(test, mapping->a_ops->release_folio(folio, + GFP_KERNEL)); + KUNIT_EXPECT_NULL(test, folio_buffers(folio)); + folio_unlock(folio); + folio_put(folio); + set_opt(ctx->sb, DELALLOC); +} + +/* ------------------------------------------------------------------------- + * 24. ext4_setattr() Permission, Mode, Extend & Shrink Truncate Tests + * ------------------------------------------------------------------------- + */ + +static void inode_test_setattr(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct inode_test_blk *dblk0 = inode_test_find_blk(ctx, + INODE_TEST_DATA_BLK0); + struct dentry dentry = { .d_sb = ctx->sb, .d_inode = inode }; + struct iattr attr = {}; + struct folio *folio; + u8 *kaddr; + u64 iv_before; + int err; + + /* 1. S_IMMUTABLE and S_APPEND permission rejections */ + inode->i_flags |= S_IMMUTABLE; + attr = (struct iattr){ .ia_valid = ATTR_MODE, .ia_mode = S_IFREG | 0600 }; + inode_lock(inode); + KUNIT_EXPECT_EQ(test, ext4_setattr(&nop_mnt_idmap, &dentry, &attr), + -EPERM); + inode_unlock(inode); + inode->i_flags &= ~S_IMMUTABLE; + + inode->i_flags |= S_APPEND; + inode_lock(inode); + KUNIT_EXPECT_EQ(test, ext4_setattr(&nop_mnt_idmap, &dentry, &attr), + -EPERM); + inode_unlock(inode); + inode->i_flags &= ~S_APPEND; + + /* 2. Directory ATTR_SIZE -> -EINVAL; indirect > maxbytes -> -EFBIG */ + inode->i_mode = S_IFDIR | 0755; + attr = (struct iattr){ .ia_valid = ATTR_SIZE, .ia_size = 0 }; + inode_lock(inode); + KUNIT_EXPECT_EQ(test, ext4_setattr(&nop_mnt_idmap, &dentry, &attr), + -EINVAL); + inode_unlock(inode); + + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS); + memset(ctx->ei.i_data, 0, sizeof(ctx->ei.i_data)); + attr = (struct iattr){ + .ia_valid = ATTR_SIZE, + .ia_size = ctx->sbi.s_bitmap_maxbytes + 4096, + }; + inode_lock(inode); + KUNIT_EXPECT_EQ(test, ext4_setattr(&nop_mnt_idmap, &dentry, &attr), + -EFBIG); + inode_unlock(inode); + + /* 3. ATTR_MODE chmod updates mode & increments iversion */ + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + iv_before = inode_peek_iversion(inode); + attr = (struct iattr){ .ia_valid = ATTR_MODE, .ia_mode = S_IFREG | 0600 }; + inode_lock(inode); + err = ext4_setattr(&nop_mnt_idmap, &dentry, &attr); + inode_unlock(inode); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, inode->i_mode & 0777, 0600); + KUNIT_EXPECT_GT(test, inode_peek_iversion(inode), iv_before); + + /* + * 4. ATTR_SIZE extend (2000 -> 8192) zeroes EOF tail [2000, 4096) in + * mapped block 0 and advances i_size and i_disksize. + */ + inode->i_size = 2000; + ctx->ei.i_disksize = 2000; + inode->i_blocks = 16; + inode_test_insert_leaf_extent(inode, 0, 2, INODE_TEST_DATA_BLK0, false); + memset(dblk0->disk_buf, 0x77, INODE_TEST_BLK_SIZE); + + attr = (struct iattr){ .ia_valid = ATTR_SIZE, .ia_size = 8192 }; + inode_lock(inode); + err = ext4_setattr(&nop_mnt_idmap, &dentry, &attr); + inode_unlock(inode); + KUNIT_ASSERT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, inode->i_size, 8192LL); + KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 8192LL); + + folio = filemap_get_folio(inode->i_mapping, 0); + KUNIT_ASSERT_FALSE(test, IS_ERR(folio)); + kaddr = kmap_local_folio(folio, 0); + KUNIT_EXPECT_EQ(test, kaddr[1999], 0x77); + KUNIT_EXPECT_EQ(test, kaddr[2000], 0x00); + KUNIT_EXPECT_EQ(test, kaddr[4095], 0x00); + kunmap_local(kaddr); + folio_put(folio); + + /* 5. ATTR_SIZE shrink (8192 -> 4096) frees trailing block 1 */ + ctx->total_freed_blocks = 0; + attr = (struct iattr){ .ia_valid = ATTR_SIZE, .ia_size = 4096 }; + inode_lock(inode); + err = ext4_setattr(&nop_mnt_idmap, &dentry, &attr); + inode_unlock(inode); + KUNIT_EXPECT_EQ(test, err, 0); + KUNIT_EXPECT_EQ(test, inode->i_size, 4096LL); + KUNIT_EXPECT_EQ(test, ctx->ei.i_disksize, 4096LL); + KUNIT_EXPECT_EQ(test, ctx->total_freed_blocks, 1UL); +} + +/* ------------------------------------------------------------------------- + * 25. ext4_page_mkwrite() Delalloc, Block-Alloc, Fast-Path & Truncate Tests + * ------------------------------------------------------------------------- + */ + +static vm_fault_t inode_test_call_page_mkwrite(struct vm_area_struct *vma, + struct folio *folio) +{ + struct vm_fault vmf = { + .vma = vma, + .page = folio_page(folio, 0), + }; + + return ext4_page_mkwrite(&vmf); +} + +static void inode_test_page_mkwrite(struct kunit *test) +{ + struct inode_test_ctx *ctx = inode_test_setup(test); + struct inode *inode = &ctx->ei.vfs_inode; + struct file *dummy_file; + struct vm_area_struct *vma; + struct folio *folio; + vm_fault_t ret; + + dummy_file = kunit_kzalloc(test, sizeof(*dummy_file), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, dummy_file); + dummy_file->f_inode = inode; + dummy_file->f_mapping = inode->i_mapping; + dummy_file->f_mode = FMODE_NOCMTIME; + + vma = kunit_kzalloc(test, sizeof(*vma), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, vma); + vma->vm_file = dummy_file; + + inode->i_size = 4096; + ctx->ei.i_disksize = 4096; + folio = __filemap_get_folio(inode->i_mapping, 0, + FGP_CREAT | FGP_LOCK, GFP_KERNEL); + KUNIT_ASSERT_FALSE(test, IS_ERR(folio)); + folio_zero_segment(folio, 0, folio_size(folio)); + folio_mark_uptodate(folio); + folio_unlock(folio); + + /* 1. S_IMMUTABLE rejects write fault with VM_FAULT_SIGBUS */ + inode->i_flags |= S_IMMUTABLE; + KUNIT_EXPECT_EQ(test, inode_test_call_page_mkwrite(vma, folio), + (vm_fault_t)VM_FAULT_SIGBUS); + inode->i_flags &= ~S_IMMUTABLE; + + /* 2. Delalloc fault reserves 1 delayed block & returns LOCKED */ + ret = inode_test_call_page_mkwrite(vma, folio); + KUNIT_EXPECT_EQ(test, ret, (vm_fault_t)VM_FAULT_LOCKED); + if (ret & VM_FAULT_LOCKED) + folio_unlock(folio); + KUNIT_EXPECT_EQ(test, ctx->ei.i_reserved_data_blocks, 1U); + folio_put(folio); + + /* 3. Non-delalloc fault allocates pblk; repeat fault hits fast-path */ + inode_test_reset_mock_inode(ctx, S_IFREG | 0644); + clear_opt(ctx->sb, DELALLOC); + ext4_set_aops(inode); + inode->i_size = 4096; + ctx->ei.i_disksize = 4096; + ctx->next_alloc_pblk = INODE_TEST_DATA_BLK0; + ctx->alloc_calls = 0; + + folio = __filemap_get_folio(inode->i_mapping, 0, + FGP_CREAT | FGP_LOCK, GFP_KERNEL); + KUNIT_ASSERT_FALSE(test, IS_ERR(folio)); + folio_zero_segment(folio, 0, folio_size(folio)); + folio_mark_uptodate(folio); + folio_unlock(folio); + + ret = inode_test_call_page_mkwrite(vma, folio); + KUNIT_EXPECT_EQ(test, ret, (vm_fault_t)VM_FAULT_LOCKED); + if (ret & VM_FAULT_LOCKED) + folio_unlock(folio); + KUNIT_EXPECT_EQ(test, ctx->alloc_calls, 1U); + + /* Second fault sees all buffers mapped and skips journal_start/alloc */ + ret = inode_test_call_page_mkwrite(vma, folio); + KUNIT_EXPECT_EQ(test, ret, (vm_fault_t)VM_FAULT_LOCKED); + if (ret & VM_FAULT_LOCKED) + folio_unlock(folio); + KUNIT_EXPECT_EQ(test, ctx->alloc_calls, 1U); + + /* 4. Folio beyond truncated i_size returns VM_FAULT_NOPAGE */ + folio_put(folio); + folio = __filemap_get_folio(inode->i_mapping, 2, + FGP_CREAT | FGP_LOCK, GFP_KERNEL); + KUNIT_ASSERT_FALSE(test, IS_ERR(folio)); + folio_unlock(folio); + KUNIT_EXPECT_EQ(test, inode_test_call_page_mkwrite(vma, folio), + (vm_fault_t)VM_FAULT_NOPAGE); + folio_put(folio); + set_opt(ctx->sb, DELALLOC); +} + static struct kunit_case ext4_inode_test_cases[] = { KUNIT_CASE_PARAM(inode_test_xtimestamp_decoding, ext4_inode_gen_params), + KUNIT_CASE(inode_test_xtimestamp_encoding_roundtrip), + KUNIT_CASE(inode_test_csum_set_and_verify), + KUNIT_CASE(inode_test_blocks_encode_decode), + KUNIT_CASE(inode_test_fill_raw_inode), + KUNIT_CASE(inode_test_mark_dirty_and_update), + KUNIT_CASE(inode_test_expand_extra_isize), + KUNIT_CASE(inode_test_get_inode_loc_variants), + KUNIT_CASE(inode_test_iget_validation_and_modes), + KUNIT_CASE(inode_test_iget_corruptions), + KUNIT_CASE(inode_test_fast_symlink_and_can_truncate), + KUNIT_CASE(inode_test_flags_aops_mapping_order), + KUNIT_CASE(inode_test_projid_dio_and_getattr), + KUNIT_CASE(inode_test_da_reserve_and_release), + KUNIT_CASE(inode_test_da_bigalloc_clusters), + KUNIT_CASE(inode_test_map_blocks_paths), + KUNIT_CASE(inode_test_get_block_bread_and_walk_buffers), + KUNIT_CASE(inode_test_iomap_and_atomic_writes), + KUNIT_CASE(inode_test_trans_credits_and_disksize), + KUNIT_CASE(inode_test_zero_partial_and_eof), + KUNIT_CASE(inode_test_punch_hole_and_truncate), + KUNIT_CASE(inode_test_write_and_sync_metadata), + KUNIT_CASE(inode_test_evict_inode_paths), + KUNIT_CASE(inode_test_buffered_write_and_folio_ops), + KUNIT_CASE(inode_test_setattr), + KUNIT_CASE(inode_test_page_mkwrite), {} }; @@ -279,5 +3720,5 @@ static struct kunit_suite ext4_inode_test_suite = { kunit_test_suites(&ext4_inode_test_suite); -MODULE_DESCRIPTION("KUnit test of ext4 inode timestamp decoding"); +MODULE_DESCRIPTION("KUnit tests for ext4 inode operations"); MODULE_LICENSE("GPL v2"); diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 67d37b39db93..98444d8ddb40 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -124,6 +124,7 @@ void ext4_inode_csum_set(struct inode *inode, struct ext4_inode *raw, EXT4_FITS_IN_INODE(raw, ei, i_checksum_hi)) raw->i_checksum_hi = cpu_to_le16(csum >> 16); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_csum_set); static inline int ext4_begin_ordered_truncate(struct inode *inode, loff_t new_size) @@ -162,6 +163,7 @@ int ext4_inode_is_fast_symlink(struct inode *inode) return S_ISLNK(inode->i_mode) && inode->i_size && (inode->i_size < EXT4_N_BLOCKS * 4); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_is_fast_symlink); /* * Called at the last iput() if i_nlink is zero. @@ -332,6 +334,7 @@ void ext4_evict_inode(struct inode *inode) ext4_fc_mark_ineligible(inode->i_sb, EXT4_FC_REASON_NOMEM, NULL); ext4_clear_inode(inode); /* We must guarantee clearing of inode... */ } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_evict_inode); #ifdef CONFIG_QUOTA qsize_t *ext4_get_reserved_space(struct inode *inode) @@ -946,6 +949,7 @@ int ext4_get_block(struct inode *inode, sector_t iblock, return _ext4_get_block(inode, iblock, bh, create ? EXT4_GET_BLOCKS_CREATE : 0); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_block); /* * Get block function used when preparing for buffered write if we require @@ -972,10 +976,34 @@ int ext4_get_block_unwritten(struct inode *inode, sector_t iblock, return ret; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_block_unwritten); /* Maximum number of blocks we map for direct IO at once. */ #define DIO_MAX_BLOCKS 4096 +struct buffer_head *ext4_sb_getblk(struct super_block *sb, ext4_fsblk_t block) +{ + KUNIT_STATIC_STUB_REDIRECT(ext4_sb_getblk, sb, block); + return sb_getblk(sb, block); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_getblk); + +struct buffer_head *ext4_sb_find_get_block(struct super_block *sb, + ext4_fsblk_t block) +{ + KUNIT_STATIC_STUB_REDIRECT(ext4_sb_find_get_block, sb, block); + return sb_find_get_block(sb, block); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_find_get_block); + +struct buffer_head *ext4_sb_getblk_unmovable(struct super_block *sb, + ext4_fsblk_t block) +{ + KUNIT_STATIC_STUB_REDIRECT(ext4_sb_getblk_unmovable, sb, block); + return getblk_unmovable(sb->s_bdev, block, sb->s_blocksize); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_getblk_unmovable); + /* * `handle' can be NULL if create is zero */ @@ -1005,15 +1033,14 @@ struct buffer_head *ext4_getblk(handle_t *handle, struct inode *inode, return ERR_PTR(err); if (nowait) - return sb_find_get_block(inode->i_sb, map.m_pblk); + return ext4_sb_find_get_block(inode->i_sb, map.m_pblk); /* * Since bh could introduce extra ref count such as referred by * journal_head etc. Try to avoid using __GFP_MOVABLE here * as it may fail the migration when journal_head remains. */ - bh = getblk_unmovable(inode->i_sb->s_bdev, map.m_pblk, - inode->i_sb->s_blocksize); + bh = ext4_sb_getblk_unmovable(inode->i_sb, map.m_pblk); if (unlikely(!bh)) return ERR_PTR(-ENOMEM); @@ -1074,6 +1101,7 @@ struct buffer_head *ext4_bread(handle_t *handle, struct inode *inode, } return bh; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_bread); /* Read a contiguous batch of blocks. */ int ext4_bread_batch(struct inode *inode, ext4_lblk_t block, int bh_count, @@ -1117,6 +1145,7 @@ int ext4_bread_batch(struct inode *inode, ext4_lblk_t block, int bh_count, } return err; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_bread_batch); int ext4_walk_page_buffers(handle_t *handle, struct inode *inode, struct buffer_head *head, @@ -1148,6 +1177,7 @@ int ext4_walk_page_buffers(handle_t *handle, struct inode *inode, } return ret; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_walk_page_buffers); /* * Helper for handling dirtying of journalled data. We also mark the folio as @@ -3858,12 +3888,14 @@ int ext4_iomap_begin(struct inode *inode, loff_t offset, loff_t length, return 0; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_iomap_begin); static DEFINE_IOMAP_ITER_NEXT(ext4_iomap_next, ext4_iomap_begin); const struct iomap_ops ext4_iomap_ops = { .iomap_next = ext4_iomap_next, }; +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_iomap_ops); static int ext4_iomap_begin_report(struct inode *inode, loff_t offset, loff_t length, unsigned int flags, @@ -3921,6 +3953,7 @@ static DEFINE_IOMAP_ITER_NEXT(ext4_iomap_next_report, ext4_iomap_begin_report); const struct iomap_ops ext4_iomap_report_ops = { .iomap_next = ext4_iomap_next_report, }; +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_iomap_report_ops); /* * For data=journal mode, folio should be marked dirty only when it was @@ -4034,6 +4067,7 @@ void ext4_set_aops(struct inode *inode) else inode->i_mapping->a_ops = &ext4_aops; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_set_aops); /* * Here we can't skip an unwritten buffer even though it usually reads zero @@ -4273,6 +4307,7 @@ int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end) return 0; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_block_zero_eof); /* * Zero out the unaligned head and tail of the [lstart, lstart+length) @@ -4333,6 +4368,7 @@ int ext4_zero_partial_blocks(struct inode *inode, loff_t lstart, loff_t length, } return err; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_zero_partial_blocks); int ext4_can_truncate(struct inode *inode) { @@ -4344,6 +4380,7 @@ int ext4_can_truncate(struct inode *inode) return !ext4_inode_is_fast_symlink(inode); return 0; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_can_truncate); /* * We have to make sure i_disksize gets properly updated before we truncate @@ -4381,6 +4418,7 @@ int ext4_update_disksize_before_punch(struct inode *inode, loff_t offset, return ret; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_update_disksize_before_punch); static inline void ext4_truncate_folio(struct inode *inode, loff_t start, loff_t end) @@ -4443,6 +4481,7 @@ int ext4_truncate_page_cache_block_range(struct inode *inode, truncate_pagecache_range(inode, start, end - 1); return 0; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_truncate_page_cache_block_range); static void ext4_wait_dax_page(struct inode *inode) { @@ -4458,6 +4497,7 @@ int ext4_break_layouts(struct inode *inode) return dax_break_layout_inode(inode, ext4_wait_dax_page); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_break_layouts); /* * ext4_punch_hole: punches a hole in a file by releasing the blocks @@ -4578,6 +4618,7 @@ int ext4_punch_hole(struct file *file, loff_t offset, loff_t length) ext4_journal_stop(handle); return ret; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_punch_hole); int ext4_inode_attach_jinode(struct inode *inode) { @@ -4608,6 +4649,7 @@ int ext4_inode_attach_jinode(struct inode *inode) jbd2_free_inode(jinode); return 0; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_attach_jinode); /* * ext4_truncate() @@ -4742,6 +4784,7 @@ int ext4_truncate(struct inode *inode) trace_ext4_truncate_exit(inode); return err; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_truncate); static inline u64 ext4_inode_peek_iversion(const struct inode *inode) { @@ -4933,7 +4976,7 @@ static int __ext4_get_inode_loc(struct super_block *sb, unsigned long ino, } block += (inode_offset / inodes_per_block); - bh = sb_getblk(sb, block); + bh = ext4_sb_getblk(sb, block); if (unlikely(!bh)) return -ENOMEM; if (ext4_buffer_uptodate(bh)) @@ -4958,7 +5001,7 @@ static int __ext4_get_inode_loc(struct super_block *sb, unsigned long ino, start = inode_offset & ~(inodes_per_block - 1); /* Is the inode bitmap in cache? */ - bitmap_bh = sb_getblk(sb, ext4_inode_bitmap(sb, gdp)); + bitmap_bh = ext4_sb_getblk(sb, ext4_inode_bitmap(sb, gdp)); if (unlikely(!bitmap_bh)) goto make_io; @@ -5115,7 +5158,7 @@ int ext4_get_inode_loc_noio(struct inode *inode, struct ext4_iloc *iloc) } block += inode_offset / inodes_per_block; - bh = sb_find_get_block(sb, block); + bh = ext4_sb_find_get_block(sb, block); if (!bh) return -EAGAIN; if (!ext4_buffer_uptodate(bh)) { @@ -5194,6 +5237,7 @@ void ext4_set_inode_flags(struct inode *inode, bool init) S_SYNC|S_APPEND|S_IMMUTABLE|S_NOATIME|S_DIRSYNC|S_DAX| S_ENCRYPTED|S_CASEFOLD|S_VERITY); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_set_inode_flags); static blkcnt_t ext4_inode_blocks(struct ext4_inode *raw_inode, struct ext4_inode_info *ei) @@ -5250,6 +5294,7 @@ int ext4_get_projid(struct inode *inode, kprojid_t *projid) *projid = EXT4_I(inode)->i_projid; return 0; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_projid); /* * ext4 has self-managed i_version for ea inodes, it stores the lower 32bit of @@ -5321,6 +5366,7 @@ void ext4_set_inode_mapping_order(struct inode *inode) mapping_set_folio_order_range(inode->i_mapping, min_order, max_order); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_set_inode_mapping_order); static int ext4_iget_match(struct inode *inode, u64 ino, void *data) { @@ -5928,6 +5974,7 @@ int ext4_write_inode(struct inode *inode, struct writeback_control *wbc) return ext4_fc_commit(EXT4_SB(inode->i_sb)->s_journal, EXT4_I(inode)->i_sync_tid); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_write_inode); int ext4_sync_inode_metadata(struct inode *inode, struct writeback_control *wbc) { @@ -5958,6 +6005,7 @@ int ext4_sync_inode_metadata(struct inode *inode, struct writeback_control *wbc) brelse(iloc.bh); return err; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sync_inode_metadata); /* * In data=journal mode ext4_journalled_invalidate_folio() may fail to invalidate @@ -6274,6 +6322,7 @@ int ext4_setattr(struct mnt_idmap *idmap, struct dentry *dentry, error = rc; return error; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_setattr); u32 ext4_dio_alignment(struct inode *inode) { @@ -6287,6 +6336,7 @@ u32 ext4_dio_alignment(struct inode *inode) return i_blocksize(inode); return 1; /* use the iomap defaults */ } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dio_alignment); int ext4_getattr(struct mnt_idmap *idmap, const struct path *path, struct kstat *stat, u32 request_mask, unsigned int query_flags) @@ -6356,6 +6406,7 @@ int ext4_getattr(struct mnt_idmap *idmap, const struct path *path, generic_fillattr(idmap, request_mask, inode, stat); return 0; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_getattr); int ext4_file_getattr(struct mnt_idmap *idmap, const struct path *path, struct kstat *stat, @@ -6390,6 +6441,7 @@ int ext4_file_getattr(struct mnt_idmap *idmap, stat->blocks += delalloc_blocks << (inode->i_sb->s_blocksize_bits - 9); return 0; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_file_getattr); static int ext4_index_trans_blocks(struct inode *inode, int lblocks, int pextents) @@ -6442,6 +6494,7 @@ int ext4_meta_trans_blocks(struct inode *inode, int lblocks, int pextents, return ret; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_meta_trans_blocks); /* * Calculate the journal credits for modifying the number of blocks @@ -6459,6 +6512,7 @@ int ext4_chunk_trans_extent(struct inode *inode, int nrblocks) ret += nrblocks; return ret; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_chunk_trans_extent); /* * Calculate the journal credits for a chunk of data modification. @@ -6473,6 +6527,7 @@ int ext4_chunk_trans_blocks(struct inode *inode, int nrblocks) { return ext4_meta_trans_blocks(inode, nrblocks, 1, 1); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_chunk_trans_blocks); /* * The caller must have previously called ext4_reserve_inode_write(). @@ -6550,6 +6605,7 @@ ext4_reserve_inode_write(handle_t *handle, struct inode *inode, ext4_std_error(inode->i_sb, err); return err; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_reserve_inode_write); static int __ext4_expand_extra_isize(struct inode *inode, unsigned int new_extra_isize, @@ -6701,6 +6757,7 @@ int ext4_expand_extra_isize(struct inode *inode, ext4_journal_stop(handle); return error; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_expand_extra_isize); /* * What we do here is to mark the in-core inode as clean with respect to inode @@ -6767,6 +6824,7 @@ void ext4_dirty_inode(struct inode *inode, int flags) ext4_mark_inode_dirty(handle, inode); ext4_journal_stop(handle); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_dirty_inode); int ext4_change_inode_journal_flag(struct inode *inode, int val) { @@ -7004,8 +7062,87 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf) sb_end_pagefault(inode->i_sb); return ret; } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_page_mkwrite); #if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS) +int ext4_inode_csum_verify_test(struct inode *inode, struct ext4_inode *raw, + struct ext4_inode_info *ei) +{ + return ext4_inode_csum_verify(inode, raw, ei); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_csum_verify_test); + +int ext4_inode_blocks_set_test(struct ext4_inode *raw_inode, + struct ext4_inode_info *ei) +{ + return ext4_inode_blocks_set(raw_inode, ei); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_blocks_set_test); + +blkcnt_t ext4_inode_blocks_test(struct ext4_inode *raw_inode, + struct ext4_inode_info *ei) +{ + return ext4_inode_blocks(raw_inode, ei); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_blocks_test); + +int ext4_fill_raw_inode_test(struct inode *inode, struct ext4_inode *raw_inode) +{ + return ext4_fill_raw_inode(inode, raw_inode); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fill_raw_inode_test); + +int ext4_expand_extra_isize_test(struct inode *inode, + unsigned int new_extra_isize, + struct ext4_iloc *iloc, + handle_t *handle, int *no_expand) +{ + return __ext4_expand_extra_isize(inode, new_extra_isize, iloc, + handle, no_expand); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_expand_extra_isize_test); + +int ext4_try_to_expand_extra_isize_test(struct inode *inode, + unsigned int new_extra_isize, + struct ext4_iloc *iloc, + handle_t *handle) +{ + return ext4_try_to_expand_extra_isize(inode, new_extra_isize, + *iloc, handle); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_try_to_expand_extra_isize_test); + +int ext4_da_reserve_space_test(struct inode *inode, int nr_resv) +{ + return ext4_da_reserve_space(inode, nr_resv); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_reserve_space_test); + +int ext4_clu_alloc_state_test(struct inode *inode, ext4_lblk_t lblk) +{ + return ext4_clu_alloc_state(inode, lblk); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_clu_alloc_state_test); + +int ext4_insert_delayed_blocks_test(struct inode *inode, ext4_lblk_t lblk, + ext4_lblk_t len) +{ + return ext4_insert_delayed_blocks(inode, lblk, len); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_insert_delayed_blocks_test); + +int ext4_da_map_blocks_test(struct inode *inode, struct ext4_map_blocks *map) +{ + return ext4_da_map_blocks(inode, map); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_map_blocks_test); + +int ext4_nonda_switch_test(struct super_block *sb) +{ + return ext4_nonda_switch(sb); +} +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_nonda_switch_test); + EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_inode_loc_noio); EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_fc_inode_loc); EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_get_block_prep); diff --git a/fs/ext4/page-io.c b/fs/ext4/page-io.c index 0236b6b9785a..7298bc81f240 100644 --- a/fs/ext4/page-io.c +++ b/fs/ext4/page-io.c @@ -296,6 +296,7 @@ void ext4_end_io_rsv_work(struct work_struct *work) i_rsv_conversion_work); ext4_do_flush_completed_IO(&ei->vfs_inode, &ei->i_rsv_conversion_list); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_end_io_rsv_work); ext4_io_end_t *ext4_init_io_end(struct inode *inode, gfp_t flags) { diff --git a/fs/ext4/super.c b/fs/ext4/super.c index 62ec5323bb25..74604a677ca4 100644 --- a/fs/ext4/super.c +++ b/fs/ext4/super.c @@ -185,6 +185,9 @@ static inline void __ext4_read_bh(struct buffer_head *bh, blk_opf_t op_flags, void ext4_read_bh_nowait(struct buffer_head *bh, blk_opf_t op_flags, bio_end_io_t end_io, bool simu_fail) { + KUNIT_STATIC_STUB_REDIRECT(ext4_read_bh_nowait, bh, op_flags, + end_io, simu_fail); + BUG_ON(!buffer_locked(bh)); if (ext4_buffer_uptodate(bh)) { @@ -193,6 +196,7 @@ void ext4_read_bh_nowait(struct buffer_head *bh, blk_opf_t op_flags, } __ext4_read_bh(bh, op_flags, end_io, simu_fail); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_bh_nowait); int ext4_read_bh(struct buffer_head *bh, blk_opf_t op_flags, bio_end_io_t end_io, bool simu_fail) @@ -214,6 +218,8 @@ int ext4_read_bh(struct buffer_head *bh, blk_opf_t op_flags, int ext4_read_bh_lock(struct buffer_head *bh, blk_opf_t op_flags, bool wait) { + KUNIT_STATIC_STUB_REDIRECT(ext4_read_bh_lock, bh, op_flags, wait); + lock_buffer(bh); if (!wait) { ext4_read_bh_nowait(bh, op_flags, NULL, false); @@ -221,6 +227,7 @@ int ext4_read_bh_lock(struct buffer_head *bh, blk_opf_t op_flags, bool wait) } return ext4_read_bh(bh, op_flags, NULL, false); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_bh_lock); /* * This works like __bread_gfp() except it uses ERR_PTR for error @@ -282,8 +289,11 @@ struct buffer_head *ext4_sb_bread_nofail(struct super_block *sb, void ext4_sb_breadahead_unmovable(struct super_block *sb, sector_t block) { - struct buffer_head *bh = bdev_getblk(sb->s_bdev, block, - sb->s_blocksize, GFP_NOWAIT); + struct buffer_head *bh; + + KUNIT_STATIC_STUB_REDIRECT(ext4_sb_breadahead_unmovable, sb, block); + + bh = bdev_getblk(sb->s_bdev, block, sb->s_blocksize, GFP_NOWAIT); if (likely(bh)) { if (trylock_buffer(bh)) @@ -291,6 +301,7 @@ void ext4_sb_breadahead_unmovable(struct super_block *sb, sector_t block) brelse(bh); } } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_sb_breadahead_unmovable); static int ext4_verify_csum_type(struct super_block *sb, struct ext4_super_block *es) @@ -401,6 +412,7 @@ void ext4_inode_bitmap_set(struct super_block *sb, if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT) bg->bg_inode_bitmap_hi = cpu_to_le32(blk >> 32); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_bitmap_set); void ext4_inode_table_set(struct super_block *sb, struct ext4_group_desc *bg, ext4_fsblk_t blk) @@ -409,6 +421,7 @@ void ext4_inode_table_set(struct super_block *sb, if (EXT4_DESC_SIZE(sb) >= EXT4_MIN_DESC_SIZE_64BIT) bg->bg_inode_table_hi = cpu_to_le32(blk >> 32); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inode_table_set); void ext4_free_group_clusters_set(struct super_block *sb, struct ext4_group_desc *bg, __u32 count) @@ -1164,6 +1177,7 @@ void ext4_update_dynamic_rev(struct super_block *sb) * can leave it up to e2fsck to clean up any inconsistencies there. */ } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_update_dynamic_rev); static inline struct inode *orphan_list_entry(struct list_head *l) { @@ -1581,6 +1595,7 @@ void ext4_clear_inode(struct inode *inode) } fscrypt_put_encryption_info(inode); } +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_clear_inode); static struct inode *ext4_nfs_get_inode(struct super_block *sb, u64 ino, u32 generation) -- 2.56.0.542.g3ef89b7020-goog