[PATCH v4 0/4] mm/truncate: fix data loss when truncating straddling large folios

From: Zhang Yi

Date: Tue Sep 22 2026 - 07:16:01 EST


From: Zhang Yi <yi.zhang@xxxxxxxxxx>

Hello,

This is the fourth version fixing data loss when truncating straddling
large folios caught on the upcomming ext4 + iomap buffered I/O
conversion.

When truncate_inode_pages_range() punches a hole or truncates a file,
truncate_inode_partial_folio() splits a large folio so that the caller
can drop the in-range sub-folios while keeping the out-of-range tail
intact. This series fixes three distinct problems in that path that
can each lose the valid out-of-range tail of a straddling folio, plus a
follow-up that clarifies the return value semantics.

Patch 01 aligns the truncation boundaries inwards to the mapping minimum
folio order in truncate_inode_pages_range(). With a non-zero min_order,
folio_split() stops at min_order instead of order 0, so a boundary
computed at page granularity can land inside a min-order-aligned
sub-folio and the truncate loop drops that whole chunk, valid tail
included, causing data loss.

Patch 02 looks the end-edge straddler up by its page index through
__filemap_get_folio() in truncate_inode_partial_folio(). After the
first split the straddler is unlocked and only transiently ref'd in the
page cache, so the page pointer derived from the original folio can be
freed and reallocated as a different folio in the same mapping, and the
mapping check cannot catch it, which may cause incorrect splitting and
potential data loss.

Patch 03 reworks the contract between truncate_inode_partial_folio() and
its callers. If the second split of the straddler fails, the function
reported success unconditionally, and the leftover incorrect end
position could cause the truncate loop to drop that valid tail. After
rework, it tells the caller the exact page range safe to discard via new
pstart/pend out-parameters, so the truncate loop never touches a
straddling folio that still holds valid out-of-range data.

Patch 04 clarifies the return value semantics to "at least one split
succeeded", which is all the shmem caller needs to decide whether to
reset its scan loop.


The second patch fixes a pre-existing race issue that is reachable
today, so it is Cc'd to stable. Patches 01 and 03 require a dirty large
folio that carries no filesystem private data, so they are not reachable
on current filesystems. They were found while developing the upcoming
ext4 iomap buffered I/O path[1].

Thanks,
Yi.

[1] https://lore.kernel.org/linux-fsdevel/a638a8fb-c184-4069-ae33-379ec12cd514@xxxxxxxxxxxxxxx/


v3->v4:
- Move the patch that fixes data loss when min_order is non-zero to the
first patch position, aligning start and end in
truncate_inode_pages_range(). (Zi Yan)
- Add patch 2, fixing the invalid folio2 issue under concurrency when
truncate_inode_partial_folio() splits at the end position. Use
__filemap_get_folio() to obtain a reliable folio2. (Jan Kara)

v2->v3:
- Rework the folio2 validity check logic to fix the invalid
folio->index issue. (sashiko)
- Clarify the pstart and pend setting logic and the corresponding
comments to make it more readable. (Brian, Joanne)
- Split the patch into 3 small patches. (Zi Yan)

v1->v2:
- Export pstart as a new parameter so that the generic and shmem
truncate paths don't need to recompute the start value from the
return value. (Brian)
- When min_order is non-zero, align [pstart, pend] to the inner
boundaries of the folio to ensure they do not point into the middle
of a large folio, which could otherwise cause valid data within the
folio to be incorrectly cleared. (Joanne)

v3: https://lore.kernel.org/linux-mm/20260916092450.654408-1-yi.zhang@xxxxxxxxxxxxxxx/
v2: https://lore.kernel.org/linux-mm/20260909062339.473816-1-yi.zhang@xxxxxxxxxxxxxxx/
v1: https://lore.kernel.org/linux-mm/20260903115018.2034541-1-yi.zhang@xxxxxxxxxxxxxxx/


Zhang Yi (4):
mm/truncate: align truncation boundaries to mapping minimum folio
order
mm/truncate: look up the end-edge straddler by index
mm/truncate: fix data loss when splitting straddling large folios
fails
mm/truncate: clarify return value of truncate_inode_partial_folio()

mm/internal.h | 4 +-
mm/shmem.c | 13 ++---
mm/truncate.c | 128 +++++++++++++++++++++++++++++++++-----------------
3 files changed, 91 insertions(+), 54 deletions(-)

--
2.54.0