Re: [PATCH 2/2] ext4: protect WRITE_ZEROES written extents with orphan list
Ojaswin Mujoo <[email protected]> Fri, 31 Jul 2026 17:36:48 +0530
| Newsgroups | org.kernel.vger.linux-ext4,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
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]> 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? > 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; > } > -- > 2.52.0 >