mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Zhang Yi <yizhang089@gmail.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@huawei.com, yi.zhang@huaweicloud.com,
	dave.hansen@linux.intel.com, akpm@linux-foundation.org,
	linux-kernel@vger.kernel.org, stable@vger.kernel.org
Subject: Re: [PATCH v2] ext4: do not advance i_size on a buffered write that copied nothing
Date: Sat, 24 Oct 2026 15:12:35 +0800	[thread overview]
Message-ID: <abcaabca-d99e-4bf2-b31b-5e96e7accd29@gmail.com> (raw)
In-Reply-To: <20261023172532.3271348-1-hengyul@cs.unc.edu>

On 10/24/2026 1:25 AM, 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.  The delalloc path does not
> trim after a failed write, so when such a write reaches past i_size,
> it also drops the folio and the delayed blocks that ->write_begin()
> set up there.  Otherwise that folio stays dirty past EOF and writeback
> never cleans it.

I feel the last sentence isn't quite accurate. Dirty folio past
EOF will be cleaned up during writeback. In mpage_process_page_bufs(),
when lblk >= blocks, if 1) there are no blocks to map, it will
directly call mpage_submit_folio(), which clears the dirty flag on
the folio, and then mpage_submit_folio() will shorten the writeback
length, so that ext4_bio_write_folio() will also clear the dirty
flag on the corresponding bh; if 2) there are blocks to map, then
after the required block allocation is done, it will likewise call
mpage_submit_folio() and do similar cleanup. Am I missing something?

> 
> Fixes: 665575cff098 ("filemap: move prefaulting out of hot write path")
> Cc: stable@vger.kernel.org
> Signed-off-by: Hengyu Liang <hengyul@cs.unc.edu>
> ---
> Changes in v2:
> - Rebase onto the ext4 dev branch.
> - Also trim when a failed write starts before EOF and ends past it
>    (pos + len > old_size), as suggested by Zhang Yi.
> - Keep the Fixes tag and the new helper, as discussed on v1.
> - v1: https://lore.kernel.org/r/20261009044656.1993196-1-hengyul@cs.unc.edu
> 
> Tested in KVM with KASAN: the reproducer above and failed writes around
> EOF on delalloc, nodelalloc, bigalloc and 1k block filesystems.
> e2fsck -fn reports no errors.
> 
>   fs/ext4/inode.c | 42 +++++++++++++++++++++++++++++++++++-------
>   1 file changed, 35 insertions(+), 7 deletions(-)
> 
> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 6ede3644a738..e96321ce17d2 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,16 @@ 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);
>   
> +	/*
> +	 * A write reaching past i_size that copied nothing leaves i_size
> +	 * alone, so trim what ->write_begin() set up beyond it.
> +	 */
> +	if (!copied && pos + len > old_size)

Why do you keep the '!copied' condition? AFAICT, if copied is non-zero
but less than len, the excess delalloc extent allocate by
ext4_da_write_begin() should still be cleaned up.

Thanks,
Yi.

> +		ext4_da_truncate_failed_write(inode);
> +
>   	if (!disksize_changed)
>   		return copied;
>   
> 
> base-commit: 8c4f6e3376348fc3162eed86a05b933921c8f9ed


      reply	other threads:[~2026-10-24  7:13 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-23 17:25 Hengyu Liang
2026-10-24  7:12 ` 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=abcaabca-d99e-4bf2-b31b-5e96e7accd29@gmail.com \
    --to=yizhang089@gmail.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=yi.zhang@huawei.com \
    --cc=yi.zhang@huaweicloud.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®