* [PATCH v4 1/9] ext4: use FGP_WRITEBEGIN for tail block zeroing
2026-07-14 8:00 [PATCH v4 0/9] ext4: fix unaligned edge handling in FALLOC_FL_WRITE_ZEROES Zhang Yi
@ 2026-07-14 8:00 ` Zhang Yi
2026-07-14 8:00 ` [PATCH v4 2/9] ext4: skip tail block zeroing for inline data files Zhang Yi
` (7 subsequent siblings)
8 siblings, 0 replies; 12+ messages in thread
From: Zhang Yi @ 2026-07-14 8:00 UTC (permalink / raw)
To: linux-ext4
Cc: linux-fsdevel, linux-kernel, tytso, adilger.kernel, libaokun,
jack, ojaswin, ritesh.list, yi.zhang, yi.zhang, yizhang089,
chengzhihao1, yangerkun, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
ext4_load_tail_bh() returns a locked folio that callers immediately
mutate through folio_zero_range() and mark_buffer_dirty(). Use
FGP_WRITEBEGIN so that, on backing devices that require stable writes,
__filemap_get_folio() waits for writeback to finish before returning
the folio; on regular devices the wait is a no-op.
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
fs/ext4/inode.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index c2c2d6ac7f3d..7b2face041ef 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4037,7 +4037,7 @@ static struct buffer_head *ext4_load_tail_bh(struct inode *inode, loff_t from)
int err = 0;
folio = __filemap_get_folio(mapping, from >> PAGE_SHIFT,
- FGP_LOCK | FGP_ACCESSED | FGP_CREAT,
+ FGP_WRITEBEGIN | FGP_ACCESSED,
mapping_gfp_constraint(mapping, ~__GFP_FS));
if (IS_ERR(folio))
return ERR_CAST(folio);
--
2.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* [PATCH v4 2/9] ext4: skip tail block zeroing for inline data files
2026-07-14 8:00 [PATCH v4 0/9] ext4: fix unaligned edge handling in FALLOC_FL_WRITE_ZEROES Zhang Yi
2026-07-14 8:00 ` [PATCH v4 1/9] ext4: use FGP_WRITEBEGIN for tail block zeroing Zhang Yi
@ 2026-07-14 8:00 ` Zhang Yi
2026-07-14 15:09 ` Jan Kara
2026-07-14 8:00 ` [PATCH v4 3/9] ext4: check return value of ext4_get_block() in ext4_load_tail_bh() Zhang Yi
` (6 subsequent siblings)
8 siblings, 1 reply; 12+ messages in thread
From: Zhang Yi @ 2026-07-14 8:00 UTC (permalink / raw)
To: linux-ext4
Cc: linux-fsdevel, linux-kernel, tytso, adilger.kernel, libaokun,
jack, ojaswin, ritesh.list, yi.zhang, yi.zhang, yizhang089,
chengzhihao1, yangerkun, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
ext4_block_zero_eof() is called from ext4_write_checks() on every
append write beyond EOF. For inline data files, ext4_get_block()
returns -ERANGE when ext4_load_tail_bh() looks up the tail block.
However, this error is currently ignored because the return value
of ext4_get_block() in ext4_load_tail_bh() is discarded.
Before we fix ext4_load_tail_bh() to properly propagate the error,
skip the zeroing for inline data inodes to avoid unnecessary
failures or confusion.
Fixes: 3f60efd65412d ("ext4: zero post-EOF partial block before appending write")
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
fs/ext4/inode.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 7b2face041ef..3ce925ca0769 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4218,6 +4218,14 @@ int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end)
offset = from & (blocksize - 1);
if (!offset || from >= end)
return 0;
+ /*
+ * Inline data has no tail block to zero out. Note that a race with
+ * ext4_page_mkwrite() converting inline data to an extent without
+ * holding i_rwsem is safe, as that path zeroes the full block before
+ * copying in the inline data.
+ */
+ if (ext4_has_inline_data(inode))
+ return 0;
/* If we are processing an encrypted inode during orphan list handling */
if (IS_ENCRYPTED(inode) && !fscrypt_has_encryption_key(inode))
return 0;
--
2.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* Re: [PATCH v4 2/9] ext4: skip tail block zeroing for inline data files
2026-07-14 8:00 ` [PATCH v4 2/9] ext4: skip tail block zeroing for inline data files Zhang Yi
@ 2026-07-14 15:09 ` Jan Kara
0 siblings, 0 replies; 12+ messages in thread
From: Jan Kara @ 2026-07-14 15:09 UTC (permalink / raw)
To: Zhang Yi
Cc: linux-ext4, linux-fsdevel, linux-kernel, tytso, adilger.kernel,
libaokun, jack, ojaswin, ritesh.list, yi.zhang, yizhang089,
chengzhihao1, yangerkun, yukuai
On Tue 14-07-26 16:00:37, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> ext4_block_zero_eof() is called from ext4_write_checks() on every
> append write beyond EOF. For inline data files, ext4_get_block()
> returns -ERANGE when ext4_load_tail_bh() looks up the tail block.
> However, this error is currently ignored because the return value
> of ext4_get_block() in ext4_load_tail_bh() is discarded.
>
> Before we fix ext4_load_tail_bh() to properly propagate the error,
> skip the zeroing for inline data inodes to avoid unnecessary
> failures or confusion.
>
> Fixes: 3f60efd65412d ("ext4: zero post-EOF partial block before appending write")
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Looks good. Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> fs/ext4/inode.c | 8 ++++++++
> 1 file changed, 8 insertions(+)
>
> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 7b2face041ef..3ce925ca0769 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
> @@ -4218,6 +4218,14 @@ int ext4_block_zero_eof(struct inode *inode, loff_t from, loff_t end)
> offset = from & (blocksize - 1);
> if (!offset || from >= end)
> return 0;
> + /*
> + * Inline data has no tail block to zero out. Note that a race with
> + * ext4_page_mkwrite() converting inline data to an extent without
> + * holding i_rwsem is safe, as that path zeroes the full block before
> + * copying in the inline data.
> + */
> + if (ext4_has_inline_data(inode))
> + return 0;
> /* If we are processing an encrypted inode during orphan list handling */
> if (IS_ENCRYPTED(inode) && !fscrypt_has_encryption_key(inode))
> return 0;
> --
> 2.52.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v4 3/9] ext4: check return value of ext4_get_block() in ext4_load_tail_bh()
2026-07-14 8:00 [PATCH v4 0/9] ext4: fix unaligned edge handling in FALLOC_FL_WRITE_ZEROES Zhang Yi
2026-07-14 8:00 ` [PATCH v4 1/9] ext4: use FGP_WRITEBEGIN for tail block zeroing Zhang Yi
2026-07-14 8:00 ` [PATCH v4 2/9] ext4: skip tail block zeroing for inline data files Zhang Yi
@ 2026-07-14 8:00 ` Zhang Yi
2026-07-14 8:00 ` [PATCH v4 4/9] ext4: move partial block zeroing earlier in ext4_zero_range() Zhang Yi
` (5 subsequent siblings)
8 siblings, 0 replies; 12+ messages in thread
From: Zhang Yi @ 2026-07-14 8:00 UTC (permalink / raw)
To: linux-ext4
Cc: linux-fsdevel, linux-kernel, tytso, adilger.kernel, libaokun,
jack, ojaswin, ritesh.list, yi.zhang, yi.zhang, yizhang089,
chengzhihao1, yangerkun, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
ext4_load_tail_bh() ignores the return value of ext4_get_block(), so an
I/O or allocation failure is silently discarded. buffer_mapped(bh) stays
false and the function returns NULL, which callers such as
ext4_block_do_zero_range() treat as "nothing to do" and return success.
This can mask real failures during zero-range, truncate, or punch-hole
operations, potentially exposing stale data if the block was not
actually a hole and needed zeroing. So propagate the error to the
callers.
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
fs/ext4/inode.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 3ce925ca0769..77ec82fbda89 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4064,7 +4064,9 @@ 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);
+ err = ext4_get_block(inode, iblock, bh, 0);
+ if (err < 0)
+ goto unlock;
/* unmapped? It's a hole - nothing to do */
if (!buffer_mapped(bh)) {
BUFFER_TRACE(bh, "still unmapped");
--
2.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* [PATCH v4 4/9] ext4: move partial block zeroing earlier in ext4_zero_range()
2026-07-14 8:00 [PATCH v4 0/9] ext4: fix unaligned edge handling in FALLOC_FL_WRITE_ZEROES Zhang Yi
` (2 preceding siblings ...)
2026-07-14 8:00 ` [PATCH v4 3/9] ext4: check return value of ext4_get_block() in ext4_load_tail_bh() Zhang Yi
@ 2026-07-14 8:00 ` Zhang Yi
2026-07-14 8:00 ` [PATCH v4 5/9] ext4: clarify return semantics of ext4_load_tail_bh() Zhang Yi
` (4 subsequent siblings)
8 siblings, 0 replies; 12+ messages in thread
From: Zhang Yi @ 2026-07-14 8:00 UTC (permalink / raw)
To: linux-ext4
Cc: linux-fsdevel, linux-kernel, tytso, adilger.kernel, libaokun,
jack, ojaswin, ritesh.list, yi.zhang, yi.zhang, yizhang089,
chengzhihao1, yangerkun, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
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 <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
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.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* [PATCH v4 5/9] ext4: clarify return semantics of ext4_load_tail_bh()
2026-07-14 8:00 [PATCH v4 0/9] ext4: fix unaligned edge handling in FALLOC_FL_WRITE_ZEROES Zhang Yi
` (3 preceding siblings ...)
2026-07-14 8:00 ` [PATCH v4 4/9] ext4: move partial block zeroing earlier in ext4_zero_range() Zhang Yi
@ 2026-07-14 8:00 ` Zhang Yi
2026-07-14 8:00 ` [PATCH v4 6/9] ext4: track partial-zero outcome per edge in ext4_zero_partial_blocks() Zhang Yi
` (3 subsequent siblings)
8 siblings, 0 replies; 12+ messages in thread
From: Zhang Yi @ 2026-07-14 8:00 UTC (permalink / raw)
To: linux-ext4
Cc: linux-fsdevel, linux-kernel, tytso, adilger.kernel, libaokun,
jack, ojaswin, ritesh.list, yi.zhang, yi.zhang, yizhang089,
chengzhihao1, yangerkun, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
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, and note that a lookup-only get_block (without
EXT4_GET_BLOCKS_CREATE) never sets BH_Mapped for clean unwritten
extents, which is why a clean unwritten bh falls through to the
"nothing to do" path.
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
fs/ext4/inode.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 77ec82fbda89..479ed73b5cda 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)
{
@@ -4067,7 +4071,12 @@ static struct buffer_head *ext4_load_tail_bh(struct inode *inode, loff_t from)
err = ext4_get_block(inode, iblock, bh, 0);
if (err < 0)
goto unlock;
- /* unmapped? It's a hole - nothing to do */
+ /*
+ * It's a hole or a clean unwritten block - nothing to do.
+ * Note that a lookup-only get_block (without
+ * EXT4_GET_BLOCKS_CREATE) never sets BH_Mapped for clean
+ * unwritten extents.
+ */
if (!buffer_mapped(bh)) {
BUFFER_TRACE(bh, "still unmapped");
goto unlock;
--
2.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* [PATCH v4 6/9] ext4: track partial-zero outcome per edge in ext4_zero_partial_blocks()
2026-07-14 8:00 [PATCH v4 0/9] ext4: fix unaligned edge handling in FALLOC_FL_WRITE_ZEROES Zhang Yi
` (4 preceding siblings ...)
2026-07-14 8:00 ` [PATCH v4 5/9] ext4: clarify return semantics of ext4_load_tail_bh() Zhang Yi
@ 2026-07-14 8:00 ` Zhang Yi
2026-07-14 8:00 ` [PATCH v4 7/9] ext4: zero out whole block for clean edges in WRITE_ZEROES Zhang Yi
` (2 subsequent siblings)
8 siblings, 0 replies; 12+ messages in thread
From: Zhang Yi @ 2026-07-14 8:00 UTC (permalink / raw)
To: linux-ext4
Cc: linux-fsdevel, linux-kernel, tytso, adilger.kernel, libaokun,
jack, ojaswin, ritesh.list, yi.zhang, yi.zhang, yizhang089,
chengzhihao1, yangerkun, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
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 <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
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 479ed73b5cda..a1615084a3b9 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -4271,13 +4271,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 that
+ * needs zeroing. When the head and tail land in the same block, both
+ * bits are set together on a successful zeroing.
+ */
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);
@@ -4289,21 +4302,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;
}
@@ -4452,7 +4476,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.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* [PATCH v4 7/9] ext4: zero out whole block for clean edges in WRITE_ZEROES
2026-07-14 8:00 [PATCH v4 0/9] ext4: fix unaligned edge handling in FALLOC_FL_WRITE_ZEROES Zhang Yi
` (5 preceding siblings ...)
2026-07-14 8:00 ` [PATCH v4 6/9] ext4: track partial-zero outcome per edge in ext4_zero_partial_blocks() Zhang Yi
@ 2026-07-14 8:00 ` Zhang Yi
2026-07-14 8:00 ` [PATCH v4 8/9] ext4: write back partial-zeroed " Zhang Yi
2026-07-14 8:00 ` [PATCH v4 9/9] ext4: protect WRITE_ZEROES written extents with orphan list Zhang Yi
8 siblings, 0 replies; 12+ messages in thread
From: Zhang Yi @ 2026-07-14 8:00 UTC (permalink / raw)
To: linux-ext4
Cc: linux-fsdevel, linux-kernel, tytso, adilger.kernel, libaokun,
jack, ojaswin, ritesh.list, yi.zhang, yi.zhang, yizhang089,
chengzhihao1, yangerkun, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
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 <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
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.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* [PATCH v4 8/9] ext4: write back partial-zeroed edges in WRITE_ZEROES
2026-07-14 8:00 [PATCH v4 0/9] ext4: fix unaligned edge handling in FALLOC_FL_WRITE_ZEROES Zhang Yi
` (6 preceding siblings ...)
2026-07-14 8:00 ` [PATCH v4 7/9] ext4: zero out whole block for clean edges in WRITE_ZEROES Zhang Yi
@ 2026-07-14 8:00 ` Zhang Yi
2026-07-14 8:00 ` [PATCH v4 9/9] ext4: protect WRITE_ZEROES written extents with orphan list Zhang Yi
8 siblings, 0 replies; 12+ messages in thread
From: Zhang Yi @ 2026-07-14 8:00 UTC (permalink / raw)
To: linux-ext4
Cc: linux-fsdevel, linux-kernel, tytso, adilger.kernel, libaokun,
jack, ojaswin, ritesh.list, yi.zhang, yi.zhang, yizhang089,
chengzhihao1, yangerkun, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
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 <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
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.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread* [PATCH v4 9/9] ext4: protect WRITE_ZEROES written extents with orphan list
2026-07-14 8:00 [PATCH v4 0/9] ext4: fix unaligned edge handling in FALLOC_FL_WRITE_ZEROES Zhang Yi
` (7 preceding siblings ...)
2026-07-14 8:00 ` [PATCH v4 8/9] ext4: write back partial-zeroed " Zhang Yi
@ 2026-07-14 8:00 ` Zhang Yi
[not found] ` <20260714084203.971381F000E9@smtp.kernel.org>
8 siblings, 1 reply; 12+ messages in thread
From: Zhang Yi @ 2026-07-14 8:00 UTC (permalink / raw)
To: linux-ext4
Cc: linux-fsdevel, linux-kernel, tytso, adilger.kernel, libaokun,
jack, ojaswin, ritesh.list, yi.zhang, yi.zhang, yizhang089,
chengzhihao1, yangerkun, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
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 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. Fold
ext4_convert_unwritten_extents() into the same handle so the orphan
add and the conversion are atomic. Also add a check before conversion
to ensure the conversion does not extend beyond the end of the file.
Reported-by: Jan Kara <jack@suse.cz>
Closes: https://lore.kernel.org/linux-ext4/3f6ao5amv7glbgigndtegcucgo3n34ij3lau6l3da3hgdxgn3v@ev66wv3r5umt/
Fixes: f4265b8d32c4 ("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>
---
fs/ext4/extents.c | 45 +++++++++++++++++++++++++++++++++++++++------
1 file changed, 39 insertions(+), 6 deletions(-)
diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
index c083703bf704..9a1b06c8dd97 100644
--- a/fs/ext4/extents.c
+++ b/fs/ext4/extents.c
@@ -4585,6 +4585,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,12 +4660,35 @@ 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))) {
+ 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,
+ if (unlikely(ret))
+ break;
+
+ handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS,
+ credits);
+ if (IS_ERR(handle)) {
+ ret = PTR_ERR(handle);
+ break;
+ }
+
+ if (new_size) {
+ ret = ext4_orphan_add(handle, inode);
+ orphan = true;
+ if (ret) {
+ ext4_journal_stop(handle);
+ break;
+ }
+ }
+
+ ret = ext4_convert_unwritten_extents(handle,
inode, (loff_t)map.m_lblk << blkbits,
(loff_t)map.m_len << blkbits);
+ ret2 = ext4_journal_stop(handle);
+ ret = ret ? ret : ret2;
if (ret)
break;
}
@@ -4678,7 +4702,7 @@ static int ext4_alloc_file_blocks(struct file *file, loff_t offset, loff_t len,
goto retry;
if (!epos || !new_size)
- return ret;
+ goto out;
/*
* Allocate blocks, update the file size to match the size of the
@@ -4687,12 +4711,16 @@ 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)) {
+ ret = ret ? ret : PTR_ERR(handle);
+ goto out;
+ }
ext4_update_inode_size(inode, epos);
ret2 = ext4_mark_inode_dirty(handle, inode);
+ if (orphan && inode->i_nlink)
+ ext4_orphan_del(handle, inode);
ext4_update_inode_fsync_trans(handle, inode, 1);
ret3 = ext4_journal_stop(handle);
ret2 = ret3 ? ret3 : ret2;
@@ -4701,6 +4729,11 @@ static int ext4_alloc_file_blocks(struct file *file, loff_t offset, loff_t len,
pagecache_isize_extended(inode, old_size, epos);
return ret ? ret : ret2;
+
+out:
+ if (orphan && inode->i_nlink)
+ ext4_orphan_del(NULL, inode);
+ return ret;
}
static int ext4_collapse_range(struct file *file, loff_t offset, loff_t len);
--
2.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread