Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
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
> 
> 


  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