| Message ID | 20260922110703.468389-4-yi.zhang@huaweicloud.com |
|---|---|
| State | Not Applicable |
| Headers | show |
| Series | mm/truncate: fix data loss when truncating straddling large folios | expand |
> 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. > [ ... ] > > 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> Sashiko has reviewed this patch and found no issues. It looks great!
On Tue 22-09-26 19:07:02, 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() fails 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 folio range to discard: > > - Add pgoff_t *pstart and *pend out-parameters that receive the folio > range fully covered by [lstart, lend] after any split (or none), > aligned inwards to min_order, i.e. the folios wholly within the > range and safe to discard. > > - Report a reliable end position to the caller. The straddler is > looked up at an index aligned inwards to the mapping minimum folio > order, and *pend is set to that boundary on success. If nothing > covers the boundary, discarding up to it stays safe. If the > straddler is locked by someone else, fall back to folio->index. > This best-effort fallback may leave the in-range sub-folios to a > later pass but never discards the out-of-range tail. If the > straddler cannot be split, fall back to folio2->index so the caller > keeps the out-of-range tail. > > - 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> Looks good to me. Feel free to add: Reviewed-by: Jan Kara <jack@suse.cz> Honza > --- > mm/internal.h | 4 +-- > mm/shmem.c | 13 +++----- > mm/truncate.c | 88 +++++++++++++++++++++++++++++++++++---------------- > 3 files changed, 68 insertions(+), 37 deletions(-) > > diff --git a/mm/internal.h b/mm/internal.h > index 38b1165212c9..3278a5e360e3 100644 > --- a/mm/internal.h > +++ b/mm/internal.h > @@ -624,8 +624,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 897fa2b61346..30e7df7d7309 100644 > --- a/mm/shmem.c > +++ b/mm/shmem.c > @@ -1176,11 +1176,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; > @@ -1190,8 +1187,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); > } > @@ -1259,7 +1255,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 a8a179b38252..81fb4de6226b 100644 > --- a/mm/truncate.c > +++ b/mm/truncate.c > @@ -206,30 +206,43 @@ 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 > + * folio range fully covered by [lstart, lend] after any split (or none), > + * 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. > */ > -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); > unsigned int offset, length; > struct page *split_at, *split_at2; > + unsigned long min_nrbytes; > 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) { > @@ -251,6 +264,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) > 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)) { > /* > @@ -259,34 +273,57 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) > * for shmem truncate > */ > struct folio *folio2; > - pgoff_t end_idx; > + pgoff_t end, aligned_end = round_down(pos + offset + length, > + min_nrbytes) >> PAGE_SHIFT; > > + if (pstart) > + *pstart = round_up(pos + offset, > + min_nrbytes) >> PAGE_SHIFT; > + > + end = aligned_end; > if (offset + length == size) > - goto no_split; > + goto out; > > /* > * After the first split at the start edge, the folio at the > * end edge may be freed and reused concurrently. > - * __filemap_get_folio() looks up the straddler at end_idx > + * __filemap_get_folio() looks up the straddler at aligned_end > * and returns it locked and ref'd with the mapping > * validated. > */ > - end_idx = (pos + offset + length) >> PAGE_SHIFT; > - folio2 = __filemap_get_folio(folio->mapping, end_idx, > + folio2 = __filemap_get_folio(folio->mapping, aligned_end, > FGP_LOCK | FGP_NOWAIT, 0); > - if (IS_ERR(folio2)) > - goto no_split; > - > - /* make sure folio2 is large */ > - if (!folio_test_large(folio2)) > + if (IS_ERR(folio2)) { > + /* > + * No sub-folio straddles the boundary when > + * aligned_end is empty, so discarding up to it is > + * safe. Otherwise the straddler is locked by > + * someone else and we cannot obtain a reliable end > + * position, so we fall back to folio->index, which > + * is safe but leaves the sub-folios split off at > + * the offset edge in the page cache. > + */ > + if (PTR_ERR(folio2) != -ENOENT) > + end = folio->index; > goto out; > + } > > - split_at2 = folio_page(folio2, (end_idx - folio2->index)); > - folio_split_or_unmap(folio2, split_at2, min_order); > -out: > + /* Already at the minimum order, nothing to split */ > + if (folio_order(folio2) == min_order) > + goto out_put; > + > + split_at2 = folio_page(folio2, (aligned_end - folio2->index)); > + > + /* Split failed, keep the straddler intact */ > + if (folio_split_or_unmap(folio2, split_at2, min_order)) > + end = folio2->index; > + > +out_put: > folio_unlock(folio2); > folio_put(folio2); > -no_split: > +out: > + if (pend) > + *pend = end; > return true; > } > if (folio_test_dirty(folio)) > @@ -421,11 +458,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; > @@ -435,8 +469,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 >
On Tue Sep 22, 2026 at 7:07 AM EDT, 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() fails 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 folio range to discard: > > - Add pgoff_t *pstart and *pend out-parameters that receive the folio > range fully covered by [lstart, lend] after any split (or none), > aligned inwards to min_order, i.e. the folios wholly within the > range and safe to discard. > > - Report a reliable end position to the caller. The straddler is > looked up at an index aligned inwards to the mapping minimum folio > order, and *pend is set to that boundary on success. If nothing > covers the boundary, discarding up to it stays safe. If the > straddler is locked by someone else, fall back to folio->index. > This best-effort fallback may leave the in-range sub-folios to a > later pass but never discards the out-of-range tail. If the > straddler cannot be split, fall back to folio2->index so the caller > keeps the out-of-range tail. > > - 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 | 88 +++++++++++++++++++++++++++++++++++---------------- > 3 files changed, 68 insertions(+), 37 deletions(-) > Thanks. Acked-by: Zi Yan <ziy@nvidia.com>
On Tue Sep 22, 2026 at 7:07 AM EDT, 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() fails 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 folio range to discard: > > - Add pgoff_t *pstart and *pend out-parameters that receive the folio > range fully covered by [lstart, lend] after any split (or none), > aligned inwards to min_order, i.e. the folios wholly within the > range and safe to discard. > > - Report a reliable end position to the caller. The straddler is > looked up at an index aligned inwards to the mapping minimum folio > order, and *pend is set to that boundary on success. If nothing > covers the boundary, discarding up to it stays safe. If the > straddler is locked by someone else, fall back to folio->index. > This best-effort fallback may leave the in-range sub-folios to a > later pass but never discards the out-of-range tail. If the > straddler cannot be split, fall back to folio2->index so the caller > keeps the out-of-range tail. > > - 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 | 88 +++++++++++++++++++++++++++++++++++---------------- > 3 files changed, 68 insertions(+), 37 deletions(-) <snip> > > @@ -251,6 +264,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) Add more context: if (!folio_test_large(folio)) > 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)) { > /* > @@ -259,34 +273,57 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) > * for shmem truncate > */ > struct folio *folio2; > - pgoff_t end_idx; > + pgoff_t end, aligned_end = round_down(pos + offset + length, > + min_nrbytes) >> PAGE_SHIFT; <snip> > + /* Already at the minimum order, nothing to split */ > + if (folio_order(folio2) == min_order) > + goto out_put; In the above, folio_test_large() is used to determine whether folio needs to be split or not, but here folio_order() == min_order is used. Should the above "if (!folio_test_large(folio))" be changed to use min_order check to match the check here?
On 9/23/2026 1:27 AM, Zi Yan wrote: > On Tue Sep 22, 2026 at 7:07 AM EDT, 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() fails 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 folio range to discard: >> >> - Add pgoff_t *pstart and *pend out-parameters that receive the folio >> range fully covered by [lstart, lend] after any split (or none), >> aligned inwards to min_order, i.e. the folios wholly within the >> range and safe to discard. >> >> - Report a reliable end position to the caller. The straddler is >> looked up at an index aligned inwards to the mapping minimum folio >> order, and *pend is set to that boundary on success. If nothing >> covers the boundary, discarding up to it stays safe. If the >> straddler is locked by someone else, fall back to folio->index. >> This best-effort fallback may leave the in-range sub-folios to a >> later pass but never discards the out-of-range tail. If the >> straddler cannot be split, fall back to folio2->index so the caller >> keeps the out-of-range tail. >> >> - 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 | 88 +++++++++++++++++++++++++++++++++++---------------- >> 3 files changed, 68 insertions(+), 37 deletions(-) > > <snip> >> >> @@ -251,6 +264,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) > > Add more context: > > if (!folio_test_large(folio)) >> 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)) { >> /* >> @@ -259,34 +273,57 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) >> * for shmem truncate >> */ >> struct folio *folio2; >> - pgoff_t end_idx; >> + pgoff_t end, aligned_end = round_down(pos + offset + length, >> + min_nrbytes) >> PAGE_SHIFT; > > <snip> > >> + /* Already at the minimum order, nothing to split */ >> + if (folio_order(folio2) == min_order) >> + goto out_put; > > In the above, folio_test_large() is used to determine whether folio > needs to be split or not, but here folio_order() == min_order is used. > Should the above "if (!folio_test_large(folio))" be changed to use > min_order check to match the check here? > Yeah, this makes sense to me. Thanks, Yi.
diff --git a/mm/internal.h b/mm/internal.h index 38b1165212c9..3278a5e360e3 100644 --- a/mm/internal.h +++ b/mm/internal.h @@ -624,8 +624,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 897fa2b61346..30e7df7d7309 100644 --- a/mm/shmem.c +++ b/mm/shmem.c @@ -1176,11 +1176,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; @@ -1190,8 +1187,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); } @@ -1259,7 +1255,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 a8a179b38252..81fb4de6226b 100644 --- a/mm/truncate.c +++ b/mm/truncate.c @@ -206,30 +206,43 @@ 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 + * folio range fully covered by [lstart, lend] after any split (or none), + * 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. */ -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); unsigned int offset, length; struct page *split_at, *split_at2; + unsigned long min_nrbytes; 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) { @@ -251,6 +264,7 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) 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)) { /* @@ -259,34 +273,57 @@ bool truncate_inode_partial_folio(struct folio *folio, loff_t start, loff_t end) * for shmem truncate */ struct folio *folio2; - pgoff_t end_idx; + pgoff_t end, aligned_end = round_down(pos + offset + length, + min_nrbytes) >> PAGE_SHIFT; + if (pstart) + *pstart = round_up(pos + offset, + min_nrbytes) >> PAGE_SHIFT; + + end = aligned_end; if (offset + length == size) - goto no_split; + goto out; /* * After the first split at the start edge, the folio at the * end edge may be freed and reused concurrently. - * __filemap_get_folio() looks up the straddler at end_idx + * __filemap_get_folio() looks up the straddler at aligned_end * and returns it locked and ref'd with the mapping * validated. */ - end_idx = (pos + offset + length) >> PAGE_SHIFT; - folio2 = __filemap_get_folio(folio->mapping, end_idx, + folio2 = __filemap_get_folio(folio->mapping, aligned_end, FGP_LOCK | FGP_NOWAIT, 0); - if (IS_ERR(folio2)) - goto no_split; - - /* make sure folio2 is large */ - if (!folio_test_large(folio2)) + if (IS_ERR(folio2)) { + /* + * No sub-folio straddles the boundary when + * aligned_end is empty, so discarding up to it is + * safe. Otherwise the straddler is locked by + * someone else and we cannot obtain a reliable end + * position, so we fall back to folio->index, which + * is safe but leaves the sub-folios split off at + * the offset edge in the page cache. + */ + if (PTR_ERR(folio2) != -ENOENT) + end = folio->index; goto out; + } - split_at2 = folio_page(folio2, (end_idx - folio2->index)); - folio_split_or_unmap(folio2, split_at2, min_order); -out: + /* Already at the minimum order, nothing to split */ + if (folio_order(folio2) == min_order) + goto out_put; + + split_at2 = folio_page(folio2, (aligned_end - folio2->index)); + + /* Split failed, keep the straddler intact */ + if (folio_split_or_unmap(folio2, split_at2, min_order)) + end = folio2->index; + +out_put: folio_unlock(folio2); folio_put(folio2); -no_split: +out: + if (pend) + *pend = end; return true; } if (folio_test_dirty(folio)) @@ -421,11 +458,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; @@ -435,8 +469,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); }