Re: [PATCH v2] mm/truncate: fix data loss when truncating straddling large folios
flat view
From: Zhang Yi <hidden>
Date: 2026-09-10 07:18:25
Also in:
linux-fsdevel, linux-mm, lkml
On 9/10/2026 7:14 AM, Andrew Morton wrote:
On Wed, 9 Sep 2026 14:23:39 +0800 Zhang Yi [off-list ref] wrote:quoted
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() -> iomap_release_folio() returns false 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.So is there any way of being hit by these issues in 7.2 and earlier?quoted
mm/internal.h | 4 +-- mm/shmem.c | 13 +++----- mm/truncate.c | 88 +++++++++++++++++++++++++++++++++++---------------- 3 files changed, 67 insertions(+), 38 deletions(-)I hope not, because that's quite a lump of code to be backporting.
I took a rough look at a few existing filesystems and had an LLM help
put things together.
Actually triggering it needs all of:
(1) a large folio straddling the punch/truncate boundary, so the
in-range part is zeroed and the out-of-range part holds valid data;
(2) that folio dirty, so the out-of-range tail exists only in the
pagecache;
(3) no filesystem private data on the folio (or ->release_folio()
returning success), so the first folio_split() in
truncate_inode_partial_folio() can succeed.
Filesystems that enable large folios fall into three groups, none of
which can currently satisfy all three:
- buffer_head / private-based (ext4, btrfs, ...): a dirty folio
always carries fs private state (buffer_head, extent state), so
->release_folio() returns false and folio_split() aborts with -EBUSY
before the first split — condition (3) fails.
- netfs (afs, nfs, smb, ...): AP_RELEASE_ALWAYS is set but
netfs_release_folio() returns false on a dirty folio — same effect,
condition (3) fails.
- iomap (xfs, zonefs, erofs): these can have dirty large folios
without private data, but none reach the unsafe window — xfs always
runs filemap_write_and_wait_range() (xfs_flush_unmap_range) before
punching, so the boundary folio is clean; zonefs has no punch-hole;
erofs is read-only. With the tail already on disk (clean), discarding
the straddling folio loses nothing.
So on 7.2 and earlier I cannot find a reachable trigger path. It only
becomes reachable on the upcoming ext4 + iomap buffered-I/O path, which
for the first time combines large folios + no private data on
fully-dirtied folios + missing pre-punch writeback.
Thanks. I'll queue it for testing and shall await your response to Zi Yan's questions. I see you've received Sashiko's feedback. Please let us know if there's any validity to it. https://sashiko.dev/#/patchset/20260909062339.473816-1-yi.zhang@huaweicloud.com
I took an initial look at Sashiko's review report. I think the issue is valid — the current fix can get the wrong folio2 index when there's a concurrent split. I need to think more about how to fix it properly. Thanks, Yi.