* [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios
@ 2026-09-22 11:06 Zhang Yi
2026-09-22 11:07 ` [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order Zhang Yi
` (4 more replies)
0 siblings, 5 replies; 21+ messages in thread
From: Zhang Yi @ 2026-09-22 11:06 UTC (permalink / raw)
To: linux-mm
Cc: linux-fsdevel, linux-kernel, linux-ext4, akpm, david, ljs, liam,
vbabka, rppt, surenb, mhocko, hughd, baolin.wang, willy, jack,
ziy, bfoster, joannelkoong, djwong, yi.zhang, yi.zhang,
yizhang089, yangerkun, chengzhihao1, wangkefeng.wang, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
Hello,
This is the fourth version fixing data loss when truncating straddling
large folios caught on the upcomming ext4 + iomap buffered I/O
conversion.
When truncate_inode_pages_range() punches a hole or truncates a file,
truncate_inode_partial_folio() splits a large folio so that the caller
can drop the in-range sub-folios while keeping the out-of-range tail
intact. This series fixes three distinct problems in that path that
can each lose the valid out-of-range tail of a straddling folio, plus a
follow-up that clarifies the return value semantics.
Patch 01 aligns the truncation boundaries inwards to the mapping minimum
folio order in truncate_inode_pages_range(). With a non-zero min_order,
folio_split() stops at min_order instead of order 0, so a boundary
computed at page granularity can land inside a min-order-aligned
sub-folio and the truncate loop drops that whole chunk, valid tail
included, causing data loss.
Patch 02 looks the end-edge straddler up by its page index through
__filemap_get_folio() in truncate_inode_partial_folio(). After the
first split the straddler is unlocked and only transiently ref'd in the
page cache, so the page pointer derived from the original folio can be
freed and reallocated as a different folio in the same mapping, and the
mapping check cannot catch it, which may cause incorrect splitting and
potential data loss.
Patch 03 reworks the contract between truncate_inode_partial_folio() and
its callers. If the second split of the straddler fails, the function
reported success unconditionally, and the leftover incorrect end
position could cause the truncate loop to drop that valid tail. After
rework, it tells the caller the exact page range safe to discard via new
pstart/pend out-parameters, so the truncate loop never touches a
straddling folio that still holds valid out-of-range data.
Patch 04 clarifies the return value semantics to "at least one split
succeeded", which is all the shmem caller needs to decide whether to
reset its scan loop.
The second patch fixes a pre-existing race issue that is reachable
today, so it is Cc'd to stable. Patches 01 and 03 require a dirty large
folio that carries no filesystem private data, so they are not reachable
on current filesystems. They were found while developing the upcoming
ext4 iomap buffered I/O path[1].
Thanks,
Yi.
[1] https://lore.kernel.org/linux-fsdevel/a638a8fb-c184-4069-ae33-379ec12cd514@huaweicloud.com/
v3->v4:
- Move the patch that fixes data loss when min_order is non-zero to the
first patch position, aligning start and end in
truncate_inode_pages_range(). (Zi Yan)
- Add patch 2, fixing the invalid folio2 issue under concurrency when
truncate_inode_partial_folio() splits at the end position. Use
__filemap_get_folio() to obtain a reliable folio2. (Jan Kara)
v2->v3:
- Rework the folio2 validity check logic to fix the invalid
folio->index issue. (sashiko)
- Clarify the pstart and pend setting logic and the corresponding
comments to make it more readable. (Brian, Joanne)
- Split the patch into 3 small patches. (Zi Yan)
v1->v2:
- Export pstart as a new parameter so that the generic and shmem
truncate paths don't need to recompute the start value from the
return value. (Brian)
- When min_order is non-zero, align [pstart, pend] to the inner
boundaries of the folio to ensure they do not point into the middle
of a large folio, which could otherwise cause valid data within the
folio to be incorrectly cleared. (Joanne)
v3: https://lore.kernel.org/linux-mm/20260916092450.654408-1-yi.zhang@huaweicloud.com/
v2: https://lore.kernel.org/linux-mm/20260909062339.473816-1-yi.zhang@huaweicloud.com/
v1: https://lore.kernel.org/linux-mm/20260903115018.2034541-1-yi.zhang@huaweicloud.com/
Zhang Yi (4):
mm/truncate: align truncation boundaries to mapping minimum folio
order
mm/truncate: look up the end-edge straddler by index
mm/truncate: fix data loss when splitting straddling large folios
fails
mm/truncate: clarify return value of truncate_inode_partial_folio()
mm/internal.h | 4 +-
mm/shmem.c | 13 ++---
mm/truncate.c | 128 +++++++++++++++++++++++++++++++++-----------------
3 files changed, 91 insertions(+), 54 deletions(-)
--
2.54.0
^ permalink raw reply [flat|nested] 21+ messages in thread
* [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order
2026-09-22 11:06 [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios Zhang Yi
@ 2026-09-22 11:07 ` Zhang Yi
2026-09-22 11:32 ` Jan Kara
` (2 more replies)
2026-09-22 11:07 ` [PATCH v4 2/4] mm/truncate: look up the end-edge straddler by index Zhang Yi
` (3 subsequent siblings)
4 siblings, 3 replies; 21+ messages in thread
From: Zhang Yi @ 2026-09-22 11:07 UTC (permalink / raw)
To: linux-mm
Cc: linux-fsdevel, linux-kernel, linux-ext4, akpm, david, ljs, liam,
vbabka, rppt, surenb, mhocko, hughd, baolin.wang, willy, jack,
ziy, bfoster, joannelkoong, djwong, yi.zhang, yi.zhang,
yizhang089, yangerkun, chengzhihao1, wangkefeng.wang, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
When the mapping has a non-zero minimum folio order (min_order),
folio_split() in truncate_inode_partial_folio() stops at min_order
instead of order 0, so the sub-folio containing a split point stays
aligned to 1 << min_order rather than to a single page. The original
boundaries in truncate_inode_pages_range() were based on page
granularity, so either boundary could land inside the min_order chunk at
its edge, and the truncation loop would drop that whole chunk, valid
out-of-range tail included.
For example, a 64K (order-4) folio with min_order = 2 (16K) punched from
offset 0 to 36K:
split @p0 -> [p0-p3, p4-p7, p8-p15] # non-uniform, min_order
folio2 = p8-p15 # straddles: p8 in range, p9-p15 tail valid
2nd split of folio2 -> [p8-p11, p12-p15] # success
end(old) = p9 # BUG: p9 inside [p8-p11]
loop truncates ... p8-p11 # p9-p11's valid tail is lost
It has gone unnoticed so far for two reasons. A non-zero min_order is
only used by filesystems with a block or sector size larger than the
page size, and those either always write back the affected range before
punching a hole or truncating, or they carry filesystem private data on
dirty folios (e.g. buffer_head), which makes filemap_release_folio()
fail and folio_split() abort with -EBUSY, so the folio is never split
and the old start/end boundaries remain valid. The bug only becomes
reachable on paths that truncate dirty large folios without prior
writeback and without filesystem private data, such as the upcoming ext4
iomap buffered I/O path.
Align both start (rounded up) and end (rounded down) to the mapping
minimum folio order so they always fall on a folio boundary.
Reported-by: Joanne Koong <joannelkoong@gmail.com>
Link: https://lore.kernel.org/linux-mm/CAJnrk1bQYUe6+1ryyJur5EEnZYrC+_5AYsy=OWzVRgD4202y1g@mail.gmail.com/
Fixes: e220917fa5077 ("mm: split a folio in minimum folio order chunks")
Suggested-by: Zi Yan <ziy@nvidia.com>
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
mm/truncate.c | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/mm/truncate.c b/mm/truncate.c
index b58ba940be47..f9625bb4916f 100644
--- a/mm/truncate.c
+++ b/mm/truncate.c
@@ -345,9 +345,11 @@ long mapping_evict_folio(struct address_space *mapping, struct folio *folio)
* @lstart: offset from which to truncate
* @lend: offset to which to truncate (inclusive)
*
- * Truncate the page cache, removing the pages that are between
- * specified offsets (and zeroing out partial pages
- * if lstart or lend + 1 is not page aligned).
+ * Truncate the page cache, removing the folios that are between specified
+ * offsets (and zeroing out partial folios if lstart or lend + 1 is not
+ * folio aligned). For mappings with a non-zero minimum folio order, the
+ * boundaries are aligned inwards to 1 << min_order so the edge sub-folio
+ * straddling the range is kept.
*
* Truncate takes two passes - the first pass is nonblocking. It will not
* block on page locks and it will not block on writeback. The second pass
@@ -374,14 +376,14 @@ void truncate_inode_pages_range(struct address_space *mapping,
int i;
struct folio *folio;
bool same_folio;
+ pgoff_t min_nrpages = mapping_min_folio_nrpages(mapping);
if (mapping_empty(mapping))
return;
/*
- * 'start' and 'end' always covers the range of pages to be fully
- * truncated. Partial pages are covered with 'partial_start' at the
- * start of the range and 'partial_end' at the end of the range.
+ * 'start' and 'end' always covers the range of folios to be fully
+ * truncated, with both boundaries aligned inwards to 1 << min_order.
* Note that 'end' is exclusive while 'lend' is inclusive.
*/
start = (lstart + PAGE_SIZE - 1) >> PAGE_SHIFT;
@@ -395,6 +397,10 @@ void truncate_inode_pages_range(struct address_space *mapping,
else
end = (lend + 1) >> PAGE_SHIFT;
+ start = round_up(start, min_nrpages);
+ if (end != (pgoff_t)-1)
+ end = round_down(end, min_nrpages);
+
folio_batch_init(&fbatch);
index = start;
while (index < end && find_lock_entries(mapping, &index, end - 1,
--
2.52.0
^ permalink raw reply [flat|nested] 21+ messages in thread
* [PATCH v4 2/4] mm/truncate: look up the end-edge straddler by index
2026-09-22 11:06 [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios Zhang Yi
2026-09-22 11:07 ` [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order Zhang Yi
@ 2026-09-22 11:07 ` Zhang Yi
2026-09-22 11:35 ` Jan Kara
2026-09-22 15:30 ` Zi Yan
2026-09-22 11:07 ` [PATCH v4 3/4] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi
` (2 subsequent siblings)
4 siblings, 2 replies; 21+ messages in thread
From: Zhang Yi @ 2026-09-22 11:07 UTC (permalink / raw)
To: linux-mm
Cc: linux-fsdevel, linux-kernel, linux-ext4, akpm, david, ljs, liam,
vbabka, rppt, surenb, mhocko, hughd, baolin.wang, willy, jack,
ziy, bfoster, joannelkoong, djwong, yi.zhang, yi.zhang,
yizhang089, yangerkun, chengzhihao1, wangkefeng.wang, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
In truncate_inode_partial_folio(), after the first split at the start
edge, folio_split() unlocks and drops the refcount of the after-split
sub-folios. The sub-folio straddling the end of the truncation range is
therefore unlocked and only transiently ref'd in the page cache while
the code still derives it from a page pointer inside the original folio.
Between the first split finishing and page_folio() resolving split_at2,
that tail page can be reclaimed, freed and reallocated as a new large
folio in the same mapping at a different file offset. folio2 then points
at a folio that does not cover the end boundary, yet
folio2->mapping == folio->mapping still holds, so the stale pointer
passes the mapping check and folio_split_or_unmap() splits a folio at a
wrong position (or, with a transient refcount, a use-after-free window
opens between try_get and the split). __folio_split()'s own
folio != page_folio(split_at) check cannot catch this either since
split_at2 has been reallocated as part of the new folio, so
page_folio(split_at2) resolves back to folio2.
Look the straddler up by its page index instead. __filemap_get_folio()
returns the folio currently covering the boundary, ref'd and locked,
with the mapping validated under the lock, so the split target is always
the real folio at the end edge.
Reported-by: Sashiko <sashiko-bot@kernel.org>
Link: https://sashiko.dev/#/message/20260916094500.C30061F00893%40smtp.kernel.org
Link: https://lore.kernel.org/linux-mm/DLGXT0ERY79Z.3C5DYVJVX6S9Z@nvidia.com/
Fixes: 7460b470a131 ("mm/truncate: use folio_split() in truncate operation")
Cc: stable@vger.kernel.org
Suggested-by: Jan Kara <jack@suse.cz>
Suggested-by: Zi Yan <ziy@nvidia.com>
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
mm/truncate.c | 30 ++++++++++++++++--------------
1 file changed, 16 insertions(+), 14 deletions(-)
diff --git a/mm/truncate.c b/mm/truncate.c
index f9625bb4916f..a8a179b38252 100644
--- a/mm/truncate.c
+++ b/mm/truncate.c
@@ -259,30 +259,32 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
* for shmem truncate
*/
struct folio *folio2;
+ pgoff_t end_idx;
if (offset + length == size)
goto no_split;
- split_at2 = folio_page(folio,
- PAGE_ALIGN_DOWN(offset + length) / PAGE_SIZE);
- folio2 = page_folio(split_at2);
-
- if (!folio_try_get(folio2))
+ /*
+ * After the first split at the start edge, the folio at the
+ * end edge may be freed and reused concurrently.
+ * __filemap_get_folio() looks up the straddler at end_idx
+ * and returns it locked and ref'd with the mapping
+ * validated.
+ */
+ end_idx = (pos + offset + length) >> PAGE_SHIFT;
+ folio2 = __filemap_get_folio(folio->mapping, end_idx,
+ FGP_LOCK | FGP_NOWAIT, 0);
+ if (IS_ERR(folio2))
goto no_split;
+ /* make sure folio2 is large */
if (!folio_test_large(folio2))
goto out;
- if (!folio_trylock(folio2))
- goto out;
-
- /* make sure folio2 is large and does not change its mapping */
- if (folio_test_large(folio2) &&
- folio2->mapping == folio->mapping)
- folio_split_or_unmap(folio2, split_at2, min_order);
-
- folio_unlock(folio2);
+ split_at2 = folio_page(folio2, (end_idx - folio2->index));
+ folio_split_or_unmap(folio2, split_at2, min_order);
out:
+ folio_unlock(folio2);
folio_put(folio2);
no_split:
return true;
--
2.52.0
^ permalink raw reply [flat|nested] 21+ messages in thread
* [PATCH v4 3/4] mm/truncate: fix data loss when splitting straddling large folios fails
2026-09-22 11:06 [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios Zhang Yi
2026-09-22 11:07 ` [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order Zhang Yi
2026-09-22 11:07 ` [PATCH v4 2/4] mm/truncate: look up the end-edge straddler by index Zhang Yi
@ 2026-09-22 11:07 ` Zhang Yi
2026-09-22 11:44 ` Jan Kara
` (2 more replies)
2026-09-22 11:07 ` [PATCH v4 4/4] mm/truncate: clarify return value of truncate_inode_partial_folio() Zhang Yi
2026-09-22 13:47 ` [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios Brian Foster
4 siblings, 3 replies; 21+ messages in thread
From: Zhang Yi @ 2026-09-22 11:07 UTC (permalink / raw)
To: linux-mm
Cc: linux-fsdevel, linux-kernel, linux-ext4, akpm, david, ljs, liam,
vbabka, rppt, surenb, mhocko, hughd, baolin.wang, willy, jack,
ziy, bfoster, joannelkoong, djwong, yi.zhang, yi.zhang,
yizhang089, yangerkun, chengzhihao1, wangkefeng.wang, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
truncate_inode_partial_folio() splits a large folio so that the caller's
truncate loop can drop the in-range sub-folios while keeping the
out-of-range tail. The first split at the punch start edge is
non-uniform, which leaves the sub-folio at the truncation end edge as
large as possible, this means it may still straddle the range, holding
both zeroed in-range and valid out-of-range data. The function then
attempts a second split at offset + length to isolate that tail.
If the second split fails the straddling sub-folio stays merged. The
function returned true unconditionally on all exit paths of the success
block, telling the caller it was fully handled. The caller kept its
default end and the truncate loop truncated every sub-folio below it,
including the merged straddler, discarding the valid out-of-range tail.
For example, a 4-page order-2 folio punched from offset 0 to the middle
of the last page:
truncate_inode_pages_range()
truncate_inode_partial_folio() # same_folio == true
1st split at page0 -> [p0, p1, p2-3] # non-uniform, success
folio2 = p2-3 # straddles: p2 zeroed, p3 tail valid
2nd split of folio2 fails / cannot lock
return true # BUG: caller keeps default end
end = 3
loop truncates p0, p1, p2-3 # p3's valid tail is lost
This became reachable after commit 7460b470a131 ("mm/truncate: use
folio_split() in truncate operation") replaced the atomic split_folio()
with folio_split(), whose non-uniform split can partially split a folio
and leave the end edge merged.
It has gone unnoticed because a dirty large folio normally carries the
filesystem's private data, for example buffer_head, so
filemap_release_folio() fails on a dirty folio and folio_split() aborts
with -EBUSY before any split, leaving the straddler safely unsplit. The
bug is only reachable on paths that produce dirty large folios without
filesystem private data, and it was caught on the upcoming ext4 iomap
buffered I/O path when no ifs is attached.
Rework the contract so the caller is told the folio range to discard:
- Add pgoff_t *pstart and *pend out-parameters that receive the folio
range fully covered by [lstart, lend] after any split (or none),
aligned inwards to min_order, i.e. the folios wholly within the
range and safe to discard.
- Report a reliable end position to the caller. The straddler is
looked up at an index aligned inwards to the mapping minimum folio
order, and *pend is set to that boundary on success. If nothing
covers the boundary, discarding up to it stays safe. If the
straddler is locked by someone else, fall back to folio->index.
This best-effort fallback may leave the in-range sub-folios to a
later pass but never discards the out-of-range tail. If the
straddler cannot be split, fall back to folio2->index so the caller
keeps the out-of-range tail.
- Rename the byte-range parameters start/end to lstart/lend to better
express their semantics.
Callers in truncate_inode_pages_range() and shmem_undo_range() pass
&pstart for the folio at the start edge and &pend for the folio at the
end edge, so the truncate loop drops exactly the fully covered pages and
never touches a straddling folio that still holds valid out-of-range
data.
Suggested-by: Brian Foster <bfoster@redhat.com>
Link: https://lore.kernel.org/linux-fsdevel/anH-WKA1coW6wtfG@bfoster/
Fixes: 7460b470a131 ("mm/truncate: use folio_split() in truncate operation")
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
mm/internal.h | 4 +--
mm/shmem.c | 13 +++-----
mm/truncate.c | 88 +++++++++++++++++++++++++++++++++++----------------
3 files changed, 68 insertions(+), 37 deletions(-)
diff --git a/mm/internal.h b/mm/internal.h
index 38b1165212c9..3278a5e360e3 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -624,8 +624,8 @@ unsigned find_lock_entries(struct address_space *mapping, pgoff_t *start,
unsigned find_get_entries(struct address_space *mapping, pgoff_t *start,
pgoff_t end, struct folio_batch *fbatch, pgoff_t *indices);
int truncate_inode_folio(struct address_space *mapping, struct folio *folio);
-bool truncate_inode_partial_folio(struct folio *folio, loff_t start,
- loff_t end);
+bool truncate_inode_partial_folio(struct folio *folio, loff_t lstart,
+ loff_t lend, pgoff_t *pstart, pgoff_t *pend);
long mapping_evict_folio(struct address_space *mapping, struct folio *folio);
unsigned long mapping_try_invalidate(struct address_space *mapping,
pgoff_t start, pgoff_t end, unsigned long *nr_failed);
diff --git a/mm/shmem.c b/mm/shmem.c
index 897fa2b61346..30e7df7d7309 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -1176,11 +1176,8 @@ static void shmem_undo_range(struct inode *inode, loff_t lstart, uoff_t lend,
if (folio) {
same_folio = lend < folio_next_pos(folio);
folio_mark_dirty(folio);
- if (!truncate_inode_partial_folio(folio, lstart, lend)) {
- start = folio_next_index(folio);
- if (same_folio)
- end = folio->index;
- }
+ truncate_inode_partial_folio(folio, lstart, lend, &start,
+ same_folio ? &end : NULL);
folio_unlock(folio);
folio_put(folio);
folio = NULL;
@@ -1190,8 +1187,7 @@ static void shmem_undo_range(struct inode *inode, loff_t lstart, uoff_t lend,
folio = shmem_get_partial_folio(inode, lend >> PAGE_SHIFT);
if (folio) {
folio_mark_dirty(folio);
- if (!truncate_inode_partial_folio(folio, lstart, lend))
- end = folio->index;
+ truncate_inode_partial_folio(folio, lstart, lend, NULL, &end);
folio_unlock(folio);
folio_put(folio);
}
@@ -1259,7 +1255,8 @@ static void shmem_undo_range(struct inode *inode, loff_t lstart, uoff_t lend,
if (!folio_test_large(folio)) {
truncate_inode_folio(mapping, folio);
- } else if (truncate_inode_partial_folio(folio, lstart, lend)) {
+ } else if (truncate_inode_partial_folio(folio,
+ lstart, lend, NULL, NULL)) {
/*
* If we split a page, reset the loop so
* that we pick up the new sub pages.
diff --git a/mm/truncate.c b/mm/truncate.c
index a8a179b38252..81fb4de6226b 100644
--- a/mm/truncate.c
+++ b/mm/truncate.c
@@ -206,30 +206,43 @@ static int folio_split_or_unmap(struct folio *folio, struct page *split_at,
/*
* Handle partial folios. The folio may be entirely within the
* range if a split has raced with us. If not, we zero the part of the
- * folio that's within the [start, end] range, and then split the folio if
+ * folio that's within the [lstart, lend] range, and then split the folio if
* it's large. split_page_range() will discard pages which now lie beyond
* i_size, and we rely on the caller to discard pages which lie within a
* newly created hole.
*
+ * When @pstart and/or @pend are non-NULL they receive the indexes of the
+ * folio range fully covered by [lstart, lend] after any split (or none),
+ * aligned inwards to min_order, i.e. the range of folios wholly within
+ * [lstart, lend] and so safe to discard.
+ *
* Returns false if splitting failed so the caller can avoid
* discarding the entire folio which is stubbornly unsplit.
*/
-bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
+bool truncate_inode_partial_folio(struct folio *folio, loff_t lstart,
+ loff_t lend, pgoff_t *pstart, pgoff_t *pend)
{
loff_t pos = folio_pos(folio);
size_t size = folio_size(folio);
unsigned int offset, length;
struct page *split_at, *split_at2;
+ unsigned long min_nrbytes;
unsigned int min_order;
- if (pos < start)
- offset = start - pos;
+ if (pos < lstart)
+ offset = lstart - pos;
else
offset = 0;
- if (pos + size <= (u64)end)
+ if (pos + size <= (u64)lend)
length = size - offset;
else
- length = end + 1 - pos - offset;
+ length = lend + 1 - pos - offset;
+
+ if (pstart)
+ *pstart = offset ? folio_next_index(folio) : folio->index;
+ if (pend)
+ *pend = (pos + size > (u64)lend) ? folio->index :
+ folio_next_index(folio);
folio_wait_writeback(folio);
if (length == size) {
@@ -251,6 +264,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
return true;
min_order = mapping_min_folio_order(folio->mapping);
+ min_nrbytes = mapping_min_folio_nrbytes(folio->mapping);
split_at = folio_page(folio, PAGE_ALIGN_DOWN(offset) / PAGE_SIZE);
if (!folio_split_or_unmap(folio, split_at, min_order)) {
/*
@@ -259,34 +273,57 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
* for shmem truncate
*/
struct folio *folio2;
- pgoff_t end_idx;
+ pgoff_t end, aligned_end = round_down(pos + offset + length,
+ min_nrbytes) >> PAGE_SHIFT;
+ if (pstart)
+ *pstart = round_up(pos + offset,
+ min_nrbytes) >> PAGE_SHIFT;
+
+ end = aligned_end;
if (offset + length == size)
- goto no_split;
+ goto out;
/*
* After the first split at the start edge, the folio at the
* end edge may be freed and reused concurrently.
- * __filemap_get_folio() looks up the straddler at end_idx
+ * __filemap_get_folio() looks up the straddler at aligned_end
* and returns it locked and ref'd with the mapping
* validated.
*/
- end_idx = (pos + offset + length) >> PAGE_SHIFT;
- folio2 = __filemap_get_folio(folio->mapping, end_idx,
+ folio2 = __filemap_get_folio(folio->mapping, aligned_end,
FGP_LOCK | FGP_NOWAIT, 0);
- if (IS_ERR(folio2))
- goto no_split;
-
- /* make sure folio2 is large */
- if (!folio_test_large(folio2))
+ if (IS_ERR(folio2)) {
+ /*
+ * No sub-folio straddles the boundary when
+ * aligned_end is empty, so discarding up to it is
+ * safe. Otherwise the straddler is locked by
+ * someone else and we cannot obtain a reliable end
+ * position, so we fall back to folio->index, which
+ * is safe but leaves the sub-folios split off at
+ * the offset edge in the page cache.
+ */
+ if (PTR_ERR(folio2) != -ENOENT)
+ end = folio->index;
goto out;
+ }
- split_at2 = folio_page(folio2, (end_idx - folio2->index));
- folio_split_or_unmap(folio2, split_at2, min_order);
-out:
+ /* Already at the minimum order, nothing to split */
+ if (folio_order(folio2) == min_order)
+ goto out_put;
+
+ split_at2 = folio_page(folio2, (aligned_end - folio2->index));
+
+ /* Split failed, keep the straddler intact */
+ if (folio_split_or_unmap(folio2, split_at2, min_order))
+ end = folio2->index;
+
+out_put:
folio_unlock(folio2);
folio_put(folio2);
-no_split:
+out:
+ if (pend)
+ *pend = end;
return true;
}
if (folio_test_dirty(folio))
@@ -421,11 +458,8 @@ void truncate_inode_pages_range(struct address_space *mapping,
folio = __filemap_get_folio(mapping, lstart >> PAGE_SHIFT, FGP_LOCK, 0);
if (!IS_ERR(folio)) {
same_folio = lend < folio_next_pos(folio);
- if (!truncate_inode_partial_folio(folio, lstart, lend)) {
- start = folio_next_index(folio);
- if (same_folio)
- end = folio->index;
- }
+ truncate_inode_partial_folio(folio, lstart, lend, &start,
+ same_folio ? &end : NULL);
folio_unlock(folio);
folio_put(folio);
folio = NULL;
@@ -435,8 +469,8 @@ void truncate_inode_pages_range(struct address_space *mapping,
folio = __filemap_get_folio(mapping, lend >> PAGE_SHIFT,
FGP_LOCK, 0);
if (!IS_ERR(folio)) {
- if (!truncate_inode_partial_folio(folio, lstart, lend))
- end = folio->index;
+ truncate_inode_partial_folio(folio, lstart, lend,
+ NULL, &end);
folio_unlock(folio);
folio_put(folio);
}
--
2.52.0
^ permalink raw reply [flat|nested] 21+ messages in thread
* [PATCH v4 4/4] mm/truncate: clarify return value of truncate_inode_partial_folio()
2026-09-22 11:06 [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios Zhang Yi
` (2 preceding siblings ...)
2026-09-22 11:07 ` [PATCH v4 3/4] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi
@ 2026-09-22 11:07 ` Zhang Yi
2026-09-22 11:58 ` Jan Kara
2026-09-22 17:28 ` Zi Yan
2026-09-22 13:47 ` [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios Brian Foster
4 siblings, 2 replies; 21+ messages in thread
From: Zhang Yi @ 2026-09-22 11:07 UTC (permalink / raw)
To: linux-mm
Cc: linux-fsdevel, linux-kernel, linux-ext4, akpm, david, ljs, liam,
vbabka, rppt, surenb, mhocko, hughd, baolin.wang, willy, jack,
ziy, bfoster, joannelkoong, djwong, yi.zhang, yi.zhang,
yizhang089, yangerkun, chengzhihao1, wangkefeng.wang, yukuai
From: Zhang Yi <yi.zhang@huawei.com>
With the earlier rework the callers no longer rely on the return value
of truncate_inode_partial_folio() to decide whether to adjust the
truncation range. The pstart/pend out-parameters carry that information
instead. The callers now only use the return value as a flag indicating
whether the loop should be reset to pick up newly split sub-folios on
the shmem path.
Return true if at least one split succeeded, and false otherwise.
This clarifies the existing confusing return value semantics.
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
---
mm/truncate.c | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/mm/truncate.c b/mm/truncate.c
index 81fb4de6226b..317cb4ae3625 100644
--- a/mm/truncate.c
+++ b/mm/truncate.c
@@ -216,8 +216,7 @@ static int folio_split_or_unmap(struct folio *folio, struct page *split_at,
* aligned inwards to min_order, i.e. the range of folios wholly within
* [lstart, lend] and so safe to discard.
*
- * Returns false if splitting failed so the caller can avoid
- * discarding the entire folio which is stubbornly unsplit.
+ * Return %true if at least one split succeeded, %false otherwise.
*/
bool truncate_inode_partial_folio(struct folio *folio, loff_t lstart,
loff_t lend, pgoff_t *pstart, pgoff_t *pend)
@@ -247,7 +246,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t lstart,
folio_wait_writeback(folio);
if (length == size) {
truncate_inode_folio(folio->mapping, folio);
- return true;
+ return false;
}
/*
@@ -261,7 +260,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t lstart,
if (folio_needs_release(folio))
folio_invalidate(folio, offset, length);
if (!folio_test_large(folio))
- return true;
+ return false;
min_order = mapping_min_folio_order(folio->mapping);
min_nrbytes = mapping_min_folio_nrbytes(folio->mapping);
@@ -326,10 +325,9 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t lstart,
*pend = end;
return true;
}
- if (folio_test_dirty(folio))
- return false;
- truncate_inode_folio(folio->mapping, folio);
- return true;
+ if (!folio_test_dirty(folio))
+ truncate_inode_folio(folio->mapping, folio);
+ return false;
}
/*
--
2.52.0
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order
2026-09-22 11:07 ` [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order Zhang Yi
@ 2026-09-22 11:32 ` Jan Kara
2026-09-22 15:29 ` Zi Yan
2026-09-24 9:31 ` Zhang Yi
2 siblings, 0 replies; 21+ messages in thread
From: Jan Kara @ 2026-09-22 11:32 UTC (permalink / raw)
To: Zhang Yi
Cc: linux-mm, linux-fsdevel, linux-kernel, linux-ext4, akpm, david,
ljs, liam, vbabka, rppt, surenb, mhocko, hughd, baolin.wang,
willy, jack, ziy, bfoster, joannelkoong, djwong, yi.zhang,
yizhang089, yangerkun, chengzhihao1, wangkefeng.wang, yukuai
On Tue 22-09-26 19:07:00, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> When the mapping has a non-zero minimum folio order (min_order),
> folio_split() in truncate_inode_partial_folio() stops at min_order
> instead of order 0, so the sub-folio containing a split point stays
> aligned to 1 << min_order rather than to a single page. The original
> boundaries in truncate_inode_pages_range() were based on page
> granularity, so either boundary could land inside the min_order chunk at
> its edge, and the truncation loop would drop that whole chunk, valid
> out-of-range tail included.
>
> For example, a 64K (order-4) folio with min_order = 2 (16K) punched from
> offset 0 to 36K:
>
> split @p0 -> [p0-p3, p4-p7, p8-p15] # non-uniform, min_order
> folio2 = p8-p15 # straddles: p8 in range, p9-p15 tail valid
> 2nd split of folio2 -> [p8-p11, p12-p15] # success
> end(old) = p9 # BUG: p9 inside [p8-p11]
> loop truncates ... p8-p11 # p9-p11's valid tail is lost
>
> It has gone unnoticed so far for two reasons. A non-zero min_order is
> only used by filesystems with a block or sector size larger than the
> page size, and those either always write back the affected range before
> punching a hole or truncating, or they carry filesystem private data on
> dirty folios (e.g. buffer_head), which makes filemap_release_folio()
> fail and folio_split() abort with -EBUSY, so the folio is never split
> and the old start/end boundaries remain valid. The bug only becomes
> reachable on paths that truncate dirty large folios without prior
> writeback and without filesystem private data, such as the upcoming ext4
> iomap buffered I/O path.
>
> Align both start (rounded up) and end (rounded down) to the mapping
> minimum folio order so they always fall on a folio boundary.
>
> Reported-by: Joanne Koong <joannelkoong@gmail.com>
> Link: https://lore.kernel.org/linux-mm/CAJnrk1bQYUe6+1ryyJur5EEnZYrC+_5AYsy=OWzVRgD4202y1g@mail.gmail.com/
> Fixes: e220917fa5077 ("mm: split a folio in minimum folio order chunks")
> Suggested-by: Zi Yan <ziy@nvidia.com>
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Looks good. Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> mm/truncate.c | 18 ++++++++++++------
> 1 file changed, 12 insertions(+), 6 deletions(-)
>
> diff --git a/mm/truncate.c b/mm/truncate.c
> index b58ba940be47..f9625bb4916f 100644
> --- a/mm/truncate.c
> +++ b/mm/truncate.c
> @@ -345,9 +345,11 @@ long mapping_evict_folio(struct address_space *mapping, struct folio *folio)
> * @lstart: offset from which to truncate
> * @lend: offset to which to truncate (inclusive)
> *
> - * Truncate the page cache, removing the pages that are between
> - * specified offsets (and zeroing out partial pages
> - * if lstart or lend + 1 is not page aligned).
> + * Truncate the page cache, removing the folios that are between specified
> + * offsets (and zeroing out partial folios if lstart or lend + 1 is not
> + * folio aligned). For mappings with a non-zero minimum folio order, the
> + * boundaries are aligned inwards to 1 << min_order so the edge sub-folio
> + * straddling the range is kept.
> *
> * Truncate takes two passes - the first pass is nonblocking. It will not
> * block on page locks and it will not block on writeback. The second pass
> @@ -374,14 +376,14 @@ void truncate_inode_pages_range(struct address_space *mapping,
> int i;
> struct folio *folio;
> bool same_folio;
> + pgoff_t min_nrpages = mapping_min_folio_nrpages(mapping);
>
> if (mapping_empty(mapping))
> return;
>
> /*
> - * 'start' and 'end' always covers the range of pages to be fully
> - * truncated. Partial pages are covered with 'partial_start' at the
> - * start of the range and 'partial_end' at the end of the range.
> + * 'start' and 'end' always covers the range of folios to be fully
> + * truncated, with both boundaries aligned inwards to 1 << min_order.
> * Note that 'end' is exclusive while 'lend' is inclusive.
> */
> start = (lstart + PAGE_SIZE - 1) >> PAGE_SHIFT;
> @@ -395,6 +397,10 @@ void truncate_inode_pages_range(struct address_space *mapping,
> else
> end = (lend + 1) >> PAGE_SHIFT;
>
> + start = round_up(start, min_nrpages);
> + if (end != (pgoff_t)-1)
> + end = round_down(end, min_nrpages);
> +
> folio_batch_init(&fbatch);
> index = start;
> while (index < end && find_lock_entries(mapping, &index, end - 1,
> --
> 2.52.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 2/4] mm/truncate: look up the end-edge straddler by index
2026-09-22 11:07 ` [PATCH v4 2/4] mm/truncate: look up the end-edge straddler by index Zhang Yi
@ 2026-09-22 11:35 ` Jan Kara
2026-09-22 15:30 ` Zi Yan
1 sibling, 0 replies; 21+ messages in thread
From: Jan Kara @ 2026-09-22 11:35 UTC (permalink / raw)
To: Zhang Yi
Cc: linux-mm, linux-fsdevel, linux-kernel, linux-ext4, akpm, david,
ljs, liam, vbabka, rppt, surenb, mhocko, hughd, baolin.wang,
willy, jack, ziy, bfoster, joannelkoong, djwong, yi.zhang,
yizhang089, yangerkun, chengzhihao1, wangkefeng.wang, yukuai
On Tue 22-09-26 19:07:01, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> In truncate_inode_partial_folio(), after the first split at the start
> edge, folio_split() unlocks and drops the refcount of the after-split
> sub-folios. The sub-folio straddling the end of the truncation range is
> therefore unlocked and only transiently ref'd in the page cache while
> the code still derives it from a page pointer inside the original folio.
>
> Between the first split finishing and page_folio() resolving split_at2,
> that tail page can be reclaimed, freed and reallocated as a new large
> folio in the same mapping at a different file offset. folio2 then points
> at a folio that does not cover the end boundary, yet
> folio2->mapping == folio->mapping still holds, so the stale pointer
> passes the mapping check and folio_split_or_unmap() splits a folio at a
> wrong position (or, with a transient refcount, a use-after-free window
> opens between try_get and the split). __folio_split()'s own
> folio != page_folio(split_at) check cannot catch this either since
> split_at2 has been reallocated as part of the new folio, so
> page_folio(split_at2) resolves back to folio2.
>
> Look the straddler up by its page index instead. __filemap_get_folio()
> returns the folio currently covering the boundary, ref'd and locked,
> with the mapping validated under the lock, so the split target is always
> the real folio at the end edge.
>
> Reported-by: Sashiko <sashiko-bot@kernel.org>
> Link: https://sashiko.dev/#/message/20260916094500.C30061F00893%40smtp.kernel.org
> Link: https://lore.kernel.org/linux-mm/DLGXT0ERY79Z.3C5DYVJVX6S9Z@nvidia.com/
> Fixes: 7460b470a131 ("mm/truncate: use folio_split() in truncate operation")
> Cc: stable@vger.kernel.org
> Suggested-by: Jan Kara <jack@suse.cz>
> Suggested-by: Zi Yan <ziy@nvidia.com>
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Looks good. Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> mm/truncate.c | 30 ++++++++++++++++--------------
> 1 file changed, 16 insertions(+), 14 deletions(-)
>
> diff --git a/mm/truncate.c b/mm/truncate.c
> index f9625bb4916f..a8a179b38252 100644
> --- a/mm/truncate.c
> +++ b/mm/truncate.c
> @@ -259,30 +259,32 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
> * for shmem truncate
> */
> struct folio *folio2;
> + pgoff_t end_idx;
>
> if (offset + length == size)
> goto no_split;
>
> - split_at2 = folio_page(folio,
> - PAGE_ALIGN_DOWN(offset + length) / PAGE_SIZE);
> - folio2 = page_folio(split_at2);
> -
> - if (!folio_try_get(folio2))
> + /*
> + * After the first split at the start edge, the folio at the
> + * end edge may be freed and reused concurrently.
> + * __filemap_get_folio() looks up the straddler at end_idx
> + * and returns it locked and ref'd with the mapping
> + * validated.
> + */
> + end_idx = (pos + offset + length) >> PAGE_SHIFT;
> + folio2 = __filemap_get_folio(folio->mapping, end_idx,
> + FGP_LOCK | FGP_NOWAIT, 0);
> + if (IS_ERR(folio2))
> goto no_split;
>
> + /* make sure folio2 is large */
> if (!folio_test_large(folio2))
> goto out;
>
> - if (!folio_trylock(folio2))
> - goto out;
> -
> - /* make sure folio2 is large and does not change its mapping */
> - if (folio_test_large(folio2) &&
> - folio2->mapping == folio->mapping)
> - folio_split_or_unmap(folio2, split_at2, min_order);
> -
> - folio_unlock(folio2);
> + split_at2 = folio_page(folio2, (end_idx - folio2->index));
> + folio_split_or_unmap(folio2, split_at2, min_order);
> out:
> + folio_unlock(folio2);
> folio_put(folio2);
> no_split:
> return true;
> --
> 2.52.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 3/4] mm/truncate: fix data loss when splitting straddling large folios fails
2026-09-22 11:07 ` [PATCH v4 3/4] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi
@ 2026-09-22 11:44 ` Jan Kara
2026-09-22 16:58 ` Zi Yan
2026-09-22 17:27 ` Zi Yan
2 siblings, 0 replies; 21+ messages in thread
From: Jan Kara @ 2026-09-22 11:44 UTC (permalink / raw)
To: Zhang Yi
Cc: linux-mm, linux-fsdevel, linux-kernel, linux-ext4, akpm, david,
ljs, liam, vbabka, rppt, surenb, mhocko, hughd, baolin.wang,
willy, jack, ziy, bfoster, joannelkoong, djwong, yi.zhang,
yizhang089, yangerkun, chengzhihao1, wangkefeng.wang, yukuai
On Tue 22-09-26 19:07:02, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> truncate_inode_partial_folio() splits a large folio so that the caller's
> truncate loop can drop the in-range sub-folios while keeping the
> out-of-range tail. The first split at the punch start edge is
> non-uniform, which leaves the sub-folio at the truncation end edge as
> large as possible, this means it may still straddle the range, holding
> both zeroed in-range and valid out-of-range data. The function then
> attempts a second split at offset + length to isolate that tail.
>
> If the second split fails the straddling sub-folio stays merged. The
> function returned true unconditionally on all exit paths of the success
> block, telling the caller it was fully handled. The caller kept its
> default end and the truncate loop truncated every sub-folio below it,
> including the merged straddler, discarding the valid out-of-range tail.
>
> For example, a 4-page order-2 folio punched from offset 0 to the middle
> of the last page:
>
> truncate_inode_pages_range()
> truncate_inode_partial_folio() # same_folio == true
> 1st split at page0 -> [p0, p1, p2-3] # non-uniform, success
> folio2 = p2-3 # straddles: p2 zeroed, p3 tail valid
> 2nd split of folio2 fails / cannot lock
> return true # BUG: caller keeps default end
> end = 3
> loop truncates p0, p1, p2-3 # p3's valid tail is lost
>
> This became reachable after commit 7460b470a131 ("mm/truncate: use
> folio_split() in truncate operation") replaced the atomic split_folio()
> with folio_split(), whose non-uniform split can partially split a folio
> and leave the end edge merged.
>
> It has gone unnoticed because a dirty large folio normally carries the
> filesystem's private data, for example buffer_head, so
> filemap_release_folio() fails on a dirty folio and folio_split() aborts
> with -EBUSY before any split, leaving the straddler safely unsplit. The
> bug is only reachable on paths that produce dirty large folios without
> filesystem private data, and it was caught on the upcoming ext4 iomap
> buffered I/O path when no ifs is attached.
>
> Rework the contract so the caller is told the folio range to discard:
>
> - Add pgoff_t *pstart and *pend out-parameters that receive the folio
> range fully covered by [lstart, lend] after any split (or none),
> aligned inwards to min_order, i.e. the folios wholly within the
> range and safe to discard.
>
> - Report a reliable end position to the caller. The straddler is
> looked up at an index aligned inwards to the mapping minimum folio
> order, and *pend is set to that boundary on success. If nothing
> covers the boundary, discarding up to it stays safe. If the
> straddler is locked by someone else, fall back to folio->index.
> This best-effort fallback may leave the in-range sub-folios to a
> later pass but never discards the out-of-range tail. If the
> straddler cannot be split, fall back to folio2->index so the caller
> keeps the out-of-range tail.
>
> - Rename the byte-range parameters start/end to lstart/lend to better
> express their semantics.
>
> Callers in truncate_inode_pages_range() and shmem_undo_range() pass
> &pstart for the folio at the start edge and &pend for the folio at the
> end edge, so the truncate loop drops exactly the fully covered pages and
> never touches a straddling folio that still holds valid out-of-range
> data.
>
> Suggested-by: Brian Foster <bfoster@redhat.com>
> Link: https://lore.kernel.org/linux-fsdevel/anH-WKA1coW6wtfG@bfoster/
> Fixes: 7460b470a131 ("mm/truncate: use folio_split() in truncate operation")
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Looks good to me. Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> mm/internal.h | 4 +--
> mm/shmem.c | 13 +++-----
> mm/truncate.c | 88 +++++++++++++++++++++++++++++++++++----------------
> 3 files changed, 68 insertions(+), 37 deletions(-)
>
> diff --git a/mm/internal.h b/mm/internal.h
> index 38b1165212c9..3278a5e360e3 100644
> --- a/mm/internal.h
> +++ b/mm/internal.h
> @@ -624,8 +624,8 @@ unsigned find_lock_entries(struct address_space *mapping, pgoff_t *start,
> unsigned find_get_entries(struct address_space *mapping, pgoff_t *start,
> pgoff_t end, struct folio_batch *fbatch, pgoff_t *indices);
> int truncate_inode_folio(struct address_space *mapping, struct folio *folio);
> -bool truncate_inode_partial_folio(struct folio *folio, loff_t start,
> - loff_t end);
> +bool truncate_inode_partial_folio(struct folio *folio, loff_t lstart,
> + loff_t lend, pgoff_t *pstart, pgoff_t *pend);
> long mapping_evict_folio(struct address_space *mapping, struct folio *folio);
> unsigned long mapping_try_invalidate(struct address_space *mapping,
> pgoff_t start, pgoff_t end, unsigned long *nr_failed);
> diff --git a/mm/shmem.c b/mm/shmem.c
> index 897fa2b61346..30e7df7d7309 100644
> --- a/mm/shmem.c
> +++ b/mm/shmem.c
> @@ -1176,11 +1176,8 @@ static void shmem_undo_range(struct inode *inode, loff_t lstart, uoff_t lend,
> if (folio) {
> same_folio = lend < folio_next_pos(folio);
> folio_mark_dirty(folio);
> - if (!truncate_inode_partial_folio(folio, lstart, lend)) {
> - start = folio_next_index(folio);
> - if (same_folio)
> - end = folio->index;
> - }
> + truncate_inode_partial_folio(folio, lstart, lend, &start,
> + same_folio ? &end : NULL);
> folio_unlock(folio);
> folio_put(folio);
> folio = NULL;
> @@ -1190,8 +1187,7 @@ static void shmem_undo_range(struct inode *inode, loff_t lstart, uoff_t lend,
> folio = shmem_get_partial_folio(inode, lend >> PAGE_SHIFT);
> if (folio) {
> folio_mark_dirty(folio);
> - if (!truncate_inode_partial_folio(folio, lstart, lend))
> - end = folio->index;
> + truncate_inode_partial_folio(folio, lstart, lend, NULL, &end);
> folio_unlock(folio);
> folio_put(folio);
> }
> @@ -1259,7 +1255,8 @@ static void shmem_undo_range(struct inode *inode, loff_t lstart, uoff_t lend,
>
> if (!folio_test_large(folio)) {
> truncate_inode_folio(mapping, folio);
> - } else if (truncate_inode_partial_folio(folio, lstart, lend)) {
> + } else if (truncate_inode_partial_folio(folio,
> + lstart, lend, NULL, NULL)) {
> /*
> * If we split a page, reset the loop so
> * that we pick up the new sub pages.
> diff --git a/mm/truncate.c b/mm/truncate.c
> index a8a179b38252..81fb4de6226b 100644
> --- a/mm/truncate.c
> +++ b/mm/truncate.c
> @@ -206,30 +206,43 @@ static int folio_split_or_unmap(struct folio *folio, struct page *split_at,
> /*
> * Handle partial folios. The folio may be entirely within the
> * range if a split has raced with us. If not, we zero the part of the
> - * folio that's within the [start, end] range, and then split the folio if
> + * folio that's within the [lstart, lend] range, and then split the folio if
> * it's large. split_page_range() will discard pages which now lie beyond
> * i_size, and we rely on the caller to discard pages which lie within a
> * newly created hole.
> *
> + * When @pstart and/or @pend are non-NULL they receive the indexes of the
> + * folio range fully covered by [lstart, lend] after any split (or none),
> + * aligned inwards to min_order, i.e. the range of folios wholly within
> + * [lstart, lend] and so safe to discard.
> + *
> * Returns false if splitting failed so the caller can avoid
> * discarding the entire folio which is stubbornly unsplit.
> */
> -bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
> +bool truncate_inode_partial_folio(struct folio *folio, loff_t lstart,
> + loff_t lend, pgoff_t *pstart, pgoff_t *pend)
> {
> loff_t pos = folio_pos(folio);
> size_t size = folio_size(folio);
> unsigned int offset, length;
> struct page *split_at, *split_at2;
> + unsigned long min_nrbytes;
> unsigned int min_order;
>
> - if (pos < start)
> - offset = start - pos;
> + if (pos < lstart)
> + offset = lstart - pos;
> else
> offset = 0;
> - if (pos + size <= (u64)end)
> + if (pos + size <= (u64)lend)
> length = size - offset;
> else
> - length = end + 1 - pos - offset;
> + length = lend + 1 - pos - offset;
> +
> + if (pstart)
> + *pstart = offset ? folio_next_index(folio) : folio->index;
> + if (pend)
> + *pend = (pos + size > (u64)lend) ? folio->index :
> + folio_next_index(folio);
>
> folio_wait_writeback(folio);
> if (length == size) {
> @@ -251,6 +264,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
> return true;
>
> min_order = mapping_min_folio_order(folio->mapping);
> + min_nrbytes = mapping_min_folio_nrbytes(folio->mapping);
> split_at = folio_page(folio, PAGE_ALIGN_DOWN(offset) / PAGE_SIZE);
> if (!folio_split_or_unmap(folio, split_at, min_order)) {
> /*
> @@ -259,34 +273,57 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
> * for shmem truncate
> */
> struct folio *folio2;
> - pgoff_t end_idx;
> + pgoff_t end, aligned_end = round_down(pos + offset + length,
> + min_nrbytes) >> PAGE_SHIFT;
>
> + if (pstart)
> + *pstart = round_up(pos + offset,
> + min_nrbytes) >> PAGE_SHIFT;
> +
> + end = aligned_end;
> if (offset + length == size)
> - goto no_split;
> + goto out;
>
> /*
> * After the first split at the start edge, the folio at the
> * end edge may be freed and reused concurrently.
> - * __filemap_get_folio() looks up the straddler at end_idx
> + * __filemap_get_folio() looks up the straddler at aligned_end
> * and returns it locked and ref'd with the mapping
> * validated.
> */
> - end_idx = (pos + offset + length) >> PAGE_SHIFT;
> - folio2 = __filemap_get_folio(folio->mapping, end_idx,
> + folio2 = __filemap_get_folio(folio->mapping, aligned_end,
> FGP_LOCK | FGP_NOWAIT, 0);
> - if (IS_ERR(folio2))
> - goto no_split;
> -
> - /* make sure folio2 is large */
> - if (!folio_test_large(folio2))
> + if (IS_ERR(folio2)) {
> + /*
> + * No sub-folio straddles the boundary when
> + * aligned_end is empty, so discarding up to it is
> + * safe. Otherwise the straddler is locked by
> + * someone else and we cannot obtain a reliable end
> + * position, so we fall back to folio->index, which
> + * is safe but leaves the sub-folios split off at
> + * the offset edge in the page cache.
> + */
> + if (PTR_ERR(folio2) != -ENOENT)
> + end = folio->index;
> goto out;
> + }
>
> - split_at2 = folio_page(folio2, (end_idx - folio2->index));
> - folio_split_or_unmap(folio2, split_at2, min_order);
> -out:
> + /* Already at the minimum order, nothing to split */
> + if (folio_order(folio2) == min_order)
> + goto out_put;
> +
> + split_at2 = folio_page(folio2, (aligned_end - folio2->index));
> +
> + /* Split failed, keep the straddler intact */
> + if (folio_split_or_unmap(folio2, split_at2, min_order))
> + end = folio2->index;
> +
> +out_put:
> folio_unlock(folio2);
> folio_put(folio2);
> -no_split:
> +out:
> + if (pend)
> + *pend = end;
> return true;
> }
> if (folio_test_dirty(folio))
> @@ -421,11 +458,8 @@ void truncate_inode_pages_range(struct address_space *mapping,
> folio = __filemap_get_folio(mapping, lstart >> PAGE_SHIFT, FGP_LOCK, 0);
> if (!IS_ERR(folio)) {
> same_folio = lend < folio_next_pos(folio);
> - if (!truncate_inode_partial_folio(folio, lstart, lend)) {
> - start = folio_next_index(folio);
> - if (same_folio)
> - end = folio->index;
> - }
> + truncate_inode_partial_folio(folio, lstart, lend, &start,
> + same_folio ? &end : NULL);
> folio_unlock(folio);
> folio_put(folio);
> folio = NULL;
> @@ -435,8 +469,8 @@ void truncate_inode_pages_range(struct address_space *mapping,
> folio = __filemap_get_folio(mapping, lend >> PAGE_SHIFT,
> FGP_LOCK, 0);
> if (!IS_ERR(folio)) {
> - if (!truncate_inode_partial_folio(folio, lstart, lend))
> - end = folio->index;
> + truncate_inode_partial_folio(folio, lstart, lend,
> + NULL, &end);
> folio_unlock(folio);
> folio_put(folio);
> }
> --
> 2.52.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 4/4] mm/truncate: clarify return value of truncate_inode_partial_folio()
2026-09-22 11:07 ` [PATCH v4 4/4] mm/truncate: clarify return value of truncate_inode_partial_folio() Zhang Yi
@ 2026-09-22 11:58 ` Jan Kara
2026-09-23 13:29 ` Zhang Yi
2026-09-22 17:28 ` Zi Yan
1 sibling, 1 reply; 21+ messages in thread
From: Jan Kara @ 2026-09-22 11:58 UTC (permalink / raw)
To: Zhang Yi
Cc: linux-mm, linux-fsdevel, linux-kernel, linux-ext4, akpm, david,
ljs, liam, vbabka, rppt, surenb, mhocko, hughd, baolin.wang,
willy, jack, ziy, bfoster, joannelkoong, djwong, yi.zhang,
yizhang089, yangerkun, chengzhihao1, wangkefeng.wang, yukuai
On Tue 22-09-26 19:07:03, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> With the earlier rework the callers no longer rely on the return value
> of truncate_inode_partial_folio() to decide whether to adjust the
> truncation range. The pstart/pend out-parameters carry that information
> instead. The callers now only use the return value as a flag indicating
> whether the loop should be reset to pick up newly split sub-folios on
> the shmem path.
>
> Return true if at least one split succeeded, and false otherwise.
> This clarifies the existing confusing return value semantics.
>
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Looks good to me. Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Just as I'm checking the only use of the return value of
truncate_inode_partial_folio() in shmem_undo_range(), the code there looks
fishy. It does:
} else if (truncate_inode_partial_folio(folio, lstart, lend)) {
/*
* If we split a page, reset the loop so
* that we pick up the new sub pages.
* Otherwise the THP was entirely
* dropped or the target range was
* zeroed, so just continue the loop as
* is.
*/
if (!folio_test_large(folio)) {
folio_unlock(folio);
index = start;
break;
}
}
However if folio was say order-2 (4 pages), lstart is inside the 3rd page,
then the split done by truncate_inode_partial_folio() can result in order-1
folio, and two order-0 folios. Hence the !folio_test_large(folio) check
fails and we won't pickup the new smaller folios so that we can free the
4th one... So I think we need to unconditionally reset the loop if
truncate_inode_partial_folio() returned true.
Honza
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios
2026-09-22 11:06 [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios Zhang Yi
` (3 preceding siblings ...)
2026-09-22 11:07 ` [PATCH v4 4/4] mm/truncate: clarify return value of truncate_inode_partial_folio() Zhang Yi
@ 2026-09-22 13:47 ` Brian Foster
2026-09-23 13:31 ` Zhang Yi
4 siblings, 1 reply; 21+ messages in thread
From: Brian Foster @ 2026-09-22 13:47 UTC (permalink / raw)
To: Zhang Yi
Cc: linux-mm, linux-fsdevel, linux-kernel, linux-ext4, akpm, david,
ljs, liam, vbabka, rppt, surenb, mhocko, hughd, baolin.wang,
willy, jack, ziy, joannelkoong, djwong, yi.zhang, yizhang089,
yangerkun, chengzhihao1, wangkefeng.wang, yukuai
On Tue, Sep 22, 2026 at 07:06:59PM +0800, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> Hello,
>
> This is the fourth version fixing data loss when truncating straddling
> large folios caught on the upcomming ext4 + iomap buffered I/O
> conversion.
>
> When truncate_inode_pages_range() punches a hole or truncates a file,
> truncate_inode_partial_folio() splits a large folio so that the caller
> can drop the in-range sub-folios while keeping the out-of-range tail
> intact. This series fixes three distinct problems in that path that
> can each lose the valid out-of-range tail of a straddling folio, plus a
> follow-up that clarifies the return value semantics.
>
> Patch 01 aligns the truncation boundaries inwards to the mapping minimum
> folio order in truncate_inode_pages_range(). With a non-zero min_order,
> folio_split() stops at min_order instead of order 0, so a boundary
> computed at page granularity can land inside a min-order-aligned
> sub-folio and the truncate loop drops that whole chunk, valid tail
> included, causing data loss.
>
> Patch 02 looks the end-edge straddler up by its page index through
> __filemap_get_folio() in truncate_inode_partial_folio(). After the
> first split the straddler is unlocked and only transiently ref'd in the
> page cache, so the page pointer derived from the original folio can be
> freed and reallocated as a different folio in the same mapping, and the
> mapping check cannot catch it, which may cause incorrect splitting and
> potential data loss.
>
> Patch 03 reworks the contract between truncate_inode_partial_folio() and
> its callers. If the second split of the straddler fails, the function
> reported success unconditionally, and the leftover incorrect end
> position could cause the truncate loop to drop that valid tail. After
> rework, it tells the caller the exact page range safe to discard via new
> pstart/pend out-parameters, so the truncate loop never touches a
> straddling folio that still holds valid out-of-range data.
>
> Patch 04 clarifies the return value semantics to "at least one split
> succeeded", which is all the shmem caller needs to decide whether to
> reset its scan loop.
>
>
> The second patch fixes a pre-existing race issue that is reachable
> today, so it is Cc'd to stable. Patches 01 and 03 require a dirty large
> folio that carries no filesystem private data, so they are not reachable
> on current filesystems. They were found while developing the upcoming
> ext4 iomap buffered I/O path[1].
>
> Thanks,
> Yi.
>
Hi Yi,
Modulo Jan's comment on patch 4, this series looks good to me. FWIW:
Reviewed-by: Brian Foster <bfoster@redhat.com>
Brian
> [1] https://lore.kernel.org/linux-fsdevel/a638a8fb-c184-4069-ae33-379ec12cd514@huaweicloud.com/
>
>
> v3->v4:
> - Move the patch that fixes data loss when min_order is non-zero to the
> first patch position, aligning start and end in
> truncate_inode_pages_range(). (Zi Yan)
> - Add patch 2, fixing the invalid folio2 issue under concurrency when
> truncate_inode_partial_folio() splits at the end position. Use
> __filemap_get_folio() to obtain a reliable folio2. (Jan Kara)
>
> v2->v3:
> - Rework the folio2 validity check logic to fix the invalid
> folio->index issue. (sashiko)
> - Clarify the pstart and pend setting logic and the corresponding
> comments to make it more readable. (Brian, Joanne)
> - Split the patch into 3 small patches. (Zi Yan)
>
> v1->v2:
> - Export pstart as a new parameter so that the generic and shmem
> truncate paths don't need to recompute the start value from the
> return value. (Brian)
> - When min_order is non-zero, align [pstart, pend] to the inner
> boundaries of the folio to ensure they do not point into the middle
> of a large folio, which could otherwise cause valid data within the
> folio to be incorrectly cleared. (Joanne)
>
> v3: https://lore.kernel.org/linux-mm/20260916092450.654408-1-yi.zhang@huaweicloud.com/
> v2: https://lore.kernel.org/linux-mm/20260909062339.473816-1-yi.zhang@huaweicloud.com/
> v1: https://lore.kernel.org/linux-mm/20260903115018.2034541-1-yi.zhang@huaweicloud.com/
>
>
> Zhang Yi (4):
> mm/truncate: align truncation boundaries to mapping minimum folio
> order
> mm/truncate: look up the end-edge straddler by index
> mm/truncate: fix data loss when splitting straddling large folios
> fails
> mm/truncate: clarify return value of truncate_inode_partial_folio()
>
> mm/internal.h | 4 +-
> mm/shmem.c | 13 ++---
> mm/truncate.c | 128 +++++++++++++++++++++++++++++++++-----------------
> 3 files changed, 91 insertions(+), 54 deletions(-)
>
> --
> 2.54.0
>
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order
2026-09-22 11:07 ` [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order Zhang Yi
2026-09-22 11:32 ` Jan Kara
@ 2026-09-22 15:29 ` Zi Yan
2026-09-23 8:26 ` Zhang Yi
2026-09-24 9:31 ` Zhang Yi
2 siblings, 1 reply; 21+ messages in thread
From: Zi Yan @ 2026-09-22 15:29 UTC (permalink / raw)
To: Zhang Yi, linux-mm
Cc: linux-fsdevel, linux-kernel, linux-ext4, akpm, david, ljs, liam,
vbabka, rppt, surenb, mhocko, hughd, baolin.wang, willy, jack,
bfoster, joannelkoong, djwong, yi.zhang, yizhang089, yangerkun,
chengzhihao1, wangkefeng.wang, yukuai
On Tue Sep 22, 2026 at 7:07 AM EDT, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> When the mapping has a non-zero minimum folio order (min_order),
> folio_split() in truncate_inode_partial_folio() stops at min_order
> instead of order 0, so the sub-folio containing a split point stays
> aligned to 1 << min_order rather than to a single page. The original
> boundaries in truncate_inode_pages_range() were based on page
> granularity, so either boundary could land inside the min_order chunk at
> its edge, and the truncation loop would drop that whole chunk, valid
> out-of-range tail included.
>
> For example, a 64K (order-4) folio with min_order = 2 (16K) punched from
> offset 0 to 36K:
>
> split @p0 -> [p0-p3, p4-p7, p8-p15] # non-uniform, min_order
> folio2 = p8-p15 # straddles: p8 in range, p9-p15 tail valid
> 2nd split of folio2 -> [p8-p11, p12-p15] # success
> end(old) = p9 # BUG: p9 inside [p8-p11]
> loop truncates ... p8-p11 # p9-p11's valid tail is lost
>
> It has gone unnoticed so far for two reasons. A non-zero min_order is
> only used by filesystems with a block or sector size larger than the
> page size, and those either always write back the affected range before
> punching a hole or truncating, or they carry filesystem private data on
> dirty folios (e.g. buffer_head), which makes filemap_release_folio()
> fail and folio_split() abort with -EBUSY, so the folio is never split
> and the old start/end boundaries remain valid. The bug only becomes
> reachable on paths that truncate dirty large folios without prior
> writeback and without filesystem private data, such as the upcoming ext4
> iomap buffered I/O path.
>
> Align both start (rounded up) and end (rounded down) to the mapping
> minimum folio order so they always fall on a folio boundary.
>
> Reported-by: Joanne Koong <joannelkoong@gmail.com>
> Link: https://lore.kernel.org/linux-mm/CAJnrk1bQYUe6+1ryyJur5EEnZYrC+_5AYsy=OWzVRgD4202y1g@mail.gmail.com/
> Fixes: e220917fa5077 ("mm: split a folio in minimum folio order chunks")
> Suggested-by: Zi Yan <ziy@nvidia.com>
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
> ---
> mm/truncate.c | 18 ++++++++++++------
> 1 file changed, 12 insertions(+), 6 deletions(-)
LGTM. Just a nit below.
Reviewed-by: Zi Yan <ziy@nvidia.com>
>
> diff --git a/mm/truncate.c b/mm/truncate.c
> index b58ba940be47..f9625bb4916f 100644
> --- a/mm/truncate.c
> +++ b/mm/truncate.c
> @@ -345,9 +345,11 @@ long mapping_evict_folio(struct address_space *mapping, struct folio *folio)
> * @lstart: offset from which to truncate
> * @lend: offset to which to truncate (inclusive)
> *
> - * Truncate the page cache, removing the pages that are between
> - * specified offsets (and zeroing out partial pages
> - * if lstart or lend + 1 is not page aligned).
> + * Truncate the page cache, removing the folios that are between specified
> + * offsets (and zeroing out partial folios if lstart or lend + 1 is not
> + * folio aligned). For mappings with a non-zero minimum folio order, the
> + * boundaries are aligned inwards to 1 << min_order so the edge sub-folio
> + * straddling the range is kept.
> *
> * Truncate takes two passes - the first pass is nonblocking. It will not
> * block on page locks and it will not block on writeback. The second pass
> @@ -374,14 +376,14 @@ void truncate_inode_pages_range(struct address_space *mapping,
> int i;
> struct folio *folio;
> bool same_folio;
> + pgoff_t min_nrpages = mapping_min_folio_nrpages(mapping);
>
It is better to put it at the top (reverse christmas tree).
--
Best Regards,
Yan, Zi
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 2/4] mm/truncate: look up the end-edge straddler by index
2026-09-22 11:07 ` [PATCH v4 2/4] mm/truncate: look up the end-edge straddler by index Zhang Yi
2026-09-22 11:35 ` Jan Kara
@ 2026-09-22 15:30 ` Zi Yan
1 sibling, 0 replies; 21+ messages in thread
From: Zi Yan @ 2026-09-22 15:30 UTC (permalink / raw)
To: Zhang Yi, linux-mm
Cc: linux-fsdevel, linux-kernel, linux-ext4, akpm, david, ljs, liam,
vbabka, rppt, surenb, mhocko, hughd, baolin.wang, willy, jack,
bfoster, joannelkoong, djwong, yi.zhang, yizhang089, yangerkun,
chengzhihao1, wangkefeng.wang, yukuai
On Tue Sep 22, 2026 at 7:07 AM EDT, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> In truncate_inode_partial_folio(), after the first split at the start
> edge, folio_split() unlocks and drops the refcount of the after-split
> sub-folios. The sub-folio straddling the end of the truncation range is
> therefore unlocked and only transiently ref'd in the page cache while
> the code still derives it from a page pointer inside the original folio.
>
> Between the first split finishing and page_folio() resolving split_at2,
> that tail page can be reclaimed, freed and reallocated as a new large
> folio in the same mapping at a different file offset. folio2 then points
> at a folio that does not cover the end boundary, yet
> folio2->mapping == folio->mapping still holds, so the stale pointer
> passes the mapping check and folio_split_or_unmap() splits a folio at a
> wrong position (or, with a transient refcount, a use-after-free window
> opens between try_get and the split). __folio_split()'s own
> folio != page_folio(split_at) check cannot catch this either since
> split_at2 has been reallocated as part of the new folio, so
> page_folio(split_at2) resolves back to folio2.
>
> Look the straddler up by its page index instead. __filemap_get_folio()
> returns the folio currently covering the boundary, ref'd and locked,
> with the mapping validated under the lock, so the split target is always
> the real folio at the end edge.
>
> Reported-by: Sashiko <sashiko-bot@kernel.org>
> Link: https://sashiko.dev/#/message/20260916094500.C30061F00893%40smtp.kernel.org
> Link: https://lore.kernel.org/linux-mm/DLGXT0ERY79Z.3C5DYVJVX6S9Z@nvidia.com/
> Fixes: 7460b470a131 ("mm/truncate: use folio_split() in truncate operation")
> Cc: stable@vger.kernel.org
> Suggested-by: Jan Kara <jack@suse.cz>
> Suggested-by: Zi Yan <ziy@nvidia.com>
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
> ---
> mm/truncate.c | 30 ++++++++++++++++--------------
> 1 file changed, 16 insertions(+), 14 deletions(-)
>
Thanks.
Acked-by: Zi Yan <ziy@nvidia.com>
--
Best Regards,
Yan, Zi
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 3/4] mm/truncate: fix data loss when splitting straddling large folios fails
2026-09-22 11:07 ` [PATCH v4 3/4] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi
2026-09-22 11:44 ` Jan Kara
@ 2026-09-22 16:58 ` Zi Yan
2026-09-22 17:27 ` Zi Yan
2 siblings, 0 replies; 21+ messages in thread
From: Zi Yan @ 2026-09-22 16:58 UTC (permalink / raw)
To: Zhang Yi, linux-mm
Cc: linux-fsdevel, linux-kernel, linux-ext4, akpm, david, ljs, liam,
vbabka, rppt, surenb, mhocko, hughd, baolin.wang, willy, jack,
bfoster, joannelkoong, djwong, yi.zhang, yizhang089, yangerkun,
chengzhihao1, wangkefeng.wang, yukuai
On Tue Sep 22, 2026 at 7:07 AM EDT, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> truncate_inode_partial_folio() splits a large folio so that the caller's
> truncate loop can drop the in-range sub-folios while keeping the
> out-of-range tail. The first split at the punch start edge is
> non-uniform, which leaves the sub-folio at the truncation end edge as
> large as possible, this means it may still straddle the range, holding
> both zeroed in-range and valid out-of-range data. The function then
> attempts a second split at offset + length to isolate that tail.
>
> If the second split fails the straddling sub-folio stays merged. The
> function returned true unconditionally on all exit paths of the success
> block, telling the caller it was fully handled. The caller kept its
> default end and the truncate loop truncated every sub-folio below it,
> including the merged straddler, discarding the valid out-of-range tail.
>
> For example, a 4-page order-2 folio punched from offset 0 to the middle
> of the last page:
>
> truncate_inode_pages_range()
> truncate_inode_partial_folio() # same_folio == true
> 1st split at page0 -> [p0, p1, p2-3] # non-uniform, success
> folio2 = p2-3 # straddles: p2 zeroed, p3 tail valid
> 2nd split of folio2 fails / cannot lock
> return true # BUG: caller keeps default end
> end = 3
> loop truncates p0, p1, p2-3 # p3's valid tail is lost
>
> This became reachable after commit 7460b470a131 ("mm/truncate: use
> folio_split() in truncate operation") replaced the atomic split_folio()
> with folio_split(), whose non-uniform split can partially split a folio
> and leave the end edge merged.
>
> It has gone unnoticed because a dirty large folio normally carries the
> filesystem's private data, for example buffer_head, so
> filemap_release_folio() fails on a dirty folio and folio_split() aborts
> with -EBUSY before any split, leaving the straddler safely unsplit. The
> bug is only reachable on paths that produce dirty large folios without
> filesystem private data, and it was caught on the upcoming ext4 iomap
> buffered I/O path when no ifs is attached.
>
> Rework the contract so the caller is told the folio range to discard:
>
> - Add pgoff_t *pstart and *pend out-parameters that receive the folio
> range fully covered by [lstart, lend] after any split (or none),
> aligned inwards to min_order, i.e. the folios wholly within the
> range and safe to discard.
>
> - Report a reliable end position to the caller. The straddler is
> looked up at an index aligned inwards to the mapping minimum folio
> order, and *pend is set to that boundary on success. If nothing
> covers the boundary, discarding up to it stays safe. If the
> straddler is locked by someone else, fall back to folio->index.
> This best-effort fallback may leave the in-range sub-folios to a
> later pass but never discards the out-of-range tail. If the
> straddler cannot be split, fall back to folio2->index so the caller
> keeps the out-of-range tail.
>
> - Rename the byte-range parameters start/end to lstart/lend to better
> express their semantics.
>
> Callers in truncate_inode_pages_range() and shmem_undo_range() pass
> &pstart for the folio at the start edge and &pend for the folio at the
> end edge, so the truncate loop drops exactly the fully covered pages and
> never touches a straddling folio that still holds valid out-of-range
> data.
>
> Suggested-by: Brian Foster <bfoster@redhat.com>
> Link: https://lore.kernel.org/linux-fsdevel/anH-WKA1coW6wtfG@bfoster/
> Fixes: 7460b470a131 ("mm/truncate: use folio_split() in truncate operation")
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
> ---
> mm/internal.h | 4 +--
> mm/shmem.c | 13 +++-----
> mm/truncate.c | 88 +++++++++++++++++++++++++++++++++++----------------
> 3 files changed, 68 insertions(+), 37 deletions(-)
>
Thanks.
Acked-by: Zi Yan <ziy@nvidia.com>
--
Best Regards,
Yan, Zi
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 3/4] mm/truncate: fix data loss when splitting straddling large folios fails
2026-09-22 11:07 ` [PATCH v4 3/4] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi
2026-09-22 11:44 ` Jan Kara
2026-09-22 16:58 ` Zi Yan
@ 2026-09-22 17:27 ` Zi Yan
2026-09-23 8:29 ` Zhang Yi
2 siblings, 1 reply; 21+ messages in thread
From: Zi Yan @ 2026-09-22 17:27 UTC (permalink / raw)
To: Zhang Yi, linux-mm
Cc: linux-fsdevel, linux-kernel, linux-ext4, akpm, david, ljs, liam,
vbabka, rppt, surenb, mhocko, hughd, baolin.wang, willy, jack,
bfoster, joannelkoong, djwong, yi.zhang, yizhang089, yangerkun,
chengzhihao1, wangkefeng.wang, yukuai
On Tue Sep 22, 2026 at 7:07 AM EDT, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> truncate_inode_partial_folio() splits a large folio so that the caller's
> truncate loop can drop the in-range sub-folios while keeping the
> out-of-range tail. The first split at the punch start edge is
> non-uniform, which leaves the sub-folio at the truncation end edge as
> large as possible, this means it may still straddle the range, holding
> both zeroed in-range and valid out-of-range data. The function then
> attempts a second split at offset + length to isolate that tail.
>
> If the second split fails the straddling sub-folio stays merged. The
> function returned true unconditionally on all exit paths of the success
> block, telling the caller it was fully handled. The caller kept its
> default end and the truncate loop truncated every sub-folio below it,
> including the merged straddler, discarding the valid out-of-range tail.
>
> For example, a 4-page order-2 folio punched from offset 0 to the middle
> of the last page:
>
> truncate_inode_pages_range()
> truncate_inode_partial_folio() # same_folio == true
> 1st split at page0 -> [p0, p1, p2-3] # non-uniform, success
> folio2 = p2-3 # straddles: p2 zeroed, p3 tail valid
> 2nd split of folio2 fails / cannot lock
> return true # BUG: caller keeps default end
> end = 3
> loop truncates p0, p1, p2-3 # p3's valid tail is lost
>
> This became reachable after commit 7460b470a131 ("mm/truncate: use
> folio_split() in truncate operation") replaced the atomic split_folio()
> with folio_split(), whose non-uniform split can partially split a folio
> and leave the end edge merged.
>
> It has gone unnoticed because a dirty large folio normally carries the
> filesystem's private data, for example buffer_head, so
> filemap_release_folio() fails on a dirty folio and folio_split() aborts
> with -EBUSY before any split, leaving the straddler safely unsplit. The
> bug is only reachable on paths that produce dirty large folios without
> filesystem private data, and it was caught on the upcoming ext4 iomap
> buffered I/O path when no ifs is attached.
>
> Rework the contract so the caller is told the folio range to discard:
>
> - Add pgoff_t *pstart and *pend out-parameters that receive the folio
> range fully covered by [lstart, lend] after any split (or none),
> aligned inwards to min_order, i.e. the folios wholly within the
> range and safe to discard.
>
> - Report a reliable end position to the caller. The straddler is
> looked up at an index aligned inwards to the mapping minimum folio
> order, and *pend is set to that boundary on success. If nothing
> covers the boundary, discarding up to it stays safe. If the
> straddler is locked by someone else, fall back to folio->index.
> This best-effort fallback may leave the in-range sub-folios to a
> later pass but never discards the out-of-range tail. If the
> straddler cannot be split, fall back to folio2->index so the caller
> keeps the out-of-range tail.
>
> - Rename the byte-range parameters start/end to lstart/lend to better
> express their semantics.
>
> Callers in truncate_inode_pages_range() and shmem_undo_range() pass
> &pstart for the folio at the start edge and &pend for the folio at the
> end edge, so the truncate loop drops exactly the fully covered pages and
> never touches a straddling folio that still holds valid out-of-range
> data.
>
> Suggested-by: Brian Foster <bfoster@redhat.com>
> Link: https://lore.kernel.org/linux-fsdevel/anH-WKA1coW6wtfG@bfoster/
> Fixes: 7460b470a131 ("mm/truncate: use folio_split() in truncate operation")
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
> ---
> mm/internal.h | 4 +--
> mm/shmem.c | 13 +++-----
> mm/truncate.c | 88 +++++++++++++++++++++++++++++++++++----------------
> 3 files changed, 68 insertions(+), 37 deletions(-)
<snip>
>
> @@ -251,6 +264,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
Add more context:
if (!folio_test_large(folio))
> return true;
>
> min_order = mapping_min_folio_order(folio->mapping);
> + min_nrbytes = mapping_min_folio_nrbytes(folio->mapping);
> split_at = folio_page(folio, PAGE_ALIGN_DOWN(offset) / PAGE_SIZE);
> if (!folio_split_or_unmap(folio, split_at, min_order)) {
> /*
> @@ -259,34 +273,57 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
> * for shmem truncate
> */
> struct folio *folio2;
> - pgoff_t end_idx;
> + pgoff_t end, aligned_end = round_down(pos + offset + length,
> + min_nrbytes) >> PAGE_SHIFT;
<snip>
> + /* Already at the minimum order, nothing to split */
> + if (folio_order(folio2) == min_order)
> + goto out_put;
In the above, folio_test_large() is used to determine whether folio
needs to be split or not, but here folio_order() == min_order is used.
Should the above "if (!folio_test_large(folio))" be changed to use
min_order check to match the check here?
--
Best Regards,
Yan, Zi
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 4/4] mm/truncate: clarify return value of truncate_inode_partial_folio()
2026-09-22 11:07 ` [PATCH v4 4/4] mm/truncate: clarify return value of truncate_inode_partial_folio() Zhang Yi
2026-09-22 11:58 ` Jan Kara
@ 2026-09-22 17:28 ` Zi Yan
1 sibling, 0 replies; 21+ messages in thread
From: Zi Yan @ 2026-09-22 17:28 UTC (permalink / raw)
To: Zhang Yi, linux-mm
Cc: linux-fsdevel, linux-kernel, linux-ext4, akpm, david, ljs, liam,
vbabka, rppt, surenb, mhocko, hughd, baolin.wang, willy, jack,
bfoster, joannelkoong, djwong, yi.zhang, yizhang089, yangerkun,
chengzhihao1, wangkefeng.wang, yukuai
On Tue Sep 22, 2026 at 7:07 AM EDT, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> With the earlier rework the callers no longer rely on the return value
> of truncate_inode_partial_folio() to decide whether to adjust the
> truncation range. The pstart/pend out-parameters carry that information
> instead. The callers now only use the return value as a flag indicating
> whether the loop should be reset to pick up newly split sub-folios on
> the shmem path.
>
> Return true if at least one split succeeded, and false otherwise.
> This clarifies the existing confusing return value semantics.
>
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
> ---
> mm/truncate.c | 14 ++++++--------
> 1 file changed, 6 insertions(+), 8 deletions(-)
>
Thanks.
Acked-by: Zi Yan <ziy@nvidia.com>
--
Best Regards,
Yan, Zi
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order
2026-09-22 15:29 ` Zi Yan
@ 2026-09-23 8:26 ` Zhang Yi
0 siblings, 0 replies; 21+ messages in thread
From: Zhang Yi @ 2026-09-23 8:26 UTC (permalink / raw)
To: Zi Yan
Cc: linux-mm, linux-fsdevel, linux-kernel, linux-ext4, akpm, david,
ljs, liam, vbabka, rppt, surenb, mhocko, hughd, baolin.wang,
willy, jack, bfoster, joannelkoong, djwong, yi.zhang, yizhang089,
yangerkun, chengzhihao1, wangkefeng.wang, yukuai
On 9/22/2026 11:29 PM, Zi Yan wrote:
> On Tue Sep 22, 2026 at 7:07 AM EDT, Zhang Yi wrote:
>> From: Zhang Yi <yi.zhang@huawei.com>
>>
>> When the mapping has a non-zero minimum folio order (min_order),
>> folio_split() in truncate_inode_partial_folio() stops at min_order
>> instead of order 0, so the sub-folio containing a split point stays
>> aligned to 1 << min_order rather than to a single page. The original
>> boundaries in truncate_inode_pages_range() were based on page
>> granularity, so either boundary could land inside the min_order chunk at
>> its edge, and the truncation loop would drop that whole chunk, valid
>> out-of-range tail included.
>>
>> For example, a 64K (order-4) folio with min_order = 2 (16K) punched from
>> offset 0 to 36K:
>>
>> split @p0 -> [p0-p3, p4-p7, p8-p15] # non-uniform, min_order
>> folio2 = p8-p15 # straddles: p8 in range, p9-p15 tail valid
>> 2nd split of folio2 -> [p8-p11, p12-p15] # success
>> end(old) = p9 # BUG: p9 inside [p8-p11]
>> loop truncates ... p8-p11 # p9-p11's valid tail is lost
>>
>> It has gone unnoticed so far for two reasons. A non-zero min_order is
>> only used by filesystems with a block or sector size larger than the
>> page size, and those either always write back the affected range before
>> punching a hole or truncating, or they carry filesystem private data on
>> dirty folios (e.g. buffer_head), which makes filemap_release_folio()
>> fail and folio_split() abort with -EBUSY, so the folio is never split
>> and the old start/end boundaries remain valid. The bug only becomes
>> reachable on paths that truncate dirty large folios without prior
>> writeback and without filesystem private data, such as the upcoming ext4
>> iomap buffered I/O path.
>>
>> Align both start (rounded up) and end (rounded down) to the mapping
>> minimum folio order so they always fall on a folio boundary.
>>
>> Reported-by: Joanne Koong <joannelkoong@gmail.com>
>> Link: https://lore.kernel.org/linux-mm/CAJnrk1bQYUe6+1ryyJur5EEnZYrC+_5AYsy=OWzVRgD4202y1g@mail.gmail.com/
>> Fixes: e220917fa5077 ("mm: split a folio in minimum folio order chunks")
>> Suggested-by: Zi Yan <ziy@nvidia.com>
>> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
>> ---
>> mm/truncate.c | 18 ++++++++++++------
>> 1 file changed, 12 insertions(+), 6 deletions(-)
>
> LGTM. Just a nit below.
>
> Reviewed-by: Zi Yan <ziy@nvidia.com>
>>
>> diff --git a/mm/truncate.c b/mm/truncate.c
>> index b58ba940be47..f9625bb4916f 100644
>> --- a/mm/truncate.c
>> +++ b/mm/truncate.c
>> @@ -345,9 +345,11 @@ long mapping_evict_folio(struct address_space *mapping, struct folio *folio)
>> * @lstart: offset from which to truncate
>> * @lend: offset to which to truncate (inclusive)
>> *
>> - * Truncate the page cache, removing the pages that are between
>> - * specified offsets (and zeroing out partial pages
>> - * if lstart or lend + 1 is not page aligned).
>> + * Truncate the page cache, removing the folios that are between specified
>> + * offsets (and zeroing out partial folios if lstart or lend + 1 is not
>> + * folio aligned). For mappings with a non-zero minimum folio order, the
>> + * boundaries are aligned inwards to 1 << min_order so the edge sub-folio
>> + * straddling the range is kept.
>> *
>> * Truncate takes two passes - the first pass is nonblocking. It will not
>> * block on page locks and it will not block on writeback. The second pass
>> @@ -374,14 +376,14 @@ void truncate_inode_pages_range(struct address_space *mapping,
>> int i;
>> struct folio *folio;
>> bool same_folio;
>> + pgoff_t min_nrpages = mapping_min_folio_nrpages(mapping);
>>
>
> It is better to put it at the top (reverse christmas tree).
>
>
Sure, will move it, thanks for the review.
Yi.
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 3/4] mm/truncate: fix data loss when splitting straddling large folios fails
2026-09-22 17:27 ` Zi Yan
@ 2026-09-23 8:29 ` Zhang Yi
0 siblings, 0 replies; 21+ messages in thread
From: Zhang Yi @ 2026-09-23 8:29 UTC (permalink / raw)
To: Zi Yan
Cc: linux-mm, linux-fsdevel, linux-kernel, linux-ext4, akpm, david,
ljs, liam, vbabka, rppt, surenb, mhocko, hughd, baolin.wang,
willy, jack, bfoster, joannelkoong, djwong, yi.zhang, yizhang089,
yangerkun, chengzhihao1, wangkefeng.wang, yukuai
On 9/23/2026 1:27 AM, Zi Yan wrote:
> On Tue Sep 22, 2026 at 7:07 AM EDT, Zhang Yi wrote:
>> From: Zhang Yi <yi.zhang@huawei.com>
>>
>> truncate_inode_partial_folio() splits a large folio so that the caller's
>> truncate loop can drop the in-range sub-folios while keeping the
>> out-of-range tail. The first split at the punch start edge is
>> non-uniform, which leaves the sub-folio at the truncation end edge as
>> large as possible, this means it may still straddle the range, holding
>> both zeroed in-range and valid out-of-range data. The function then
>> attempts a second split at offset + length to isolate that tail.
>>
>> If the second split fails the straddling sub-folio stays merged. The
>> function returned true unconditionally on all exit paths of the success
>> block, telling the caller it was fully handled. The caller kept its
>> default end and the truncate loop truncated every sub-folio below it,
>> including the merged straddler, discarding the valid out-of-range tail.
>>
>> For example, a 4-page order-2 folio punched from offset 0 to the middle
>> of the last page:
>>
>> truncate_inode_pages_range()
>> truncate_inode_partial_folio() # same_folio == true
>> 1st split at page0 -> [p0, p1, p2-3] # non-uniform, success
>> folio2 = p2-3 # straddles: p2 zeroed, p3 tail valid
>> 2nd split of folio2 fails / cannot lock
>> return true # BUG: caller keeps default end
>> end = 3
>> loop truncates p0, p1, p2-3 # p3's valid tail is lost
>>
>> This became reachable after commit 7460b470a131 ("mm/truncate: use
>> folio_split() in truncate operation") replaced the atomic split_folio()
>> with folio_split(), whose non-uniform split can partially split a folio
>> and leave the end edge merged.
>>
>> It has gone unnoticed because a dirty large folio normally carries the
>> filesystem's private data, for example buffer_head, so
>> filemap_release_folio() fails on a dirty folio and folio_split() aborts
>> with -EBUSY before any split, leaving the straddler safely unsplit. The
>> bug is only reachable on paths that produce dirty large folios without
>> filesystem private data, and it was caught on the upcoming ext4 iomap
>> buffered I/O path when no ifs is attached.
>>
>> Rework the contract so the caller is told the folio range to discard:
>>
>> - Add pgoff_t *pstart and *pend out-parameters that receive the folio
>> range fully covered by [lstart, lend] after any split (or none),
>> aligned inwards to min_order, i.e. the folios wholly within the
>> range and safe to discard.
>>
>> - Report a reliable end position to the caller. The straddler is
>> looked up at an index aligned inwards to the mapping minimum folio
>> order, and *pend is set to that boundary on success. If nothing
>> covers the boundary, discarding up to it stays safe. If the
>> straddler is locked by someone else, fall back to folio->index.
>> This best-effort fallback may leave the in-range sub-folios to a
>> later pass but never discards the out-of-range tail. If the
>> straddler cannot be split, fall back to folio2->index so the caller
>> keeps the out-of-range tail.
>>
>> - Rename the byte-range parameters start/end to lstart/lend to better
>> express their semantics.
>>
>> Callers in truncate_inode_pages_range() and shmem_undo_range() pass
>> &pstart for the folio at the start edge and &pend for the folio at the
>> end edge, so the truncate loop drops exactly the fully covered pages and
>> never touches a straddling folio that still holds valid out-of-range
>> data.
>>
>> Suggested-by: Brian Foster <bfoster@redhat.com>
>> Link: https://lore.kernel.org/linux-fsdevel/anH-WKA1coW6wtfG@bfoster/
>> Fixes: 7460b470a131 ("mm/truncate: use folio_split() in truncate operation")
>> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
>> ---
>> mm/internal.h | 4 +--
>> mm/shmem.c | 13 +++-----
>> mm/truncate.c | 88 +++++++++++++++++++++++++++++++++++----------------
>> 3 files changed, 68 insertions(+), 37 deletions(-)
>
> <snip>
>>
>> @@ -251,6 +264,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
>
> Add more context:
>
> if (!folio_test_large(folio))
>> return true;
>>
>> min_order = mapping_min_folio_order(folio->mapping);
>> + min_nrbytes = mapping_min_folio_nrbytes(folio->mapping);
>> split_at = folio_page(folio, PAGE_ALIGN_DOWN(offset) / PAGE_SIZE);
>> if (!folio_split_or_unmap(folio, split_at, min_order)) {
>> /*
>> @@ -259,34 +273,57 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end)
>> * for shmem truncate
>> */
>> struct folio *folio2;
>> - pgoff_t end_idx;
>> + pgoff_t end, aligned_end = round_down(pos + offset + length,
>> + min_nrbytes) >> PAGE_SHIFT;
>
> <snip>
>
>> + /* Already at the minimum order, nothing to split */
>> + if (folio_order(folio2) == min_order)
>> + goto out_put;
>
> In the above, folio_test_large() is used to determine whether folio
> needs to be split or not, but here folio_order() == min_order is used.
> Should the above "if (!folio_test_large(folio))" be changed to use
> min_order check to match the check here?
>
Yeah, this makes sense to me.
Thanks,
Yi.
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 4/4] mm/truncate: clarify return value of truncate_inode_partial_folio()
2026-09-22 11:58 ` Jan Kara
@ 2026-09-23 13:29 ` Zhang Yi
0 siblings, 0 replies; 21+ messages in thread
From: Zhang Yi @ 2026-09-23 13:29 UTC (permalink / raw)
To: Jan Kara, Zhang Yi
Cc: linux-mm, linux-fsdevel, linux-kernel, linux-ext4, akpm, david,
ljs, liam, vbabka, rppt, surenb, mhocko, hughd, baolin.wang,
willy, ziy, bfoster, joannelkoong, djwong, yi.zhang, yangerkun,
chengzhihao1, wangkefeng.wang, yukuai
On 9/22/2026 7:58 PM, Jan Kara wrote:
> On Tue 22-09-26 19:07:03, Zhang Yi wrote:
>> From: Zhang Yi <yi.zhang@huawei.com>
>>
>> With the earlier rework the callers no longer rely on the return value
>> of truncate_inode_partial_folio() to decide whether to adjust the
>> truncation range. The pstart/pend out-parameters carry that information
>> instead. The callers now only use the return value as a flag indicating
>> whether the loop should be reset to pick up newly split sub-folios on
>> the shmem path.
>>
>> Return true if at least one split succeeded, and false otherwise.
>> This clarifies the existing confusing return value semantics.
>>
>> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
>
> Looks good to me. Feel free to add:
>
> Reviewed-by: Jan Kara <jack@suse.cz>
>
> Just as I'm checking the only use of the return value of
> truncate_inode_partial_folio() in shmem_undo_range(), the code there looks
> fishy. It does:
>
> } else if (truncate_inode_partial_folio(folio, lstart, lend)) {
> /*
> * If we split a page, reset the loop so
> * that we pick up the new sub pages.
> * Otherwise the THP was entirely
> * dropped or the target range was
> * zeroed, so just continue the loop as
> * is.
> */
> if (!folio_test_large(folio)) {
> folio_unlock(folio);
> index = start;
> break;
> }
> }
>
> However if folio was say order-2 (4 pages), lstart is inside the 3rd page,
> then the split done by truncate_inode_partial_folio() can result in order-1
> folio, and two order-0 folios. Hence the !folio_test_large(folio) check
> fails and we won't pickup the new smaller folios so that we can free the
> 4th one... So I think we need to unconditionally reset the loop if
> truncate_inode_partial_folio() returned true.
>
> Honza
Indeed, that's a good point. Since this is a pre-existing and
independent issue, I'll send a separate patch to fix it.
Thanks,
Yi.
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios
2026-09-22 13:47 ` [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios Brian Foster
@ 2026-09-23 13:31 ` Zhang Yi
0 siblings, 0 replies; 21+ messages in thread
From: Zhang Yi @ 2026-09-23 13:31 UTC (permalink / raw)
To: Brian Foster, Zhang Yi
Cc: linux-mm, linux-fsdevel, linux-kernel, linux-ext4, akpm, david,
ljs, liam, vbabka, rppt, surenb, mhocko, hughd, baolin.wang,
willy, jack, ziy, joannelkoong, djwong, yi.zhang, yangerkun,
chengzhihao1, wangkefeng.wang, yukuai
On 9/22/2026 9:47 PM, Brian Foster wrote:
> On Tue, Sep 22, 2026 at 07:06:59PM +0800, Zhang Yi wrote:
>> From: Zhang Yi <yi.zhang@huawei.com>
>>
>> Hello,
>>
>> This is the fourth version fixing data loss when truncating straddling
>> large folios caught on the upcomming ext4 + iomap buffered I/O
>> conversion.
>>
>> When truncate_inode_pages_range() punches a hole or truncates a file,
>> truncate_inode_partial_folio() splits a large folio so that the caller
>> can drop the in-range sub-folios while keeping the out-of-range tail
>> intact. This series fixes three distinct problems in that path that
>> can each lose the valid out-of-range tail of a straddling folio, plus a
>> follow-up that clarifies the return value semantics.
>>
>> Patch 01 aligns the truncation boundaries inwards to the mapping minimum
>> folio order in truncate_inode_pages_range(). With a non-zero min_order,
>> folio_split() stops at min_order instead of order 0, so a boundary
>> computed at page granularity can land inside a min-order-aligned
>> sub-folio and the truncate loop drops that whole chunk, valid tail
>> included, causing data loss.
>>
>> Patch 02 looks the end-edge straddler up by its page index through
>> __filemap_get_folio() in truncate_inode_partial_folio(). After the
>> first split the straddler is unlocked and only transiently ref'd in the
>> page cache, so the page pointer derived from the original folio can be
>> freed and reallocated as a different folio in the same mapping, and the
>> mapping check cannot catch it, which may cause incorrect splitting and
>> potential data loss.
>>
>> Patch 03 reworks the contract between truncate_inode_partial_folio() and
>> its callers. If the second split of the straddler fails, the function
>> reported success unconditionally, and the leftover incorrect end
>> position could cause the truncate loop to drop that valid tail. After
>> rework, it tells the caller the exact page range safe to discard via new
>> pstart/pend out-parameters, so the truncate loop never touches a
>> straddling folio that still holds valid out-of-range data.
>>
>> Patch 04 clarifies the return value semantics to "at least one split
>> succeeded", which is all the shmem caller needs to decide whether to
>> reset its scan loop.
>>
>>
>> The second patch fixes a pre-existing race issue that is reachable
>> today, so it is Cc'd to stable. Patches 01 and 03 require a dirty large
>> folio that carries no filesystem private data, so they are not reachable
>> on current filesystems. They were found while developing the upcoming
>> ext4 iomap buffered I/O path[1].
>>
>> Thanks,
>> Yi.
>>
>
> Hi Yi,
>
> Modulo Jan's comment on patch 4, this series looks good to me. FWIW:
>
> Reviewed-by: Brian Foster <bfoster@redhat.com>
>
> Brian
Thank you for the review!
Yi.
>
>> [1] https://lore.kernel.org/linux-fsdevel/a638a8fb-c184-4069-ae33-379ec12cd514@huaweicloud.com/
>>
>>
>> v3->v4:
>> - Move the patch that fixes data loss when min_order is non-zero to the
>> first patch position, aligning start and end in
>> truncate_inode_pages_range(). (Zi Yan)
>> - Add patch 2, fixing the invalid folio2 issue under concurrency when
>> truncate_inode_partial_folio() splits at the end position. Use
>> __filemap_get_folio() to obtain a reliable folio2. (Jan Kara)
>>
>> v2->v3:
>> - Rework the folio2 validity check logic to fix the invalid
>> folio->index issue. (sashiko)
>> - Clarify the pstart and pend setting logic and the corresponding
>> comments to make it more readable. (Brian, Joanne)
>> - Split the patch into 3 small patches. (Zi Yan)
>>
>> v1->v2:
>> - Export pstart as a new parameter so that the generic and shmem
>> truncate paths don't need to recompute the start value from the
>> return value. (Brian)
>> - When min_order is non-zero, align [pstart, pend] to the inner
>> boundaries of the folio to ensure they do not point into the middle
>> of a large folio, which could otherwise cause valid data within the
>> folio to be incorrectly cleared. (Joanne)
>>
>> v3: https://lore.kernel.org/linux-mm/20260916092450.654408-1-yi.zhang@huaweicloud.com/
>> v2: https://lore.kernel.org/linux-mm/20260909062339.473816-1-yi.zhang@huaweicloud.com/
>> v1: https://lore.kernel.org/linux-mm/20260903115018.2034541-1-yi.zhang@huaweicloud.com/
>>
>>
>> Zhang Yi (4):
>> mm/truncate: align truncation boundaries to mapping minimum folio
>> order
>> mm/truncate: look up the end-edge straddler by index
>> mm/truncate: fix data loss when splitting straddling large folios
>> fails
>> mm/truncate: clarify return value of truncate_inode_partial_folio()
>>
>> mm/internal.h | 4 +-
>> mm/shmem.c | 13 ++---
>> mm/truncate.c | 128 +++++++++++++++++++++++++++++++++-----------------
>> 3 files changed, 91 insertions(+), 54 deletions(-)
>>
>> --
>> 2.54.0
>>
>
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order
2026-09-22 11:07 ` [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order Zhang Yi
2026-09-22 11:32 ` Jan Kara
2026-09-22 15:29 ` Zi Yan
@ 2026-09-24 9:31 ` Zhang Yi
2026-09-24 10:31 ` Jan Kara
2 siblings, 1 reply; 21+ messages in thread
From: Zhang Yi @ 2026-09-24 9:31 UTC (permalink / raw)
To: linux-mm, linux-fsdevel, jack, bfoster, ziy
Cc: linux-kernel, linux-ext4, akpm, david, ljs, liam, vbabka, rppt,
surenb, mhocko, hughd, baolin.wang, willy, joannelkoong, djwong,
yi.zhang, yizhang089, yangerkun, chengzhihao1, wangkefeng.wang,
yukuai
On 9/22/2026 7:07 PM, Zhang Yi wrote:
> From: Zhang Yi <yi.zhang@huawei.com>
>
> When the mapping has a non-zero minimum folio order (min_order),
> folio_split() in truncate_inode_partial_folio() stops at min_order
> instead of order 0, so the sub-folio containing a split point stays
> aligned to 1 << min_order rather than to a single page. The original
> boundaries in truncate_inode_pages_range() were based on page
> granularity, so either boundary could land inside the min_order chunk at
> its edge, and the truncation loop would drop that whole chunk, valid
> out-of-range tail included.
>
> For example, a 64K (order-4) folio with min_order = 2 (16K) punched from
> offset 0 to 36K:
>
> split @p0 -> [p0-p3, p4-p7, p8-p15] # non-uniform, min_order
> folio2 = p8-p15 # straddles: p8 in range, p9-p15 tail valid
> 2nd split of folio2 -> [p8-p11, p12-p15] # success
> end(old) = p9 # BUG: p9 inside [p8-p11]
> loop truncates ... p8-p11 # p9-p11's valid tail is lost
>
> It has gone unnoticed so far for two reasons. A non-zero min_order is
> only used by filesystems with a block or sector size larger than the
> page size, and those either always write back the affected range before
> punching a hole or truncating, or they carry filesystem private data on
> dirty folios (e.g. buffer_head), which makes filemap_release_folio()
> fail and folio_split() abort with -EBUSY, so the folio is never split
> and the old start/end boundaries remain valid. The bug only becomes
> reachable on paths that truncate dirty large folios without prior
> writeback and without filesystem private data, such as the upcoming ext4
> iomap buffered I/O path.
>
> Align both start (rounded up) and end (rounded down) to the mapping
> minimum folio order so they always fall on a folio boundary.
>
> Reported-by: Joanne Koong <joannelkoong@gmail.com>
> Link: https://lore.kernel.org/linux-mm/CAJnrk1bQYUe6+1ryyJur5EEnZYrC+_5AYsy=OWzVRgD4202y1g@mail.gmail.com/
> Fixes: e220917fa5077 ("mm: split a folio in minimum folio order chunks")
> Suggested-by: Zi Yan <ziy@nvidia.com>
> Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
> ---
> mm/truncate.c | 18 ++++++++++++------
> 1 file changed, 12 insertions(+), 6 deletions(-)
>
> diff --git a/mm/truncate.c b/mm/truncate.c
> index b58ba940be47..f9625bb4916f 100644
> --- a/mm/truncate.c
> +++ b/mm/truncate.c
> @@ -345,9 +345,11 @@ long mapping_evict_folio(struct address_space *mapping, struct folio *folio)
> * @lstart: offset from which to truncate
> * @lend: offset to which to truncate (inclusive)
> *
> - * Truncate the page cache, removing the pages that are between
> - * specified offsets (and zeroing out partial pages
> - * if lstart or lend + 1 is not page aligned).
> + * Truncate the page cache, removing the folios that are between specified
> + * offsets (and zeroing out partial folios if lstart or lend + 1 is not
> + * folio aligned). For mappings with a non-zero minimum folio order, the
> + * boundaries are aligned inwards to 1 << min_order so the edge sub-folio
> + * straddling the range is kept.
> *
> * Truncate takes two passes - the first pass is nonblocking. It will not
> * block on page locks and it will not block on writeback. The second pass
> @@ -374,14 +376,14 @@ void truncate_inode_pages_range(struct address_space *mapping,
> int i;
> struct folio *folio;
> bool same_folio;
> + pgoff_t min_nrpages = mapping_min_folio_nrpages(mapping);
>
> if (mapping_empty(mapping))
> return;
>
> /*
> - * 'start' and 'end' always covers the range of pages to be fully
> - * truncated. Partial pages are covered with 'partial_start' at the
> - * start of the range and 'partial_end' at the end of the range.
> + * 'start' and 'end' always covers the range of folios to be fully
> + * truncated, with both boundaries aligned inwards to 1 << min_order.
> * Note that 'end' is exclusive while 'lend' is inclusive.
> */
> start = (lstart + PAGE_SIZE - 1) >> PAGE_SHIFT;
> @@ -395,6 +397,10 @@ void truncate_inode_pages_range(struct address_space *mapping,
> else
> end = (lend + 1) >> PAGE_SHIFT;
>
> + start = round_up(start, min_nrpages);
> + if (end != (pgoff_t)-1)
> + end = round_down(end, min_nrpages);
> +
Hi, all
The sashiko pointed out an issue on this patch:
> [Severity: Critical]
> Can this integer overflow on 32-bit architectures?
>
> In truncate_inode_pages_range(), pgoff_t is a 32-bit integer on 32-bit
> systems. If a hole punch or truncate operation occurs at an offset near
> MAX_LFS_FILESIZE, the start page index will be close to 0xFFFFFFFF.
>
> When min_nrpages > 1 (e.g., on filesystems supporting min_order > 0 like
> Ext4 with bigalloc), rounding up this index wraps it to 0.
>
> Would this wrap-around cause the entire page cache for the file to be
> truncated instead of just the intended tail, leading to data loss?
This is a pre-existing issue, and it is not specific to the truncate
path.
On 32-bit systems, for a mapping with a non-zero minimum folio order,
the last min_order-aligned block of a file of MAX_LFS_FILESIZE is also
the last block of the page index space, so the folio covering it ends at
or past the last representable page index. All places that do offset
calculations on such end folios carry an overflow risk, e.g.,
folio_next_index() wraps to 0 for that folio, and the values derived
from it are wrong.
For example, on a 32-bit environment I create an ext4 filesystem with a
16KB blocksize, where the maximum file size is 0xFFFFFFFF000
(MAX_LFS_FILESIZE). If we run:
xfs_io -f -c "pwrite -b 0x10000 0xFFFFFFFC000 0x3000" /mnt/foo
The last folio starts at 0xFFFFFFFC000, but its length is 0x4000,
so calling folio_next_index() on it will overflow, leading to
unpredictable errors.
I think the root cause is that sb->s_maxbytes is set to
MAX_LFS_FILESIZE, which on 32-bit is (loff_t)ULONG_MAX << PAGE_SHIFT. A
folio is at least min_order pages and min_order aligned, so the file
must have at most ULONG_MAX + 1 - (1 << min_order) pages for the last
folio's next index to stay representable.
I suppose filesystems that use a non-zero minimum folio order should cap
sb->s_maxbytes instead of using MAX_LFS_FILESIZE directly, something
like this:
static inline loff_t max_lfs_filesize(unsigned int min_order)
{
#if BITS_PER_LONG == 32
return ((loff_t)ULONG_MAX + 1 - (1UL << min_order)) << PAGE_SHIFT;
#else
return MAX_LFS_FILESIZE;
#endif
}
Any thoughts?
Besides, I understand that since this is a pre-existing issue, it
shouldn't block the merging of this series?
Thanks,
Yi.
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order
2026-09-24 9:31 ` Zhang Yi
@ 2026-09-24 10:31 ` Jan Kara
0 siblings, 0 replies; 21+ messages in thread
From: Jan Kara @ 2026-09-24 10:31 UTC (permalink / raw)
To: Zhang Yi
Cc: linux-mm, linux-fsdevel, jack, bfoster, ziy, linux-kernel,
linux-ext4, akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
hughd, baolin.wang, willy, joannelkoong, djwong, yi.zhang,
yizhang089, yangerkun, chengzhihao1, wangkefeng.wang, yukuai
On Thu 24-09-26 17:31:40, Zhang Yi wrote:
> > @@ -395,6 +397,10 @@ void truncate_inode_pages_range(struct address_space *mapping,
> > else
> > end = (lend + 1) >> PAGE_SHIFT;
> >
> > + start = round_up(start, min_nrpages);
> > + if (end != (pgoff_t)-1)
> > + end = round_down(end, min_nrpages);
> > +
> Hi, all
>
> The sashiko pointed out an issue on this patch:
>
> > [Severity: Critical]
> > Can this integer overflow on 32-bit architectures?
> >
> > In truncate_inode_pages_range(), pgoff_t is a 32-bit integer on 32-bit
> > systems. If a hole punch or truncate operation occurs at an offset near
> > MAX_LFS_FILESIZE, the start page index will be close to 0xFFFFFFFF.
> >
> > When min_nrpages > 1 (e.g., on filesystems supporting min_order > 0 like
> > Ext4 with bigalloc), rounding up this index wraps it to 0.
> >
> > Would this wrap-around cause the entire page cache for the file to be
> > truncated instead of just the intended tail, leading to data loss?
>
> This is a pre-existing issue, and it is not specific to the truncate
> path.
>
> On 32-bit systems, for a mapping with a non-zero minimum folio order,
> the last min_order-aligned block of a file of MAX_LFS_FILESIZE is also
> the last block of the page index space, so the folio covering it ends at
> or past the last representable page index. All places that do offset
> calculations on such end folios carry an overflow risk, e.g.,
> folio_next_index() wraps to 0 for that folio, and the values derived
> from it are wrong.
>
> For example, on a 32-bit environment I create an ext4 filesystem with a
> 16KB blocksize, where the maximum file size is 0xFFFFFFFF000
> (MAX_LFS_FILESIZE). If we run:
>
> xfs_io -f -c "pwrite -b 0x10000 0xFFFFFFFC000 0x3000" /mnt/foo
>
> The last folio starts at 0xFFFFFFFC000, but its length is 0x4000,
> so calling folio_next_index() on it will overflow, leading to
> unpredictable errors.
>
>
> I think the root cause is that sb->s_maxbytes is set to
> MAX_LFS_FILESIZE, which on 32-bit is (loff_t)ULONG_MAX << PAGE_SHIFT. A
> folio is at least min_order pages and min_order aligned, so the file
> must have at most ULONG_MAX + 1 - (1 << min_order) pages for the last
> folio's next index to stay representable.
>
> I suppose filesystems that use a non-zero minimum folio order should cap
> sb->s_maxbytes instead of using MAX_LFS_FILESIZE directly, something
> like this:
>
> static inline loff_t max_lfs_filesize(unsigned int min_order)
> {
> #if BITS_PER_LONG == 32
> return ((loff_t)ULONG_MAX + 1 - (1UL << min_order)) << PAGE_SHIFT;
> #else
> return MAX_LFS_FILESIZE;
> #endif
> }
>
> Any thoughts?
Yeah, I guess it makes sense.
> Besides, I understand that since this is a pre-existing issue, it
> shouldn't block the merging of this series?
Right, I don't think this belongs to this patchset.
Honza
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 21+ messages in thread
end of thread, other threads:[~2026-09-24 10:31 UTC | newest]
Thread overview: 21+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-22 11:06 [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios Zhang Yi
2026-09-22 11:07 ` [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order Zhang Yi
2026-09-22 11:32 ` Jan Kara
2026-09-22 15:29 ` Zi Yan
2026-09-23 8:26 ` Zhang Yi
2026-09-24 9:31 ` Zhang Yi
2026-09-24 10:31 ` Jan Kara
2026-09-22 11:07 ` [PATCH v4 2/4] mm/truncate: look up the end-edge straddler by index Zhang Yi
2026-09-22 11:35 ` Jan Kara
2026-09-22 15:30 ` Zi Yan
2026-09-22 11:07 ` [PATCH v4 3/4] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi
2026-09-22 11:44 ` Jan Kara
2026-09-22 16:58 ` Zi Yan
2026-09-22 17:27 ` Zi Yan
2026-09-23 8:29 ` Zhang Yi
2026-09-22 11:07 ` [PATCH v4 4/4] mm/truncate: clarify return value of truncate_inode_partial_folio() Zhang Yi
2026-09-22 11:58 ` Jan Kara
2026-09-23 13:29 ` Zhang Yi
2026-09-22 17:28 ` Zi Yan
2026-09-22 13:47 ` [PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios Brian Foster
2026-09-23 13:31 ` Zhang Yi
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®