From: Usama Arif <usama.arif@linux.dev>
To: Rik van Riel <riel@surriel.com>
Cc: Usama Arif <usama.arif@linux.dev>,
linux-kernel@vger.kernel.org,
Andrew Morton <akpm@linux-foundation.org>,
kernel-team@meta.com, David Hildenbrand <david@kernel.org>,
Lorenzo Stoakes <ljs@kernel.org>,
"Liam R. Howlett" <liam@infradead.org>,
Vlastimil Babka <vbabka@kernel.org>,
Mike Rapoport <rppt@kernel.org>,
Suren Baghdasaryan <surenb@google.com>,
Michal Hocko <mhocko@suse.com>,
linux-mm@kvack.org, Jason Gunthorpe <jgg@ziepe.ca>,
John Hubbard <jhubbard@nvidia.com>, Peter Xu <peterx@redhat.com>
Subject: Re: [PATCH RFC v3 4/6] mm/gup: add get_user_page_vma() to fault in a page under a held lock
Date: Mon, 20 Jul 2026 05:35:04 -0700 [thread overview]
Message-ID: <20260720123505.1234897-1-usama.arif@linux.dev> (raw)
In-Reply-To: <20260717170036.743149-5-riel@surriel.com>
On Fri, 17 Jul 2026 13:00:34 -0400 Rik van Riel <riel@surriel.com> wrote:
> __access_remote_vm() needs a single page from a VMA it has already
> looked up and locked, faulting it in when necessary, under either the
> mmap lock or the per-VMA lock. get_user_pages_remote() does not fit: it
> hard codes the mmap lock and re-looks-up the VMA, neither of which is
> wanted here.
>
> Add get_user_page_vma(), a simplified __get_user_pages() that walks the
> page tables with follow_page_mask(), faults a missing page in with
> faultin_page(), and on success returns it with a reference and the
> caller's lock still held. Like __get_user_pages() it runs
> check_vma_flags(), so callers need not pre-check the VMA.
>
> A VM_IO/VM_PFNMAP VMA is the exception to that check: it can still hold
> COWed pages that have a struct page, so follow_page_mask() is allowed to
> look for one. Memory with no struct page -- a raw PFN, or a present PFN
> reported as -EEXIST -- is returned as -EFAULT, so the caller can reach it
> through vma->vm_ops->access().
>
> The caller sets FOLL_VMA_LOCK when it holds the per-VMA lock rather than
> the mmap lock, which reaches the fault code as FAULT_FLAG_VMA_LOCK.
>
> Anything that cannot complete under the per-VMA lock -- a dropped fault,
> a userfaultfd VMA (uffd assumes current is the faulting task), a hard
> error, or ->access() memory -- releases the lock and returns -EAGAIN,
> so the caller retries under the mmap lock.
>
> faultin_page() reports these retries as -EAGAIN for both lock types;
> only the mmap caller records the dropped lock in *locked. A
> VM_FAULT_ERROR that decodes to no errno warns and returns -EFAULT
> rather than BUG(), since the mmap-lock retry produces the definitive
> result.
>
> Assisted-by: Claude:claude-opus-4.8
> Signed-off-by: Rik van Riel <riel@surriel.com>
> ---
> mm/gup.c | 154 +++++++++++++++++++++++++++++++++++++++++++-------
> mm/internal.h | 6 +-
> 2 files changed, 139 insertions(+), 21 deletions(-)
>
> diff --git a/mm/gup.c b/mm/gup.c
> index 0692119b7904..69b834a71708 100644
> --- a/mm/gup.c
> +++ b/mm/gup.c
> @@ -1082,7 +1082,13 @@ static int get_gate_page(struct mm_struct *mm, unsigned long address,
> /*
> * mmap_lock must be held on entry. If @flags has FOLL_UNLOCKABLE but not
Does the above line also need to be changed? mmap_lock is not held on entry in
faultin_page() if passing FOLL_VMA_LOCK, right?
> * FOLL_NOWAIT, the mmap_lock may be released. If it is, *@locked will be set
> - * to 0 and -EBUSY returned.
> + * to 0 and -EAGAIN returned.
> + *
> + * The return value does not depend on the lock type: a fault that made
> + * progress but needs a retry (VM_FAULT_RETRY / VM_FAULT_COMPLETED) is reported
> + * as -EAGAIN for both the mmap lock and the per-VMA lock (FOLL_VMA_LOCK). Only
> + * the *@locked side effect is lock-type specific, as the per-VMA lock path has
> + * no unlockable mmap_lock to drop.
> */
> static int faultin_page(struct vm_area_struct *vma,
> unsigned long address, unsigned int flags, bool unshare,
> @@ -1097,6 +1103,8 @@ static int faultin_page(struct vm_area_struct *vma,
> fault_flags |= FAULT_FLAG_WRITE;
> if (flags & FOLL_REMOTE)
> fault_flags |= FAULT_FLAG_REMOTE;
> + if (flags & FOLL_VMA_LOCK)
> + fault_flags |= FAULT_FLAG_VMA_LOCK;
> if (flags & FOLL_UNLOCKABLE) {
> fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;
> /*
> @@ -1125,41 +1133,147 @@ static int faultin_page(struct vm_area_struct *vma,
>
> ret = handle_mm_fault(vma, address, fault_flags, NULL);
>
> + /*
> + * A fully completed fault (VM_FAULT_COMPLETED) or one that needs a retry
> + * (VM_FAULT_RETRY) has released the lock it was holding. Report both as
> + * -EAGAIN so the caller retries: the mmap lock caller retakes it here,
> + * the per-VMA lock caller (FOLL_VMA_LOCK) falls back to the mmap lock.
> + *
> + * Dropping the mmap lock is recorded in *@locked. There is no such lock
> + * to drop under the per-VMA lock, where @locked is not used, so leave it
> + * alone in that case.
> + */
> if (ret & VM_FAULT_COMPLETED) {
> - /*
> - * With FAULT_FLAG_RETRY_NOWAIT we'll never release the
> - * mmap lock in the page fault handler. Sanity check this.
> - */
> - WARN_ON_ONCE(fault_flags & FAULT_FLAG_RETRY_NOWAIT);
> - *locked = 0;
> -
> - /*
> - * We should do the same as VM_FAULT_RETRY, but let's not
> - * return -EBUSY since that's not reflecting the reality of
> - * what has happened - we've just fully completed a page
> - * fault, with the mmap lock released. Use -EAGAIN to show
> - * that we want to take the mmap lock _again_.
> - */
> + if (!(flags & FOLL_VMA_LOCK)) {
> + /*
> + * With FAULT_FLAG_RETRY_NOWAIT we'll never release the
> + * mmap lock in the page fault handler. Sanity check this.
> + */
> + WARN_ON_ONCE(fault_flags & FAULT_FLAG_RETRY_NOWAIT);
> + *locked = 0;
> + }
> return -EAGAIN;
> }
>
> if (ret & VM_FAULT_ERROR) {
> int err = vm_fault_to_errno(ret, flags);
>
> - if (err)
> - return err;
> - BUG();
> + /*
> + * VM_FAULT_ERROR always decodes to an errno; a zero here would
> + * mean handle_mm_fault() returned an unexpected combination.
> + * Report -EFAULT rather than crash: under the per-VMA lock the
> + * mmap lock retry produces the definitive result.
> + */
> + VM_WARN_ON_ONCE(!err);
> + return err ? err : -EFAULT;
> }
>
> if (ret & VM_FAULT_RETRY) {
> - if (!(fault_flags & FAULT_FLAG_RETRY_NOWAIT))
> + if (!(flags & FOLL_VMA_LOCK) &&
> + !(fault_flags & FAULT_FLAG_RETRY_NOWAIT))
> *locked = 0;
> - return -EBUSY;
> + return -EAGAIN;
> }
>
> return 0;
> }
>
> +/*
> + * get_user_page_vma - get one page from @vma, whose lock the caller already
> + * holds: the mmap lock, or (with FOLL_VMA_LOCK) the per-VMA lock. Walks the
> + * page tables, faulting the page in if needed, and on success returns it with
> + * a reference and the lock still held.
> + *
> + * Runs check_vma_flags() like __get_user_pages(), so callers need not pre-check
> + * the VMA; most rejections are returned as their error. A VM_IO/VM_PFNMAP VMA
> + * is the exception: a COWed page with a struct page is returned, while a raw
> + * PFN has none and yields -EFAULT, to be reached via vma->vm_ops->access().
> + *
> + * Under FOLL_VMA_LOCK, anything that cannot be finished under the per-VMA lock
> + * (a dropped fault, userfaultfd, a hard error, or ->access() memory) releases
> + * the lock and returns -EAGAIN, so the caller retries under the mmap lock.
> + */
> +struct page *get_user_page_vma(struct vm_area_struct *vma, unsigned long addr,
> + unsigned int gup_flags)
> +{
> + bool vma_locked = gup_flags & FOLL_VMA_LOCK;
> + unsigned long page_mask;
> + struct page *page;
> + int locked = 1;
> + bool pfnmap;
> + int ret;
> +
> + /*
> + * Validate the VMA up front, like __get_user_pages(). A VM_IO/VM_PFNMAP
> + * VMA is not rejected outright: it can hold COWed pages that have a
> + * struct page, so let follow_page_mask() look for one, and treat only
> + * its struct-page-less PFNs as unreachable. Any other rejection
> + * (secretmem, bad permissions, ...) is final.
> + */
> + ret = check_vma_flags(vma, gup_flags);
> + if (ret && !(vma->vm_flags & (VM_IO | VM_PFNMAP)))
Do you need to somehow restructure check_vma_flags()?
The first thing that check_vma_flags() does is:
if (vm_flags & (VM_IO | VM_PFNMAP))
return -EFAULT;
check_vma_flags() returns before evaluating VM_READ,
FOLL_FORCE, FOLL_ANON or the architecture permission check.
The code then ignores that early error and may return a COWed
page through follow_page_mask(). A non-FOLL_FORCE caller
can therefore read a COWed page from a PROT_NONE PFNMAP VMA.
> + goto fail;
> + pfnmap = ret;
> +
> + for (;;) {
> + if (fatal_signal_pending(current)) {
> + ret = -EINTR;
> + goto fail;
> + }
> + cond_resched();
> +
> + page = follow_page_mask(vma, addr,
> + gup_flags | FOLL_TOUCH | FOLL_GET,
> + &page_mask);
> + if (!IS_ERR_OR_NULL(page))
> + return page;
__get_user_pages() does
flush_anon_page(vma, subpage, start + j * PAGE_SIZE);
flush_dcache_page(subpage);
before returning the page. Do you need to that here as well above?
> +
> + /*
> + * No struct page: a raw PFN of a VM_IO/VM_PFNMAP VMA, whether
> + * seen by the up-front check (@pfnmap) or reported as -EEXIST
> + * for a present PFN. Return -EFAULT so the caller reaches it
> + * through vma->vm_ops->access().
> + */
> + if (pfnmap || PTR_ERR(page) == -EEXIST) {
> + ret = -EFAULT;
> + goto fail;
> + }
> + /* A hard error from the walk itself. */
> + if (page && PTR_ERR(page) != -EMLINK) {
> + ret = PTR_ERR(page);
> + goto fail;
> + }
> +
> + /*
> + * The page is not present, or needs unsharing. A remote fault
> + * under the per-VMA lock cannot deliver userfaultfd (which
> + * assumes current is the faulting task), so fall back for those.
> + */
> + if (vma_locked && userfaultfd_armed(vma)) {
> + ret = -EAGAIN;
> + goto fail;
> + }
> + ret = faultin_page(vma, addr, gup_flags | FOLL_REMOTE | FOLL_GET,
> + PTR_ERR(page) == -EMLINK, &locked);
> + if (ret == -EAGAIN)
> + return ERR_PTR(-EAGAIN); /* fault released the per-VMA lock */
> + if (ret)
> + goto fail;
> + }
> +
> +fail:
> + /*
> + * Under the per-VMA lock the caller cannot reach ->access() or act on a
> + * hard error (both need the mmap lock), so release the lock and have it
> + * retry there; the mmap-lock pass produces the definitive error.
> + */
> + if (vma_locked) {
> + vma_end_read(vma);
> + return ERR_PTR(-EAGAIN);
> + }
> + return ERR_PTR(ret);
> +}
> +
> /*
> * Writing to file-backed mappings which require folio dirty tracking using GUP
> * is a fundamentally broken operation, as kernel write access to GUP mappings
> diff --git a/mm/internal.h b/mm/internal.h
> index 181e79f1d6a2..0899a37907c1 100644
> --- a/mm/internal.h
> +++ b/mm/internal.h
> @@ -1595,6 +1595,8 @@ struct vm_struct *__get_vm_area_node(unsigned long size,
> */
> int __must_check try_grab_folio(struct folio *folio, int refs,
> unsigned int flags);
> +struct page *get_user_page_vma(struct vm_area_struct *vma, unsigned long addr,
> + unsigned int gup_flags);
>
> /*
> * mm/huge_memory.c
> @@ -1641,11 +1643,13 @@ enum {
> FOLL_UNLOCKABLE = 1 << 21,
> /* VMA lookup+checks compatible with MADV_POPULATE_(READ|WRITE) */
> FOLL_MADV_POPULATE = 1 << 22,
> + /* caller holds the per-VMA lock, not the mmap lock */
> + FOLL_VMA_LOCK = 1 << 23,
> };
>
> #define INTERNAL_GUP_FLAGS (FOLL_TOUCH | FOLL_TRIED | FOLL_REMOTE | FOLL_PIN | \
> FOLL_FAST_ONLY | FOLL_UNLOCKABLE | \
> - FOLL_MADV_POPULATE)
> + FOLL_MADV_POPULATE | FOLL_VMA_LOCK)
>
> /*
> * Indicates for which pages that are write-protected in the page table,
> --
> 2.53.0-Meta
>
>
next prev parent reply other threads:[~2026-07-20 12:35 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-17 17:00 [PATCH v3 0/6] mm: access remote process memory under the per-VMA lock Rik van Riel
2026-07-17 17:00 ` [PATCH RFC v3 1/6] x86/mm: add untagged_addr_remote_unlocked() Rik van Riel
2026-07-20 11:12 ` Usama Arif
2026-07-17 17:00 ` [PATCH RFC v3 2/6] riscv/mm: " Rik van Riel
2026-07-20 11:57 ` Usama Arif
2026-07-20 15:08 ` Rik van Riel
2026-07-20 16:46 ` Usama Arif
2026-07-20 17:34 ` Rik van Riel
2026-07-20 18:46 ` Usama Arif
2026-07-20 19:21 ` Rik van Riel
2026-07-20 19:39 ` Usama Arif
2026-07-17 17:00 ` [PATCH RFC v3 3/6] mm: rename get_user_page_vma_remote() to get_user_page_lookup_vma() Rik van Riel
2026-07-20 12:00 ` Usama Arif
2026-07-17 17:00 ` [PATCH RFC v3 4/6] mm/gup: add get_user_page_vma() to fault in a page under a held lock Rik van Riel
2026-07-20 12:35 ` Usama Arif [this message]
2026-07-20 15:24 ` Rik van Riel
2026-07-17 17:00 ` [PATCH RFC v3 5/6] mm: use per-VMA lock in __access_remote_vm() for single-VMA accesses Rik van Riel
2026-07-17 17:00 ` [PATCH RFC v3 6/6] selftests/mm: cover /proc/pid/mem access to VM_PFNMAP memory Rik van Riel
2026-07-21 18:12 ` [PATCH v3 0/6] mm: access remote process memory under the per-VMA lock 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=20260720123505.1234897-1-usama.arif@linux.dev \
--to=usama.arif@linux.dev \
--cc=akpm@linux-foundation.org \
--cc=david@kernel.org \
--cc=jgg@ziepe.ca \
--cc=jhubbard@nvidia.com \
--cc=kernel-team@meta.com \
--cc=liam@infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=mhocko@suse.com \
--cc=peterx@redhat.com \
--cc=riel@surriel.com \
--cc=rppt@kernel.org \
--cc=surenb@google.com \
--cc=vbabka@kernel.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