From: Zhang Yi <yi.zhang@huaweicloud.com>
To: Hengyu Liang <hengyul@cs.unc.edu>
Cc: linux-ext4@vger.kernel.org, tytso@mit.edu,
adilger.kernel@dilger.ca, libaokun@linux.alibaba.com,
jack@suse.cz, ojaswin@linux.ibm.com, ritesh.list@gmail.com,
yi.zhang@huaweicloud.com, yizhang089@gmail.com,
dave.hansen@linux.intel.com, akpm@linux-foundation.org,
linux-kernel@vger.kernel.org, stable@vger.kernel.org
Subject: Re: [PATCH v3] ext4: do not advance i_size on a buffered write that copied nothing
Date: Sat, 31 Oct 2026 16:22:48 +0800 [thread overview]
Message-ID: <1828cae6-692e-4fe7-a5da-bdfea4ea90e7@huaweicloud.com> (raw)
In-Reply-To: <20261030040520.4138904-1-hengyul@cs.unc.edu>
On 10/30/2026 12:05 PM, Hengyu Liang wrote:
> Commit 665575cff098 ("filemap: move prefaulting out of hot write path")
> made generic_perform_write() call ->write_begin() and ->write_end()
> before it finds out that the source buffer cannot be read. In that
> case ->write_end() is called with copied == 0.
>
> However, ext4_write_end(), ext4_journalled_write_end() and
> ext4_da_do_write_end() set i_size to pos + copied without checking
> copied. As of now, a buffered write() past EOF that fails with EFAULT
> grows the file to the write offset, and the new size can reach the
> disk.
>
> The issue can be reproduced on an ext4 mount with:
>
> char *p = mmap(NULL, 4096, PROT_NONE,
> MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> int fd = open("/mnt/ext4/file", O_CREAT | O_TRUNC | O_RDWR, 0600);
> pwrite(fd, p, 4096, 1024 * 1024); /* fails with EFAULT */
> fstat(fd, &st);
>
> Before that commit, the result is st_size = 0.
> After it, the result is st_size = 1048576.
>
> This patch will leave i_size alone when nothing was copied, as
> ext4_write_inline_data_end() already does. Unlike ext4_write_end(),
> the delalloc path does not trim after a short write, so the folio and
> the delayed blocks that ->write_begin() set up beyond i_size are left
> behind, and the blocks stay reserved until the inode is evicted. When
> the write ends beyond i_size, drop them as well.
>
> Fixes: 665575cff098 ("filemap: move prefaulting out of hot write path")
> Cc: stable@vger.kernel.org
> Signed-off-by: Hengyu Liang <hengyul@cs.unc.edu>
Thank you for the update! This looks good to me.
Reviewed-by: Zhang Yi <yi.zhang@huawei.com>
Please note that I went through the three scenarios reported by sashiko.
I think the first one is a false positive, and I had already sent a
patch to fix the third one [1].
The second scenario, however, is indeed a real issue. But I think it is
a pre-existing problem and needs a separate fix, because there is
another way to end up with a half-converted inline inode passing
through. That is, a truncate to 0 can catch up in the middle between
ext4_da_convert_inline_data_to_extent() starting the conversion and
ext4_do_writepages() completing it. I'm not sure whether this outcome is
expected, though, since apart from the subsequent ext4_write_begin()
path, I don't see any other damage.
Perhaps we should, in ext4_write_begin(), after the
EXT4_STATE_MAY_INLINE_DATA snippet, also check whether there is still
inline data, and if so call something like ext4_convert_inline_data() to
let it finish the conversion before copying data.
[1] https://lore.kernel.org/linux-ext4/20261030021338.1768292-3-yi.zhang@huaweicloud.com/
Thanks,
Yi.
> ---
> Changes in v3:
> - Also trim after a short copy: drop the '!copied' check and compare
> pos + len with i_size after the update, as ext4_write_end() does
> (Zhang Yi).
> - Reword the last paragraph of the commit message.
> - v2: https://lore.kernel.org/r/20261023172532.3271348-1-hengyul@cs.unc.edu
>
> Tested in KVM with KASAN: the reproducer above, failed writes around
> EOF, and short copies on delalloc, nodelalloc and 1k block filesystems.
> e2fsck -fn reports no errors.
>
> fs/ext4/inode.c | 39 ++++++++++++++++++++++++++++++++-------
> 1 file changed, 32 insertions(+), 7 deletions(-)
>
> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 6ede3644a738..9c20876629bc 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
> @@ -1578,15 +1578,18 @@ static int ext4_write_end(const struct kiocb *iocb,
> * it's important to update i_size while still holding folio lock:
> * page writeout could otherwise come in and zero beyond i_size.
> *
> + * If nothing was copied (e.g. the source buffer was unreadable), do
> + * not move i_size up to pos.
> + *
> * If FS_IOC_ENABLE_VERITY is running on this inode, then Merkle tree
> * blocks are being written past EOF, so skip the i_size update.
> */
> - if (!verity)
> + if (copied && !verity)
> i_size_changed = ext4_update_inode_size(inode, pos + copied);
> folio_unlock(folio);
> folio_put(folio);
>
> - if (old_size < pos && !verity)
> + if (copied && old_size < pos && !verity)
> pagecache_isize_extended(inode, old_size, pos);
>
> /*
> @@ -1697,13 +1700,13 @@ static int ext4_journalled_write_end(const struct kiocb *iocb,
> if (!partial)
> folio_mark_uptodate(folio);
> }
> - if (!verity)
> + if (copied && !verity)
> size_changed = ext4_update_inode_size(inode, pos + copied);
> EXT4_I(inode)->i_datasync_tid = handle->h_transaction->t_tid;
> folio_unlock(folio);
> folio_put(folio);
>
> - if (old_size < pos && !verity)
> + if (copied && old_size < pos && !verity)
> pagecache_isize_extended(inode, old_size, pos);
>
> if (size_changed) {
> @@ -3349,6 +3352,24 @@ static int ext4_da_should_update_i_disksize(struct folio *folio,
> return 1;
> }
>
> +/*
> + * Delalloc counterpart of ext4_truncate_failed_write(): ->write_begin() only
> + * reserved delayed blocks, so drop the page cache and the delayed extents
> + * beyond i_size and leave the blocks that are allocated there alone.
> + */
> +static void ext4_da_truncate_failed_write(struct inode *inode)
> +{
> + struct address_space *mapping = inode->i_mapping;
> + ext4_lblk_t lblk = EXT4_B_TO_LBLK(inode, inode->i_size);
> +
> + filemap_invalidate_lock(mapping);
> + truncate_inode_pages(mapping, inode->i_size);
> + down_write(&EXT4_I(inode)->i_data_sem);
> + ext4_es_remove_extent(inode, lblk, EXT_MAX_BLOCKS - lblk);
> + up_write(&EXT4_I(inode)->i_data_sem);
> + filemap_invalidate_unlock(mapping);
> +}
> +
> static int ext4_da_do_write_end(struct address_space *mapping,
> loff_t pos, unsigned len, unsigned copied,
> struct folio *folio)
> @@ -3386,12 +3407,12 @@ static int ext4_da_do_write_end(struct address_space *mapping,
> * checked, we need to update i_disksize here as certain
> * ext4_writepages() paths not allocating blocks and update i_disksize.
> */
> - if (new_i_size > inode->i_size) {
> + if (copied && new_i_size > inode->i_size) {
> unsigned long end;
>
> i_size_write(inode, new_i_size);
> end = offset_in_folio(folio, new_i_size - 1);
> - if (copied && ext4_da_should_update_i_disksize(folio, end)) {
> + if (ext4_da_should_update_i_disksize(folio, end)) {
> ext4_update_i_disksize(inode, new_i_size);
> disksize_changed = true;
> }
> @@ -3400,9 +3421,13 @@ static int ext4_da_do_write_end(struct address_space *mapping,
> folio_unlock(folio);
> folio_put(folio);
>
> - if (pos > old_size)
> + if (copied && pos > old_size)
> pagecache_isize_extended(inode, old_size, pos);
>
> + /* Drop what ->write_begin() prepared beyond the resulting i_size. */
> + if (pos + len > inode->i_size)
> + ext4_da_truncate_failed_write(inode);
> +
> if (!disksize_changed)
> return copied;
>
>
> base-commit: 8c4f6e3376348fc3162eed86a05b933921c8f9ed
prev parent reply other threads:[~2026-10-31 8:23 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-30 4:05 Hengyu Liang
2026-10-31 8:22 ` Zhang Yi [this message]
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=1828cae6-692e-4fe7-a5da-bdfea4ea90e7@huaweicloud.com \
--to=yi.zhang@huaweicloud.com \
--cc=adilger.kernel@dilger.ca \
--cc=akpm@linux-foundation.org \
--cc=dave.hansen@linux.intel.com \
--cc=hengyul@cs.unc.edu \
--cc=jack@suse.cz \
--cc=libaokun@linux.alibaba.com \
--cc=linux-ext4@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=ojaswin@linux.ibm.com \
--cc=ritesh.list@gmail.com \
--cc=stable@vger.kernel.org \
--cc=tytso@mit.edu \
--cc=yizhang089@gmail.com \
/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
all inboxes | Powered by JetHome®