Thread (12 messages) 12 messages, 7 authors, 2026-09-11

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.
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help