Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Pratyush Yadav" <pratyush@kernel.org>,
	"Alexander Viro" <viro@zeniv.linux.org.uk>,
	"Christian Brauner" <brauner@kernel.org>,
	"Jan Kara" <jack@suse.cz>,
	"Matthew Wilcox (Oracle)" <willy@infradead.org>,
	"Hugh Dickins" <hughd@google.com>,
	"Baolin Wang" <baolin.wang@linux.alibaba.com>,
	"Andrew Morton" <akpm@linux-foundation.org>,
	"David Hildenbrand" <david@kernel.org>, "Zi Yan" <ziy@nvidia.com>,
	"Liam R. Howlett" <liam@infradead.org>,
	"Nico Pache" <nico.pache@linux.dev>,
	"Ryan Roberts" <ryan.roberts@arm.com>,
	"Dev Jain" <dev.jain@arm.com>, "Barry Song" <baohua@kernel.org>,
	"Lance Yang" <lance.yang@linux.dev>,
	"Usama Arif" <usama.arif@linux.dev>,
	"Kiryl Shutsemau" <kas@kernel.org>,
	"Pasha Tatashin" <pasha.tatashin@soleen.com>,
	"Mike Rapoport" <rppt@kernel.org>,
	"Matthew Brost" <matthew.brost@intel.com>,
	"Joshua Hahn" <joshua.hahnjy@gmail.com>,
	"Byungchul Park" <byungchul@sk.com>,
	"Gregory Price" <gourry@gourry.net>,
	"Ying Huang" <ying.huang@linux.alibaba.com>,
	"Alistair Popple" <apopple@nvidia.com>,
	"Kairui Song" <kasong@tencent.com>,
	"Qi Zheng" <qi.zheng@linux.dev>,
	"Shakeel Butt" <shakeel.butt@linux.dev>,
	"Axel Rasmussen" <axelrasmussen@google.com>,
	"Yuanchu Xie" <yuanchu@google.com>, "Wei Xu" <weixugc@google.com>,
	"Baoquan He" <baoquan.he@linux.dev>,
	"Johannes Weiner" <hannes@cmpxchg.org>,
	"Michal Hocko" <mhocko@kernel.org>,
	"Lorenzo Stoakes" <ljs@kernel.org>
Cc: linux-mm@kvack.org, kexec@lists.infradead.org
Subject: Re: [PATCH 2/3] mm: allow freezing i_pages
Date: Wed, 28 Oct 2026 17:31:19 +0000	[thread overview]
Message-ID: <sashiko-outbox-188236@kernel.org> (raw)
In-Reply-To: <20261027173031.763213-3-pratyush@kernel.org>

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] mm: shmem: race between shmem_writeout() and mapping_set_frozen()
- [High] mm: filemap_fault() returns VM_FAULT_OOM on -EBUSY from __filemap_get_folio()
- [High] mm: truncate: unlocked mapping_frozen() check and infinite loop on -EBUSY
- [High] mm: shmem: unhandled -EBUSY in shmem_unuse_swap_entries() on frozen mapping
- [Medium] mm: race between mapping_set_frozen() and filemap_remove_folio() rollback
--

commit 22b54347e6ef0325973e1e89c921cfa0dc0a8784
Author: Pratyush Yadav (Google) <pratyush@kernel.org>

mm: allow freezing i_pages

This commit introduces the AS_FROZEN address_space flag to prevent
additions, removals, replacements, splits, and collapses in mapping->i_pages
while a file's page cache is serialized for live update. Core page cache,
truncation, reclaim, migration, shmem, and THP paths check mapping_frozen()
and reject changes with -EBUSY or warnings.

[ ... ]

