All of lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Theodore Ts'o" <tytso@mit.edu>,
	"Ext4 Developers List" <linux-ext4@vger.kernel.org>
Subject: Re: [PATCH 07/27] ext4: add KUnit tests for fs/ext4/inline.c
Date: Thu, 12 Nov 2026 04:29:18 +0000	[thread overview]
Message-ID: <sashiko-outbox-210339@kernel.org> (raw)
In-Reply-To: <20261112040638.1576836-8-tytso@mit.edu>

Thank you for your contribution! Sashiko AI review found 4 potential
issues to consider:
- [Low] ext4: unexercised dioread_nolock branch in inline conversion
  KUnit test
- [Low] ext4: unexercised iversion revalidation in test_filldir_actor()
- [Low] ext4: inaccurate comment on ext4_inlinedir_to_tree() in
  inline-test.c
- [Low] ext4: missing i_size_ordered_init() on mock inode in
  inline_test_init()
It also noticed 2 related pre-existing issues this patch does not
introduce:
- [Critical] ext4: TOCTOU race condition in
  ext4_convert_inline_data_nolock() [1]
- [Critical] ext4: 32-bit i_size truncation in
  ext4_inline_data_truncate() [2]
--

commit 2474f0318caf555f2d6839372e8f1734082921e5
Author: Theodore Ts'o <tytso@mit.edu>

ext4: add KUnit tests for fs/ext4/inline.c

This commit adds a KUnit test suite in fs/ext4/inline-test.c to exercise
ext4 inline data operations for regular files, symlinks, and directories,
along with static stub hooks and symbol exports for the tested functions.

