* FAILED: patch "[PATCH] ext4: protect WRITE_ZEROES written extents with orphan list" failed to apply to 7.1-stable tree
@ 2026-08-24 12:27 gregkh
2026-08-26 0:49 ` [PATCH 7.1.y 1/2] ext4: export converted block count from ext4_convert_unwritten_extents() Sasha Levin
0 siblings, 1 reply; 3+ messages in thread
From: gregkh @ 2026-08-24 12:27 UTC (permalink / raw)
To: yi.zhang, jack, tytso; +Cc: stable
The patch below does not apply to the 7.1-stable tree.
If someone wants it applied there, or to any other stable or longterm
tree, then please email the backport, including the original git commit
id to <stable@vger.kernel.org>.
To reproduce the conflict and resubmit, you may use the following commands:
git fetch https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/ linux-7.1.y
git checkout FETCH_HEAD
git cherry-pick -x f7237a775c8f3e99e0e77b7c10fb626815fb2877
# <resolve conflicts, build, test, etc.>
git commit -s
git send-email --to '<stable@vger.kernel.org>' --in-reply-to '2026082448-destiny-guzzler-a437@gregkh' --subject-prefix 'PATCH 7.1.y' 'HEAD^..'
Possible dependencies:
thanks,
greg k-h
------------------ original commit in Linus's tree ------------------
From f7237a775c8f3e99e0e77b7c10fb626815fb2877 Mon Sep 17 00:00:00 2001
From: Zhang Yi <yi.zhang@huawei.com>
Date: Wed, 29 Jul 2026 16:59:18 +0800
Subject: [PATCH] ext4: protect WRITE_ZEROES written extents with orphan list
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.
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>
Link: https://patch.msgid.link/20260729085918.3336221-3-yi.zhang@huaweicloud.com
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
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);
+
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;
}
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH 7.1.y 1/2] ext4: export converted block count from ext4_convert_unwritten_extents()
2026-08-24 12:27 FAILED: patch "[PATCH] ext4: protect WRITE_ZEROES written extents with orphan list" failed to apply to 7.1-stable tree gregkh
@ 2026-08-26 0:49 ` Sasha Levin
2026-08-26 0:49 ` [PATCH 7.1.y 2/2] ext4: protect WRITE_ZEROES written extents with orphan list Sasha Levin
0 siblings, 1 reply; 3+ messages in thread
From: Sasha Levin @ 2026-08-26 0:49 UTC (permalink / raw)
To: stable; +Cc: Zhang Yi, Jan Kara, Theodore Ts'o, Sasha Levin
From: Zhang Yi <yi.zhang@huawei.com>
[ Upstream commit 5b3dcb924a38ecd2c0d828ff30c357357eda1da8 ]
ext4_convert_unwritten_extents() currently returns only a success or a
failure indication. A zero return means all requested blocks were
converted, and a negative value means the conversion failed. However,
some blocks may have already been converted when the function fails
partway through, and callers have no way to learn how many were done.
The WRITE_ZEROES caller in ext4_alloc_file_blocks() needs this
information to decide whether to add the inode to the orphan list before
updating i_disksize to cover the already-converted written extents, so
that a crash before i_disksize catches up can be recovered via orphan
truncation.
Switch the function to pass out the number of converted blocks through
the new output parameter @converted, which will be used by later
patches.
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Reviewed-by: Jan Kara <jack@suse.cz>
Link: https://patch.msgid.link/20260729085918.3336221-2-yi.zhang@huaweicloud.com
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
Stable-dep-of: f7237a775c8f ("ext4: protect WRITE_ZEROES written extents with orphan list")
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/ext4/ext4.h | 3 ++-
fs/ext4/extents.c | 53 ++++++++++++++++++++++++++++++-----------------
fs/ext4/file.c | 3 ++-
3 files changed, 38 insertions(+), 21 deletions(-)
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 6569d1d575a01..f16d3af8ca839 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3813,7 +3813,8 @@ extern void ext4_ext_release(struct super_block *);
extern long ext4_fallocate(struct file *file, int mode, loff_t offset,
loff_t len);
extern int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode,
- loff_t offset, ssize_t len);
+ loff_t offset, ssize_t len,
+ ext4_lblk_t *converted);
extern int ext4_convert_unwritten_extents_atomic(handle_t *handle,
struct inode *inode, loff_t offset, ssize_t len);
extern int ext4_convert_unwritten_io_end_vec(handle_t *handle,
diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
index 125f628e738ab..cf45b25ebfa83 100644
--- a/fs/ext4/extents.c
+++ b/fs/ext4/extents.c
@@ -4664,7 +4664,7 @@ static int ext4_alloc_file_blocks(struct file *file, loff_t offset, loff_t len,
if (likely(!ret))
ret = ext4_convert_unwritten_extents(NULL,
inode, (loff_t)map.m_lblk << blkbits,
- (loff_t)map.m_len << blkbits);
+ (loff_t)map.m_len << blkbits, NULL);
if (ret)
break;
}
@@ -5026,21 +5026,26 @@ int ext4_convert_unwritten_extents_atomic(handle_t *handle, struct inode *inode,
* all unwritten extents within this range will be converted to
* written extents.
*
- * This function is called from the direct IO end io call back
- * function, to convert the fallocated extents after IO is completed.
- * Returns 0 on success.
+ * This function is called from the direct/buffered I/O end io call back
+ * function and FALLOC_FL_WRITE_ZEROES, to convert the fallocated
+ * unwritten extents after data I/O is completed.
+ *
+ * Returns 0 on full success, or a negative error code on partial
+ * success or failure. The number of blocks converted is returned via
+ * @converted.
*/
int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode,
- loff_t offset, ssize_t len)
+ loff_t offset, ssize_t len,
+ ext4_lblk_t *converted)
{
- unsigned int max_blocks;
+ ext4_lblk_t max_blocks, conv_blocks = 0;
int ret = 0, ret2 = 0, ret3 = 0;
struct ext4_map_blocks map;
unsigned int blkbits = inode->i_blkbits;
unsigned int credits = 0;
map.m_lblk = offset >> blkbits;
- max_blocks = EXT4_MAX_BLOCKS(len, offset, blkbits);
+ map.m_len = max_blocks = EXT4_MAX_BLOCKS(len, offset, blkbits);
if (!handle) {
/*
@@ -5048,9 +5053,8 @@ int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode,
*/
credits = ext4_chunk_trans_blocks(inode, max_blocks);
}
- while (ret >= 0 && ret < max_blocks) {
- map.m_lblk += ret;
- map.m_len = (max_blocks -= ret);
+
+ while (max_blocks) {
if (credits) {
handle = ext4_journal_start(inode, EXT4_HT_MAP_BLOCKS,
credits);
@@ -5067,23 +5071,34 @@ int ext4_convert_unwritten_extents(handle_t *handle, struct inode *inode,
ret = ext4_map_blocks(handle, inode, &map,
EXT4_GET_BLOCKS_IO_CONVERT_EXT |
EXT4_EX_NOCACHE);
- if (ret <= 0)
+ if (ret <= 0) {
ext4_warning(inode->i_sb,
- "inode #%llu: block %u: len %u: "
- "ext4_ext_map_blocks returned %d",
- inode->i_ino, map.m_lblk,
- map.m_len, ret);
+ "inode #%llu: block %u: len %u: ext4_map_blocks returned %d",
+ inode->i_ino, map.m_lblk, map.m_len, ret);
+ if (unlikely(ret == 0))
+ ret = -EINVAL;
+ } else {
+ conv_blocks += map.m_len;
+ }
+
ret2 = ext4_mark_inode_dirty(handle, inode);
if (credits) {
ret3 = ext4_journal_stop(handle);
if (unlikely(ret3))
ret2 = ret3;
}
-
- if (ret <= 0 || ret2)
+ ret = ret < 0 ? ret : ret2;
+ if (ret)
break;
+
+ map.m_lblk += map.m_len;
+ map.m_len = (max_blocks -= map.m_len);
}
- return ret > 0 ? ret2 : ret;
+ /* Converted some or all blocks successfully? */
+ if (converted)
+ *converted = conv_blocks;
+
+ return ret;
}
int ext4_convert_unwritten_io_end_vec(handle_t *handle, ext4_io_end_t *io_end)
@@ -5106,7 +5121,7 @@ int ext4_convert_unwritten_io_end_vec(handle_t *handle, ext4_io_end_t *io_end)
list_for_each_entry(io_end_vec, &io_end->list_vec, list) {
ret = ext4_convert_unwritten_extents(handle, io_end->inode,
io_end_vec->offset,
- io_end_vec->size);
+ io_end_vec->size, NULL);
if (ret)
break;
}
diff --git a/fs/ext4/file.c b/fs/ext4/file.c
index eb1a323962b10..78db5749cbcc3 100644
--- a/fs/ext4/file.c
+++ b/fs/ext4/file.c
@@ -400,7 +400,8 @@ static int ext4_dio_write_end_io(struct kiocb *iocb, ssize_t size,
error = ext4_convert_unwritten_extents_atomic(NULL, inode, pos,
size);
else if (!error && size && flags & IOMAP_DIO_UNWRITTEN)
- error = ext4_convert_unwritten_extents(NULL, inode, pos, size);
+ error = ext4_convert_unwritten_extents(NULL, inode, pos, size,
+ NULL);
if (error)
return error;
/*
--
2.53.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH 7.1.y 2/2] ext4: protect WRITE_ZEROES written extents with orphan list
2026-08-26 0:49 ` [PATCH 7.1.y 1/2] ext4: export converted block count from ext4_convert_unwritten_extents() Sasha Levin
@ 2026-08-26 0:49 ` Sasha Levin
0 siblings, 0 replies; 3+ messages in thread
From: Sasha Levin @ 2026-08-26 0:49 UTC (permalink / raw)
To: stable; +Cc: Zhang Yi, Jan Kara, Theodore Ts'o, Sasha Levin
From: Zhang Yi <yi.zhang@huawei.com>
[ Upstream commit f7237a775c8f3e99e0e77b7c10fb626815fb2877 ]
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.
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>
Link: https://patch.msgid.link/20260729085918.3336221-3-yi.zhang@huaweicloud.com
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/ext4/extents.c | 76 ++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 69 insertions(+), 7 deletions(-)
diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c
index cf45b25ebfa83..951d9b394ccd8 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,
+ 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, NULL);
+ (loff_t)map.m_len << blkbits,
+ &converted);
if (ret)
- break;
+ 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);
+
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.53.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-08-26 0:49 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-24 12:27 FAILED: patch "[PATCH] ext4: protect WRITE_ZEROES written extents with orphan list" failed to apply to 7.1-stable tree gregkh
2026-08-26 0:49 ` [PATCH 7.1.y 1/2] ext4: export converted block count from ext4_convert_unwritten_extents() Sasha Levin
2026-08-26 0:49 ` [PATCH 7.1.y 2/2] ext4: protect WRITE_ZEROES written extents with orphan list Sasha Levin
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.