Re: [PATCH 2/2] ext4: protect WRITE_ZEROES written extents with orphan list

Ojaswin Mujoo <[email protected]>
Newsgroups gmane.comp.file-systems.ext4,gmane.linux.file-systems,gmane.linux.kernel
Message-ID <[email protected]>
On Mon, Aug 03, 2026 at 09:31:53AM +0800, Zhang Yi wrote:
> On 7/31/2026 8:06 PM, Ojaswin Mujoo wrote:
> > On Wed, Jul 29, 2026 at 04:59:18PM +0800, Zhang Yi wrote:
> >> From: Zhang Yi <[email protected]>
> >>
> >> 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.
> > 
> > Hi Yi,
> > 
> > Look good,
> > 
> > Reviewed-by: Ojaswin Mujoo <[email protected]>
> 
> Hi, Ojaswin,
> 
> Thank you for the review!
> 
> > 
> > Just a small question below:
> >>
> >> Reported-by: Jan Kara <[email protected]>
> >> Closes: https://lore.kernel.org/linux-ext4/3f6ao5amv7glbgigndtegcucgo3n34ij3lau6l3da3hgdxgn3v@ev66wv3r5umt/
> >> Fixes: f4265b8d32c4 ("ext4: add FALLOC_FL_WRITE_ZEROES support")
> >> Cc: [email protected]
> >> Signed-off-by: Zhang Yi <[email protected]>
> >> ---
> >>  fs/ext4/extents.c | 78 ++++++++++++++++++++++++++++++++++++++++++-----
> >>  1 file changed, 70 insertions(+), 8 deletions(-)
> >>
> >> diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
> >> index 1ab1a6e2ed83..a3dde7ba0d23 100644
> >> --- a/fs/ext4/extents.c
> >> +++ b/fs/ext4/extents.c
> >> @@ -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);
> >> +
> > 
> > So I believe the i_nlink check is incase the inode is already at i_nlink
> > = 0 before the fallocate() was called and hence mostly already on the
> > orphan list. We need to skip deletion from the list as inode eviction
> > will handle cleaning it up, right?
> 
> Yes, after ext4_evict_inode() has reclaimed all the file blocks, the
> inode will be cleared from the orphan list.

Cool, thanks for confirming :)

Regards,
ojaswin

> 
> Thanks,
> Yi.
> 
>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.