From: Zhang Yi 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 Link: https://lore.kernel.org/linux-mm/CAJnrk1bQYUe6+1ryyJur5EEnZYrC+_5AYsy=OWzVRgD4202y1g@mail.gmail.com/ Fixes: e220917fa5077 ("mm: split a folio in minimum folio order chunks") Suggested-by: Zi Yan Reviewed-by: Jan Kara Reviewed-by: Brian Foster Reviewed-by: Zi Yan Signed-off-by: Zhang Yi --- mm/truncate.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/mm/truncate.c b/mm/truncate.c index b58ba940be47..8a28f4a21267 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 @@ -366,6 +368,7 @@ long mapping_evict_folio(struct address_space *mapping, struct folio *folio) void truncate_inode_pages_range(struct address_space *mapping, loff_t lstart, uoff_t lend) { + pgoff_t min_nrpages = mapping_min_folio_nrpages(mapping); pgoff_t start; /* inclusive */ pgoff_t end; /* exclusive */ struct folio_batch fbatch; @@ -379,9 +382,8 @@ void truncate_inode_pages_range(struct address_space *mapping, return; /* - * 'start' and 'end' always covers the range of pages to be fully - * truncated. Partial pages are covered with 'partial_start' at the - * start of the range and 'partial_end' at the end of the range. + * 'start' and 'end' always covers the range of folios to be fully + * truncated, with both boundaries aligned inwards to 1 << min_order. * Note that 'end' is exclusive while 'lend' is inclusive. */ start = (lstart + PAGE_SIZE - 1) >> PAGE_SHIFT; @@ -395,6 +397,10 @@ void truncate_inode_pages_range(struct address_space *mapping, else end = (lend + 1) >> PAGE_SHIFT; + start = round_up(start, min_nrpages); + if (end != (pgoff_t)-1) + end = round_down(end, min_nrpages); + folio_batch_init(&fbatch); index = start; while (index < end && find_lock_entries(mapping, &index, end - 1, -- 2.52.0