From: Zhang Yi In ext4_zero_range(), move the ext4_zero_partial_blocks() call, which handles unaligned edges, into the same branch where the unaligned range is preallocated, immediately after ext4_alloc_file_blocks(). This is safe because there is no dependency between partial block handling and the subsequent full block handling. This change will be used by later patches that handle unaligned FALLOC_FL_WRITE_ZEROES operations, which will need to check the partial zeroed result. Signed-off-by: Zhang Yi --- fs/ext4/extents.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 125f628e738a..27ca641e701b 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -4734,10 +4734,16 @@ static long ext4_zero_range(struct file *file, loff_t offset, } flags = EXT4_GET_BLOCKS_CREATE_UNWRIT_EXT; - /* Preallocate the range including the unaligned edges */ + /* + * Preallocate the range including the unaligned edges, and zero + * out partial blocks if they already contain data. + */ if (!IS_ALIGNED(offset | end, blocksize)) { ret = ext4_alloc_file_blocks(file, offset, len, new_size, flags); + if (!ret) + ret = ext4_zero_partial_blocks(inode, offset, len, + &partial_zeroed); if (ret) return ret; } @@ -4770,10 +4776,6 @@ static long ext4_zero_range(struct file *file, loff_t offset, if (IS_ALIGNED(offset | end, blocksize)) return ret; - /* Zero out partial block at the edges of the range */ - ret = ext4_zero_partial_blocks(inode, offset, len, &partial_zeroed); - if (ret) - return ret; if (((file->f_flags & O_SYNC) || IS_SYNC(inode)) && partial_zeroed) { ret = filemap_write_and_wait_range(inode->i_mapping, offset, end - 1); -- 2.53.0 From: Zhang Yi ext4_load_tail_bh() returns NULL for both holes and clean unwritten buffers, but the conditions that lead to this are not obvious from the code alone. Document this behavior to clarify the return value, so that readers do not mistakenly assume that only holes result in a NULL return. Also update the inline comment following the ext4_get_block() call to reflect this: both holes and clean unwritten buffers fall through to the "nothing to do" path. Signed-off-by: Zhang Yi --- fs/ext4/inode.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index c2c2d6ac7f3d..0b31fa873743 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -4026,6 +4026,10 @@ void ext4_set_aops(struct inode *inode) * because it might have data in pagecache (eg, if called from ext4_zero_range, * ext4_punch_hole, etc) which needs to be properly zeroed out. Otherwise a * racing writeback can come later and flush the stale pagecache to disk. + * + * Return the loaded bh if it actually needs zeroing - in written, dirty + * unwritten, or delalloc state. Return NULL if it's clean (i.e., a hole or + * a clean unwritten block). */ static struct buffer_head *ext4_load_tail_bh(struct inode *inode, loff_t from) { @@ -4065,7 +4069,7 @@ static struct buffer_head *ext4_load_tail_bh(struct inode *inode, loff_t from) if (!buffer_mapped(bh)) { BUFFER_TRACE(bh, "unmapped"); ext4_get_block(inode, iblock, bh, 0); - /* unmapped? It's a hole - nothing to do */ + /* It's a hole or a clean unwritten block - nothing to do */ if (!buffer_mapped(bh)) { BUFFER_TRACE(bh, "still unmapped"); goto unlock; -- 2.53.0 From: Zhang Yi Replace the single bool did_zero output of ext4_zero_partial_blocks() with a bitmask that records which edge (start, end, or both in the single-block case) was actually partial-zeroed. This allows callers to distinguish which edges have been zeroed, preparing for unaligned FALLOC_FL_WRITE_ZEROES handling in later patches. Signed-off-by: Zhang Yi --- fs/ext4/ext4.h | 5 ++++- fs/ext4/extents.c | 2 +- fs/ext4/inode.c | 36 ++++++++++++++++++++++++++++++------ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 94283a991e5c..b2e55876d0e3 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h @@ -3099,8 +3099,11 @@ extern int ext4_chunk_trans_extent(struct inode *inode, int nrblocks); extern int ext4_meta_trans_blocks(struct inode *inode, int lblocks, int pextents); extern int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end); + +#define EXT4_PARTIAL_ZERO_START 0x1 +#define EXT4_PARTIAL_ZERO_END 0x2 extern int ext4_zero_partial_blocks(struct inode *inode, loff_t lstart, - loff_t length, bool *did_zero); + loff_t length, unsigned int *partial_zeroed); extern vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf); extern qsize_t *ext4_get_reserved_space(struct inode *inode); extern int ext4_get_projid(struct inode *inode, kprojid_t *projid); diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 27ca641e701b..7a8f9f188a29 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -4715,7 +4715,7 @@ static long ext4_zero_range(struct file *file, loff_t offset, loff_t align_start, align_end, new_size = 0; loff_t end = offset + len; unsigned int blocksize = i_blocksize(inode); - bool partial_zeroed = false; + unsigned int partial_zeroed = 0; int ret, flags; trace_ext4_zero_range(inode, offset, len, mode); diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 0b31fa873743..d68f7be4ff02 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -4256,13 +4256,26 @@ int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end) return 0; } +/* + * Zero out the unaligned head and tail of the [lstart, lstart+length) + * range. + * + * On return, @partial_zeroed records which edges actually got + * partial-zeroed. Set EXT4_PARTIAL_ZERO_START/EXT4_PARTIAL_ZERO_END if + * the head/tail block got actually partially zeroed (in written, dirty + * unwritten or delalloc state). Cleared if the head/tail block is a + * hole or a clean unwritten block, in which case there is nothing needs + * to zero. When the head and tail land in the same block, both bits + * are set together on a successful zero. + */ int ext4_zero_partial_blocks(struct inode *inode, loff_t lstart, loff_t length, - bool *did_zero) + unsigned int *partial_zeroed) { struct super_block *sb = inode->i_sb; unsigned partial_start, partial_end; ext4_fsblk_t start, end; loff_t byte_end = (lstart + length - 1); + bool did_zero = false; int err = 0; partial_start = lstart & (sb->s_blocksize - 1); @@ -4274,21 +4287,32 @@ int ext4_zero_partial_blocks(struct inode *inode, loff_t lstart, loff_t length, /* Handle partial zero within the single block */ if (start == end && (partial_start || (partial_end != sb->s_blocksize - 1))) { - err = ext4_block_zero_range(inode, lstart, length, did_zero, + err = ext4_block_zero_range(inode, lstart, length, &did_zero, NULL); + if (did_zero) + *partial_zeroed |= (EXT4_PARTIAL_ZERO_START | + EXT4_PARTIAL_ZERO_END); return err; } /* Handle partial zero out on the start of the range */ if (partial_start) { err = ext4_block_zero_range(inode, lstart, sb->s_blocksize, - did_zero, NULL); + &did_zero, NULL); if (err) return err; + if (did_zero) + *partial_zeroed |= EXT4_PARTIAL_ZERO_START; } /* Handle partial zero out on the end of the range */ - if (partial_end != sb->s_blocksize - 1) + if (partial_end != sb->s_blocksize - 1) { + did_zero = false; err = ext4_block_zero_range(inode, byte_end - partial_end, - partial_end + 1, did_zero, NULL); + partial_end + 1, &did_zero, NULL); + if (err) + return err; + if (did_zero) + *partial_zeroed |= EXT4_PARTIAL_ZERO_END; + } return err; } @@ -4437,7 +4461,7 @@ int ext4_punch_hole(struct file *file, loff_t offset, loff_t length) loff_t end = offset + length; handle_t *handle; unsigned int credits; - bool partial_zeroed = false; + unsigned int partial_zeroed = 0; int ret; trace_ext4_punch_hole(inode, offset, length, 0); -- 2.53.0 From: Zhang Yi FALLOC_FL_WRITE_ZEROES requires that all blocks in the requested range end up as written extents with zeroed content. For unaligned edges that were already allocated, ext4_zero_partial_blocks() zeros them directly. However, for unaligned edges whose underlying extent is a clean unwritten extent or a hole, the extent type remains unwritten after partial zeroing, which does not align with the semantics of WRITE_ZEROES. Therefore, when ext4_zero_partial_blocks() skips partial zeroing, it indicates that the corresponding edges are clean unwritten extents or holes. In this case, we need to expand the aligned allocation range outward to cover such edges, so that ext4_alloc_file_blocks() can correctly allocate blocks for the unaligned range. Edges that were partial-zeroed (i.e., written or dirty) are left untouched. Fixes: f4265b8d32c4 ("ext4: add FALLOC_FL_WRITE_ZEROES support") Cc: stable@vger.kernel.org Signed-off-by: Zhang Yi --- fs/ext4/extents.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 7a8f9f188a29..c34505765521 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -4760,6 +4760,21 @@ static long ext4_zero_range(struct file *file, loff_t offset, /* Zero range excluding the unaligned edges */ align_start = round_up(offset, blocksize); align_end = round_down(end, blocksize); + + /* + * In WRITE_ZEROES mode, edges that were not partial-zeroed (clean + * unwritten or hole) must be allocated and zeroed as whole blocks. + * Expand the aligned range outward to cover them. + */ + if (mode & FALLOC_FL_WRITE_ZEROES) { + if (!IS_ALIGNED(offset, blocksize) && + !(partial_zeroed & EXT4_PARTIAL_ZERO_START)) + align_start = round_down(offset, blocksize); + if (!IS_ALIGNED(end, blocksize) && + !(partial_zeroed & EXT4_PARTIAL_ZERO_END)) + align_end = round_up(end, blocksize); + } + if (align_end > align_start) { if (mode & FALLOC_FL_WRITE_ZEROES) flags = EXT4_GET_BLOCKS_CREATE_ZERO | EXT4_EX_NOCACHE; -- 2.53.0 From: Zhang Yi FALLOC_FL_WRITE_ZEROES requires that all blocks in the requested range end up as written extents with zeroed content. For unaligned edges that were partial-zeroed in dirty unwritten or delalloc state, the buffer is left dirty while the underlying extent may not yet be converted to written. As a result, a subsequent SYNC write to this range would still trigger metadata changes, which violates the semantics of WRITE_ZEROES. Fix this by calling filemap_write_and_wait_range() for partial-zeroed edges to flush out the zeroed data and ensure the extent conversion is complete. Fixes: f4265b8d32c4 ("ext4: add FALLOC_FL_WRITE_ZEROES support") Cc: stable@vger.kernel.org Signed-off-by: Zhang Yi --- fs/ext4/extents.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index c34505765521..c083703bf704 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -4791,7 +4791,15 @@ static long ext4_zero_range(struct file *file, loff_t offset, if (IS_ALIGNED(offset | end, blocksize)) return ret; - if (((file->f_flags & O_SYNC) || IS_SYNC(inode)) && partial_zeroed) { + /* + * In FALLOC_FL_WRITE_ZEROES mode, edges that have been partially + * zeroed must be written back to ensure the entire zeroed range + * is converted to the written state. In SYNC mode, writeback is + * also required to persist the zeroed data to disk. + */ + if (partial_zeroed && + ((mode & FALLOC_FL_WRITE_ZEROES) || + (file->f_flags & O_SYNC) || IS_SYNC(inode))) { ret = filemap_write_and_wait_range(inode->i_mapping, offset, end - 1); if (ret) -- 2.53.0 From: Zhang Yi FALLOC_FL_WRITE_ZEROES is mutually exclusive with FALLOC_FL_KEEP_SIZE. In ext4_zero_range(), when end > i_disksize, new_size is always set to end for WRITE_ZEROES, so the i_disksize update should not be lost. The ext4_update_disksize_before_punch() call is only needed for the ZERO_RANGE path in ext4_zero_range(), as it handles the case where KEEP_SIZE may preserve the original disk size. Skip it for WRITE_ZEROES to avoid unnecessary work and potential confusion. Signed-off-by: Zhang Yi --- fs/ext4/extents.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index c083703bf704..38753c2ef910 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c @@ -4748,9 +4748,11 @@ static long ext4_zero_range(struct file *file, loff_t offset, return ret; } - ret = ext4_update_disksize_before_punch(inode, offset, len); - if (ret) - return ret; + if (mode & FALLOC_FL_ZERO_RANGE) { + ret = ext4_update_disksize_before_punch(inode, offset, len); + if (ret) + return ret; + } /* Now release the pages and zero block aligned part of pages */ ret = ext4_truncate_page_cache_block_range(inode, offset, end); -- 2.53.0