* + mm-truncate-fix-data-loss-when-truncating-straddling-large-folios.patch added to mm-hotfixes-unstable branch
@ 2026-09-09 23:16 Andrew Morton
0 siblings, 0 replies; 2+ messages in thread
From: Andrew Morton @ 2026-09-09 23:16 UTC (permalink / raw)
To: mm-commits, ziy, yangerkun, willy, wangkefeng.wang, vbabka,
surenb, rppt, mhocko, ljs, liam, joannelkoong, jack, hughd,
djwong, david, chengzhihao1, bfoster, baolin.wang, yi.zhang, akpm
The patch titled
Subject: mm/truncate: fix data loss when truncating straddling large folios
has been added to the -mm mm-hotfixes-unstable branch. Its filename is
mm-truncate-fix-data-loss-when-truncating-straddling-large-folios.patch
This patch will shortly appear at
https://git.kernel.org/pub/scm/linux/kernel/git/akpm/25-new.git/tree/patches/mm-truncate-fix-data-loss-when-truncating-straddling-large-folios.patch
This patch will later appear in the mm-hotfixes-unstable branch at
git://git.kernel.org/pub/scm/linux/kernel/git/akpm/mm
Before you just go and hit "reply", please:
a) Consider who else should be cc'ed
b) Prefer to cc a suitable mailing list as well
c) Ideally: find the original patch on the mailing list and do a
reply-to-all to that, adding suitable additional cc's
*** Remember to use Documentation/process/submit-checklist.rst when testing your code ***
The -mm tree is included into linux-next via various
branches at git://git.kernel.org/pub/scm/linux/kernel/git/akpm/mm
and is updated there most days
------------------------------------------------------
From: Zhang Yi <yi.zhang@huawei.com>
Subject: mm/truncate: fix data loss when truncating straddling large folios
Date: Wed, 9 Sep 2026 14:23:39 +0800
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() -> iomap_release_folio() returns false 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.
In addition, even when both splits succeed, data can still be lost when
the mapping's minimum folio order (min_order) is non-zero. folio_split()
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 page. The original
success path left start at the page-aligned head of the range and set end
to the exact page index of the end edge, neither of which is a folio
boundary in general. Either one could land inside the large folio at its
edge, and the truncate loop would drop that straddling folio together with
its valid out-of-range tail.
For example, a 64K (order-4) folio with min_order = 2 punched from
offset 0 to 36K:
truncate_inode_pages_range()
truncate_inode_partial_folio() # same_folio == true
1st split at 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 = p9 # BUG: p9 inside [p8-p11]
loop truncates ... p8-p11 # p9-p11's valid tail is lost
Rework the contract so the caller is told the page range to discard:
- Return true only when a split occurred, false otherwise. This
clarifies the existing confusing return value semantics.
- Add pgoff_t *pstart and *pend out-parameters that receive the page
range fully covered by [lstart, lend] after any split (or none),
i.e. the pages wholly within the range and safe to discard. They are
aligned up (pstart) and down (pend) to the mapping's minimum folio
order so they always fall on a folio boundary.
- Rename the byte-range parameters start/end to lstart/lend to avoid
clashing with the new outputs and to separate byte offsets from
folio indices.
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.
Link: https://lore.kernel.org/20260909062339.473816-1-yi.zhang@huaweicloud.com
Fixes: 7460b470a131 ("mm/truncate: use folio_split() in truncate operation")
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Suggested-by: Brian Foster <bfoster@redhat.com>
Link: https://lore.kernel.org/linux-fsdevel/anH-WKA1coW6wtfG@bfoster/
Cc: Baolin Wang <baolin.wang@linux.alibaba.com>
Cc: Brian foster <bfoster@redhat.com>
Cc: "Darrick J. Wong" <djwong@kernel.org>
Cc: David Hildenbrand <david@kernel.org>
Cc: ErKun Yang <yangerkun@huawei.com>
Cc: Hugh Dickins <hughd@google.com>
Cc: Jan Kara <jack@suse.cz>
Cc: Joanne Koong <joannelkoong@gmail.com>
Cc: Kefeng Wang <wangkefeng.wang@huawei.com>
Cc: Liam R. Howlett <liam@infradead.org>
Cc: Lorenzo Stoakes <ljs@kernel.org>
Cc: Matthew Wilcox (Oracle) <willy@infradead.org>
Cc: Michal Hocko <mhocko@suse.com>
Cc: Mike Rapoport <rppt@kernel.org>
Cc: Suren Baghdasaryan <surenb@google.com>
Cc: Vlastimil Babka <vbabka@kernel.org>
Cc: Zhihao Cheng <chengzhihao1@huawei.com>
Cc: Zi Yan <ziy@nvidia.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
---
mm/internal.h | 4 +-
mm/shmem.c | 13 ++-----
mm/truncate.c | 88 ++++++++++++++++++++++++++++++++----------------
3 files changed, 67 insertions(+), 38 deletions(-)
--- a/mm/internal.h~mm-truncate-fix-data-loss-when-truncating-straddling-large-folios
+++ a/mm/internal.h
@@ -624,8 +624,8 @@ unsigned find_lock_entries(struct addres
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);
--- a/mm/shmem.c~mm-truncate-fix-data-loss-when-truncating-straddling-large-folios
+++ a/mm/shmem.c
@@ -1176,11 +1176,8 @@ static void shmem_undo_range(struct inod
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 inod
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 @@ whole_folios:
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.
--- a/mm/truncate.c~mm-truncate-fix-data-loss-when-truncating-straddling-large-folios
+++ a/mm/truncate.c
@@ -206,35 +206,41 @@ static int folio_split_or_unmap(struct f
/*
* 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.
*
- * Returns false if splitting failed so the caller can avoid
- * discarding the entire folio which is stubbornly unsplit.
+ * When @pstart and/or @pend are non-NULL they receive the indexes of the
+ * page range fully covered by [lstart, lend] after any split (or none),
+ * i.e. the range of pages that are wholly within [lstart, lend] and so safe
+ * to discard.
+ *
+ * Return %true if the folio was split, %false otherwise.
*/
-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;
folio_wait_writeback(folio);
if (length == size) {
truncate_inode_folio(folio->mapping, folio);
- return true;
+ goto no_split;
}
/*
@@ -248,9 +254,10 @@ bool truncate_inode_partial_folio(struct
if (folio_needs_release(folio))
folio_invalidate(folio, offset, length);
if (!folio_test_large(folio))
- return true;
+ goto no_split;
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,38 +266,67 @@ bool truncate_inode_partial_folio(struct
* for shmem truncate
*/
struct folio *folio2;
+ bool tail_isolated = true;
- if (offset + length == size)
- goto no_split;
+ if (pend)
+ *pend = round_down(pos + offset + length,
+ min_nrbytes) >> PAGE_SHIFT;
+ if (offset + length == size)
+ goto split;
+retry:
split_at2 = folio_page(folio,
PAGE_ALIGN_DOWN(offset + length) / PAGE_SIZE);
folio2 = page_folio(split_at2);
if (!folio_try_get(folio2))
- goto no_split;
+ goto split;
if (!folio_test_large(folio2))
goto out;
- if (!folio_trylock(folio2))
+ if (!folio_trylock(folio2)) {
+ tail_isolated = false;
goto out;
+ }
+
+ /*
+ * split_at2 may no longer belong to folio2 due to concurrent
+ * split. Retry to find the correct folio in case it's still
+ * large.
+ */
+ if (page_folio(split_at2) != folio2) {
+ folio_unlock(folio2);
+ folio_put(folio2);
+ goto retry;
+ }
/* 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);
+ folio2->mapping == folio->mapping &&
+ folio_split_or_unmap(folio2, split_at2, min_order))
+ tail_isolated = false;
folio_unlock(folio2);
out:
+ if (!tail_isolated && pend)
+ *pend = folio2->index;
folio_put(folio2);
-no_split:
+split:
+ if (pstart)
+ *pstart = round_up(pos + offset,
+ min_nrbytes) >> PAGE_SHIFT;
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);
+no_split:
+ if (pstart)
+ *pstart = offset ? folio_next_index(folio) : folio->index;
+ if (pend)
+ *pend = (pos + size > (u64)lend) ? folio->index :
+ folio_next_index(folio);
+ return false;
}
/*
@@ -413,11 +449,8 @@ void truncate_inode_pages_range(struct a
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;
@@ -427,8 +460,7 @@ void truncate_inode_pages_range(struct a
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);
}
_
Patches currently in -mm which might be from yi.zhang@huawei.com are
mm-truncate-fix-data-loss-when-truncating-straddling-large-folios.patch
^ permalink raw reply [flat|nested] 2+ messages in thread
* + mm-truncate-fix-data-loss-when-truncating-straddling-large-folios.patch added to mm-hotfixes-unstable branch
@ 2026-09-09 23:17 Andrew Morton
0 siblings, 0 replies; 2+ messages in thread
From: Andrew Morton @ 2026-09-09 23:17 UTC (permalink / raw)
To: mm-commits, ziy, yangerkun, willy, wangkefeng.wang, vbabka,
surenb, rppt, mhocko, ljs, liam, joannelkoong, jack, hughd,
djwong, david, chengzhihao1, bfoster, baolin.wang, yi.zhang, akpm
The patch titled
Subject: mm/truncate: fix data loss when truncating straddling large folios
has been added to the -mm mm-hotfixes-unstable branch. Its filename is
mm-truncate-fix-data-loss-when-truncating-straddling-large-folios.patch
This patch will shortly appear at
https://git.kernel.org/pub/scm/linux/kernel/git/akpm/25-new.git/tree/patches/mm-truncate-fix-data-loss-when-truncating-straddling-large-folios.patch
This patch will later appear in the mm-hotfixes-unstable branch at
git://git.kernel.org/pub/scm/linux/kernel/git/akpm/mm
Before you just go and hit "reply", please:
a) Consider who else should be cc'ed
b) Prefer to cc a suitable mailing list as well
c) Ideally: find the original patch on the mailing list and do a
reply-to-all to that, adding suitable additional cc's
*** Remember to use Documentation/process/submit-checklist.rst when testing your code ***
The -mm tree is included into linux-next via various
branches at git://git.kernel.org/pub/scm/linux/kernel/git/akpm/mm
and is updated there most days
------------------------------------------------------
From: Zhang Yi <yi.zhang@huawei.com>
Subject: mm/truncate: fix data loss when truncating straddling large folios
Date: Wed, 9 Sep 2026 14:23:39 +0800
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() -> iomap_release_folio() returns false 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.
In addition, even when both splits succeed, data can still be lost when
the mapping's minimum folio order (min_order) is non-zero. folio_split()
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 page. The original
success path left start at the page-aligned head of the range and set end
to the exact page index of the end edge, neither of which is a folio
boundary in general. Either one could land inside the large folio at its
edge, and the truncate loop would drop that straddling folio together with
its valid out-of-range tail.
For example, a 64K (order-4) folio with min_order = 2 punched from
offset 0 to 36K:
truncate_inode_pages_range()
truncate_inode_partial_folio() # same_folio == true
1st split at 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 = p9 # BUG: p9 inside [p8-p11]
loop truncates ... p8-p11 # p9-p11's valid tail is lost
Rework the contract so the caller is told the page range to discard:
- Return true only when a split occurred, false otherwise. This
clarifies the existing confusing return value semantics.
- Add pgoff_t *pstart and *pend out-parameters that receive the page
range fully covered by [lstart, lend] after any split (or none),
i.e. the pages wholly within the range and safe to discard. They are
aligned up (pstart) and down (pend) to the mapping's minimum folio
order so they always fall on a folio boundary.
- Rename the byte-range parameters start/end to lstart/lend to avoid
clashing with the new outputs and to separate byte offsets from
folio indices.
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.
Link: https://lore.kernel.org/20260909062339.473816-1-yi.zhang@huaweicloud.com
Fixes: 7460b470a131 ("mm/truncate: use folio_split() in truncate operation")
Signed-off-by: Zhang Yi <yi.zhang@huawei.com>
Suggested-by: Brian Foster <bfoster@redhat.com>
Link: https://lore.kernel.org/linux-fsdevel/anH-WKA1coW6wtfG@bfoster/
Reviewed-by: Jan Kara <jack@suse.cz>
Reviewed-by: Joanne Koong <joannelkoong@gmail.com>
Cc: Baolin Wang <baolin.wang@linux.alibaba.com>
Cc: Brian foster <bfoster@redhat.com>
Cc: "Darrick J. Wong" <djwong@kernel.org>
Cc: David Hildenbrand <david@kernel.org>
Cc: ErKun Yang <yangerkun@huawei.com>
Cc: Hugh Dickins <hughd@google.com>
Cc: Kefeng Wang <wangkefeng.wang@huawei.com>
Cc: Liam R. Howlett <liam@infradead.org>
Cc: Lorenzo Stoakes <ljs@kernel.org>
Cc: Matthew Wilcox (Oracle) <willy@infradead.org>
Cc: Michal Hocko <mhocko@suse.com>
Cc: Mike Rapoport <rppt@kernel.org>
Cc: Suren Baghdasaryan <surenb@google.com>
Cc: Vlastimil Babka <vbabka@kernel.org>
Cc: Zhihao Cheng <chengzhihao1@huawei.com>
Cc: Zi Yan <ziy@nvidia.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
---
mm/internal.h | 4 +-
mm/shmem.c | 13 ++-----
mm/truncate.c | 88 ++++++++++++++++++++++++++++++++----------------
3 files changed, 67 insertions(+), 38 deletions(-)
--- a/mm/internal.h~mm-truncate-fix-data-loss-when-truncating-straddling-large-folios
+++ a/mm/internal.h
@@ -624,8 +624,8 @@ unsigned find_lock_entries(struct addres
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);
--- a/mm/shmem.c~mm-truncate-fix-data-loss-when-truncating-straddling-large-folios
+++ a/mm/shmem.c
@@ -1176,11 +1176,8 @@ static void shmem_undo_range(struct inod
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 inod
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 @@ whole_folios:
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.
--- a/mm/truncate.c~mm-truncate-fix-data-loss-when-truncating-straddling-large-folios
+++ a/mm/truncate.c
@@ -206,35 +206,41 @@ static int folio_split_or_unmap(struct f
/*
* 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.
*
- * Returns false if splitting failed so the caller can avoid
- * discarding the entire folio which is stubbornly unsplit.
+ * When @pstart and/or @pend are non-NULL they receive the indexes of the
+ * page range fully covered by [lstart, lend] after any split (or none),
+ * i.e. the range of pages that are wholly within [lstart, lend] and so safe
+ * to discard.
+ *
+ * Return %true if the folio was split, %false otherwise.
*/
-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;
folio_wait_writeback(folio);
if (length == size) {
truncate_inode_folio(folio->mapping, folio);
- return true;
+ goto no_split;
}
/*
@@ -248,9 +254,10 @@ bool truncate_inode_partial_folio(struct
if (folio_needs_release(folio))
folio_invalidate(folio, offset, length);
if (!folio_test_large(folio))
- return true;
+ goto no_split;
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,38 +266,67 @@ bool truncate_inode_partial_folio(struct
* for shmem truncate
*/
struct folio *folio2;
+ bool tail_isolated = true;
- if (offset + length == size)
- goto no_split;
+ if (pend)
+ *pend = round_down(pos + offset + length,
+ min_nrbytes) >> PAGE_SHIFT;
+ if (offset + length == size)
+ goto split;
+retry:
split_at2 = folio_page(folio,
PAGE_ALIGN_DOWN(offset + length) / PAGE_SIZE);
folio2 = page_folio(split_at2);
if (!folio_try_get(folio2))
- goto no_split;
+ goto split;
if (!folio_test_large(folio2))
goto out;
- if (!folio_trylock(folio2))
+ if (!folio_trylock(folio2)) {
+ tail_isolated = false;
goto out;
+ }
+
+ /*
+ * split_at2 may no longer belong to folio2 due to concurrent
+ * split. Retry to find the correct folio in case it's still
+ * large.
+ */
+ if (page_folio(split_at2) != folio2) {
+ folio_unlock(folio2);
+ folio_put(folio2);
+ goto retry;
+ }
/* 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);
+ folio2->mapping == folio->mapping &&
+ folio_split_or_unmap(folio2, split_at2, min_order))
+ tail_isolated = false;
folio_unlock(folio2);
out:
+ if (!tail_isolated && pend)
+ *pend = folio2->index;
folio_put(folio2);
-no_split:
+split:
+ if (pstart)
+ *pstart = round_up(pos + offset,
+ min_nrbytes) >> PAGE_SHIFT;
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);
+no_split:
+ if (pstart)
+ *pstart = offset ? folio_next_index(folio) : folio->index;
+ if (pend)
+ *pend = (pos + size > (u64)lend) ? folio->index :
+ folio_next_index(folio);
+ return false;
}
/*
@@ -413,11 +449,8 @@ void truncate_inode_pages_range(struct a
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;
@@ -427,8 +460,7 @@ void truncate_inode_pages_range(struct a
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);
}
_
Patches currently in -mm which might be from yi.zhang@huawei.com are
mm-truncate-fix-data-loss-when-truncating-straddling-large-folios.patch
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-09-09 23:17 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-09 23:16 + mm-truncate-fix-data-loss-when-truncating-straddling-large-folios.patch added to mm-hotfixes-unstable branch Andrew Morton
-- strict thread matches above, loose matches on Subject: below --
2026-09-09 23:17 Andrew Morton
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.