> diff --git a/fs/ext4/inline-test.c b/fs/ext4/inline-test.c
> new file mode 100644
> index 0000000000000..a75f46475c4a9
> --- /dev/null
> +++ b/fs/ext4/inline-test.c
[ ... ]
> +static int inline_test_init(struct kunit *test)
> +{
[ ... ]
> +	/* Initialize mock ext4_inode_info and embedded vfs_inode */
> +	inode = &ctx->ei.vfs_inode;
> +	inode->i_sb = &ctx->sb;
> +	inode->i_ino = TEST_INO;
> +	inode->i_blkbits = TEST_BLOCK_BITS;
> +	set_nlink(inode, 1);
> +	atomic_set(&inode->i_count, 1);
> +	spin_lock_init(&inode->i_lock);
> +	init_rwsem(&inode->i_rwsem);

[Severity: Low]
Does the mock inode initialization in inline_test_init() miss a call to
i_size_ordered_init(inode) (or inode_init_once())?

On 32-bit SMP kernels with CONFIG_DEBUG_LOCK_ALLOC enabled, struct inode
embeds seqcount_t i_size_seqcount.  Because ctx is allocated from the heap
via kunit_kzalloc(), calling i_size_write() or i_size_read() in
create_inline_file() and the tested inline helpers accesses an
uninitialized lockdep_map key on inode->i_size_seqcount, triggering a
lockdep warning and disabling lockdep.

[ ... ]

> +static void test_write_triggers_inline_to_extent_conversion(struct kunit *test)
> +{
[ ... ]
> +	/*
> +	 * 2. Conversion with DIOREAD_NOLOCK + DELALLOC enabled and journal_data
> +	 * mode (exercises ext4_should_dioread_nolock and ext4_should_journal_data
> +	 * branches in ext4_convert_inline_data_to_extent).
> +	 */
> +	reset_inode_state(ctx, S_IFREG | 0644);
> +	ret = create_inline_file(inode, buf, sizeof(buf));
> +	KUNIT_ASSERT_EQ(test, ret, 0);
> +
> +	set_opt(&ctx->sb, DIOREAD_NOLOCK);
> +	set_opt(&ctx->sb, DELALLOC);
> +	ctx->journal_mode = EXT4_INODE_JOURNAL_DATA_MODE;
> +
> +	inode_lock(inode);
> +	folio = NULL;
> +	ret = ext4_try_to_write_inline_data(&inode->i_data, inode, 0, 200,
> +					    &folio);
> +	inode_unlock(inode);

[Severity: Low]
Does setting ctx->journal_mode to EXT4_INODE_JOURNAL_DATA_MODE here prevent
the ext4_should_dioread_nolock() branch in
ext4_convert_inline_data_to_extent() from being exercised?

ext4_should_dioread_nolock() returns 0 whenever
ext4_should_journal_data(inode) is true:

fs/ext4/ext4_jbd2.h:ext4_should_dioread_nolock() {
    ...
	if (ext4_should_journal_data(inode))
		return 0;
    ...
}

Testing the DIOREAD_NOLOCK + DELALLOC unwritten-extent conversion path
requires a separate call with a non-journaled data mode such as
EXT4_INODE_WRITEBACK_DATA_MODE.

[ ... ]

> +static bool test_filldir_actor(struct dir_context *dctx, const char *name,
> +			       int namelen, loff_t offset, u64 ino,
> +			       unsigned int d_type)
> +{
> +	struct test_dir_ctx *tctx =
> +		container_of(dctx, struct test_dir_ctx, ctx);
> +
> +	if (tctx->max_emit > 0 && tctx->count >= tctx->max_emit)
> +		return false;

[ ... ]

> +	/* Mutating inode iversion during iteration triggers offset revalidation */
> +	memset(&env->dctx, 0, sizeof(env->dctx));
> +	env->dctx.ctx.actor = test_filldir_actor;
> +	env->dctx.ctx.pos = 0;
> +	env->dctx.mutate_inode_on_second = dir;
> +	ret = ext4_read_inline_dir(&env->filp, &env->dctx.ctx,
> +				   &has_inline_data);
> +	KUNIT_EXPECT_EQ(test, ret, 0);
> +	KUNIT_EXPECT_EQ(test, env->dctx.count, 6);

[Severity: Low]
Does this subtest ever exercise partial iteration or the iversion
revalidation loop in ext4_read_inline_dir()?

tctx->max_emit is never set to a non-zero value anywhere in the file, and
ext4_read_inline_dir() is only called once starting from ctx.pos = 0.
When test_filldir_actor() increments dir's iversion on the second entry,
ext4_read_inline_dir() has already passed the
!inode_eq_iversion(inode, info->cookie) check at the top of the function
and continues emitting all 6 entries in the same call without ever
re-entering at a non-zero ctx.pos.

[ ... ]

> +static void test_inline_dir_conversion_and_corruptions(struct kunit *test)
> +{
[ ... ]
> +	/*
> +	 * 4. Corrupted inline directory checks:
> +	 *   4a. Parent ino ("..") == 0 -> empty_inline_dir returns false.
> +	 *   4b. Zero rec_len dirent -> empty_inline_dir returns false,
> +	 *       ext4_inlinedir_to_tree returns -EFSCORRUPTED, and
> +	 *       ext4_convert_inline_data_nolock aborts via ext4_check_all_de.
> +	 */
> +	reset_inode_state(ctx, S_IFDIR | 0755);
> +	handle = ext4_journal_start(dir, EXT4_HT_DIR, 1);
> +	KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
> +	ret = ext4_try_create_inline_dir(handle, &env->parent, dir);
> +	ext4_journal_stop(handle);
> +	KUNIT_ASSERT_EQ(test, ret, 0);
> +
> +	/* Corrupt parent ino to 0 */
> +	*(__le32 *)raw_inline_i_block(ctx) = cpu_to_le32(0);
> +	KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
> +	*(__le32 *)raw_inline_i_block(ctx) = cpu_to_le32(TEST_PARENT_INO);
> +
> +	/* Corrupt first dirent rec_len to 0 */
> +	de = (struct ext4_dir_entry_2 *)((char *)raw_inline_i_block(ctx) +
> +					 EXT4_INLINE_DOTDOT_SIZE);
> +	de->rec_len = cpu_to_le16(0);
> +	KUNIT_EXPECT_FALSE(test, empty_inline_dir(dir, &has_inline_data));
> +
> +	/* ext4_convert_inline_data on corrupted inline dir fails in ext4_check_all_de */
> +	ret = ext4_convert_inline_data(dir);
> +	KUNIT_EXPECT_EQ(test, ret, -EFSCORRUPTED);
> +	KUNIT_EXPECT_TRUE(test, ext4_has_inline_data(dir));
> +}

[Severity: Low]
This isn't a bug, but comment 4b states that a zero rec_len dirent causes
ext4_inlinedir_to_tree() to return -EFSCORRUPTED, whereas the test does
not call ext4_inlinedir_to_tree() here and ext4_inlinedir_to_tree() breaks
out of its loop and returns the count of already-processed entries (2 for
"." and "..") when ext4_check_dir_entry() fails.

> diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
> index 0e662a9f7574c..97cc2c61ce25d 100644
> --- a/fs/ext4/inline.c
> +++ b/fs/ext4/inline.c
[ ... ]
> @@ -1144,7 +1152,7 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
>  		goto out_restore;
>  	}
>  
> -	data_bh = sb_getblk(inode->i_sb, map.m_pblk);
> +	data_bh = ext4_inline_getblk(inode->i_sb, map.m_pblk);
>  	if (!data_bh) {
>  		error = -ENOMEM;
>  		goto out_restore;

[Severity: Critical]
This problem wasn't introduced by this patch, but can unlocking data_bh
before ext4_init_dirblock() in ext4_convert_inline_data_nolock() expose an
uninitialized directory block to concurrent readers [1]?

CPU0                                    CPU1
ext4_convert_inline_data_nolock()
  ext4_destroy_inline_data_nolock()
  ext4_map_blocks()
  data_bh = ext4_inline_getblk()
  lock_buffer(data_bh)
  memset(data_bh->b_data, 0, ...)
  unlock_buffer(data_bh)
  i_size_write(inode, s_blocksize)
                                        ext4_get_parent()
                                          ext4_find_entry()
                                            ...
                                              ext4_dirblock_csum_verify()
                                                <-- fails on zeroed block
  ext4_init_dirblock(data_bh)
    set_buffer_uptodate(data_bh)
    set_buffer_verified(data_bh)
    ext4_handle_dirty_dirblock()

In the S_ISDIR branch of ext4_convert_inline_data_nolock(), data_bh is
zeroed, unlocked, and published via i_size_write() before
ext4_init_dirblock() populates the directory entries and computes the
block checksum:

fs/ext4/inline.c:ext4_convert_inline_data_nolock() {
    ...
	} else {
		unlock_buffer(data_bh);
		inode->i_size = inode->i_sb->s_blocksize;
		i_size_write(inode, inode->i_sb->s_blocksize);
		EXT4_I(inode)->i_disksize = inode->i_sb->s_blocksize;

		error = ext4_init_dirblock(handle, inode, data_bh,
			  le32_to_cpu(((struct ext4_dir_entry_2 *)buf)->inode),
			  buf + EXT4_INLINE_DOTDOT_SIZE,
			  inline_size - EXT4_INLINE_DOTDOT_SIZE);
    ...
}

In addition, ext4_init_dirblock() sets buffer_uptodate and buffer_verified
before ext4_handle_dirty_dirblock() calculates the checksum.  A concurrent
reader (such as NFS export lookup via ext4_get_parent()) can read the
partially initialized or unchecksummed block and fail
ext4_dirblock_csum_verify(), triggering an ext4 filesystem error (and a
read-only remount or panic).

> @@ -2018,5 +2026,25 @@ int ext4_convert_inline_data(struct inode *inode)
>  }
>  
>  #if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
[ ... ]
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_inline_data_truncate);
> +EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_convert_inline_data);
>  #endif

[Severity: Critical]
This problem wasn't introduced by this patch, but can
ext4_inline_data_truncate() truncate the 64-bit inode->i_size into a
32-bit size_t on 32-bit architectures [2]?

fs/ext4/inline.c:ext4_inline_data_truncate() {
    ...
	size_t i_size;
    ...
	down_write(&EXT4_I(inode)->i_data_sem);
	i_size = inode->i_size;
	inline_size = ext4_get_inline_size(inode);
	EXT4_I(inode)->i_disksize = i_size;

	if (i_size < inline_size) {
		if (!ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA)) {
			ext4_lblk_t lblk = EXT4_B_TO_LBLK(inode, i_size);

			ext4_es_remove_extent(inode, lblk,
					      EXT_MAX_BLOCKS - lblk);
		}
    ...
}

When an inline file is converted to delayed-allocation extents via a
buffered write at an offset of 4 GiB or greater, EXT4_STATE_MAY_INLINE_DATA
is cleared while EXT4_INODE_INLINE_DATA remains set until writeback.  If a
subsequent truncate or ext4_truncate_failed_write() calls
ext4_inline_data_truncate() before writeback on a 32-bit system, assigning
inode->i_size to the 32-bit i_size local variable discards the upper 32
bits, corrupting EXT4_I(inode)->i_disksize.  When the lower 32 bits are
smaller than inline_size, it also enters the i_size < inline_size branch,
removing live delayed extents from the extent status tree and zeroing the
inline data buffer.

[1] https://sashiko.dev/#/bug/linux-48900fa2-4764-4787-990d-c127e7cf2417
[2] https://sashiko.dev/#/bug/linux-d1c56a9e-62e9-4eeb-addd-143c371dbdca

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=7

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

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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-210339@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=linux-ext4@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=tytso@mit.edu \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.