[PATCH v2 2/5] mm/huge_memory: refuse to split a file folio when the anchor is beyond EOF

From: Kiryl Shutsemau

Date: Tue Jul 14 2026 - 08:25:11 EST


From: "Kiryl Shutsemau (Meta)" <kas@xxxxxxxxxx>

__folio_split() dereferences the mapping after the split completes:
shmem_uncharge(mapping->host) for folios dropped beyond EOF and
i_mmap_unlock_read(mapping) on the way out. Nothing holds an inode
reference for that duration; the split relies on the caller's locked
@lock_at folio, while it is locked and present in the page cache, to keep
the inode alive through eviction's truncate_inode_pages_final().

If @lock_at lies beyond EOF, __folio_freeze_and_split_unmapped() removes
it from the page cache while keeping it locked for the caller. That drops
the pin and lets a concurrent final iput() evict and free the inode under
the still-running split. On the anon side __folio_split() already pins
its anchor explicitly (folio_get_anon_vma()); the file side's anchor was
always the locked in-cache folio, just never enforced.

The only in-tree caller that passed a beyond-EOF @lock_at was
memory_failure(), fixed in the previous patch to anchor on the head. Make
the requirement explicit so it cannot be reintroduced: refuse the split
with -EBUSY when @lock_at is at or beyond the sampled EOF. Such a folio
is racing truncation, so there is nothing useful to split; -EBUSY is
already handled by every caller.

The check uses the same @end sampled under the folio lock that the drop
loop uses, so it does not race the trimming it guards against.

Fixes: baa355fd3314 ("thp: file pages support for split_huge_page()")
Cc: <stable@xxxxxxxxxxxxxxx>
Signed-off-by: Kiryl Shutsemau (Meta) <kas@xxxxxxxxxx>
---
mm/huge_memory.c | 14 ++++++++++++++
1 file changed, 14 insertions(+)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 2bccb0a53a0a..0e3ca7178d8c 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -4065,6 +4065,20 @@ static int __folio_split(struct folio *folio, unsigned int new_order,
end = DIV_ROUND_UP(i_size_read(mapping->host), PAGE_SIZE);
if (shmem_mapping(mapping))
end = shmem_fallocend(mapping->host, end);
+
+ /*
+ * @lock_at is returned locked to the caller, and while it is
+ * locked and present in the page cache it is what keeps the
+ * inode alive: the mapping is still dereferenced after the split
+ * (shmem_uncharge(), i_mmap_unlock_read()). If it lies beyond
+ * EOF the split would drop it from the page cache while handing
+ * it back locked, removing that pin. Such a folio is racing
+ * truncation and there is nothing useful to split; bail out.
+ */
+ if (folio->index + folio_page_idx(folio, lock_at) >= end) {
+ ret = -EBUSY;
+ goto out_unlock;
+ }
}

/*
--
2.54.0