| Message ID | 20260916092450.654408-3-yi.zhang@huaweicloud.com |
|---|---|
| State | New |
| Headers | show |
| Series | mm/truncate: fix data loss when truncating straddling large folios | expand |
> 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 > [ ... ] > > 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> Sashiko has reviewed this patch and found no issues. It looks great!
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,
On 9/17/2026 5:14 AM, Zi Yan wrote: > 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. Yeah, this makes sense to me, thanks for your suggestion. Yi. > > 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, >
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;