Re: [PATCH 2/2] ext4: protect WRITE_ZEROES written extents with orphan list
Ojaswin Mujoo <[email protected]> Mon, 3 Aug 2026 11:59:57 +0530
| Newsgroups | org.kernel.vger.linux-ext4,org.kernel.vger.linux-fsdevel,org.kernel.vger.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. > >