> diff --git a/mm/filemap.c b/mm/filemap.c
> index b74bc1e5015c6..d040c69177f79 100644
> --- a/mm/filemap.c
> +++ b/mm/filemap.c
> @@ -138,6 +138,7 @@ static void page_cache_delete(struct address_space *mapping,
>  	nr = folio_nr_pages(folio);
>  
>  	VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
> +	VM_WARN_ON_ONCE_FOLIO(mapping_frozen(mapping), folio);

[Severity: Medium]
Can an in-progress page cache insertion race with mapping_set_frozen() and
trigger this warning during error rollback?

Several paths insert a folio into mapping->i_pages under
xa_lock_irq(&mapping->i_pages), drop the lock, and then call
filemap_remove_folio() -> __filemap_remove_folio() -> page_cache_delete() if
a subsequent step fails, without holding inode_lock:

1. In mm/shmem.c, shmem_get_folio_gfp() -> shmem_alloc_and_add_folio() calls
shmem_add_to_page_cache() before shmem_inode_acct_blocks() and before the
post-insertion i_size check:

    error = shmem_add_to_page_cache(folio, mapping, index, NULL, gfp);
    if (error)
        goto unlock;

    error = shmem_inode_acct_blocks(inode, pages);
    if (error) {
        ...
        error = shmem_inode_acct_blocks(inode, pages);
        if (error) {
            filemap_remove_folio(folio);
            goto unlock;
        }
    }

and in shmem_get_folio_gfp():

    /* Perhaps the file has been truncated since we checked */
    if (sgp <= SGP_CACHE &&
        ((loff_t)index << PAGE_SHIFT) >= i_size_read(inode)) {
        error = -EINVAL;
        goto unlock;
    }
    ...
unlock:
    if (alloced)
        filemap_remove_folio(folio);

If a fault checks index < i_size before a concurrent ftruncate() shrinks the
file, and mapping_set_frozen() runs after shmem_add_to_page_cache() succeeds
(where memfd_luo_preserve_folios() only scans up to the new i_size), the
rollback calls filemap_remove_folio() on the frozen mapping.

2. In mm/userfaultfd.c, __mfill_atomic_pte() adds the folio to i_pages via
ops->filemap_add() (shmem_mfill_filemap_add()) and then calls
mfill_atomic_install_pte():

    if (ops->filemap_add) {
        ret = ops->filemap_add(folio, state->vma, state->dst_addr);
        if (ret)
            goto err_folio_put;
    }

    ret = mfill_atomic_install_pte(state->pmd, state->vma, dst_addr,
                       &folio->page, flags);
    if (ret)
        goto err_filemap_remove;

    return 0;

err_filemap_remove:
    if (ops->filemap_remove)
        ops->filemap_remove(folio, state->vma);

If mapping_set_frozen() runs before mfill_atomic_install_pte() fails (for
example with -EEXIST or via mfill_file_over_size()),
shmem_mfill_filemap_remove() calls filemap_remove_folio() on the frozen
mapping.

3. In mm/readahead.c, read_pages() inserts folios into mapping->i_pages
before calling aops->readahead(), and removes any unprocessed folios with
filemap_remove_folio().

How should these post-insertion rollback paths synchronize with
mapping_set_frozen()?

[ ... ]

> @@ -873,6 +876,10 @@ noinline int __filemap_add_folio(struct address_space *mapping,
>  		void *entry, *old = NULL;
>  
>  		xas_lock_irq(&xas);
> +		if (unlikely(mapping_frozen(mapping))) {
> +			xas_set_err(&xas, -EBUSY);
> +			goto unlock;
> +		}
>  		xas_for_each_conflict(&xas, entry) {
>  			old = entry;
>  			if (!xa_is_value(entry)) {

[Severity: High]
How does filemap_fault() in mm/filemap.c handle -EBUSY when
__filemap_add_folio() rejects adding a folio to a frozen mapping?

When __filemap_add_folio() returns -EBUSY, it propagates through
filemap_add_folio() and __filemap_get_folio_mpol() as ERR_PTR(-EBUSY).
However, in mm/filemap.c, filemap_fault() treats any error from
__filemap_get_folio() as -ENOMEM and returns VM_FAULT_OOM:

    folio = __filemap_get_folio(mapping, index,
                      FGP_CREAT|FGP_FOR_MMAP,
                      vmf->gfp_mask);
    if (IS_ERR(folio)) {
        if (fpin)
            goto out_retry;
        filemap_invalidate_unlock_shared(mapping);
        return VM_FAULT_OOM;
    }

When a page fault hits an unpopulated index within i_size on a frozen
mapping using filemap_fault(), returning VM_FAULT_OOM to the architecture
page fault handler triggers pagefault_out_of_memory() and retries the fault
in an infinite loop (or triggers a memcg OOM kill) instead of returning
VM_FAULT_SIGBUS.

Should filemap_fault() convert PTR_ERR(folio) via vmf_error(PTR_ERR(folio))
here instead of unconditionally returning VM_FAULT_OOM?

[ ... ]

> diff --git a/mm/shmem.c b/mm/shmem.c
> index 13e0786436cda..6507b4246c8af 100644
> --- a/mm/shmem.c
> +++ b/mm/shmem.c

[ ... ]

> @@ -1822,6 +1830,10 @@ int shmem_writeout(struct swap_io_ctx *ctx, struct folio *folio,
>  	if ((info->flags & SHMEM_F_LOCKED) || sbinfo->noswap)
>  		goto redirty;
>  
> +	/* Swapping out would replace the folio in a frozen mapping. */
> +	if (mapping_frozen(mapping))
> +		goto redirty;
> +
>  	if (!total_swap_pages)
>  		goto redirty;

[Severity: Critical]
Is this lockless mapping_frozen(mapping) check in shmem_writeout() racy
against mapping_set_frozen()?

Page reclaim (shrink_folio_list() -> pageout() -> shmem_writeout()) runs
with only the folio lock held (for instance, on a folio allocated beyond
i_size with FALLOC_FL_KEEP_SIZE, which shmem_fallocate() marks dirty and
memfd_luo_preserve_folios() does not pin).

After shmem_writeout() checks mapping_frozen(mapping) without holding
xa_lock_irq(&mapping->i_pages), it zeroes any !uptodate folio, allocates swap
via folio_alloc_swap(), and updates accounting in shmem_recalc_inode() before
calling shmem_delete_from_page_cache():

    xa_lock_irq(&mapping->i_pages);
    error = shmem_replace_entry(mapping, folio->index, folio, radswap);
    folio->mapping = NULL;
    mapping->nrpages -= nr;
    shmem_update_stats(folio, -nr);
    xa_unlock_irq(&mapping->i_pages);

If mapping_set_frozen(mapping) runs on another CPU between the unlocked check
in shmem_writeout() and shmem_delete_from_page_cache(),
shmem_delete_from_page_cache() replaces the folio in mapping->i_pages with a
swap entry after the mapping was frozen.

Even worse, if swap_writeout() then returns AOP_WRITEPAGE_ACTIVATE without
writing the folio (for example, when zswap_store() fails and
!mem_cgroup_zswap_writeback_enabled()), shmem_writeout() attempts to put the
folio back into the page cache:

    error = shmem_add_to_page_cache(folio, mapping, index,
            swp_to_radix_entry(folio->swap),
            __GFP_HIGH | __GFP_NOMEMALLOC | __GFP_NOWARN);
    /* Swap entry might be erased by racing shmem_free_swap() */
    if (!error) {
        shmem_recalc_inode(inode, 0, -nr_pages);
        folio_put_swap(folio, NULL);
    }
    ...
    swap_cache_del_folio(folio);
    goto redirty;

Because AS_FROZEN is now set, shmem_add_to_page_cache() fails with -EBUSY,
so shmem_writeout() skips folio_put_swap() and calls
swap_cache_del_folio(folio), discarding the dirty unwritten folio from the
swap cache while leaving the unwritten swap entry in mapping->i_pages.

Should shmem_delete_from_page_cache() check mapping_frozen(mapping) under
xa_lock_irq(&mapping->i_pages) and abort the swap-out if the mapping has
been frozen?

> @@ -2483,6 +2495,13 @@ static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
>  	if (softleaf_is_poison_marker(index_entry))
>  		return -EIO;
>  
> +	/*
> +	 * Swapping in would change the i_pages of a frozen mapping. A frozen
> +	 * mapping should not have swap entries in the first place.
> +	 */
> +	if (unlikely(mapping_frozen(mapping)))
> +		return -EBUSY;
> +
>  	si = get_swap_device(index_entry);
>  	order = shmem_confirm_swap(mapping, index, index_entry);
>  	if (IS_ERR_OR_NULL(si)) {

[Severity: High]
What happens during swapoff in shmem_unuse_inode() if a frozen shmem mapping
still contains swap entries (for example, from pages fallocated with
FALLOC_FL_KEEP_SIZE beyond i_size that were swapped out before the mapping
was frozen, or via the shmem_writeout() race above)?

In mm/shmem.c, shmem_unuse_inode() loops over swap entries calling
shmem_unuse_swap_entries():

    do {
        folio_batch_init(&fbatch);
        if (!shmem_find_swap_entries(mapping, start, &fbatch,
                         indices, type)) {
            ret = 0;
            break;
        }

        ret = shmem_unuse_swap_entries(inode, &fbatch, indices);
        if (ret < 0)
            break;

        if (signal_pending(current)) {
            ret = -ERESTARTSYS;
            break;
        }
        start = indices[folio_batch_count(&fbatch) - 1];
    } while (true);

And shmem_unuse_swap_entries() ignores all errors from shmem_swapin_folio()
other than -ENOMEM:

    error = shmem_swapin_folio(inode, indices[i], &folio, SGP_CACHE,
                    mapping_gfp_mask(mapping), NULL, NULL);
    if (error == 0) {
        folio_unlock(folio);
        folio_put(folio);
        ret++;
    }
    if (error == -ENOMEM)
        break;
    error = 0;

When shmem_swapin_folio() returns -EBUSY on a frozen mapping without
removing the swap entry, shmem_unuse_swap_entries() resets error = 0 and
returns a non-negative value. shmem_unuse_inode() then sets
start = indices[folio_batch_count(&fbatch) - 1], and the next
shmem_find_swap_entries() call (which scans inclusively from start) finds the
same swap entry again, spinning forever in the kernel.

Should shmem_unuse_swap_entries() also propagate -EBUSY (or should
shmem_unuse_inode() bail out when mapping_frozen(mapping) is set)?

> diff --git a/mm/truncate.c b/mm/truncate.c
> index 733d2713a875a..6512f460a28e9 100644
> --- a/mm/truncate.c
> +++ b/mm/truncate.c
> @@ -171,6 +171,8 @@ int truncate_inode_folio(struct address_space *mapping, struct folio *folio)
>  {
>  	if (folio->mapping != mapping)
>  		return -EIO;
> +	if (mapping_frozen(mapping))
> +		return -EBUSY;
>  
>  	truncate_cleanup_folio(folio);
>  	filemap_remove_folio(folio);

[Severity: High]
Could this unlocked mapping_frozen(mapping) check in truncate_inode_folio()
both race with mapping_set_frozen() and cause an infinite loop in truncation
callers?

First, callers like memory_failure() -> truncate_error_folio() ->
generic_error_remove_folio() -> truncate_inode_folio() only hold the folio
lock, not filesystem locks. Because mapping_frozen(mapping) is checked
before acquiring xa_lock_irq(&mapping->i_pages) in filemap_remove_folio(),
mapping_set_frozen() can run while truncate_cleanup_folio() is executing,
causing filemap_remove_folio() -> __filemap_remove_folio() ->
page_cache_delete() to hit VM_WARN_ON_ONCE_FOLIO(mapping_frozen(mapping),
folio) and delete the folio from the frozen mapping.

Second, the commit message notes that racing mapping_set_frozen() with
truncation causes truncation to fail mid-way, but in mm/truncate.c, the
second loop of truncate_inode_pages_range() ignores the return value of
truncate_inode_folio():

    while (index < end) {
        cond_resched();
        if (!find_get_entries(mapping, &index, end - 1, &fbatch,
                indices)) {
            /* If all gone from start onwards, we're done */
            if (index == start)
                break;
            /* Otherwise restart to make sure all gone */
            index = start;
            continue;
        }
        ...
            truncate_inode_folio(mapping, folio);
            folio_unlock(folio);
        }
        truncate_folio_batch_exceptionals(mapping, &fbatch, indices);
        folio_batch_release(&fbatch);
    }

And in mm/shmem.c, shmem_undo_range() has the same restart loop when
truncating (end == -1):

        if (!find_get_entries(mapping, &index, end - 1, &fbatch,
                indices)) {
            /* If all gone or hole-punch or unfalloc, we're done */
            if (index == start || end != -1)
                break;
            /* But if truncating, restart to make sure all gone */
            index = start;
            continue;
        }
        ...
                if (!folio_test_large(folio)) {
                    truncate_inode_folio(mapping, folio);

If AS_FROZEN is set after truncate_inode_pages_range() or shmem_undo_range()
passes its initial entry check, truncate_inode_folio() returns -EBUSY (and
truncate_inode_partial_folio() fails to split large folios with -EBUSY)
without removing the folios, while find_get_entries() advances index past
start. When find_get_entries() reaches the end of the range and returns 0
with index != start, both loops reset index = start and retry forever.

Should truncate_inode_folio() verify mapping_frozen(mapping) under
xa_lock_irq(&mapping->i_pages), and should the retry loops in
truncate_inode_pages_range() and shmem_undo_range() abort if the mapping
becomes frozen?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261027173031.763213-1-pratyush@kernel.org?part=2


  reply	other threads:[~2026-10-28 17:31 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-27 17:30 [PATCH 0/3] mm: allow freezing mappings Pratyush Yadav
2026-10-27 17:30 ` [PATCH 1/3] mm/memfd_luo: don't use memfd_pin_folios() Pratyush Yadav
2026-10-28 20:18   ` Pasha Tatashin
2026-10-28 23:04     ` Pratyush Yadav
2026-10-29  0:01       ` Pasha Tatashin
2026-10-29 13:15         ` Pratyush Yadav
2026-10-27 17:30 ` [PATCH 2/3] mm: allow freezing i_pages Pratyush Yadav
2026-10-28 17:31   ` sashiko-bot [this message]
2026-10-27 17:30 ` [PATCH 3/3] mm: shmem: use mapping_set_frozen() for memfd preservation Pratyush Yadav
2026-10-28 17:31   ` sashiko-bot
2026-10-27 17:35 ` [PATCH 0/3] mm: allow freezing mappings David Hildenbrand (Arm)
2026-10-27 17:38   ` Pratyush Yadav
2026-10-27 18:59     ` David Hildenbrand (Arm)

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=sashiko-outbox-188236@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=akpm@linux-foundation.org \
    --cc=apopple@nvidia.com \
    --cc=axelrasmussen@google.com \
    --cc=baohua@kernel.org \
    --cc=baolin.wang@linux.alibaba.com \
    --cc=baoquan.he@linux.dev \
    --cc=brauner@kernel.org \
    --cc=byungchul@sk.com \
    --cc=david@kernel.org \
    --cc=dev.jain@arm.com \
    --cc=gourry@gourry.net \
    --cc=hannes@cmpxchg.org \
    --cc=hughd@google.com \
    --cc=jack@suse.cz \
    --cc=joshua.hahnjy@gmail.com \
    --cc=kas@kernel.org \
    --cc=kasong@tencent.com \
    --cc=kexec@lists.infradead.org \
    --cc=lance.yang@linux.dev \
    --cc=liam@infradead.org \
    --cc=linux-mm@kvack.org \
    --cc=ljs@kernel.org \
    --cc=matthew.brost@intel.com \
    --cc=mhocko@kernel.org \
    --cc=nico.pache@linux.dev \
    --cc=pasha.tatashin@soleen.com \
    --cc=pratyush@kernel.org \
    --cc=qi.zheng@linux.dev \
    --cc=rppt@kernel.org \
    --cc=ryan.roberts@arm.com \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=shakeel.butt@linux.dev \
    --cc=usama.arif@linux.dev \
    --cc=viro@zeniv.linux.org.uk \
    --cc=weixugc@google.com \
    --cc=willy@infradead.org \
    --cc=ying.huang@linux.alibaba.com \
    --cc=yuanchu@google.com \
    --cc=ziy@nvidia.com \
    /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