Re: [PATCH v4 1/4] mm/truncate: align truncation boundaries to mapping minimum folio order

From: Zhang Yi

Date: Wed Sep 23 2026 - 04:31:51 EST


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@xxxxxxxxxx>
>>
>> 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@xxxxxxxxx>
>> Link: https://lore.kernel.org/linux-mm/CAJnrk1bQYUe6+1ryyJur5EEnZYrC+_5AYsy=OWzVRgD4202y1g@xxxxxxxxxxxxxx/
>> Fixes: e220917fa5077 ("mm: split a folio in minimum folio order chunks")
>> Suggested-by: Zi Yan <ziy@xxxxxxxxxx>
>> Signed-off-by: Zhang Yi <yi.zhang@xxxxxxxxxx>
>> ---
>> mm/truncate.c | 18 ++++++++++++------
>> 1 file changed, 12 insertions(+), 6 deletions(-)
>
> LGTM. Just a nit below.
>
> Reviewed-by: Zi Yan <ziy@xxxxxxxxxx>
>>
>> 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.