From: "David Hildenbrand (Arm)" <david@kernel.org>
To: "Matthew Wilcox (Oracle)" <willy@infradead.org>,
Jan Kara <jack@suse.cz>,
Andrew Morton <akpm@linux-foundation.org>,
William Kucharski <william.kucharski@oracle.com>
Cc: linux-fsdevel@vger.kernel.org, linux-mm@kvack.org,
linux-kernel@vger.kernel.org, Chris Mason <mason@kernel.org>,
"David Hildenbrand (Arm)" <david@kernel.org>
Subject: [PATCH RFC] mm/filemap: fix lseek() skipping a new large folio
Date: Tue, 10 Nov 2026 21:33:12 +0100 [thread overview]
Message-ID: <20261110-lseek_hole-v1-1-c089c960b846@kernel.org> (raw)
mapping_seek_hole_data() walks the page cache under RCU and only calls
xas_set() after entries larger than a page. Between base-page entries,
it continues the walk with xas_find(), which skips sibling entries
to return each multi-index entry only once. If a large folio replaces
small folio entries between iterations, the slot holding the new folio
can lie behind the cursor. Skipping its sibling entries then skips data
that has not yet been examined.
On tmpfs, SEEK_HOLE can then return an offset inside existing data.
SEEK_DATA can skip data after a fallocated page that is not uptodate.
For example, assume a 2 MiB tmpfs file backed by 4 KiB pages on x86-64,
with page index 63 fallocated but not uptodate and all other pages
uptodate. Index 63 occupies the last slot in the old xarray leaf. A
SEEK_DATA starting there can skip that page, then race with collapse of
pages 0-511 into one 2 MiB folio. The next xas_find() moves past the old
leaf and skips the new folio's sibling entries in the parent, returning
-ENXIO instead of finding data at page index 64, or 256 KiB.
The inode lock held by lseek() does not exclude collapse or swapin.
Restarting the xarray walk after every entry fixes this, but increases
SEEK_HOLE time about 24%, when scanning a fully written 1 GiB memfd backed
by base-page folios in an x86-64 microbenchmark.
Instead, use xas_next() for adjacent indices. Restart the lookup from the
root at the requested index if it returns an empty slot or an internal
entry, including a sibling entry. Also restart for nonadjacent indices,
invalid iterator state, or failed folio validation. This avoids skipping
unexamined data without restarting each lookup during base-page scans.
Fixes: 6b24ca4a1a8d ("mm: Use multi-index entries in the page cache")
Reported-by: Chris Mason <mason@kernel.org>
Signed-off-by: David Hildenbrand (Arm) <david@kernel.org>
---
I'll be more than happy if someone of the pagecache folks wants to take
over ;)
---
mm/filemap.c | 38 +++++++++++++++++++++++++++++++++++---
1 file changed, 35 insertions(+), 3 deletions(-)
diff --git a/mm/filemap.c b/mm/filemap.c
index 648f783454c3..87c583509567 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -3233,6 +3233,40 @@ static inline size_t seek_folio_size(struct xa_state *xas, struct folio *folio)
return folio_size(folio);
}
+static inline struct folio *seek_get_entry(struct xa_state *xas, pgoff_t index,
+ pgoff_t max)
+{
+ struct folio *folio;
+
+ if (!xas_valid(xas) || xas->xa_index + 1 != index)
+ goto restart;
+
+ /*
+ * A lookup within a multi-index entry can leave xa_offset at its
+ * first slot. Restore the slot corresponding to xa_index before
+ * advancing.
+ */
+ xas_advance(xas, xas->xa_index);
+ folio = xas_next(xas);
+
+ /*
+ * A concurrent collapse can replace the next slot with a sibling entry
+ * referring back to the slot holding the folio.
+ */
+ if (!folio || xa_is_internal(folio))
+ goto restart;
+ if (xa_is_value(folio))
+ return folio;
+ if (!folio_try_get(folio))
+ goto restart;
+ if (folio == xas_reload(xas))
+ return folio;
+ folio_put(folio);
+restart:
+ xas_set(xas, index);
+ return find_get_entry(xas, max, XA_PRESENT);
+}
+
/**
* mapping_seek_hole_data - Seek for SEEK_DATA / SEEK_HOLE in the page cache.
* @mapping: Address space to search.
@@ -3263,7 +3297,7 @@ loff_t mapping_seek_hole_data(struct address_space *mapping, loff_t start,
return -ENXIO;
rcu_read_lock();
- while ((folio = find_get_entry(&xas, max, XA_PRESENT))) {
+ while ((folio = seek_get_entry(&xas, start >> PAGE_SHIFT, max))) {
loff_t pos = (u64)xas.xa_index << PAGE_SHIFT;
size_t seek_size;
u64 next;
@@ -3286,8 +3320,6 @@ loff_t mapping_seek_hole_data(struct address_space *mapping, loff_t start,
goto unlock;
if (start >= end)
break;
- if (seek_size > PAGE_SIZE)
- xas_set(&xas, pos >> PAGE_SHIFT);
if (!xa_is_value(folio))
folio_put(folio);
}
---
base-commit: 93d7a816a2df3e907e2719bd12b0073f1285fc5c
change-id: 20261110-lseek_hole-4a9eb450d473
--
Cheers,
David
reply other threads:[~2026-11-10 20:33 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261110-lseek_hole-v1-1-c089c960b846@kernel.org \
--to=david@kernel.org \
--cc=akpm@linux-foundation.org \
--cc=jack@suse.cz \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=mason@kernel.org \
--cc=william.kucharski@oracle.com \
--cc=willy@infradead.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®