* [PATCH v3 0/3] mm/truncate: fix data loss when truncating straddling large folios
@ 2026-09-16 9:24 Zhang Yi
2026-09-16 9:24 ` [PATCH v3 1/3] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi
` (2 more replies)
0 siblings, 3 replies; 8+ messages in thread
From: Zhang Yi @ 2026-09-16 9:24 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 third version fixing data loss when truncating straddling
large folios caught on the upcomming ext4 + iomap buffered I/O
conversion[1]. I've clarify the split logic as Joanne and Brian
suggested, also slove the concurrent issue pointed out by sashiko and
split the patch into 3 small ones as Zi Yan suggested.
Patch 1: Add pstart and pend parameters to
truncate_inode_partial_folio() and fix the data loss issue when
the second splitting straddling large folios fails.
Patch 2: Fix the similar data loss issue also could encountered on the
filesystem with min_order > 0.
Patch 3: Clarify the existing confusing return value of
truncate_inode_partial_folio().
Thanks,
Yi.
[1] https://lore.kernel.org/linux-fsdevel/a638a8fb-c184-4069-ae33-379ec12cd514@huaweicloud.com/
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 nonzero, 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)
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 (3):
mm/truncate: fix data loss when splitting straddling large folios
fails
mm/truncate: align truncation boundaries to mapping minimum folio
order
mm/truncate: clarify return value of truncate_inode_partial_folio()
mm/internal.h | 4 +-
mm/shmem.c | 13 +++----
mm/truncate.c | 105 ++++++++++++++++++++++++++++++++++----------------
3 files changed, 79 insertions(+), 43 deletions(-)
--
2.52.0
^ permalink raw reply [flat|nested] 8+ messages in thread* [PATCH v3 1/3] mm/truncate: fix data loss when splitting straddling large folios fails 2026-09-16 9:24 [PATCH v3 0/3] mm/truncate: fix data loss when truncating straddling large folios Zhang Yi @ 2026-09-16 9:24 ` Zhang Yi 2026-09-16 12:02 ` Jan Kara 2026-09-16 17:38 ` Brian Foster 2026-09-16 9:24 ` [PATCH v3 2/3] mm/truncate: align truncation boundaries to mapping minimum folio order Zhang Yi 2026-09-16 9:24 ` [PATCH v3 3/3] mm/truncate: clarify return value of truncate_inode_partial_folio() Zhang Yi 2 siblings, 2 replies; 8+ messages in thread From: Zhang Yi @ 2026-09-16 9:24 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() -> 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. Rework the contract so the caller is told the page range to discard: - 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. - Adjust the ordering of the validate check when splitting folio2. folio2->index is only reliable after the reference count and lock have been successfully acquired, since it may have been split concurrently, or freed and recycled to an unrelated mapping. On any failure to obtain a reliable end position, fall back to folio->index, which is safe but leaves the sub-folios split off at the offset edge in the page cache. - 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 | 89 ++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 71 insertions(+), 35 deletions(-) diff --git a/mm/internal.h b/mm/internal.h index 68db5abd0a4c..6e6ad3187378 100644 --- a/mm/internal.h +++ b/mm/internal.h @@ -627,8 +627,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 89a1495e55f7..712dc3effe02 100644 --- a/mm/shmem.c +++ b/mm/shmem.c @@ -1175,11 +1175,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; @@ -1189,8 +1186,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); } @@ -1258,7 +1254,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 b58ba940be47..bec6d881d022 100644 --- a/mm/truncate.c +++ b/mm/truncate.c @@ -206,15 +206,21 @@ 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 + * page range fully covered by [lstart, lend] after any split (or none), + * i.e. the range of pages 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); @@ -222,14 +228,20 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) struct page *split_at, *split_at2; 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) { @@ -259,32 +271,62 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) * for shmem truncate */ struct folio *folio2; + pgoff_t end, aligned_end = (pos + offset + length) >> + PAGE_SHIFT; - if (offset + length == size) - goto no_split; + if (pstart) + *pstart = round_up(pos + offset, PAGE_SIZE) >> + PAGE_SHIFT; + + if (offset + length == size) { + end = aligned_end; + goto out; + } split_at2 = folio_page(folio, PAGE_ALIGN_DOWN(offset + length) / PAGE_SIZE); folio2 = page_folio(split_at2); + /* + * folio2 may become stale due to a concurrent split or + * freeing, so validate it before and after taking its lock. + * If it fails, we can't get an accurate end position and fall + * back to folio->index, which may leave sub-folios split off + * at the offset edge in the page cache this round. + */ + end = folio->index; if (!folio_try_get(folio2)) - goto no_split; - - if (!folio_test_large(folio2)) goto out; + if (folio2->mapping != folio->mapping || + !folio_test_large(folio2)) + goto out_put; + if (!folio_trylock(folio2)) - goto out; + goto out_put; - /* 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); + if (page_folio(split_at2) != folio2) { + folio_unlock(folio2); + goto out_put; + } + if (!folio_test_large(folio2)) { + end = aligned_end; + folio_unlock(folio2); + goto out_put; + } + + /* Split failed: back off to the head of the straddler */ + if (folio_split_or_unmap(folio2, split_at2, min_order)) + end = folio2->index; + else + end = aligned_end; folio_unlock(folio2); -out: +out_put: folio_put(folio2); -no_split: +out: + if (pend) + *pend = end; return true; } if (folio_test_dirty(folio)) @@ -413,11 +455,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; @@ -427,8 +466,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] 8+ messages in thread
* Re: [PATCH v3 1/3] mm/truncate: fix data loss when splitting straddling large folios fails 2026-09-16 9:24 ` [PATCH v3 1/3] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi @ 2026-09-16 12:02 ` Jan Kara 2026-09-16 18:03 ` Zi Yan 2026-09-16 17:38 ` Brian Foster 1 sibling, 1 reply; 8+ messages in thread From: Jan Kara @ 2026-09-16 12:02 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 Wed 16-09-26 17:24:48, 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() -> 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. > > Rework the contract so the caller is told the page range to discard: > > - 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. > > - Adjust the ordering of the validate check when splitting folio2. > folio2->index is only reliable after the reference count and lock > have been successfully acquired, since it may have been split > concurrently, or freed and recycled to an unrelated mapping. On any > failure to obtain a reliable end position, fall back to > folio->index, which is safe but leaves the sub-folios split off at > the offset edge in the page cache. > > - 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> The changes mostly look good to me but I have some confusion around the folio2 splitting below. > @@ -259,32 +271,62 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) > * for shmem truncate > */ > struct folio *folio2; > + pgoff_t end, aligned_end = (pos + offset + length) >> > + PAGE_SHIFT; > > - if (offset + length == size) > - goto no_split; > + if (pstart) > + *pstart = round_up(pos + offset, PAGE_SIZE) >> > + PAGE_SHIFT; > + > + if (offset + length == size) { > + end = aligned_end; > + goto out; > + } > > split_at2 = folio_page(folio, > PAGE_ALIGN_DOWN(offset + length) / PAGE_SIZE); > folio2 = page_folio(split_at2); > > + /* > + * folio2 may become stale due to a concurrent split or > + * freeing, so validate it before and after taking its lock. > + * If it fails, we can't get an accurate end position and fall > + * back to folio->index, which may leave sub-folios split off > + * at the offset edge in the page cache this round. > + */ > + end = folio->index; > if (!folio_try_get(folio2)) > - goto no_split; > - > - if (!folio_test_large(folio2)) > goto out; > > + if (folio2->mapping != folio->mapping || > + !folio_test_large(folio2)) > + goto out_put; > + > if (!folio_trylock(folio2)) > - goto out; > + goto out_put; > > - /* 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); > + if (page_folio(split_at2) != folio2) { > + folio_unlock(folio2); > + goto out_put; > + } > + if (!folio_test_large(folio2)) { > + end = aligned_end; > + folio_unlock(folio2); > + goto out_put; > + } So I always found this folio2 lookup and revalidation somewhat suspicious and now that we're digging into it I'll ask: As you note in the changelog, by the time we compute split_at2 the page can be already freed and reused because it was split off from the original 'folio'. It can be for example a slab page or anything else. So is it guaranteed that page_folio() actually returns something sensible? What guarantees we properly detect the "reuse for something else" case in all possible cases for which the page can be reused? I understand this is mostly a preexisting issue so maybe these questions are more for MM guys than you... So for me as an filesystem guy I'd appreciate some comment in this code explaining why grabbing folio2 this way is actually safe. The really safe way of getting to folio2 would be to use __filemap_get_folio(mapping, (offset+length) >> PAGE_SHIFT, FGP_LOCK, 0) but I suppose we don't use the mapping lookup as it is more expensive? Honza > + > + /* Split failed: back off to the head of the straddler */ > + if (folio_split_or_unmap(folio2, split_at2, min_order)) > + end = folio2->index; > + else > + end = aligned_end; > > folio_unlock(folio2); > -out: > +out_put: > folio_put(folio2); > -no_split: > +out: > + if (pend) > + *pend = end; > return true; > } > if (folio_test_dirty(folio)) > @@ -413,11 +455,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; > @@ -427,8 +466,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] 8+ messages in thread
* Re: [PATCH v3 1/3] mm/truncate: fix data loss when splitting straddling large folios fails 2026-09-16 12:02 ` Jan Kara @ 2026-09-16 18:03 ` Zi Yan 0 siblings, 0 replies; 8+ messages in thread From: Zi Yan @ 2026-09-16 18:03 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, bfoster, joannelkoong, djwong, yi.zhang, yizhang089, yangerkun, chengzhihao1, wangkefeng.wang, yukuai On Wed Sep 16, 2026 at 8:02 AM EDT, Jan Kara wrote: > On Wed 16-09-26 17:24:48, 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() -> 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. >> >> Rework the contract so the caller is told the page range to discard: >> >> - 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. >> >> - Adjust the ordering of the validate check when splitting folio2. >> folio2->index is only reliable after the reference count and lock >> have been successfully acquired, since it may have been split >> concurrently, or freed and recycled to an unrelated mapping. On any >> failure to obtain a reliable end position, fall back to >> folio->index, which is safe but leaves the sub-folios split off at >> the offset edge in the page cache. >> >> - 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> > > The changes mostly look good to me but I have some confusion around the > folio2 splitting below. > >> @@ -259,32 +271,62 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) >> * for shmem truncate >> */ >> struct folio *folio2; >> + pgoff_t end, aligned_end = (pos + offset + length) >> >> + PAGE_SHIFT; >> >> - if (offset + length == size) >> - goto no_split; >> + if (pstart) >> + *pstart = round_up(pos + offset, PAGE_SIZE) >> >> + PAGE_SHIFT; >> + >> + if (offset + length == size) { >> + end = aligned_end; >> + goto out; >> + } >> >> split_at2 = folio_page(folio, >> PAGE_ALIGN_DOWN(offset + length) / PAGE_SIZE); >> folio2 = page_folio(split_at2); >> >> + /* >> + * folio2 may become stale due to a concurrent split or >> + * freeing, so validate it before and after taking its lock. >> + * If it fails, we can't get an accurate end position and fall >> + * back to folio->index, which may leave sub-folios split off >> + * at the offset edge in the page cache this round. >> + */ >> + end = folio->index; >> if (!folio_try_get(folio2)) >> - goto no_split; >> - >> - if (!folio_test_large(folio2)) >> goto out; >> >> + if (folio2->mapping != folio->mapping || >> + !folio_test_large(folio2)) >> + goto out_put; >> + >> if (!folio_trylock(folio2)) >> - goto out; >> + goto out_put; >> >> - /* 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); >> + if (page_folio(split_at2) != folio2) { >> + folio_unlock(folio2); >> + goto out_put; >> + } >> + if (!folio_test_large(folio2)) { >> + end = aligned_end; >> + folio_unlock(folio2); >> + goto out_put; >> + } > > So I always found this folio2 lookup and revalidation somewhat suspicious > and now that we're digging into it I'll ask: As you note in the changelog, > by the time we compute split_at2 the page can be already freed and reused > because it was split off from the original 'folio'. It can be for example a > slab page or anything else. So is it guaranteed that page_folio() actually For a slab or unrelated page, folio->mapping check rejects them. > returns something sensible? What guarantees we properly detect the "reuse No, but folio_try_get() prevents non ref'd folios. > for something else" case in all possible cases for which the page can be > reused? I understand this is mostly a preexisting issue so maybe these > questions are more for MM guys than you... I agree with you that trying to split a unlocked and not ref'd folio2 is flaky. And it almost does a __filemap_get_folio() like you proposed below. > > So for me as an filesystem guy I'd appreciate some comment in this code > explaining why grabbing folio2 this way is actually safe. The really safe > way of getting to folio2 would be to use > __filemap_get_folio(mapping, (offset+length) >> PAGE_SHIFT, FGP_LOCK, 0) > but I suppose we don't use the mapping lookup as it is more expensive? __filemap_get_folio() is a better version of the existing folio2 finding code. When I wrote the code, I never thought about using __filemap_get_folio(), but I probably should have done it. BTW, __filemap_get_folio(mapping, folio->index + (offset+length) >> PAGE_SHIFT, FGP_LOCK | FGP_NOWAIT, 0) might be better: 1. folio->index is needed to get the right folio2, 2. nowait can us nonblocking. >> + >> + /* Split failed: back off to the head of the straddler */ >> + if (folio_split_or_unmap(folio2, split_at2, min_order)) >> + end = folio2->index; >> + else >> + end = aligned_end; >> >> folio_unlock(folio2); >> -out: >> +out_put: >> folio_put(folio2); >> -no_split: >> +out: >> + if (pend) >> + *pend = end; >> return true; >> } >> if (folio_test_dirty(folio)) >> @@ -413,11 +455,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; >> @@ -427,8 +466,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 >> -- Best Regards, Yan, Zi ^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v3 1/3] mm/truncate: fix data loss when splitting straddling large folios fails 2026-09-16 9:24 ` [PATCH v3 1/3] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi 2026-09-16 12:02 ` Jan Kara @ 2026-09-16 17:38 ` Brian Foster 1 sibling, 0 replies; 8+ messages in thread From: Brian Foster @ 2026-09-16 17:38 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 Wed, Sep 16, 2026 at 05:24:48PM +0800, 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() -> 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. > > Rework the contract so the caller is told the page range to discard: > > - 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. > > - Adjust the ordering of the validate check when splitting folio2. > folio2->index is only reliable after the reference count and lock > have been successfully acquired, since it may have been split > concurrently, or freed and recycled to an unrelated mapping. On any > failure to obtain a reliable end position, fall back to > folio->index, which is safe but leaves the sub-folios split off at > the offset edge in the page cache. > > - 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 | 89 ++++++++++++++++++++++++++++++++++++--------------- > 3 files changed, 71 insertions(+), 35 deletions(-) > ... > diff --git a/mm/truncate.c b/mm/truncate.c > index b58ba940be47..bec6d881d022 100644 > --- a/mm/truncate.c > +++ b/mm/truncate.c ... > @@ -259,32 +271,62 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) > * for shmem truncate > */ > struct folio *folio2; > + pgoff_t end, aligned_end = (pos + offset + length) >> > + PAGE_SHIFT; > > - if (offset + length == size) > - goto no_split; > + if (pstart) > + *pstart = round_up(pos + offset, PAGE_SIZE) >> > + PAGE_SHIFT; > + > + if (offset + length == size) { > + end = aligned_end; > + goto out; > + } > > split_at2 = folio_page(folio, > PAGE_ALIGN_DOWN(offset + length) / PAGE_SIZE); > folio2 = page_folio(split_at2); > > + /* > + * folio2 may become stale due to a concurrent split or > + * freeing, so validate it before and after taking its lock. > + * If it fails, we can't get an accurate end position and fall > + * back to folio->index, which may leave sub-folios split off > + * at the offset edge in the page cache this round. > + */ > + end = folio->index; > if (!folio_try_get(folio2)) > - goto no_split; > - > - if (!folio_test_large(folio2)) > goto out; > > + if (folio2->mapping != folio->mapping || > + !folio_test_large(folio2)) > + goto out_put; > + Hi Zhang, The only thing that sticks out to me in this version is the !folio_test_large() check above. If we split (or something happens) such that folio2 is no longer large, wouldn't it be more likely to catch it here and fail to update end like we do in the same check a bit further down after the folio lock? Maybe I'm missing something here, but it seems a little odd for the same condition to return two possible states like this. Otherwise I like the changes you've made and the rest of the patch LGTM. Brian > if (!folio_trylock(folio2)) > - goto out; > + goto out_put; > > - /* 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); > + if (page_folio(split_at2) != folio2) { > + folio_unlock(folio2); > + goto out_put; > + } > + if (!folio_test_large(folio2)) { > + end = aligned_end; > + folio_unlock(folio2); > + goto out_put; > + } > + > + /* Split failed: back off to the head of the straddler */ > + if (folio_split_or_unmap(folio2, split_at2, min_order)) > + end = folio2->index; > + else > + end = aligned_end; > > folio_unlock(folio2); > -out: > +out_put: > folio_put(folio2); > -no_split: > +out: > + if (pend) > + *pend = end; > return true; > } > if (folio_test_dirty(folio)) > @@ -413,11 +455,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; > @@ -427,8 +466,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] 8+ messages in thread
* [PATCH v3 2/3] mm/truncate: align truncation boundaries to mapping minimum folio order 2026-09-16 9:24 [PATCH v3 0/3] mm/truncate: fix data loss when truncating straddling large folios Zhang Yi 2026-09-16 9:24 ` [PATCH v3 1/3] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi @ 2026-09-16 9:24 ` Zhang Yi 2026-09-16 21:14 ` Zi Yan 2026-09-16 9:24 ` [PATCH v3 3/3] mm/truncate: clarify return value of truncate_inode_partial_folio() Zhang Yi 2 siblings, 1 reply; 8+ messages in thread From: Zhang Yi @ 2026-09-16 9:24 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() 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 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") Signed-off-by: Zhang Yi <yi.zhang@huawei.com> --- mm/truncate.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mm/truncate.c b/mm/truncate.c index bec6d881d022..5ab7a40b1e25 100644 --- a/mm/truncate.c +++ b/mm/truncate.c @@ -213,8 +213,8 @@ static int folio_split_or_unmap(struct folio *folio, struct page *split_at, * * 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 wholly within [lstart, lend] and so safe to - * discard. + * 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. @@ -226,6 +226,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t lstart, 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 < lstart) @@ -263,6 +264,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t lstart, 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)) { /* @@ -271,12 +273,12 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t lstart, * for shmem truncate */ struct folio *folio2; - pgoff_t end, aligned_end = (pos + offset + length) >> - PAGE_SHIFT; + pgoff_t end, aligned_end = round_down(pos + offset + length, + min_nrbytes) >> PAGE_SHIFT; if (pstart) - *pstart = round_up(pos + offset, PAGE_SIZE) >> - PAGE_SHIFT; + *pstart = round_up(pos + offset, + min_nrbytes) >> PAGE_SHIFT; if (offset + length == size) { end = aligned_end; -- 2.52.0 ^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v3 2/3] mm/truncate: align truncation boundaries to mapping minimum folio order 2026-09-16 9:24 ` [PATCH v3 2/3] mm/truncate: align truncation boundaries to mapping minimum folio order Zhang Yi @ 2026-09-16 21:14 ` Zi Yan 0 siblings, 0 replies; 8+ messages in thread From: Zi Yan @ 2026-09-16 21:14 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 Wed Sep 16, 2026 at 5:24 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() 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 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") > Signed-off-by: Zhang Yi <yi.zhang@huawei.com> > --- > mm/truncate.c | 14 ++++++++------ > 1 file changed, 8 insertions(+), 6 deletions(-) > This should be the first patch, since Fixes is older than the one in patch 1. It can adjust start and end in truncate_inode_pages_range() instead, since shmem's min_order is always 0. Something like below. Am I missing anything? Thanks. diff --git a/mm/truncate.c b/mm/truncate.c index b58ba940be474..803ee61ebf624 100644 --- a/mm/truncate.c +++ b/mm/truncate.c @@ -374,6 +374,7 @@ 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; @@ -395,6 +396,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, -- Best Regards, Yan, Zi ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 3/3] mm/truncate: clarify return value of truncate_inode_partial_folio() 2026-09-16 9:24 [PATCH v3 0/3] mm/truncate: fix data loss when truncating straddling large folios Zhang Yi 2026-09-16 9:24 ` [PATCH v3 1/3] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi 2026-09-16 9:24 ` [PATCH v3 2/3] mm/truncate: align truncation boundaries to mapping minimum folio order Zhang Yi @ 2026-09-16 9:24 ` Zhang Yi 2 siblings, 0 replies; 8+ messages in thread From: Zhang Yi @ 2026-09-16 9:24 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 5ab7a40b1e25..23c90f00b530 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); @@ -331,10 +330,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] 8+ messages in thread
end of thread, other threads:[~2026-09-16 21:15 UTC | newest] Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-09-16 9:24 [PATCH v3 0/3] mm/truncate: fix data loss when truncating straddling large folios Zhang Yi 2026-09-16 9:24 ` [PATCH v3 1/3] mm/truncate: fix data loss when splitting straddling large folios fails Zhang Yi 2026-09-16 12:02 ` Jan Kara 2026-09-16 18:03 ` Zi Yan 2026-09-16 17:38 ` Brian Foster 2026-09-16 9:24 ` [PATCH v3 2/3] mm/truncate: align truncation boundaries to mapping minimum folio order Zhang Yi 2026-09-16 21:14 ` Zi Yan 2026-09-16 9:24 ` [PATCH v3 3/3] mm/truncate: clarify return value of truncate_inode_partial_folio() 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®