From: sashiko-bot@kernel.org
To: "Theodore Ts'o" <tytso@mit.edu>,
"Ext4 Developers List" <linux-ext4@vger.kernel.org>
Subject: Re: [PATCH 10/27] ext4: expand KUnit tests for fs/ext4/inode.c
Date: Thu, 12 Nov 2026 04:36:05 +0000 [thread overview]
Message-ID: <sashiko-outbox-210367@kernel.org> (raw)
In-Reply-To: <20261112040638.1576836-11-tytso@mit.edu>
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] ext4: use-after-free of mock superblock and inodes in
inode_test_cleanup()
- [Medium] ext4: highmem NULL deref on bh->b_data in
inode_test_read_bh_lock_stub()
It also noticed 2 related pre-existing issues this patch does not
introduce:
- [Medium] ext4: user-triggerable WARN_ON_ONCE in ext4_iomap_begin() [1]
- [Critical] ext4: stale data leak to concurrent readers in
ext4_setattr() [2]
--
commit f093b56ce7ca8d901aefc501ae9f381a7877b64a
Author: Theodore Ts'o <tytso@mit.edu>
ext4: expand KUnit tests for fs/ext4/inode.c
This commit expands the ext4_inode_test KUnit test suite to exercise
fs/ext4/inode.c using an in-memory VFS superblock and mock block pool, and
adds KUnit static stub redirects and symbol exports for the tested entry
points.
> diff --git a/fs/ext4/inode-test.c b/fs/ext4/inode-test.c
> index 749af7ad4e09c..e8d19c1d42707 100644
> --- a/fs/ext4/inode-test.c
> +++ b/fs/ext4/inode-test.c
> @@ -267,8 +284,3432 @@ static void inode_test_xtimestamp_decoding(struct kunit *test)
[ ... ]
> +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;
> +}
[Severity: Medium]
Can copying directly to bh->b_data in inode_test_read_bh_lock_stub() and
inode_test_read_bh_nowait_stub() cause a NULL or low-address pointer
dereference on 32-bit CONFIG_HIGHMEM=y kernels?
inode_test_setup() configures inode->i_data with GFP_HIGHUSER_MOVABLE.
When inode_test_zero_partial_and_eof() or inode_test_setattr() calls
ext4_zero_partial_blocks() -> ext4_block_zero_range() -> ext4_load_tail_bh(),
the pagecache folio allocated for inode->i_mapping can come from highmem.
create_empty_buffers() -> folio_set_bh() sets bh->b_data on a highmem folio
to (char *)(0 + offset) rather than a kernel virtual address, so
memcpy(bh->b_data, ...) faults when ext4_load_tail_bh() calls
ext4_read_bh_lock(). Using memcpy_to_folio(bh->b_folio, bh_offset(bh), ...)
when bh->b_folio is present (or dropping __GFP_HIGHMEM from inode->i_data)
would avoid this.
[ ... ]
> +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;
[Severity: Medium]
Can setting SB_ACTIVE while leaving sb->s_root NULL cause a use-after-free
of ctx->sb, ctx->sbi, and inodes after inode_test_cleanup() runs?
sget_fc() -> alloc_super() calls fserror_mount(sb), which sets
sb->s_pending_errors to 1. When tests exercise corruption paths that call
__ext4_error() or __ext4_error_inode(), fserror_report() sees SB_ACTIVE and
s_pending_errors > 0, takes a reference on the inode via igrab(), and queues
fserror_worker() on system_dfl_wq. Because ctx->sb->s_root is NULL,
deactivate_super() -> generic_shutdown_super() skips fserror_unmount(sb):
CPU0 (KUnit test) CPU1 (kworker)
inode_test_iget_corruptions()
ext4_iget()
__ext4_error_inode()
fserror_report()
igrab(inode)
schedule_work(&event->work)
inode_test_cleanup()
deactivate_super(ctx->sb)
generic_shutdown_super()
/* s_root is NULL: skips
fserror_unmount() */
kfree(ctx)
fserror_worker()
/* accesses freed event->sb */
iput(event->inode)
ext4_evict_inode()
/* accesses freed ctx->sbi */
<-- use-after-free
In addition, when __ext4_error_inode() is called on the embedded
&ctx->ei.vfs_inode (for example in inode_test_expand_extra_isize()),
fserror_worker() races with inode_test_reset_mock_inode() and calls iput()
on the embedded inode after ctx is freed. Calling fserror_unmount(sb) in
inode_test_setup() (or before freeing ctx in inode_test_cleanup()) would
prevent fserror_report() from queuing async work.
> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 67d37b39db938..98444d8ddb400 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
[ ... ]
> @@ -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);
[Severity: Medium]
This problem wasn't introduced by this patch, but can an atomic write to an
encrypted file trigger WARN_ON_ONCE(1) in ext4_iomap_begin() [1]?
Before checking the IOMAP_ATOMIC length constraint, ext4_iomap_begin()
calls fscrypt_limit_io_blocks() to truncate map.m_len when an I/O crosses a
32-bit DUN boundary:
fs/ext4/inode.c:ext4_iomap_begin() {
...
map.m_len = fscrypt_limit_io_blocks(inode, map.m_lblk, map.m_len);
/*
* Before returning to iomap, let's ensure the allocated mapping
* covers the entire requested length for atomic writes.
*/
if (flags & IOMAP_ATOMIC) {
if (map.m_len < (length >> blkbits)) {
WARN_ON_ONCE(1);
return -EINVAL;
}
}
...
}
When an unprivileged user issues an atomic write across a DUN boundary on
an encrypted file, fscrypt_limit_io_blocks() shortens map.m_len below
length >> blkbits and triggers WARN_ON_ONCE(1).
[ ... ]
> @@ -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);
[Severity: Critical]
This problem wasn't introduced by this patch, but when extending a file via
ext4_setattr(), can updating i_size before calling
pagecache_isize_extended() expose stale post-EOF pagecache data to
concurrent readers [2]?
When extending a file whose old size is block-aligned (or where blocksize <
PAGE_SIZE) and whose tail folio already has uptodate post-EOF data in the
pagecache (such as from an earlier mmap write past EOF), ext4_setattr()
writes the new i_size under i_data_sem before stopping the journal handle
and zeroing the extended pagecache range:
CPU0 CPU1
ext4_setattr()
i_size_write(inode, attr->ia_size)
generic_file_read_iter()
filemap_read()
/* sees new i_size and copies
unzeroed tail folio <-- stale data */
ext4_journal_stop(handle)
pagecache_isize_extended()
Because buffered reads do not take i_rwsem, CPU1 can read the unzeroed
post-EOF portion of the folio before pagecache_isize_extended() zeroes it.
[1] https://sashiko.dev/#/bug/linux-38b43030-f0ce-4abf-8653-7266f768a948
[2] https://sashiko.dev/#/bug/linux-808e5d6d-ae12-4689-877c-3d6dc1f711d5
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112040638.1576836-1-tytso@mit.edu?part=10
next prev parent reply other threads:[~2026-11-12 4:36 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
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 [this message]
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-210367@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