From: sashiko-bot@kernel.org
To: "Theodore Tso" <tytso@mit.edu>,
"Ext4 Developers List" <linux-ext4@vger.kernel.org>
Subject: Re: [PATCH -RFC] ext4: add KUinit tests for fs/ext4/extents_status.c
Date: Tue, 20 Oct 2026 08:17:05 +0000 [thread overview]
Message-ID: <sashiko-outbox-178509@kernel.org> (raw)
In-Reply-To: <atbPbe-i8DV3kLr8@mit.edu>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] ext4: use-after-free in es_cleanup_inode() due to KUnit teardown order
- [Medium] ext4: lock leak and sleep-in-atomic in assert_es_tree() under i_es_lock
- [Medium] ext4: extents_status-test: unlock sb->s_umount before kunit_activate_static_stub()
--
commit 68a97fa816f24b4b23fae3c4dae5fc1c10033a0d
Author: Theodore Tso <tytso@mit.edu>
ext4: add KUinit tests for fs/ext4/extents_status.c
This commit adds KUnit tests for the ext4 extents status tree and bigalloc
pending reservations, along with static stub redirects and symbol exports
needed by the test module.
> diff --git a/fs/ext4/extents_status-test.c b/fs/ext4/extents_status-test.c
> new file mode 100644
> index 0000000000000..65376666a362f
> --- /dev/null
> +++ b/fs/ext4/extents_status-test.c
> @@ -0,0 +1,1509 @@
[ ... ]
> +static void es_cleanup_inode(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;
> +
> + sbi->s_mount_state &= ~EXT4_FC_REPLAY;
> + 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));
> + }
> +}
> +
> +KUNIT_DEFINE_ACTION_WRAPPER(es_cleanup_inode_action, es_cleanup_inode,
> + struct inode *);
> +
> +static struct inode *es_alloc_test_inode(struct kunit *test,
> + struct super_block *sb,
> + unsigned long ino)
> +{
> + struct ext4_inode_info *ei;
> + int ret;
> +
> + ei = kunit_kzalloc(test, sizeof(*ei), GFP_KERNEL);
> + KUNIT_ASSERT_NOT_NULL(test, ei);
> + es_init_mock_inode(ei, sb, ino);
> +
> + ret = kunit_add_action_or_reset(test, es_cleanup_inode_action,
> + &ei->vfs_inode);
> + KUNIT_ASSERT_EQ(test, ret, 0);
> +
> + return &ei->vfs_inode;
> +}
[ ... ]
> +static int es_kunit_init(struct kunit *test)
> +{
[ ... ]
> + es_init_mock_inode(ctx->ei, sb, 12);
> + ctx->sb = sb;
> + ctx->sbi = sbi;
> + ctx->shrinker_registered = true;
> + test->priv = ctx;
> +
> + kunit_activate_static_stub(test, ext4_da_update_reserve_space,
> + ext4_da_update_reserve_space_stub);
> + kunit_activate_static_stub(test, ext4_da_release_space,
> + ext4_da_release_space_stub);
> +
> + up_write(&sb->s_umount);
> + return 0;
> +}
[Severity: Medium]
Should up_write(&sb->s_umount) happen before kunit_activate_static_stub()?
sget_fc() returns with sb->s_umount held write-locked, and test->priv = ctx is
assigned before calling kunit_activate_static_stub(). Inside
__kunit_activate_static_stub(), an allocation failure triggers
KUNIT_ASSERT_NOT_ERR_OR_NULL(), which aborts the init kthread via
kunit_try_catch_throw() before up_write(&sb->s_umount) is reached.
When KUnit then runs es_kunit_exit() in the cleanup kthread,
deactivate_super(ctx->sb) -> __super_lock_excl() will attempt to acquire
sb->s_umount and deadlock.
> +
> +static void es_kunit_exit(struct kunit *test)
> +{
> + struct es_test_ctx *ctx = test->priv;
> +
> + if (!ctx)
> + return;
> +
> + if (ctx->ei && ctx->sb)
> + es_cleanup_inode(&ctx->ei->vfs_inode);
> + if (ctx->shrinker_registered)
> + ext4_es_unregister_shrinker(ctx->sbi);
> + if (ctx->sb)
> + deactivate_super(ctx->sb);
> +}
[Severity: High]
Can registering es_cleanup_inode_action() via kunit_add_action_or_reset() in
es_alloc_test_inode() cause a use-after-free when KUnit tears down the test?
In lib/kunit/test.c, kunit_run_case_cleanup() runs suite->exit(test) before
unwinding test resources via kunit_case_internal_cleanup():
kunit_run_case_cleanup()
suite->exit(test) -> es_kunit_exit()
ext4_es_unregister_shrinker(ctx->sbi)
deactivate_super(ctx->sb)
kunit_case_internal_cleanup(test)
kunit_cleanup(test)
es_cleanup_inode_action(inode2)
es_cleanup_inode(inode2)
When test_es_shrinker_skips_precached_inode_until_last_resort() allocates
inode2 with es_alloc_test_inode(), es_kunit_exit() destroys the s_es_stats
per-CPU counters via ext4_es_unregister_shrinker() and drops the superblock
reference via deactivate_super(ctx->sb) before es_cleanup_inode(inode2) runs.
es_cleanup_inode(inode2) and ext4_es_remove_extent() then dereference
inode->i_sb after deactivate_super(ctx->sb). If inode2 still has any extents
in i_es_tree (for example if an earlier assertion fails before inode2 is
shrunk), ext4_es_remove_extent() -> ext4_es_free_extent() will also call
percpu_counter_dec() on the already-destroyed s_es_stats counters where
fbc->counters is NULL.
> +
> +static void assert_es_tree(struct kunit *test, struct inode *inode,
> + const struct expected_es *exp, unsigned int nr_exp)
> +{
> + struct ext4_inode_info *ei = EXT4_I(inode);
> + struct rb_node *node;
> + unsigned int i = 0;
> + ext4_lblk_t prev_end = 0;
> +
> + read_lock(&ei->i_es_lock);
> + for (node = rb_first(&ei->i_es_tree.root); node; node = rb_next(node)) {
> + struct extent_status *es =
> + rb_entry(node, struct extent_status, rb_node);
> +
> + KUNIT_ASSERT_LT_MSG(test, i, nr_exp,
> + "unexpected extra extent [%u/%u) type 0x%x",
> + es->es_lblk, es->es_len, ext4_es_type(es));
> +
> + if (i > 0)
> + KUNIT_EXPECT_GT_MSG(test, es->es_lblk, prev_end,
> + "extent %u not strictly ordered",
> + i);
> +
> + KUNIT_EXPECT_EQ_MSG(test, es->es_lblk, exp[i].lblk,
> + "extent %u lblk mismatch", i);
> + KUNIT_EXPECT_EQ_MSG(test, es->es_len, exp[i].len,
> + "extent %u len mismatch", i);
> + KUNIT_EXPECT_EQ_MSG(test, ext4_es_type(es), exp[i].type,
> + "extent %u type mismatch", i);
> +
> + if (exp[i].type & (EXTENT_STATUS_WRITTEN |
> + EXTENT_STATUS_UNWRITTEN))
> + KUNIT_EXPECT_EQ_MSG(test, ext4_es_pblock(es),
> + exp[i].pblk,
> + "extent %u pblk mismatch", i);
> +
> + prev_end = es->es_lblk + es->es_len - 1;
> + i++;
> + }
> + read_unlock(&ei->i_es_lock);
> +
> + KUNIT_EXPECT_EQ(test, i, nr_exp);
> + KUNIT_EXPECT_EQ(test, ei->i_es_all_nr, nr_exp);
> +}
[Severity: Medium]
Is it safe to invoke KUNIT_ASSERT_LT_MSG() and KUNIT_EXPECT_*_MSG() while
holding read_lock(&ei->i_es_lock)?
When a KUnit expectation or assertion fails, __kunit_do_failed_assertion() ->
kunit_fail() calls kunit_alloc_string_stream(test, GFP_KERNEL), which performs
a sleeping allocation under the ei->i_es_lock rwlock.
In addition, if KUNIT_ASSERT_LT_MSG() fails, __kunit_abort() exits the test
kthread immediately without calling read_unlock(&ei->i_es_lock). During
teardown, es_kunit_exit() -> es_cleanup_inode() -> ext4_es_remove_extent()
will then call write_lock(&EXT4_I(inode)->i_es_lock) and deadlock on the
leaked read lock.
[ ... ]
> +static void
> +test_es_shrinker_skips_precached_inode_until_last_resort(struct kunit *test)
> +{
> + struct es_test_ctx *ctx = test->priv;
> + struct inode *inode1 = &ctx->ei->vfs_inode;
> + struct inode *inode2;
> + struct shrinker *shrink = ctx->sbi->s_es_shrinker;
> + struct shrink_control sc;
> + unsigned long shrunk;
> +
> + inode2 = es_alloc_test_inode(test, ctx->sb, 13);
[ ... ]
> +}
[ ... ]
> --
> 2.51.2
>
--
Sashiko AI review · https://sashiko.dev/#/patchset/atbPbe-i8DV3kLr8@mit.edu?part=1
next prev parent reply other threads:[~2026-10-20 8:17 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-20 2:30 [PATCH -RFC] ext4: add KUinit tests for fs/ext4/extents_status.c Theodore Tso
2026-10-20 3:01 ` Theodore Tso
2026-10-20 8:17 ` sashiko-bot [this message]
2026-10-21 3:12 ` [PATCH] " Theodore Ts'o
2026-10-21 3:21 ` 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-178509@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox