* [PATCH RFC] mm/filemap: fix lseek() skipping a new large folio
@ 2026-11-10 20:33 David Hildenbrand (Arm)
2026-11-11 11:45 ` Jan Kara
0 siblings, 1 reply; 3+ messages in thread
From: David Hildenbrand (Arm) @ 2026-11-10 20:33 UTC (permalink / raw)
To: Matthew Wilcox (Oracle), Jan Kara, Andrew Morton, William Kucharski
Cc: linux-fsdevel, linux-mm, linux-kernel, Chris Mason,
David Hildenbrand (Arm)
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
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH RFC] mm/filemap: fix lseek() skipping a new large folio
2026-11-10 20:33 [PATCH RFC] mm/filemap: fix lseek() skipping a new large folio David Hildenbrand (Arm)
@ 2026-11-11 11:45 ` Jan Kara
2026-11-11 12:52 ` David Hildenbrand (Arm)
0 siblings, 1 reply; 3+ messages in thread
From: Jan Kara @ 2026-11-11 11:45 UTC (permalink / raw)
To: David Hildenbrand (Arm)
Cc: Matthew Wilcox (Oracle),
Jan Kara, Andrew Morton, William Kucharski, linux-fsdevel,
linux-mm, linux-kernel, Chris Mason
On Tue 10-11-26 21:33:12, David Hildenbrand (Arm) wrote:
> 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 ;)
This looks good to me. Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
I just find it unfortunate that we have to play complicated games like
these for such a simple operation as sequentially iterating over all folios
in a mapping. And it is not a first bug of this nature trigered by
compaction. So longer term I believe we need to provide saner apis
shielding pagecache users from these nasty details.
Honza
> ---
> 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
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH RFC] mm/filemap: fix lseek() skipping a new large folio
2026-11-11 11:45 ` Jan Kara
@ 2026-11-11 12:52 ` David Hildenbrand (Arm)
0 siblings, 0 replies; 3+ messages in thread
From: David Hildenbrand (Arm) @ 2026-11-11 12:52 UTC (permalink / raw)
To: Jan Kara
Cc: Matthew Wilcox (Oracle),
Andrew Morton, William Kucharski, linux-fsdevel, linux-mm,
linux-kernel, Chris Mason
On 11/11/26 12:45, Jan Kara wrote:
> On Tue 10-11-26 21:33:12, David Hildenbrand (Arm) wrote:
>> 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 ;)
>
> This looks good to me. Feel free to add:
>
> Reviewed-by: Jan Kara <jack@suse.cz>
>
> I just find it unfortunate that we have to play complicated games like
"unfortunate" is a good word. I'd have used "annoying" :)
> these for such a simple operation as sequentially iterating over all folios
> in a mapping. And it is not a first bug of this nature trigered by
> compaction. So longer term I believe we need to provide saner apis
> shielding pagecache users from these nasty details.
Yeah, I would have preferred the simpler fix, but ~24% slowdown gave me a pause :(
thanks!
--
Cheers,
David
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-11 12:52 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-10 20:33 [PATCH RFC] mm/filemap: fix lseek() skipping a new large folio David Hildenbrand (Arm)
2026-11-11 11:45 ` Jan Kara
2026-11-11 12:52 ` David Hildenbrand (Arm)
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®