| Message ID | 20260928120833.3440834-2-yi.zhang@huaweicloud.com |
|---|---|
| State | Not Applicable |
| Headers | show |
| Series | mm/truncate: fix data loss when truncating straddling large folios | expand |
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Critical] mm: integer overflow in round_up() during truncation causes complete file data loss -- commit 583804d300760dcf60e3ec5aab6503a560ad3e7e Author: Zhang Yi <yi.zhang@huawei.com> mm/truncate: align truncation boundaries to mapping minimum folio order When the mapping has a non-zero minimum folio order, this patch changes folio_split() in truncate_inode_partial_folio() to stop at min_order instead of order 0. It also aligns both start and end to the mapping minimum folio order so they always fall on a folio boundary, preventing valid out-of-range tails from being dropped. Link: https://lore.kernel.org/linux-mm/CAJnrk1bQYUe6+1ryyJur5EEnZYrC+_5AYsy=OWzVRgD4202y1g@mail.gmail.com/ > diff --git a/mm/truncate.c b/mm/truncate.c > index b58ba940be474..8a28f4a212677 100644 > --- a/mm/truncate.c > +++ b/mm/truncate.c [ ... ] > @@ -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); [Severity: Critical] Could this round_up() result in an integer overflow that changes a very large start index to 0 on 32-bit architectures? When an unprivileged user issues fallocate(PUNCH_HOLE) or truncate at an offset near ULONG_MAX << PAGE_SHIFT (e.g., 16TB on 32-bit systems) on a filesystem with min_nrpages > 1 (like ext4 with large folio/iomap), start approaches ULONG_MAX (e.g., 0xFFFFFFFF on 32-bit x86). The macro round_up(start, min_nrpages) performs 32-bit arithmetic. With start approaching 0xFFFFFFFF, start + min_nrpages - 1 can overflow to a value less than min_nrpages, causing round_up to evaluate to 0. This would corrupt the start index to 0 in truncate_inode_pages_range(), and the subsequent loop would unintentionally truncate the entire page cache from index 0 to end, causing complete data loss of unwritten data. > + 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/28/2026 8:31 PM, sashiko-bot@kernel.org wrote: > Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: > - [Critical] mm: integer overflow in round_up() during truncation causes complete file data loss > -- As mentioned in the cover litter, this is a pre-existing issue related to the file system that needs to be addressed separately. Thanks, Yi. > > commit 583804d300760dcf60e3ec5aab6503a560ad3e7e > Author: Zhang Yi <yi.zhang@huawei.com> > > mm/truncate: align truncation boundaries to mapping minimum folio order > > When the mapping has a non-zero minimum folio order, this patch changes > folio_split() in truncate_inode_partial_folio() to stop at min_order instead > of order 0. It also aligns both start and end to the mapping minimum folio > order so they always fall on a folio boundary, preventing valid out-of-range > tails from being dropped. > > Link: https://lore.kernel.org/linux-mm/CAJnrk1bQYUe6+1ryyJur5EEnZYrC+_5AYsy=OWzVRgD4202y1g@mail.gmail.com/ > >> diff --git a/mm/truncate.c b/mm/truncate.c >> index b58ba940be474..8a28f4a212677 100644 >> --- a/mm/truncate.c >> +++ b/mm/truncate.c > [ ... ] >> @@ -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); > > [Severity: Critical] > Could this round_up() result in an integer overflow that changes a very large > start index to 0 on 32-bit architectures? > > When an unprivileged user issues fallocate(PUNCH_HOLE) or truncate at an offset > near ULONG_MAX << PAGE_SHIFT (e.g., 16TB on 32-bit systems) on a filesystem with > min_nrpages > 1 (like ext4 with large folio/iomap), start approaches > ULONG_MAX (e.g., 0xFFFFFFFF on 32-bit x86). > > The macro round_up(start, min_nrpages) performs 32-bit arithmetic. With > start approaching 0xFFFFFFFF, start + min_nrpages - 1 can overflow to a value > less than min_nrpages, causing round_up to evaluate to 0. > > This would corrupt the start index to 0 in truncate_inode_pages_range(), and > the subsequent loop would unintentionally truncate the entire page cache from > index 0 to end, causing complete data loss of unwritten data. > >> + 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 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,