ext4: protect WRITE_ZEROES written extents with orphan list

In ext4_alloc_file_blocks(), the WRITE_ZEROES path converts unwritten
extents to written in one transaction, while i_disksize is updated to
cover them only in a later transaction. A crash in between leaves
written extents beyond i_disksize on disk, which fsck will complain
about.

To fix this, add the inode to the orphan list in the same handle that
does the conversion, and remove it once i_disksize has caught up.
Also add a sanity check to ensure conversion does not extend beyond EOF.

Since ext4_alloc_file_blocks() is called from the fallocate() path,
partial allocation is safe. On partial conversion failure, advance
i_disksize only up to the boundary of successfully converted blocks, so
that orphan cleanup sees a consistent state. Document this behavior in
the function comment.

Reported-by: Jan Kara <jack@suse.cz>
Closes: https://lore.kernel.org/linux-ext4/3f6ao5amv7glbgigndtegcucgo3n34ij3lau6l3da3hgdxgn3v@ev66wv3r5umt/
Fixes: f4265b8d32 ("ext4: add FALLOC_FL_WRITE_ZEROES support")
Cc: stable@vger.kernel.org
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
Link: https://patch.msgid.link/20260729085918.3336221-3-yi.zhang@huaweicloud.com
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
This commit is contained in:
Zhang Yi 2026-07-29 16:59:18 +08:00 committed by Theodore Ts'o
parent 5b3dcb924a
commit f7237a775c

View File

@ -4571,6 +4571,22 @@ int ext4_ext_truncate(handle_t *handle, struct inode *inode)
return err;
}
/*
* Pre-allocate blocks for the range [@offset, @offset + @len). Allocated
* blocks are marked as unwritten by default. If EXT4_GET_BLOCKS_ZERO is
* set, the allocated blocks are zeroed on disk and their extents are
* converted to written state.
*
* When @new_size is nonzero, the caller intends to extend the file, and
* the file size should be updated to the end of the allocated blocks.
*
* Allocation may partially succeed due to some non-fatal issues. In that
* case, i_disksize (and i_size) is advanced up to the successfully
* processed portion of the range.
*
* Return 0 on success, or a negative error code on failure or partial
* failure.
*/
static int ext4_alloc_file_blocks(struct file *file, loff_t offset, loff_t len,
loff_t new_size, int flags)
{
@ -4585,6 +4601,7 @@ static int ext4_alloc_file_blocks(struct file *file, loff_t offset, loff_t len,
loff_t epos = 0, old_size = i_size_read(inode);
unsigned int blkbits = inode->i_blkbits;
bool alloc_zero = false;
bool orphan = false;
BUG_ON(!ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
map.m_lblk = offset >> blkbits;
@ -4659,19 +4676,49 @@ static int ext4_alloc_file_blocks(struct file *file, loff_t offset, loff_t len,
if (alloc_zero &&
(map.m_flags & (EXT4_MAP_MAPPED | EXT4_MAP_UNWRITTEN))) {
ext4_lblk_t converted;
WARN_ON_ONCE(map.m_lblk + map.m_len >
EXT4_B_TO_LBLK(inode, new_size ?: old_size));
ret = ext4_issue_zeroout(inode, map.m_lblk, map.m_pblk,
map.m_len);
if (likely(!ret))
ret = ext4_convert_unwritten_extents(NULL,
inode, (loff_t)map.m_lblk << blkbits,
(loff_t)map.m_len << blkbits, NULL);
if (ret)
if (unlikely(ret))
break;
handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS,
credits);
if (IS_ERR(handle)) {
ret = PTR_ERR(handle);
break;
}
ret = ext4_convert_unwritten_extents(handle,
inode, (loff_t)map.m_lblk << blkbits,
(loff_t)map.m_len << blkbits,
&converted);
if (ret)
map.m_len = converted;
/*
* If blocks beyond i_disksize are converted, add
* the inode to the orphan list and advance the epos.
*/
if (new_size && converted) {
ret2 = ext4_orphan_add(handle, inode);
ret = ret ? ret : ret2;
orphan = true;
}
ret3 = ext4_journal_stop(handle);
ret = ret ? ret : ret3;
}
map.m_lblk += map.m_len;
map.m_len = len_lblk = len_lblk - map.m_len;
epos = EXT4_LBLK_TO_B(inode, map.m_lblk);
if (ret)
break;
}
if (ret == -ENOSPC && ext4_should_retry_alloc(inode->i_sb, &retries))
@ -4687,11 +4734,23 @@ static int ext4_alloc_file_blocks(struct file *file, loff_t offset, loff_t len,
if (epos > new_size)
epos = new_size;
handle = ext4_journal_start(inode, EXT4_HT_MISC, 1);
if (IS_ERR(handle))
return ret ? ret : PTR_ERR(handle);
handle = ext4_journal_start(inode, EXT4_HT_MISC, 2);
if (IS_ERR(handle)) {
/*
* The conversion has successfully completed. Not much to
* do with the error here so just cleanup the orphan list
* and hope for the best.
*/
if (orphan && inode->i_nlink)
ext4_orphan_del(NULL, inode);
ret2 = PTR_ERR(handle);
goto out;
}
ext4_update_inode_size(inode, epos);
if (orphan && inode->i_nlink)
ext4_orphan_del(handle, inode);
ret2 = ext4_mark_inode_dirty(handle, inode);
ext4_update_inode_fsync_trans(handle, inode, 1);
ret3 = ext4_journal_stop(handle);
@ -4699,6 +4758,9 @@ static int ext4_alloc_file_blocks(struct file *file, loff_t offset, loff_t len,
if (epos > old_size)
pagecache_isize_extended(inode, old_size, epos);
out:
if (ret2)
ext4_std_error(inode->i_sb, ret2);
return ret ? ret : ret2;
}