AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/07/26 10:07 flow patch-triage
1m Args:
null

Results:
{
  "EnableConfigs": [
    "TRANSPARENT_HUGEPAGE",
    "BPF_SYSCALL",
    "ADDRESS_MASKING"
  ],
  "FocusSymbols": [
    "get_user_page_vma",
    "get_user_page_lookup_vma",
    "access_remote_vm",
    "access_process_vm",
    "copy_remote_vm_str"
  ],
  "Reasoning": "The patch introduces a new remote VM walk mechanism (`remote_vm_walk`) and replaces `get_user_page_vma_remote` with `get_user_page_lookup_vma` and `get_user_page_vma`. It also adds support for batching contiguous pages for PTE-mapped large folios (mTHP) in the slow GUP path. These are core memory management changes that affect page faulting, remote memory access (e.g., ptrace, /proc/pid/mem), and BPF string copying. Fuzzing should focus on the new entry points and remote access APIs.",
  "WorthFuzzing": true
}

1/1 2026/07/26 10:07 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 67c20987de6d92d7efc21a001885ad318b6054fe\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Sun Jul 26 10:07:06 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/arch/arm64/kernel/mte.c b/arch/arm64/kernel/mte.c\nindex 1a9aad6ef22a04..7a6ecc3d92948c 100644\n--- a/arch/arm64/kernel/mte.c\n+++ b/arch/arm64/kernel/mte.c\n@@ -459,7 +459,7 @@ static int __access_remote_tags(struct mm_struct *mm, unsigned long addr,\n \t\tstruct vm_area_struct *vma;\n \t\tunsigned long tags, offset;\n \t\tvoid *maddr;\n-\t\tstruct page *page = get_user_page_vma_remote(mm, addr,\n+\t\tstruct page *page = get_user_page_lookup_vma(mm, addr,\n \t\t\t\t\t\t\t     gup_flags, \u0026vma);\n \t\tstruct folio *folio;\n \ndiff --git a/arch/riscv/include/asm/mmu_context.h b/arch/riscv/include/asm/mmu_context.h\nindex dbf27a78df6c87..3ce16796e5a25d 100644\n--- a/arch/riscv/include/asm/mmu_context.h\n+++ b/arch/riscv/include/asm/mmu_context.h\n@@ -21,7 +21,7 @@ static inline void activate_mm(struct mm_struct *prev,\n \t\t\t       struct mm_struct *next)\n {\n #ifdef CONFIG_RISCV_ISA_SUPM\n-\tnext-\u003econtext.pmlen = 0;\n+\tWRITE_ONCE(next-\u003econtext.pmlen, 0);\n #endif\n \tswitch_mm(prev, next, NULL);\n }\n@@ -44,7 +44,7 @@ DECLARE_STATIC_KEY_FALSE(use_asid_allocator);\n #define mm_untag_mask mm_untag_mask\n static inline unsigned long mm_untag_mask(struct mm_struct *mm)\n {\n-\treturn -1UL \u003e\u003e mm-\u003econtext.pmlen;\n+\treturn -1UL \u003e\u003e READ_ONCE(mm-\u003econtext.pmlen);\n }\n #endif\n \ndiff --git a/arch/riscv/include/asm/uaccess.h b/arch/riscv/include/asm/uaccess.h\nindex 5d4ec15584cfa4..53806e0f7dcfe3 100644\n--- a/arch/riscv/include/asm/uaccess.h\n+++ b/arch/riscv/include/asm/uaccess.h\n@@ -16,7 +16,7 @@\n static inline unsigned long __untagged_addr_remote(struct mm_struct *mm, unsigned long addr)\n {\n \tif (riscv_has_extension_unlikely(RISCV_ISA_EXT_SUPM)) {\n-\t\tu8 pmlen = mm-\u003econtext.pmlen;\n+\t\tu8 pmlen = READ_ONCE(mm-\u003econtext.pmlen);\n \n \t\t/* Virtual addresses are sign-extended; physical addresses are zero-extended. */\n \t\tif (IS_ENABLED(CONFIG_MMU))\n@@ -33,12 +33,16 @@ static inline unsigned long __untagged_addr_remote(struct mm_struct *mm, unsigne\n \t(__force __typeof__(addr))__untagged_addr_remote(current-\u003emm, __addr);\t\\\n })\n \n-#define untagged_addr_remote(mm, addr) ({\t\t\t\t\t\\\n+#define untagged_addr_remote_unlocked(mm, addr) ({\t\t\t\t\\\n \tunsigned long __addr = (__force unsigned long)(addr);\t\t\t\\\n-\tmmap_assert_locked(mm);\t\t\t\t\t\t\t\\\n \t(__force __typeof__(addr))__untagged_addr_remote(mm, __addr);\t\t\\\n })\n \n+#define untagged_addr_remote(mm, addr) ({\t\t\t\t\t\\\n+\tmmap_assert_locked(mm);\t\t\t\t\t\t\t\\\n+\tuntagged_addr_remote_unlocked(mm, addr);\t\t\t\t\\\n+})\n+\n #define access_ok(addr, size) likely(__access_ok(untagged_addr(addr), size))\n #else\n #define untagged_addr(addr) (addr)\ndiff --git a/arch/riscv/kernel/process.c b/arch/riscv/kernel/process.c\nindex b2df7f72241a5f..6ae7552fed09f3 100644\n--- a/arch/riscv/kernel/process.c\n+++ b/arch/riscv/kernel/process.c\n@@ -357,13 +357,15 @@ long set_tagged_addr_ctrl(struct task_struct *task, unsigned long arg)\n \tif (mmap_write_lock_killable(mm))\n \t\treturn -EINTR;\n \n-\tif (test_bit(MM_CONTEXT_LOCK_PMLEN, \u0026mm-\u003econtext.flags) \u0026\u0026 mm-\u003econtext.pmlen != pmlen) {\n-\t\tmmap_write_unlock(mm);\n-\t\treturn -EBUSY;\n+\tif (test_bit(MM_CONTEXT_LOCK_PMLEN, \u0026mm-\u003econtext.flags)) {\n+\t\tif (READ_ONCE(mm-\u003econtext.pmlen) != pmlen) {\n+\t\t\tmmap_write_unlock(mm);\n+\t\t\treturn -EBUSY;\n+\t\t}\n \t}\n \n \tenvcfg_update_bits(task, ENVCFG_PMM, pmm);\n-\tmm-\u003econtext.pmlen = pmlen;\n+\tWRITE_ONCE(mm-\u003econtext.pmlen, pmlen);\n \n \tmmap_write_unlock(mm);\n \n@@ -394,7 +396,7 @@ long get_tagged_addr_ctrl(struct task_struct *task)\n \t\tbreak;\n \t}\n \n-\tif (task-\u003emm-\u003econtext.pmlen)\n+\tif (READ_ONCE(task-\u003emm-\u003econtext.pmlen))\n \t\tret |= PR_TAGGED_ADDR_ENABLE;\n \n \treturn ret;\ndiff --git a/arch/x86/include/asm/mmu_context.h b/arch/x86/include/asm/mmu_context.h\nindex ef5b507de34e2e..cee710f64658aa 100644\n--- a/arch/x86/include/asm/mmu_context.h\n+++ b/arch/x86/include/asm/mmu_context.h\n@@ -100,18 +100,18 @@ static inline unsigned long mm_lam_cr3_mask(struct mm_struct *mm)\n static inline void dup_lam(struct mm_struct *oldmm, struct mm_struct *mm)\n {\n \tmm-\u003econtext.lam_cr3_mask = oldmm-\u003econtext.lam_cr3_mask;\n-\tmm-\u003econtext.untag_mask = oldmm-\u003econtext.untag_mask;\n+\tWRITE_ONCE(mm-\u003econtext.untag_mask, READ_ONCE(oldmm-\u003econtext.untag_mask));\n }\n \n #define mm_untag_mask mm_untag_mask\n static inline unsigned long mm_untag_mask(struct mm_struct *mm)\n {\n-\treturn mm-\u003econtext.untag_mask;\n+\treturn READ_ONCE(mm-\u003econtext.untag_mask);\n }\n \n static inline void mm_reset_untag_mask(struct mm_struct *mm)\n {\n-\tmm-\u003econtext.untag_mask = -1UL;\n+\tWRITE_ONCE(mm-\u003econtext.untag_mask, -1UL);\n }\n \n #define arch_pgtable_dma_compat arch_pgtable_dma_compat\ndiff --git a/arch/x86/include/asm/uaccess_64.h b/arch/x86/include/asm/uaccess_64.h\nindex 20de34cc9aa6e7..a095165eb648eb 100644\n--- a/arch/x86/include/asm/uaccess_64.h\n+++ b/arch/x86/include/asm/uaccess_64.h\n@@ -39,18 +39,27 @@ static inline unsigned long __untagged_addr(unsigned long addr)\n \t(__force __typeof__(addr))__untagged_addr(__addr);\t\t\\\n })\n \n+/*\n+ * mm-\u003econtext.untag_mask changes only when the target enables LAM or execs,\n+ * always single-threaded. An unlocked remote reader can race that; READ_ONCE\n+ * keeps the value whole and the untag is best-effort.\n+ */\n static inline unsigned long __untagged_addr_remote(struct mm_struct *mm,\n \t\t\t\t\t\t   unsigned long addr)\n {\n-\tmmap_assert_locked(mm);\n-\treturn addr \u0026 (mm)-\u003econtext.untag_mask;\n+\treturn addr \u0026 READ_ONCE(mm-\u003econtext.untag_mask);\n }\n \n-#define untagged_addr_remote(mm, addr)\t({\t\t\t\t\\\n+#define untagged_addr_remote_unlocked(mm, addr)\t({\t\t\t\\\n \tunsigned long __addr = (__force unsigned long)(addr);\t\t\\\n \t(__force __typeof__(addr))__untagged_addr_remote(mm, __addr);\t\\\n })\n \n+#define untagged_addr_remote(mm, addr)\t({\t\t\t\t\\\n+\tmmap_assert_locked(mm);\t\t\t\t\t\t\\\n+\tuntagged_addr_remote_unlocked(mm, addr);\t\t\t\\\n+})\n+\n #endif\n \n #define valid_user_address(x) \\\ndiff --git a/arch/x86/kernel/process_64.c b/arch/x86/kernel/process_64.c\nindex d44afbe005bb51..9fa659117f3826 100644\n--- a/arch/x86/kernel/process_64.c\n+++ b/arch/x86/kernel/process_64.c\n@@ -814,7 +814,7 @@ static void enable_lam_func(void *__mm)\n static void mm_enable_lam(struct mm_struct *mm)\n {\n \tmm-\u003econtext.lam_cr3_mask = X86_CR3_LAM_U57;\n-\tmm-\u003econtext.untag_mask =  ~GENMASK(62, 57);\n+\tWRITE_ONCE(mm-\u003econtext.untag_mask, ~GENMASK(62, 57));\n \n \t/*\n \t * Even though the process must still be single-threaded at this\n@@ -952,7 +952,7 @@ long do_arch_prctl_64(struct task_struct *task, int option, unsigned long arg2)\n #endif\n #ifdef CONFIG_ADDRESS_MASKING\n \tcase ARCH_GET_UNTAG_MASK:\n-\t\treturn put_user(task-\u003emm-\u003econtext.untag_mask,\n+\t\treturn put_user(mm_untag_mask(task-\u003emm),\n \t\t\t\t(unsigned long __user *)arg2);\n \tcase ARCH_ENABLE_TAGGED_ADDR:\n \t\treturn prctl_enable_tagged_addr(task-\u003emm, arg2);\ndiff --git a/arch/x86/kernel/uprobes.c b/arch/x86/kernel/uprobes.c\nindex 3af979fb41d38d..329efac0cfb37d 100644\n--- a/arch/x86/kernel/uprobes.c\n+++ b/arch/x86/kernel/uprobes.c\n@@ -1036,7 +1036,7 @@ static int copy_from_vaddr(struct mm_struct *mm, unsigned long vaddr, void *dst,\n \tstruct vm_area_struct *vma;\n \tstruct page *page;\n \n-\tpage = get_user_page_vma_remote(mm, vaddr, gup_flags, \u0026vma);\n+\tpage = get_user_page_lookup_vma(mm, vaddr, gup_flags, \u0026vma);\n \tif (IS_ERR(page))\n \t\treturn PTR_ERR(page);\n \tuprobe_copy_from_page(page, vaddr, dst, len);\ndiff --git a/include/linux/mm.h b/include/linux/mm.h\nindex 485df9c2dbddb3..0f66d76e6ca719 100644\n--- a/include/linux/mm.h\n+++ b/include/linux/mm.h\n@@ -3235,35 +3235,9 @@ long pin_user_pages_remote(struct mm_struct *mm,\n \t\t\t   unsigned int gup_flags, struct page **pages,\n \t\t\t   int *locked);\n \n-/*\n- * Retrieves a single page alongside its VMA. Does not support FOLL_NOWAIT.\n- */\n-static inline struct page *get_user_page_vma_remote(struct mm_struct *mm,\n-\t\t\t\t\t\t    unsigned long addr,\n-\t\t\t\t\t\t    int gup_flags,\n-\t\t\t\t\t\t    struct vm_area_struct **vmap)\n-{\n-\tstruct page *page;\n-\tstruct vm_area_struct *vma;\n-\tint got;\n-\n-\tif (WARN_ON_ONCE(unlikely(gup_flags \u0026 FOLL_NOWAIT)))\n-\t\treturn ERR_PTR(-EINVAL);\n-\n-\tgot = get_user_pages_remote(mm, addr, 1, gup_flags, \u0026page, NULL);\n-\n-\tif (got \u003c 0)\n-\t\treturn ERR_PTR(got);\n-\n-\tvma = vma_lookup(mm, addr);\n-\tif (WARN_ON_ONCE(!vma)) {\n-\t\tput_page(page);\n-\t\treturn ERR_PTR(-EINVAL);\n-\t}\n-\n-\t*vmap = vma;\n-\treturn page;\n-}\n+struct page *get_user_page_lookup_vma(struct mm_struct *mm, unsigned long addr,\n+\t\t\t\t      int gup_flags,\n+\t\t\t\t      struct vm_area_struct **vmap);\n \n long get_user_pages(unsigned long start, unsigned long nr_pages,\n \t\t    unsigned int gup_flags, struct page **pages);\ndiff --git a/include/linux/uaccess.h b/include/linux/uaccess.h\nindex eddbbb65ccc4f8..7e6e4c89184cd7 100644\n--- a/include/linux/uaccess.h\n+++ b/include/linux/uaccess.h\n@@ -34,6 +34,13 @@\n })\n #endif\n \n+#ifndef untagged_addr_remote_unlocked\n+#define untagged_addr_remote_unlocked(mm, addr)\t({\t\\\n+\t(void)(mm);\t\t\t\t\t\\\n+\tuntagged_addr(addr);\t\t\t\t\\\n+})\n+#endif\n+\n #ifdef masked_user_access_begin\n  #define can_do_masked_user_access() 1\n # ifndef masked_user_write_access_begin\ndiff --git a/mm/gup.c b/mm/gup.c\nindex 0692119b790431..4910c93ba003e8 100644\n--- a/mm/gup.c\n+++ b/mm/gup.c\n@@ -647,8 +647,9 @@ static inline bool can_follow_write_pud(pud_t pud, struct page *page,\n }\n \n static struct page *follow_huge_pud(struct vm_area_struct *vma,\n-\t\t\t\t    unsigned long addr, pud_t *pudp,\n-\t\t\t\t    int flags, unsigned long *page_mask)\n+\t\t\t\t    unsigned long addr, unsigned long end,\n+\t\t\t\t    pud_t *pudp, int flags,\n+\t\t\t\t    unsigned long *nr_pages)\n {\n \tstruct mm_struct *mm = vma-\u003evm_mm;\n \tstruct page *page;\n@@ -672,10 +673,13 @@ static struct page *follow_huge_pud(struct vm_area_struct *vma,\n \t\treturn ERR_PTR(-EMLINK);\n \n \tret = try_grab_folio(page_folio(page), 1, flags);\n-\tif (ret)\n+\tif (ret) {\n \t\tpage = ERR_PTR(ret);\n-\telse\n-\t\t*page_mask = HPAGE_PUD_NR - 1;\n+\t} else {\n+\t\tunsigned long off = (addr \u0026 ~PUD_MASK) \u003e\u003e PAGE_SHIFT;\n+\n+\t\t*nr_pages = min(HPAGE_PUD_NR - off, (end - addr) \u003e\u003e PAGE_SHIFT);\n+\t}\n \n \treturn page;\n }\n@@ -699,9 +703,9 @@ static inline bool can_follow_write_pmd(pmd_t pmd, struct page *page,\n }\n \n static struct page *follow_huge_pmd(struct vm_area_struct *vma,\n-\t\t\t\t    unsigned long addr, pmd_t *pmd,\n-\t\t\t\t    unsigned int flags,\n-\t\t\t\t    unsigned long *page_mask)\n+\t\t\t\t    unsigned long addr, unsigned long end,\n+\t\t\t\t    pmd_t *pmd, unsigned int flags,\n+\t\t\t\t    unsigned long *nr_pages)\n {\n \tstruct mm_struct *mm = vma-\u003evm_mm;\n \tpmd_t pmdval = *pmd;\n@@ -738,23 +742,25 @@ static struct page *follow_huge_pmd(struct vm_area_struct *vma,\n #endif\t/* CONFIG_TRANSPARENT_HUGEPAGE */\n \n \tpage += (addr \u0026 ~HPAGE_PMD_MASK) \u003e\u003e PAGE_SHIFT;\n-\t*page_mask = HPAGE_PMD_NR - 1;\n+\t*nr_pages = min(HPAGE_PMD_NR - ((addr \u0026 ~HPAGE_PMD_MASK) \u003e\u003e PAGE_SHIFT),\n+\t\t\t(end - addr) \u003e\u003e PAGE_SHIFT);\n \n \treturn page;\n }\n \n #else  /* CONFIG_PGTABLE_HAS_HUGE_LEAVES */\n static struct page *follow_huge_pud(struct vm_area_struct *vma,\n-\t\t\t\t    unsigned long addr, pud_t *pudp,\n-\t\t\t\t    int flags, unsigned long *page_mask)\n+\t\t\t\t    unsigned long addr, unsigned long end,\n+\t\t\t\t    pud_t *pudp, int flags,\n+\t\t\t\t    unsigned long *nr_pages)\n {\n \treturn NULL;\n }\n \n static struct page *follow_huge_pmd(struct vm_area_struct *vma,\n-\t\t\t\t    unsigned long addr, pmd_t *pmd,\n-\t\t\t\t    unsigned int flags,\n-\t\t\t\t    unsigned long *page_mask)\n+\t\t\t\t    unsigned long addr, unsigned long end,\n+\t\t\t\t    pmd_t *pmd, unsigned int flags,\n+\t\t\t\t    unsigned long *nr_pages)\n {\n \treturn NULL;\n }\n@@ -799,8 +805,46 @@ static inline bool can_follow_write_pte(pte_t pte, struct page *page,\n \treturn !userfaultfd_pte_wp(vma, pte);\n }\n \n+/*\n+ * Count the pages, starting at @address and bounded by @end, that a PTE-mapped\n+ * large @folio maps contiguously and that can be returned together with the\n+ * page at @address: consecutive present PTEs mapping consecutive pages of\n+ * @folio with a uniform write bit, within this VMA and a single page table.\n+ * Returns at least 1.\n+ *\n+ * gup_must_unshare() and the write-fault check are per PTE. A writable run is\n+ * always safe: a writable anon page is exclusive, and FOLL_WRITE is satisfied.\n+ * A read-only run is only safe for a plain read; FOLL_WRITE would need a COW\n+ * fault per page and FOLL_PIN would need a per-page gup_must_unshare() check,\n+ * so those fall back to a single page.\n+ */\n+static unsigned long follow_pte_batch(struct vm_area_struct *vma,\n+\t\tunsigned long address, unsigned long end, struct folio *folio,\n+\t\tstruct page *page, pte_t *ptep, pte_t pte, unsigned int flags)\n+{\n+\tpte_t batch_pte = pte;\n+\tunsigned long max;\n+\n+\tif (!pte_write(pte) \u0026\u0026 (flags \u0026 (FOLL_WRITE | FOLL_PIN)))\n+\t\treturn 1;\n+\n+\t/*\n+\t * folio_pte_batch_flags() scans forward from @ptep, so the run must\n+\t * stay within this page table: bound it by the PMD as well as @end and\n+\t * the VMA, since a large folio can be PTE-mapped across a PMD boundary.\n+\t */\n+\tmax = min((pmd_addr_end(address, end) - address) \u003e\u003e PAGE_SHIFT,\n+\t\t  (vma-\u003evm_end - address) \u003e\u003e PAGE_SHIFT);\n+\tif (max \u003c= 1)\n+\t\treturn 1;\n+\n+\treturn folio_pte_batch_flags(folio, vma, ptep, \u0026batch_pte, max,\n+\t\t\t\t     FPB_RESPECT_WRITE);\n+}\n+\n static struct page *follow_page_pte(struct vm_area_struct *vma,\n-\t\tunsigned long address, pmd_t *pmd, unsigned int flags)\n+\t\tunsigned long address, unsigned long end, pmd_t *pmd,\n+\t\tunsigned int flags, unsigned long *nr_pages)\n {\n \tstruct mm_struct *mm = vma-\u003evm_mm;\n \tstruct folio *folio;\n@@ -885,6 +929,15 @@ static struct page *follow_page_pte(struct vm_area_struct *vma,\n \t\t */\n \t\tfolio_mark_accessed(folio);\n \t}\n+\n+\t/*\n+\t * A PTE-mapped large folio can be handed back as a contiguous batch,\n+\t * so the caller advances over the whole run in one step instead of\n+\t * walking the page tables for every page.\n+\t */\n+\tif (folio_test_large(folio))\n+\t\t*nr_pages = follow_pte_batch(vma, address, end, folio, page,\n+\t\t\t\t\t     ptep, pte, flags);\n out:\n \tpte_unmap_unlock(ptep, ptl);\n \treturn page;\n@@ -896,9 +949,9 @@ static struct page *follow_page_pte(struct vm_area_struct *vma,\n }\n \n static struct page *follow_pmd_mask(struct vm_area_struct *vma,\n-\t\t\t\t    unsigned long address, pud_t *pudp,\n-\t\t\t\t    unsigned int flags,\n-\t\t\t\t    unsigned long *page_mask)\n+\t\t\t\t    unsigned long address, unsigned long end,\n+\t\t\t\t    pud_t *pudp, unsigned int flags,\n+\t\t\t\t    unsigned long *nr_pages)\n {\n \tpmd_t *pmd, pmdval;\n \tspinlock_t *ptl;\n@@ -912,7 +965,7 @@ static struct page *follow_pmd_mask(struct vm_area_struct *vma,\n \tif (!pmd_present(pmdval))\n \t\treturn no_page_table(vma, flags, address);\n \tif (likely(!pmd_leaf(pmdval)))\n-\t\treturn follow_page_pte(vma, address, pmd, flags);\n+\t\treturn follow_page_pte(vma, address, end, pmd, flags, nr_pages);\n \n \tif (pmd_protnone(pmdval) \u0026\u0026 !gup_can_follow_protnone(vma, flags))\n \t\treturn no_page_table(vma, flags, address);\n@@ -925,24 +978,24 @@ static struct page *follow_pmd_mask(struct vm_area_struct *vma,\n \t}\n \tif (unlikely(!pmd_leaf(pmdval))) {\n \t\tspin_unlock(ptl);\n-\t\treturn follow_page_pte(vma, address, pmd, flags);\n+\t\treturn follow_page_pte(vma, address, end, pmd, flags, nr_pages);\n \t}\n \tif (pmd_trans_huge(pmdval) \u0026\u0026 (flags \u0026 FOLL_SPLIT_PMD)) {\n \t\tspin_unlock(ptl);\n \t\tsplit_huge_pmd(vma, pmd, address);\n \t\t/* If pmd was left empty, stuff a page table in there quickly */\n \t\treturn pte_alloc(mm, pmd) ? ERR_PTR(-ENOMEM) :\n-\t\t\tfollow_page_pte(vma, address, pmd, flags);\n+\t\t\tfollow_page_pte(vma, address, end, pmd, flags, nr_pages);\n \t}\n-\tpage = follow_huge_pmd(vma, address, pmd, flags, page_mask);\n+\tpage = follow_huge_pmd(vma, address, end, pmd, flags, nr_pages);\n \tspin_unlock(ptl);\n \treturn page;\n }\n \n static struct page *follow_pud_mask(struct vm_area_struct *vma,\n-\t\t\t\t    unsigned long address, p4d_t *p4dp,\n-\t\t\t\t    unsigned int flags,\n-\t\t\t\t    unsigned long *page_mask)\n+\t\t\t\t    unsigned long address, unsigned long end,\n+\t\t\t\t    p4d_t *p4dp, unsigned int flags,\n+\t\t\t\t    unsigned long *nr_pages)\n {\n \tpud_t *pudp, pud;\n \tspinlock_t *ptl;\n@@ -955,7 +1008,7 @@ static struct page *follow_pud_mask(struct vm_area_struct *vma,\n \t\treturn no_page_table(vma, flags, address);\n \tif (pud_leaf(pud)) {\n \t\tptl = pud_lock(mm, pudp);\n-\t\tpage = follow_huge_pud(vma, address, pudp, flags, page_mask);\n+\t\tpage = follow_huge_pud(vma, address, end, pudp, flags, nr_pages);\n \t\tspin_unlock(ptl);\n \t\tif (page)\n \t\t\treturn page;\n@@ -964,13 +1017,13 @@ static struct page *follow_pud_mask(struct vm_area_struct *vma,\n \tif (unlikely(pud_bad(pud)))\n \t\treturn no_page_table(vma, flags, address);\n \n-\treturn follow_pmd_mask(vma, address, pudp, flags, page_mask);\n+\treturn follow_pmd_mask(vma, address, end, pudp, flags, nr_pages);\n }\n \n static struct page *follow_p4d_mask(struct vm_area_struct *vma,\n-\t\t\t\t    unsigned long address, pgd_t *pgdp,\n-\t\t\t\t    unsigned int flags,\n-\t\t\t\t    unsigned long *page_mask)\n+\t\t\t\t    unsigned long address, unsigned long end,\n+\t\t\t\t    pgd_t *pgdp, unsigned int flags,\n+\t\t\t\t    unsigned long *nr_pages)\n {\n \tp4d_t *p4dp, p4d;\n \n@@ -981,15 +1034,16 @@ static struct page *follow_p4d_mask(struct vm_area_struct *vma,\n \tif (!p4d_present(p4d) || p4d_bad(p4d))\n \t\treturn no_page_table(vma, flags, address);\n \n-\treturn follow_pud_mask(vma, address, p4dp, flags, page_mask);\n+\treturn follow_pud_mask(vma, address, end, p4dp, flags, nr_pages);\n }\n \n /**\n  * follow_page_mask - look up a page descriptor from a user-virtual address\n  * @vma: vm_area_struct mapping @address\n  * @address: virtual address to look up\n+ * @end: virtual address at which to stop batching contiguous pages\n  * @flags: flags modifying lookup behaviour\n- * @page_mask: a pointer to output page_mask\n+ * @nr_pages: output; number of contiguous pages the caller can read\n  *\n  * @flags can have FOLL_ flags set, defined in \u003clinux/mm.h\u003e\n  *\n@@ -998,15 +1052,17 @@ static struct page *follow_p4d_mask(struct vm_area_struct *vma,\n  * trigger a fault with FAULT_FLAG_UNSHARE set. Note that unsharing is only\n  * relevant with FOLL_PIN and !FOLL_WRITE.\n  *\n- * On output, @page_mask is set according to the size of the page.\n+ * On output, @nr_pages holds how many contiguous pages the folio that includes\n+ * the returned page has mapped into this process, so the caller can advance\n+ * over a large folio in one step.\n  *\n  * Return: the mapped (struct page *), %NULL if no mapping exists, or\n  * an error pointer if there is a mapping to something not represented\n  * by a page descriptor (see also vm_normal_page()).\n  */\n static struct page *follow_page_mask(struct vm_area_struct *vma,\n-\t\t\t      unsigned long address, unsigned int flags,\n-\t\t\t      unsigned long *page_mask)\n+\t\t\t      unsigned long address, unsigned long end,\n+\t\t\t      unsigned int flags, unsigned long *nr_pages)\n {\n \tpgd_t *pgd;\n \tstruct mm_struct *mm = vma-\u003evm_mm;\n@@ -1014,13 +1070,13 @@ static struct page *follow_page_mask(struct vm_area_struct *vma,\n \n \tvma_pgtable_walk_begin(vma);\n \n-\t*page_mask = 0;\n+\t*nr_pages = 1;\n \tpgd = pgd_offset(mm, address);\n \n \tif (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))\n \t\tpage = no_page_table(vma, flags, address);\n \telse\n-\t\tpage = follow_p4d_mask(vma, address, pgd, flags, page_mask);\n+\t\tpage = follow_p4d_mask(vma, address, end, pgd, flags, nr_pages);\n \n \tvma_pgtable_walk_end(vma);\n \n@@ -1080,9 +1136,16 @@ static int get_gate_page(struct mm_struct *mm, unsigned long address,\n }\n \n /*\n- * mmap_lock must be held on entry.  If @flags has FOLL_UNLOCKABLE but not\n- * FOLL_NOWAIT, the mmap_lock may be released.  If it is, *@locked will be set\n- * to 0 and -EBUSY returned.\n+ * The caller holds either the mmap lock, or the per-VMA lock (with\n+ * FOLL_VMA_LOCK), on entry. If @flags has FOLL_UNLOCKABLE but not FOLL_NOWAIT,\n+ * the mmap_lock may be released. If it is, *@locked will be set to 0 and\n+ * -EAGAIN returned.\n+ *\n+ * The return value does not depend on the lock type: a fault that made\n+ * progress but needs a retry (VM_FAULT_RETRY / VM_FAULT_COMPLETED) is reported\n+ * as -EAGAIN for both the mmap lock and the per-VMA lock (FOLL_VMA_LOCK). Only\n+ * the *@locked side effect is lock-type specific, as the per-VMA lock path has\n+ * no unlockable mmap_lock to drop.\n  */\n static int faultin_page(struct vm_area_struct *vma,\n \t\tunsigned long address, unsigned int flags, bool unshare,\n@@ -1097,6 +1160,8 @@ static int faultin_page(struct vm_area_struct *vma,\n \t\tfault_flags |= FAULT_FLAG_WRITE;\n \tif (flags \u0026 FOLL_REMOTE)\n \t\tfault_flags |= FAULT_FLAG_REMOTE;\n+\tif (flags \u0026 FOLL_VMA_LOCK)\n+\t\tfault_flags |= FAULT_FLAG_VMA_LOCK;\n \tif (flags \u0026 FOLL_UNLOCKABLE) {\n \t\tfault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;\n \t\t/*\n@@ -1125,41 +1190,195 @@ static int faultin_page(struct vm_area_struct *vma,\n \n \tret = handle_mm_fault(vma, address, fault_flags, NULL);\n \n+\t/*\n+\t * A fully completed fault (VM_FAULT_COMPLETED) or one that needs a retry\n+\t * (VM_FAULT_RETRY) has released the lock it was holding. Report both as\n+\t * -EAGAIN so the caller retries: the mmap lock caller retakes it here,\n+\t * the per-VMA lock caller (FOLL_VMA_LOCK) falls back to the mmap lock.\n+\t *\n+\t * Dropping the mmap lock is recorded in *@locked. There is no such lock\n+\t * to drop under the per-VMA lock, where @locked is not used, so leave it\n+\t * alone in that case.\n+\t */\n \tif (ret \u0026 VM_FAULT_COMPLETED) {\n-\t\t/*\n-\t\t * With FAULT_FLAG_RETRY_NOWAIT we'll never release the\n-\t\t * mmap lock in the page fault handler. Sanity check this.\n-\t\t */\n-\t\tWARN_ON_ONCE(fault_flags \u0026 FAULT_FLAG_RETRY_NOWAIT);\n-\t\t*locked = 0;\n-\n-\t\t/*\n-\t\t * We should do the same as VM_FAULT_RETRY, but let's not\n-\t\t * return -EBUSY since that's not reflecting the reality of\n-\t\t * what has happened - we've just fully completed a page\n-\t\t * fault, with the mmap lock released.  Use -EAGAIN to show\n-\t\t * that we want to take the mmap lock _again_.\n-\t\t */\n+\t\tif (!(flags \u0026 FOLL_VMA_LOCK)) {\n+\t\t\t/*\n+\t\t\t * With FAULT_FLAG_RETRY_NOWAIT we'll never release the\n+\t\t\t * mmap lock in the page fault handler. Sanity check this.\n+\t\t\t */\n+\t\t\tWARN_ON_ONCE(fault_flags \u0026 FAULT_FLAG_RETRY_NOWAIT);\n+\t\t\t*locked = 0;\n+\t\t}\n \t\treturn -EAGAIN;\n \t}\n \n \tif (ret \u0026 VM_FAULT_ERROR) {\n \t\tint err = vm_fault_to_errno(ret, flags);\n \n-\t\tif (err)\n-\t\t\treturn err;\n-\t\tBUG();\n+\t\t/*\n+\t\t * VM_FAULT_ERROR always decodes to an errno; a zero here would\n+\t\t * mean handle_mm_fault() returned an unexpected combination.\n+\t\t * Report -EFAULT rather than crash: under the per-VMA lock the\n+\t\t * mmap lock retry produces the definitive result.\n+\t\t */\n+\t\tVM_WARN_ON_ONCE(!err);\n+\t\treturn err ? err : -EFAULT;\n \t}\n \n \tif (ret \u0026 VM_FAULT_RETRY) {\n-\t\tif (!(fault_flags \u0026 FAULT_FLAG_RETRY_NOWAIT))\n+\t\tif (!(flags \u0026 FOLL_VMA_LOCK) \u0026\u0026\n+\t\t    !(fault_flags \u0026 FAULT_FLAG_RETRY_NOWAIT))\n \t\t\t*locked = 0;\n-\t\treturn -EBUSY;\n+\t\treturn -EAGAIN;\n \t}\n \n \treturn 0;\n }\n \n+/*\n+ * get_user_page_vma - get one page from @vma, whose lock the caller already\n+ * holds: the mmap lock, or (with FOLL_VMA_LOCK) the per-VMA lock. Walks the\n+ * page tables, faulting the page in if needed, and on success returns it with\n+ * a reference and the lock still held.\n+ *\n+ * Runs check_vma_flags() like __get_user_pages(), so callers need not pre-check\n+ * the VMA; most rejections are returned as their error. A VM_IO/VM_PFNMAP VMA\n+ * is the exception: a COWed page with a struct page is returned, while a raw\n+ * PFN has none and yields -EFAULT, to be reached via vma-\u003evm_ops-\u003eaccess().\n+ *\n+ * Under FOLL_VMA_LOCK, anything that cannot be finished under the per-VMA lock\n+ * (a dropped fault, userfaultfd, a hard error, or -\u003eaccess() memory) releases\n+ * the lock and returns -EAGAIN, so the caller retries under the mmap lock.\n+ */\n+struct page *get_user_page_vma(struct vm_area_struct *vma, unsigned long addr,\n+\t\t\t       unsigned int gup_flags)\n+{\n+\tbool vma_locked = gup_flags \u0026 FOLL_VMA_LOCK;\n+\tunsigned long nr_pages;\n+\tstruct page *page;\n+\tint locked = 1;\n+\tbool pfnmap;\n+\tint ret;\n+\n+\t/*\n+\t * Two lock modes are supported: the mmap lock, with neither flag, or\n+\t * the per-VMA lock, with both FOLL_VMA_LOCK and FOLL_UNLOCKABLE. An\n+\t * unlockable mmap fault would drop the lock and report it in *locked,\n+\t * which this function does not relay, so reject a lone flag.\n+\t */\n+\tVM_WARN_ON_ONCE(!(gup_flags \u0026 FOLL_VMA_LOCK) != !(gup_flags \u0026 FOLL_UNLOCKABLE));\n+\n+\t/*\n+\t * Validate the VMA up front, like __get_user_pages(). A VM_IO/VM_PFNMAP\n+\t * VMA is not rejected outright: it can hold COWed pages that have a\n+\t * struct page; let follow_page_mask() look for them, and treat only\n+\t * its struct-page-less PFNs as unreachable. Other rejections\n+\t * (secretmem, permissions, etc) result in immediate failure.\n+\t */\n+\tret = check_vma_flags(vma, gup_flags, VM_IO | VM_PFNMAP);\n+\tif (ret)\n+\t\tgoto fail;\n+\tpfnmap = vma-\u003evm_flags \u0026 (VM_IO | VM_PFNMAP);\n+\n+\tfor (;;) {\n+\t\tif (fatal_signal_pending(current)) {\n+\t\t\tret = -EINTR;\n+\t\t\tgoto fail;\n+\t\t}\n+\t\tcond_resched();\n+\n+\t\t/* This helper hands back a single page; cap the batch at one. */\n+\t\tpage = follow_page_mask(vma, addr, addr + PAGE_SIZE,\n+\t\t\t\t\tgup_flags | FOLL_TOUCH | FOLL_GET,\n+\t\t\t\t\t\u0026nr_pages);\n+\t\tif (!IS_ERR_OR_NULL(page)) {\n+\t\t\t/* Match __get_user_pages(): flush for VIVT/aliasing caches. */\n+\t\t\tflush_anon_page(vma, page, addr);\n+\t\t\tflush_dcache_page(page);\n+\t\t\treturn page;\n+\t\t}\n+\n+\t\t/*\n+\t\t * No struct page: a raw PFN of a VM_IO/VM_PFNMAP VMA, whether\n+\t\t * seen by the up-front check (@pfnmap) or reported as -EEXIST\n+\t\t * for a present PFN. Return -EFAULT so the caller reaches it\n+\t\t * through vma-\u003evm_ops-\u003eaccess().\n+\t\t */\n+\t\tif (pfnmap || PTR_ERR(page) == -EEXIST) {\n+\t\t\tret = -EFAULT;\n+\t\t\tgoto fail;\n+\t\t}\n+\t\t/* A hard error from the walk itself. */\n+\t\tif (page \u0026\u0026 PTR_ERR(page) != -EMLINK) {\n+\t\t\tret = PTR_ERR(page);\n+\t\t\tgoto fail;\n+\t\t}\n+\n+\t\t/*\n+\t\t * The page is not present, or needs unsharing. A remote fault\n+\t\t * under the per-VMA lock cannot deliver userfaultfd (which\n+\t\t * assumes current is the faulting task), so fall back for those.\n+\t\t */\n+\t\tif (vma_locked \u0026\u0026 userfaultfd_armed(vma)) {\n+\t\t\tret = -EAGAIN;\n+\t\t\tgoto fail;\n+\t\t}\n+\t\tret = faultin_page(vma, addr, gup_flags | FOLL_REMOTE | FOLL_GET,\n+\t\t\t\t   PTR_ERR(page) == -EMLINK, \u0026locked);\n+\t\tif (ret == -EAGAIN)\n+\t\t\treturn ERR_PTR(-EAGAIN);\t/* fault released the per-VMA lock */\n+\t\tif (ret)\n+\t\t\tgoto fail;\n+\t}\n+\n+fail:\n+\t/*\n+\t * Under the per-VMA lock the caller cannot reach -\u003eaccess() or act on a\n+\t * hard error (both need the mmap lock), so release the lock and have it\n+\t * retry there; the mmap-lock pass produces the definitive error.\n+\t */\n+\tif (vma_locked) {\n+\t\tvma_end_read(vma);\n+\t\treturn ERR_PTR(-EAGAIN);\n+\t}\n+\treturn ERR_PTR(ret);\n+}\n+\n+/*\n+ * get_user_page_lookup_vma - fault in one page of a remote mm and hand back the\n+ * page along with the VMA that covers it. The caller must hold the mmap_lock.\n+ * Returns with the mmap_lock still held.\n+ *\n+ * Looks up the VMA, and gets a reference to the page through\n+ * get_user_page_vma(), faulting in the page if needed.\n+ *\n+ * FOLL_NOWAIT and FOLL_UNLOCKABLE are rejected: both let the fault handler\n+ * drop the mmap lock, which could invalidate the looked-up VMA.\n+ */\n+struct page *get_user_page_lookup_vma(struct mm_struct *mm, unsigned long addr,\n+\t\t\t\t      int gup_flags,\n+\t\t\t\t      struct vm_area_struct **vmap)\n+{\n+\tstruct vm_area_struct *vma;\n+\tstruct page *page;\n+\n+\tif (WARN_ON_ONCE(unlikely(gup_flags \u0026 (FOLL_NOWAIT | FOLL_UNLOCKABLE))))\n+\t\treturn ERR_PTR(-EINVAL);\n+\n+\tmmap_assert_locked(mm);\n+\n+\tvma = vma_lookup(mm, addr);\n+\tif (!vma)\n+\t\treturn ERR_PTR(-EFAULT);\n+\n+\tpage = get_user_page_vma(vma, addr, gup_flags | FOLL_REMOTE | FOLL_TOUCH);\n+\tif (IS_ERR(page))\n+\t\treturn page;\n+\n+\t*vmap = vma;\n+\treturn page;\n+}\n+\n /*\n  * Writing to file-backed mappings which require folio dirty tracking using GUP\n  * is a fundamentally broken operation, as kernel write access to GUP mappings\n@@ -1197,13 +1416,21 @@ static bool writable_file_mapping_allowed(struct vm_area_struct *vma,\n \treturn !vma_needs_dirty_tracking(vma);\n }\n \n-static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags)\n+int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags,\n+\t\t    vm_flags_t ignore_flags)\n {\n \tvm_flags_t vm_flags = vma-\u003evm_flags;\n \tint write = (gup_flags \u0026 FOLL_WRITE);\n \tint foreign = (gup_flags \u0026 FOLL_REMOTE);\n \tbool vma_anon = vma_is_anonymous(vma);\n \n+\t/*\n+\t * Opt out of the flag checks that read this local copy (the\n+\t * VM_IO/VM_PFNMAP gate and the write/read/cow bits); checks that\n+\t * re-read vma-\u003evm_flags through helpers are unaffected.\n+\t */\n+\tvm_flags \u0026= ~ignore_flags;\n+\n \tif (vm_flags \u0026 (VM_IO | VM_PFNMAP))\n \t\treturn -EFAULT;\n \n@@ -1358,7 +1585,6 @@ static long __get_user_pages(struct mm_struct *mm,\n {\n \tlong ret = 0, i = 0;\n \tstruct vm_area_struct *vma = NULL;\n-\tunsigned long page_mask = 0;\n \n \tif (!nr_pages)\n \t\treturn 0;\n@@ -1373,7 +1599,7 @@ static long __get_user_pages(struct mm_struct *mm,\n \n \tdo {\n \t\tstruct page *page;\n-\t\tunsigned int page_increm;\n+\t\tunsigned long page_increm;\n \n \t\t/* first iteration or cross vma bound */\n \t\tif (!vma || start \u003e= vma-\u003evm_end) {\n@@ -1387,7 +1613,7 @@ static long __get_user_pages(struct mm_struct *mm,\n \t\t\t\t\tret = -ENOMEM;\n \t\t\t\t\tgoto out;\n \t\t\t\t}\n-\t\t\t\tif (check_vma_flags(vma, gup_flags)) {\n+\t\t\t\tif (check_vma_flags(vma, gup_flags, 0)) {\n \t\t\t\t\tret = -EINVAL;\n \t\t\t\t\tgoto out;\n \t\t\t\t}\n@@ -1400,7 +1626,7 @@ static long __get_user_pages(struct mm_struct *mm,\n \t\t\t\t\t\tpages ? \u0026page : NULL);\n \t\t\t\tif (ret)\n \t\t\t\t\tgoto out;\n-\t\t\t\tpage_mask = 0;\n+\t\t\t\tpage_increm = 1;\n \t\t\t\tgoto next_page;\n \t\t\t}\n \n@@ -1408,7 +1634,7 @@ static long __get_user_pages(struct mm_struct *mm,\n \t\t\t\tret = -EFAULT;\n \t\t\t\tgoto out;\n \t\t\t}\n-\t\t\tret = check_vma_flags(vma, gup_flags);\n+\t\t\tret = check_vma_flags(vma, gup_flags, 0);\n \t\t\tif (ret)\n \t\t\t\tgoto out;\n \t\t}\n@@ -1423,7 +1649,8 @@ static long __get_user_pages(struct mm_struct *mm,\n \t\t}\n \t\tcond_resched();\n \n-\t\tpage = follow_page_mask(vma, start, gup_flags, \u0026page_mask);\n+\t\tpage = follow_page_mask(vma, start, start + nr_pages * PAGE_SIZE,\n+\t\t\t\t\tgup_flags, \u0026page_increm);\n \t\tif (!page || PTR_ERR(page) == -EMLINK) {\n \t\t\tret = faultin_page(vma, start, gup_flags,\n \t\t\t\t\t   PTR_ERR(page) == -EMLINK, locked);\n@@ -1456,7 +1683,6 @@ static long __get_user_pages(struct mm_struct *mm,\n \t\t\tgoto out;\n \t\t}\n next_page:\n-\t\tpage_increm = 1 + (~(start \u003e\u003e PAGE_SHIFT) \u0026 page_mask);\n \t\tif (page_increm \u003e nr_pages)\n \t\t\tpage_increm = nr_pages;\n \ndiff --git a/mm/internal.h b/mm/internal.h\nindex 181e79f1d6a207..706f7f08fd1815 100644\n--- a/mm/internal.h\n+++ b/mm/internal.h\n@@ -1595,6 +1595,10 @@ struct vm_struct *__get_vm_area_node(unsigned long size,\n  */\n int __must_check try_grab_folio(struct folio *folio, int refs,\n \t\t\t\tunsigned int flags);\n+int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags,\n+\t\t    vm_flags_t ignore_flags);\n+struct page *get_user_page_vma(struct vm_area_struct *vma, unsigned long addr,\n+\t\t\t       unsigned int gup_flags);\n \n /*\n  * mm/huge_memory.c\n@@ -1641,11 +1645,13 @@ enum {\n \tFOLL_UNLOCKABLE = 1 \u003c\u003c 21,\n \t/* VMA lookup+checks compatible with MADV_POPULATE_(READ|WRITE) */\n \tFOLL_MADV_POPULATE = 1 \u003c\u003c 22,\n+\t/* caller holds the per-VMA lock, not the mmap lock */\n+\tFOLL_VMA_LOCK = 1 \u003c\u003c 23,\n };\n \n #define INTERNAL_GUP_FLAGS (FOLL_TOUCH | FOLL_TRIED | FOLL_REMOTE | FOLL_PIN | \\\n \t\t\t    FOLL_FAST_ONLY | FOLL_UNLOCKABLE | \\\n-\t\t\t    FOLL_MADV_POPULATE)\n+\t\t\t    FOLL_MADV_POPULATE | FOLL_VMA_LOCK)\n \n /*\n  * Indicates for which pages that are write-protected in the page table,\ndiff --git a/mm/memory.c b/mm/memory.c\nindex ff338c2abe9231..aaf620017f3442 100644\n--- a/mm/memory.c\n+++ b/mm/memory.c\n@@ -7015,86 +7015,244 @@ EXPORT_SYMBOL_GPL(generic_access_phys);\n #endif\n \n /*\n- * Access another process' address space as given in mm.\n+ * VM_IO / VM_PFNMAP memory, such as an ioremapped device mapping, maps\n+ * PFNs that have no struct page, so get_user_page_vma() cannot fetch it\n+ * even though the page tables are populated. It can still be reached\n+ * through vma-\u003evm_ops-\u003eaccess().\n+ *\n+ * Returns the number of bytes transferred, or \u003c= 0 if @vma cannot be\n+ * accessed this way.\n  */\n-static int __access_remote_vm(struct mm_struct *mm, unsigned long addr,\n-\t\t\t      void *buf, int len, unsigned int gup_flags)\n+static int access_remote_vma_ops(struct vm_area_struct *vma, unsigned long addr,\n+\t\t\t\t void *buf, int len, int write)\n+{\n+#ifdef CONFIG_HAVE_IOREMAP_PROT\n+\tif (vma-\u003evm_ops \u0026\u0026 vma-\u003evm_ops-\u003eaccess)\n+\t\treturn vma-\u003evm_ops-\u003eaccess(vma, addr, buf, len, write);\n+#endif\n+\treturn 0;\n+}\n+\n+/*\n+ * Lock @mm to reach the remote range [@addr, @addr + @len).\n+ *\n+ * Take the per-VMA lock when the whole range fits in a single VMA whose\n+ * flags permit the access. The RCU freed page tables then keep page table\n+ * memory from being reused with unexpected contents while the lock is held.\n+ * Otherwise fall back to the mmap lock, which also covers multi-VMA ranges,\n+ * stack expansion, and -\u003eaccess() memory.\n+ *\n+ * Return whether the mmap lock is held. The per-VMA locked VMA, when one is\n+ * taken, is stored in *@vmap; it is NULL on the mmap lock path. *@vmap is\n+ * set to an ERR_PTR() when the mmap lock could not be taken, so callers must\n+ * check IS_ERR(*@vmap) before using either result.\n+ */\n+static bool remote_access_lock(struct mm_struct *mm, unsigned long addr,\n+\t\t\t       int len, unsigned int gup_flags,\n+\t\t\t       struct vm_area_struct **vmap)\n+{\n+\tstruct vm_area_struct *vma = NULL;\n+\n+#if defined(CONFIG_PER_VMA_LOCK) \u0026\u0026 defined(CONFIG_MMU_GATHER_RCU_TABLE_FREE)\n+\tvma = lock_vma_under_rcu(mm, addr);\n+\tif (vma) {\n+\t\t/* addr + len must not wrap, and must fit within the one VMA. */\n+\t\tif (addr + len \u003c addr || addr + len \u003e vma-\u003evm_end ||\n+\t\t    check_vma_flags(vma, gup_flags, 0)) {\n+\t\t\tvma_end_read(vma);\n+\t\t\tvma = NULL;\n+\t\t}\n+\t}\n+#endif\n+\n+\tif (!vma) {\n+\t\tif (mmap_read_lock_killable(mm)) {\n+\t\t\t*vmap = ERR_PTR(-EINTR);\n+\t\t\treturn false;\n+\t\t}\n+\t\t*vmap = NULL;\n+\t\treturn true;\n+\t}\n+\n+\t*vmap = vma;\n+\treturn false;\n+}\n+\n+/* Release the lock taken by remote_access_lock(). */\n+static void remote_access_unlock(struct mm_struct *mm,\n+\t\t\t\t struct vm_area_struct *vma, bool have_mmap_lock)\n+{\n+\tif (have_mmap_lock)\n+\t\tmmap_read_unlock(mm);\n+\telse if (vma)\n+\t\tvma_end_read(vma);\n+}\n+\n+/*\n+ * Per-page action for a remote VM walk. Handle up to @len bytes at @addr on\n+ * @page, advancing *@buf past the bytes read from or written to it. @page is\n+ * NULL for struct-page-less memory (VM_IO / VM_PFNMAP) reached under the mmap\n+ * lock.\n+ *\n+ * Return the number of source bytes handled at @addr, 0 to end the walk (a\n+ * string reached its NUL, or -\u003eaccess() memory could not be reached), or a\n+ * negative errno to abort.\n+ */\n+typedef int (*remote_vm_action)(struct vm_area_struct *vma, struct page *page,\n+\t\t\t\tunsigned long addr, void **buf, int len,\n+\t\t\t\tint write);\n+\n+/*\n+ * Walk the remote range [@addr, @addr + @len) of @mm, handing each page to\n+ * @action. Use the per-VMA lock when the range fits one VMA, and fall back to\n+ * the mmap lock for multi-VMA ranges, stack expansion (when @can_expand_stack\n+ * is set), or when the per-VMA lock cannot finish a fault.\n+ *\n+ * Each page is faulted in with get_user_page_vma() under whichever lock is\n+ * held. Return the number of bytes @action consumed; *@err is a negative\n+ * errno when the walk aborted, else 0.\n+ */\n+static int remote_vm_walk(struct mm_struct *mm, unsigned long addr, void *buf,\n+\t\t\t  int len, unsigned int gup_flags, bool can_expand_stack,\n+\t\t\t  remote_vm_action action, int *err)\n {\n \tvoid *old_buf = buf;\n \tint write = gup_flags \u0026 FOLL_WRITE;\n+\tbool have_mmap_lock;\n+\tstruct vm_area_struct *vma;\n \n-\tif (mmap_read_lock_killable(mm))\n-\t\treturn 0;\n+\t*err = 0;\n+\n+\t/*\n+\t * Set FOLL_REMOTE so check_vma_flags() applies the same protection key\n+\t * rules as get_user_pages_remote() did: the current PKRU is not checked\n+\t * against a VMA reached on @mm's behalf.\n+\t */\n+\tgup_flags |= FOLL_REMOTE;\n \n-\t/* Untag the address before looking up the VMA */\n-\taddr = untagged_addr_remote(mm, addr);\n+\taddr = untagged_addr_remote_unlocked(mm, addr);\n \n-\t/* Avoid triggering the temporary warning in __get_user_pages */\n-\tif (!vma_lookup(mm, addr) \u0026\u0026 !expand_stack(mm, addr))\n+\thave_mmap_lock = remote_access_lock(mm, addr, len, gup_flags, \u0026vma);\n+\tif (IS_ERR(vma)) {\n+\t\t*err = -EFAULT;\n \t\treturn 0;\n+\t}\n \n-\t/* ignore errors, just check how much was successfully transferred */\n \twhile (len) {\n-\t\tint bytes, offset;\n-\t\tvoid *maddr;\n-\t\tstruct folio *folio;\n-\t\tstruct vm_area_struct *vma = NULL;\n-\t\tstruct page *page = get_user_page_vma_remote(mm, addr,\n-\t\t\t\t\t\t\t     gup_flags, \u0026vma);\n+\t\tunsigned int foll_flags = gup_flags;\n+\t\tstruct page *page;\n+\t\tint ret;\n \n-\t\tif (IS_ERR(page)) {\n-\t\t\t/* We might need to expand the stack to access it */\n+\t\tif (!vma || addr \u003e= vma-\u003evm_end) {\n+\t\t\t/* Any lookup here holds the mmap lock. */\n+\t\t\tVM_BUG_ON(!have_mmap_lock);\n \t\t\tvma = vma_lookup(mm, addr);\n-\t\t\tif (!vma) {\n+\t\t\tif (!vma \u0026\u0026 can_expand_stack) {\n+\t\t\t\t/* expand_stack() drops the mmap lock if it fails */\n \t\t\t\tvma = expand_stack(mm, addr);\n-\n-\t\t\t\t/* mmap_lock was dropped on failure */\n \t\t\t\tif (!vma)\n-\t\t\t\t\treturn buf - old_buf;\n-\n-\t\t\t\t/* Try again if stack expansion worked */\n-\t\t\t\tcontinue;\n+\t\t\t\t\thave_mmap_lock = false;\n+\t\t\t}\n+\t\t\tif (!vma) {\n+\t\t\t\t*err = -EFAULT;\n+\t\t\t\tbreak;\n \t\t\t}\n+\t\t}\n+\n+\t\t/*\n+\t\t * FOLL_UNLOCKABLE lets the per-VMA fault retry, dropping the\n+\t\t * lock, so the walk can fall back to the mmap lock.\n+\t\t */\n+\t\tif (!have_mmap_lock)\n+\t\t\tfoll_flags |= FOLL_VMA_LOCK | FOLL_UNLOCKABLE;\n \n+\t\tpage = get_user_page_vma(vma, addr, foll_flags);\n+\t\tif (IS_ERR(page)) {\n \t\t\t/*\n-\t\t\t * Check if this is a VM_IO | VM_PFNMAP VMA, which\n-\t\t\t * we can access using slightly different code.\n+\t\t\t * get_user_page_vma() returns -EAGAIN, with the per-VMA\n+\t\t\t * lock released, for anything it could not finish under\n+\t\t\t * it; retake the mmap lock and retry. A different error\n+\t\t\t * therefore only arrives under the mmap lock, where\n+\t\t\t * struct-page-less memory can be reached via -\u003eaccess().\n \t\t\t */\n-\t\t\tbytes = 0;\n-#ifdef CONFIG_HAVE_IOREMAP_PROT\n-\t\t\tif (vma-\u003evm_ops \u0026\u0026 vma-\u003evm_ops-\u003eaccess)\n-\t\t\t\tbytes = vma-\u003evm_ops-\u003eaccess(vma, addr, buf,\n-\t\t\t\t\t\t\t    len, write);\n-#endif\n-\t\t\tif (bytes \u003c= 0)\n-\t\t\t\tbreak;\n-\t\t} else {\n-\t\t\tfolio = page_folio(page);\n-\t\t\tbytes = len;\n-\t\t\toffset = addr \u0026 (PAGE_SIZE-1);\n-\t\t\tif (bytes \u003e PAGE_SIZE-offset)\n-\t\t\t\tbytes = PAGE_SIZE-offset;\n-\n-\t\t\tmaddr = kmap_local_folio(folio, folio_page_idx(folio, page) * PAGE_SIZE);\n-\t\t\tif (write) {\n-\t\t\t\tcopy_to_user_page(vma, page, addr,\n-\t\t\t\t\t\t  maddr + offset, buf, bytes);\n-\t\t\t\tfolio_mark_dirty_lock(folio);\n-\t\t\t} else {\n-\t\t\t\tcopy_from_user_page(vma, page, addr,\n-\t\t\t\t\t\t    buf, maddr + offset, bytes);\n+\t\t\tif (PTR_ERR(page) == -EAGAIN) {\n+\t\t\t\tvma = NULL;\n+\t\t\t\tif (mmap_read_lock_killable(mm)) {\n+\t\t\t\t\t*err = -EFAULT;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t\thave_mmap_lock = true;\n+\t\t\t\tcontinue;\n \t\t\t}\n-\t\t\tfolio_release_kmap(folio, maddr);\n+\t\t\tif (WARN_ON_ONCE(!have_mmap_lock))\n+\t\t\t\tbreak;\n+\t\t\tpage = NULL;\n+\t\t}\n+\n+\t\tret = action(vma, page, addr, \u0026buf, len, write);\n+\t\tif (ret \u003c= 0) {\n+\t\t\tif (ret \u003c 0)\n+\t\t\t\t*err = ret;\n+\t\t\tbreak;\n \t\t}\n-\t\tlen -= bytes;\n-\t\tbuf += bytes;\n-\t\taddr += bytes;\n+\t\taddr += ret;\n+\t\tlen -= ret;\n \t}\n-\tmmap_read_unlock(mm);\n+\n+\tremote_access_unlock(mm, vma, have_mmap_lock);\n \n \treturn buf - old_buf;\n }\n \n+/*\n+ * Copy one page's worth of [@addr, @addr + @len) to or from *@buf. Reaches\n+ * struct-page-less VM_IO / VM_PFNMAP memory through vma-\u003evm_ops-\u003eaccess().\n+ */\n+static int access_vm_page(struct vm_area_struct *vma, struct page *page,\n+\t\t\t  unsigned long addr, void **buf, int len, int write)\n+{\n+\tstruct folio *folio;\n+\tint bytes, offset;\n+\tvoid *maddr;\n+\n+\tif (!page) {\n+\t\tbytes = access_remote_vma_ops(vma, addr, *buf, len, write);\n+\t\tif (bytes \u003e 0)\n+\t\t\t*buf += bytes;\n+\t\treturn bytes;\n+\t}\n+\n+\tbytes = len;\n+\toffset = addr \u0026 (PAGE_SIZE - 1);\n+\tif (bytes \u003e PAGE_SIZE - offset)\n+\t\tbytes = PAGE_SIZE - offset;\n+\n+\tfolio = page_folio(page);\n+\tmaddr = kmap_local_folio(folio, folio_page_idx(folio, page) * PAGE_SIZE);\n+\tif (write) {\n+\t\tcopy_to_user_page(vma, page, addr, maddr + offset, *buf, bytes);\n+\t\tfolio_mark_dirty_lock(folio);\n+\t} else {\n+\t\tcopy_from_user_page(vma, page, addr, *buf, maddr + offset, bytes);\n+\t}\n+\tfolio_release_kmap(folio, maddr);\n+\n+\t*buf += bytes;\n+\treturn bytes;\n+}\n+\n+/*\n+ * Access another process' address space as given in mm.\n+ */\n+static int __access_remote_vm(struct mm_struct *mm, unsigned long addr,\n+\t\t\t      void *buf, int len, unsigned int gup_flags)\n+{\n+\tint err;\n+\n+\treturn remote_vm_walk(mm, addr, buf, len, gup_flags, true,\n+\t\t\t      access_vm_page, \u0026err);\n+}\n+\n /**\n  * access_remote_vm - access another process' address space\n  * @mm:\t\tthe mm_struct of the target address space\n@@ -7138,84 +7296,67 @@ EXPORT_SYMBOL_GPL(access_process_vm);\n \n #ifdef CONFIG_BPF_SYSCALL\n /*\n- * Copy a string from another process's address space as given in mm.\n- * If there is any error return -EFAULT.\n+ * Copy a NUL-terminated string from @addr into *@buf, up to @len bytes,\n+ * stopping at the NUL. strscpy() always NUL terminates, so recopy the last\n+ * byte of a page when more pages follow. A string is never read from\n+ * struct-page-less VM_IO / VM_PFNMAP memory.\n  */\n-static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,\n-\t\t\t\tvoid *buf, int len, unsigned int gup_flags)\n+static int copy_vm_str(struct vm_area_struct *vma, struct page *page,\n+\t\t       unsigned long addr, void **buf, int len, int write)\n {\n-\tvoid *old_buf = buf;\n-\tint err = 0;\n-\n-\t*(char *)buf = '\\0';\n+\tstruct folio *folio;\n+\tint bytes, offset, retval;\n+\tvoid *maddr;\n \n-\tif (mmap_read_lock_killable(mm))\n+\tif (!page)\n \t\treturn -EFAULT;\n \n-\taddr = untagged_addr_remote(mm, addr);\n+\tbytes = len;\n+\toffset = addr \u0026 (PAGE_SIZE - 1);\n+\tif (bytes \u003e PAGE_SIZE - offset)\n+\t\tbytes = PAGE_SIZE - offset;\n \n-\t/* Avoid triggering the temporary warning in __get_user_pages */\n-\tif (!vma_lookup(mm, addr)) {\n-\t\terr = -EFAULT;\n-\t\tgoto out;\n+\tfolio = page_folio(page);\n+\tmaddr = kmap_local_folio(folio, folio_page_idx(folio, page) * PAGE_SIZE);\n+\tretval = strscpy(*buf, maddr + offset, bytes);\n+\tif (retval \u003e= 0) {\n+\t\t/* Found the end of the string. */\n+\t\t*buf += retval;\n+\t\tfolio_release_kmap(folio, maddr);\n+\t\treturn 0;\n \t}\n \n-\twhile (len) {\n-\t\tint bytes, offset, retval;\n-\t\tvoid *maddr;\n-\t\tstruct folio *folio;\n-\t\tstruct page *page;\n-\t\tstruct vm_area_struct *vma = NULL;\n+\t*buf += bytes - 1;\n+\tif (bytes != len) {\n+\t\tcopy_from_user_page(vma, page, addr + bytes - 1, *buf,\n+\t\t\t\t    maddr + (PAGE_SIZE - 1), 1);\n+\t\t*buf += 1;\n+\t}\n+\tfolio_release_kmap(folio, maddr);\n \n-\t\tpage = get_user_page_vma_remote(mm, addr, gup_flags, \u0026vma);\n-\t\tif (IS_ERR(page)) {\n-\t\t\t/*\n-\t\t\t * Treat as a total failure for now until we decide how\n-\t\t\t * to handle the CONFIG_HAVE_IOREMAP_PROT case and\n-\t\t\t * stack expansion.\n-\t\t\t */\n-\t\t\t*(char *)buf = '\\0';\n-\t\t\terr = -EFAULT;\n-\t\t\tgoto out;\n-\t\t}\n+\treturn bytes;\n+}\n \n-\t\tfolio = page_folio(page);\n-\t\tbytes = len;\n-\t\toffset = addr \u0026 (PAGE_SIZE - 1);\n-\t\tif (bytes \u003e PAGE_SIZE - offset)\n-\t\t\tbytes = PAGE_SIZE - offset;\n-\n-\t\tmaddr = kmap_local_folio(folio, folio_page_idx(folio, page) * PAGE_SIZE);\n-\t\tretval = strscpy(buf, maddr + offset, bytes);\n-\t\tif (retval \u003e= 0) {\n-\t\t\t/* Found the end of the string */\n-\t\t\tbuf += retval;\n-\t\t\tfolio_release_kmap(folio, maddr);\n-\t\t\tbreak;\n-\t\t}\n+/*\n+ * Copy a string from another process's address space as given in mm.\n+ * If there is any error return -EFAULT.\n+ */\n+static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,\n+\t\t\t\tvoid *buf, int len, unsigned int gup_flags)\n+{\n+\tint bytes, err;\n \n-\t\tbuf += bytes - 1;\n-\t\t/*\n-\t\t * Because strscpy always NUL terminates we need to\n-\t\t * copy the last byte in the page if we are going to\n-\t\t * load more pages\n-\t\t */\n-\t\tif (bytes != len) {\n-\t\t\taddr += bytes - 1;\n-\t\t\tcopy_from_user_page(vma, page, addr, buf, maddr + (PAGE_SIZE - 1), 1);\n-\t\t\tbuf += 1;\n-\t\t\taddr += 1;\n-\t\t}\n-\t\tlen -= bytes;\n+\t*(char *)buf = '\\0';\n \n-\t\tfolio_release_kmap(folio, maddr);\n+\tbytes = remote_vm_walk(mm, addr, buf, len, gup_flags, false,\n+\t\t\t       copy_vm_str, \u0026err);\n+\tif (err) {\n+\t\t/* The contract guarantees a terminated buffer even on error. */\n+\t\t((char *)buf)[bytes] = '\\0';\n+\t\treturn err;\n \t}\n \n-out:\n-\tmmap_read_unlock(mm);\n-\tif (err)\n-\t\treturn err;\n-\treturn buf - old_buf;\n+\treturn bytes;\n }\n \n /**\ndiff --git a/mm/rmap.c b/mm/rmap.c\nindex 1c77d5dc06e9fa..b36f2e219b8f49 100644\n--- a/mm/rmap.c\n+++ b/mm/rmap.c\n@@ -2838,7 +2838,7 @@ struct page *make_device_exclusive(struct mm_struct *mm, unsigned long addr,\n \t * (non-device-exclusive) PTE and issue a MMU_NOTIFY_EXCLUSIVE.\n \t */\n retry:\n-\tpage = get_user_page_vma_remote(mm, addr,\n+\tpage = get_user_page_lookup_vma(mm, addr,\n \t\t\t\t\tFOLL_GET | FOLL_WRITE | FOLL_SPLIT_PMD,\n \t\t\t\t\t\u0026vma);\n \tif (IS_ERR(page))\ndiff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile\nindex e6df968f0971c8..6b917a4f73e00f 100644\n--- a/tools/testing/selftests/mm/Makefile\n+++ b/tools/testing/selftests/mm/Makefile\n@@ -83,6 +83,7 @@ TEST_GEN_FILES += mrelease_test\n TEST_GEN_FILES += mremap_dontunmap\n TEST_GEN_FILES += mremap_test\n TEST_GEN_FILES += mseal_test\n+TEST_GEN_FILES += mthp_gup_cow_test\n TEST_GEN_FILES += on-fault-limit\n TEST_GEN_FILES += pagemap_ioctl\n TEST_GEN_FILES += pfnmap\ndiff --git a/tools/testing/selftests/mm/mthp_gup_cow_test.c b/tools/testing/selftests/mm/mthp_gup_cow_test.c\nnew file mode 100644\nindex 00000000000000..52ee329d8641c0\n--- /dev/null\n+++ b/tools/testing/selftests/mm/mthp_gup_cow_test.c\n@@ -0,0 +1,213 @@\n+// SPDX-License-Identifier: GPL-2.0-only\n+/*\n+ * Verify that the slow GUP path (pin_user_pages -\u003e follow_page_mask -\u003e\n+ * follow_pte_batch) returns the correct pages for a PTE-mapped large folio\n+ * (mTHP), including that COW copies produce the right content.\n+ *\n+ * Uses the CONFIG_GUP_TEST PIN_LONGTERM interface: START pins a range on the\n+ * slow path, READ copies the pinned pages' bytes back so we can compare them\n+ * against the pattern we wrote.\n+ */\n+#define _GNU_SOURCE\n+#include \u003cstdio.h\u003e\n+#include \u003cstdlib.h\u003e\n+#include \u003cstring.h\u003e\n+#include \u003cstdint.h\u003e\n+#include \u003cfcntl.h\u003e\n+#include \u003cunistd.h\u003e\n+#include \u003cerrno.h\u003e\n+#include \u003csys/ioctl.h\u003e\n+#include \u003csys/mman.h\u003e\n+#include \u003csys/wait.h\u003e\n+#include \u003clinux/types.h\u003e\n+\n+#define ARRAY_SIZE(a)\t(sizeof(a) / sizeof((a)[0]))\n+\n+#define GUP_DEV \"/sys/kernel/debug/gup_test\"\n+\n+#define PIN_LONGTERM_TEST_START\t_IOW('g', 7, struct pin_longterm_test)\n+#define PIN_LONGTERM_TEST_STOP\t_IO('g', 8)\n+#define PIN_LONGTERM_TEST_READ\t_IOW('g', 9, __u64)\n+#define USE_WRITE\t1\n+#define USE_FAST\t2\n+\n+struct pin_longterm_test {\n+\t__u64 addr;\n+\t__u64 size;\n+\t__u32 flags;\n+};\n+\n+#define ORDER_KB\t64\n+#define NR_FOLIOS\t16\n+#define REGION\t\t((size_t)ORDER_KB * 1024 * NR_FOLIOS)\n+\n+static long PS;\n+static int fails;\n+static int tap;\n+\n+static void ok(int cond, const char *desc)\n+{\n+\tprintf(\"%s %d %s\\n\", cond ? \"ok\" : \"not ok\", ++tap, desc);\n+\tif (!cond)\n+\t\tfails++;\n+}\n+\n+/* Deterministic, per-page-distinct pattern so any mis-order or leak shows. */\n+static void fill(char *base, size_t sz, uint32_t salt)\n+{\n+\tfor (size_t off = 0; off \u003c sz; off += PS) {\n+\t\tuint32_t k = off / PS;\n+\t\tuint64_t v = ((uint64_t)salt \u003c\u003c 32) ^ (k * 0x9E3779B1u + 0x1234);\n+\n+\t\tfor (size_t i = 0; i \u003c PS; i += sizeof(v))\n+\t\t\tmemcpy(base + off + i, \u0026v, sizeof(v));\n+\t}\n+}\n+\n+static int wsysfs(const char *path, const char *val)\n+{\n+\tint fd = open(path, O_WRONLY);\n+\n+\tif (fd \u003c 0)\n+\t\treturn -1;\n+\tint r = write(fd, val, strlen(val));\n+\n+\tclose(fd);\n+\treturn r \u003c 0 ? -1 : 0;\n+}\n+\n+/* Force sub-PMD 64kB mTHP only, so faults produce PTE-mapped large folios. */\n+static void setup_mthp(void)\n+{\n+\tconst char *thp = \"/sys/kernel/mm/transparent_hugepage\";\n+\tchar p[256];\n+\tstatic const int kb[] = { 16, 32, 64, 128, 256, 512, 1024, 2048 };\n+\n+\twsysfs(\"/sys/kernel/mm/transparent_hugepage/enabled\", \"never\");\n+\tfor (unsigned int i = 0; i \u003c ARRAY_SIZE(kb); i++) {\n+\t\tsnprintf(p, sizeof(p), \"%s/hugepages-%dkB/enabled\", thp, kb[i]);\n+\t\twsysfs(p, kb[i] == ORDER_KB ? \"always\" : \"never\");\n+\t}\n+}\n+\n+/* Count how many pages sit in a contiguous \u003e=ORDER_KB PFN run (via pagemap). */\n+static int count_large_pages(char *base, size_t sz)\n+{\n+\tint pm = open(\"/proc/self/pagemap\", O_RDONLY);\n+\tsize_t n = sz / PS, large = 0;\n+\tuint64_t *pfn = calloc(n, sizeof(*pfn));\n+\n+\tif (pm \u003c 0)\n+\t\treturn -1;\n+\tfor (size_t k = 0; k \u003c n; k++) {\n+\t\tuint64_t ent;\n+\t\toff_t idx = ((uintptr_t)base + k * PS) / PS * sizeof(ent);\n+\n+\t\tif (pread(pm, \u0026ent, sizeof(ent), idx) != sizeof(ent) ||\n+\t\t    !(ent \u0026 (1ULL \u003c\u003c 63)))\n+\t\t\tpfn[k] = 0;\n+\t\telse\n+\t\t\tpfn[k] = ent \u0026 ((1ULL \u003c\u003c 55) - 1);\n+\t}\n+\tclose(pm);\n+\tfor (size_t k = 0; k \u003c n; k++)\n+\t\tif (k + 1 \u003c n \u0026\u0026 pfn[k] \u0026\u0026 pfn[k + 1] == pfn[k] + 1)\n+\t\t\tlarge++;\n+\tfree(pfn);\n+\treturn large;\n+}\n+\n+/* Pin @base..@sz on the slow path, read the pinned bytes back, compare to exp. */\n+static int pin_verify(int fd, char *base, size_t sz, uint32_t wr, char *exp)\n+{\n+\tstruct pin_longterm_test a = {\n+\t\t.addr = (uintptr_t)base, .size = sz,\n+\t\t.flags = wr ? USE_WRITE : 0,\t\t/* USE_FAST unset =\u003e slow */\n+\t};\n+\tchar *got = mmap(NULL, sz, PROT_READ | PROT_WRITE,\n+\t\t\t MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n+\t__u64 ga = (uintptr_t)got;\n+\tint rc = -1;\n+\n+\tif (got == MAP_FAILED)\n+\t\treturn -1;\n+\tif (ioctl(fd, PIN_LONGTERM_TEST_START, \u0026a)) {\n+\t\tfprintf(stderr, \"START(%s) failed: %s\\n\",\n+\t\t\twr ? \"write\" : \"read\", strerror(errno));\n+\t\tgoto out;\n+\t}\n+\tif (ioctl(fd, PIN_LONGTERM_TEST_READ, \u0026ga)) {\n+\t\tfprintf(stderr, \"READ failed: %s\\n\", strerror(errno));\n+\t\tioctl(fd, PIN_LONGTERM_TEST_STOP);\n+\t\tgoto out;\n+\t}\n+\tioctl(fd, PIN_LONGTERM_TEST_STOP);\n+\trc = memcmp(got, exp, sz) ? 1 : 0;\n+out:\n+\tmunmap(got, sz);\n+\treturn rc;\n+}\n+\n+int main(void)\n+{\n+\tPS = sysconf(_SC_PAGESIZE);\n+\tsetup_mthp();\n+\n+\tint fd = open(GUP_DEV, O_RDWR);\n+\n+\tif (fd \u003c 0) {\n+\t\tfprintf(stderr, \"open %s: %s (CONFIG_GUP_TEST?)\\n\",\n+\t\t\tGUP_DEV, strerror(errno));\n+\t\treturn 2;\n+\t}\n+\n+\tchar *r = mmap(NULL, REGION, PROT_READ | PROT_WRITE,\n+\t\t       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n+\tif (r == MAP_FAILED) {\n+\t\tperror(\"mmap\");\n+\t\treturn 2;\n+\t}\n+\tfill(r, REGION, 0xA1);\t\t\t\t/* pattern P */\n+\tchar *expP = malloc(REGION);\n+\n+\tmemcpy(expP, r, REGION);\n+\n+\tint large = count_large_pages(r, REGION);\n+\n+\tprintf(\"# %d/%zu pages in contiguous large-folio runs\\n\",\n+\t       large, REGION / PS);\n+\tif (large \u003c (int)(REGION / PS) / 4)\n+\t\tprintf(\"# WARN: little mTHP backing; batch path lightly covered\\n\");\n+\n+\t/* A: read pin over writable mTHP -\u003e batches -\u003e content must equal P. */\n+\tok(pin_verify(fd, r, REGION, 0, expP) == 0,\n+\t   \"slow read-pin of mTHP returns correct contents\");\n+\n+\t/* B: write pin -\u003e FOLL_WRITE batch path -\u003e content must equal P. */\n+\tok(pin_verify(fd, r, REGION, 1, expP) == 0,\n+\t   \"slow write-pin of mTHP returns correct contents\");\n+\n+\t/* C: COW isolation. Child write-pins (unshares) then rewrites; parent P. */\n+\tpid_t pid = fork();\n+\n+\tif (pid == 0) {\n+\t\tint cfd = open(GUP_DEV, O_RDWR);\n+\t\tint a = pin_verify(cfd, r, REGION, 1, expP);\t/* COW copy == P */\n+\n+\t\tfill(r, REGION, 0xB2);\t\t\t/* child writes Q */\n+\t\t_exit(a == 0 ? 0 : 1);\n+\t}\n+\tint st = 0;\n+\n+\twaitpid(pid, \u0026st, 0);\n+\tok(WIFEXITED(st) \u0026\u0026 WEXITSTATUS(st) == 0,\n+\t   \"child write-pin after COW returns correct (copied) contents\");\n+\tok(memcmp(r, expP, REGION) == 0,\n+\t   \"parent contents intact after child COW writes\");\n+\t/* D: parent read-pin again after the COW split still correct. */\n+\tok(pin_verify(fd, r, REGION, 0, expP) == 0,\n+\t   \"parent slow read-pin after COW still correct\");\n+\n+\tprintf(\"# totals: pass:%d fail:%d\\n\", tap - fails, fails);\n+\treturn fails ? 1 : 0;\n+}\ndiff --git a/tools/testing/selftests/mm/pfnmap.c b/tools/testing/selftests/mm/pfnmap.c\nindex 4f550822385a20..6ff5d1029517e3 100644\n--- a/tools/testing/selftests/mm/pfnmap.c\n+++ b/tools/testing/selftests/mm/pfnmap.c\n@@ -31,6 +31,7 @@ static sigjmp_buf sigjmp_buf_env;\n static char *file = \"/dev/mem\";\n static off_t file_offset;\n static int fd;\n+static int target_is_ram;\n \n static void signal_handler(int sig)\n {\n@@ -113,6 +114,7 @@ static void pfnmap_init(void)\n \t\tif (err)\n \t\t\tksft_exit_skip(\"Cannot find ram target in '/proc/iomem': %s\\n\",\n \t\t\t\t       strerror(-err));\n+\t\ttarget_is_ram = 1;\n \t} else {\n \t\tfile_offset = 0;\n \t}\n@@ -271,6 +273,70 @@ TEST_F(pfnmap, fork)\n \tASSERT_EQ(ret, 0);\n }\n \n+TEST_F(pfnmap, procmem_cow_read)\n+{\n+\tchar *priv, *buf;\n+\tssize_t rc;\n+\tint mem_fd;\n+\n+\t/*\n+\t * A COWed page in a VM_PFNMAP mapping has a struct page, so reading it\n+\t * through /proc/self/mem -- __access_remote_vm() -\u003e get_user_page_vma()\n+\t * -- returns it directly, instead of routing to vma-\u003evm_ops-\u003eaccess(),\n+\t * which ioremaps the PFN and cannot reach a COWed RAM page.\n+\t *\n+\t * Map the file MAP_PRIVATE and writable, write to COW a page into anon\n+\t * memory, then read the page back through /proc/self/mem.\n+\t */\n+\tself-\u003esize2 = self-\u003epagesize;\n+\tself-\u003eaddr2 = mmap(NULL, self-\u003esize2, PROT_READ | PROT_WRITE,\n+\t\t\t   MAP_PRIVATE, fd, file_offset);\n+\tif (self-\u003eaddr2 == MAP_FAILED)\n+\t\tSKIP(return, \"Cannot create a writable private pfnmap mapping\");\n+\tpriv = self-\u003eaddr2;\n+\n+\t/* COW the page and stamp known bytes into the anon copy. */\n+\tpriv[0] = 0x42;\n+\tpriv[self-\u003epagesize - 1] = 0x24;\n+\n+\tbuf = malloc(self-\u003epagesize);\n+\tASSERT_NE(buf, NULL);\n+\n+\tmem_fd = open(\"/proc/self/mem\", O_RDONLY);\n+\tASSERT_GE(mem_fd, 0);\n+\trc = pread(mem_fd, buf, self-\u003epagesize, (off_t)(uintptr_t)priv);\n+\tclose(mem_fd);\n+\n+\tASSERT_EQ(rc, (ssize_t)self-\u003epagesize);\n+\tEXPECT_EQ(buf[0], 0x42);\n+\tEXPECT_EQ(buf[self-\u003epagesize - 1], 0x24);\n+\n+\tfree(buf);\n+}\n+\n+TEST_F(pfnmap, procmem_pfn_read)\n+{\n+\tchar buf[64];\n+\tssize_t rc;\n+\tint mem_fd;\n+\n+\t/*\n+\t * A raw PFN of a VM_IO/VM_PFNMAP mapping has no struct page, so\n+\t * __access_remote_vm() reaches it through vma-\u003evm_ops-\u003eaccess()\n+\t * (generic_access_phys()). That ioremaps the PFN, which is rejected for\n+\t * RAM, so this only applies to genuine device memory.\n+\t */\n+\tif (target_is_ram)\n+\t\tSKIP(return, \"Target is System RAM; -\u003eaccess() cannot ioremap RAM\");\n+\n+\tmem_fd = open(\"/proc/self/mem\", O_RDONLY);\n+\tASSERT_GE(mem_fd, 0);\n+\trc = pread(mem_fd, buf, sizeof(buf), (off_t)(uintptr_t)self-\u003eaddr1);\n+\tclose(mem_fd);\n+\n+\tASSERT_EQ(rc, (ssize_t)sizeof(buf));\n+}\n+\n int main(int argc, char **argv)\n {\n \tfor (int i = 1; i \u003c argc; i++) {\ndiff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh\nindex 8c296dedf0474d..b49a3eb0c20508 100755\n--- a/tools/testing/selftests/mm/run_vmtests.sh\n+++ b/tools/testing/selftests/mm/run_vmtests.sh\n@@ -289,6 +289,7 @@ fi\n # Dump pages 0, 19, and 4096, using pin_user_pages:\n CATEGORY=\"gup_test\" run_test ./gup_test -ct -F 0x1 0 19 0x1000\n CATEGORY=\"gup_test\" run_test ./gup_longterm\n+CATEGORY=\"gup_test\" run_test ./mthp_gup_cow_test\n \n CATEGORY=\"userfaultfd\" run_test ./uffd-unit-tests\n uffd_stress_bin=./uffd-stress\n"
}

2/1 2026/07/26 10:07 agent patch-evaluator
1m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "TRANSPARENT_HUGEPAGE",
    "BPF_SYSCALL",
    "ADDRESS_MASKING"
  ],
  "FocusSymbols": [
    "get_user_page_vma",
    "get_user_page_lookup_vma",
    "access_remote_vm",
    "access_process_vm",
    "copy_remote_vm_str"
  ],
  "Reasoning": "The patch introduces a new remote VM walk mechanism (`remote_vm_walk`) and replaces `get_user_page_vma_remote` with `get_user_page_lookup_vma` and `get_user_page_vma`. It also adds support for batching contiguous pages for PTE-mapped large folios (mTHP) in the slow GUP path. These are core memory management changes that affect page faulting, remote memory access (e.g., ptrace, /proc/pid/mem), and BPF string copying. Fuzzing should focus on the new entry points and remote access APIs.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
  even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).
- Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed,
  ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU.
- Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove)
  that are executed only during PCI hot-unplug or sysfs driver unbind operations.

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
   Prefer non-static, non-inlined API entrypoint functions over internal static helper functions
   (which are inlined by the compiler and do not have distinct symbol addresses).
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs
   needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs
   output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 67c20987de6d92d7efc21a001885ad318b6054fe
Author: syz-cluster <triage@syzkaller.com>
Date:   Sun Jul 26 10:07:06 2026 +0000

    syz-cluster: applied patch under review

diff --git a/arch/arm64/kernel/mte.c b/arch/arm64/kernel/mte.c
index 1a9aad6ef22a04..7a6ecc3d92948c 100644
--- a/arch/arm64/kernel/mte.c
+++ b/arch/arm64/kernel/mte.c
@@ -459,7 +459,7 @@ static int __access_remote_tags(struct mm_struct *mm, unsigned long addr,
 		struct vm_area_struct *vma;
 		unsigned long tags, offset;
 		void *maddr;
-		struct page *page = get_user_page_vma_remote(mm, addr,
+		struct page *page = get_user_page_lookup_vma(mm, addr,
 							     gup_flags, &vma);
 		struct folio *folio;
 
diff --git a/arch/riscv/include/asm/mmu_context.h b/arch/riscv/include/asm/mmu_context.h
index dbf27a78df6c87..3ce16796e5a25d 100644
--- a/arch/riscv/include/asm/mmu_context.h
+++ b/arch/riscv/include/asm/mmu_context.h
@@ -21,7 +21,7 @@ static inline void activate_mm(struct mm_struct *prev,
 			       struct mm_struct *next)
 {
 #ifdef CONFIG_RISCV_ISA_SUPM
-	next->context.pmlen = 0;
+	WRITE_ONCE(next->context.pmlen, 0);
 #endif
 	switch_mm(prev, next, NULL);
 }
@@ -44,7 +44,7 @@ DECLARE_STATIC_KEY_FALSE(use_asid_allocator);
 #define mm_untag_mask mm_untag_mask
 static inline unsigned long mm_untag_mask(struct mm_struct *mm)
 {
-	return -1UL >> mm->context.pmlen;
+	return -1UL >> READ_ONCE(mm->context.pmlen);
 }
 #endif
 
diff --git a/arch/riscv/include/asm/uaccess.h b/arch/riscv/include/asm/uaccess.h
index 5d4ec15584cfa4..53806e0f7dcfe3 100644
--- a/arch/riscv/include/asm/uaccess.h
+++ b/arch/riscv/include/asm/uaccess.h
@@ -16,7 +16,7 @@
 static inline unsigned long __untagged_addr_remote(struct mm_struct *mm, unsigned long addr)
 {
 	if (riscv_has_extension_unlikely(RISCV_ISA_EXT_SUPM)) {
-		u8 pmlen = mm->context.pmlen;
+		u8 pmlen = READ_ONCE(mm->context.pmlen);
 
 		/* Virtual addresses are sign-extended; physical addresses are zero-extended. */
 		if (IS_ENABLED(CONFIG_MMU))
@@ -33,12 +33,16 @@ static inline unsigned long __untagged_addr_remote(struct mm_struct *mm, unsigne
 	(__force __typeof__(addr))__untagged_addr_remote(current->mm, __addr);	\
 })
 
-#define untagged_addr_remote(mm, addr) ({					\
+#define untagged_addr_remote_unlocked(mm, addr) ({				\
 	unsigned long __addr = (__force unsigned long)(addr);			\
-	mmap_assert_locked(mm);							\
 	(__force __typeof__(addr))__untagged_addr_remote(mm, __addr);		\
 })
 
+#define untagged_addr_remote(mm, addr) ({					\
+	mmap_assert_locked(mm);							\
+	untagged_addr_remote_unlocked(mm, addr);				\
+})
+
 #define access_ok(addr, size) likely(__access_ok(untagged_addr(addr), size))
 #else
 #define untagged_addr(addr) (addr)
diff --git a/arch/riscv/kernel/process.c b/arch/riscv/kernel/process.c
index b2df7f72241a5f..6ae7552fed09f3 100644
--- a/arch/riscv/kernel/process.c
+++ b/arch/riscv/kernel/process.c
@@ -357,13 +357,15 @@ long set_tagged_addr_ctrl(struct task_struct *task, unsigned long arg)
 	if (mmap_write_lock_killable(mm))
 		return -EINTR;
 
-	if (test_bit(MM_CONTEXT_LOCK_PMLEN, &mm->context.flags) && mm->context.pmlen != pmlen) {
-		mmap_write_unlock(mm);
-		return -EBUSY;
+	if (test_bit(MM_CONTEXT_LOCK_PMLEN, &mm->context.flags)) {
+		if (READ_ONCE(mm->context.pmlen) != pmlen) {
+			mmap_write_unlock(mm);
+			return -EBUSY;
+		}
 	}
 
 	envcfg_update_bits(task, ENVCFG_PMM, pmm);
-	mm->context.pmlen = pmlen;
+	WRITE_ONCE(mm->context.pmlen, pmlen);
 
 	mmap_write_unlock(mm);
 
@@ -394,7 +396,7 @@ long get_tagged_addr_ctrl(struct task_struct *task)
 		break;
 	}
 
-	if (task->mm->context.pmlen)
+	if (READ_ONCE(task->mm->context.pmlen))
 		ret |= PR_TAGGED_ADDR_ENABLE;
 
 	return ret;
diff --git a/arch/x86/include/asm/mmu_context.h b/arch/x86/include/asm/mmu_context.h
index ef5b507de34e2e..cee710f64658aa 100644
--- a/arch/x86/include/asm/mmu_context.h
+++ b/arch/x86/include/asm/mmu_context.h
@@ -100,18 +100,18 @@ static inline unsigned long mm_lam_cr3_mask(struct mm_struct *mm)
 static inline void dup_lam(struct mm_struct *oldmm, struct mm_struct *mm)
 {
 	mm->context.lam_cr3_mask = oldmm->context.lam_cr3_mask;
-	mm->context.untag_mask = oldmm->context.untag_mask;
+	WRITE_ONCE(mm->context.untag_mask, READ_ONCE(oldmm->context.untag_mask));
 }
 
 #define mm_untag_mask mm_untag_mask
 static inline unsigned long mm_untag_mask(struct mm_struct *mm)
 {
-	return mm->context.untag_mask;
+	return READ_ONCE(mm->context.untag_mask);
 }
 
 static inline void mm_reset_untag_mask(struct mm_struct *mm)
 {
-	mm->context.untag_mask = -1UL;
+	WRITE_ONCE(mm->context.untag_mask, -1UL);
 }
 
 #define arch_pgtable_dma_compat arch_pgtable_dma_compat
diff --git a/arch/x86/include/asm/uaccess_64.h b/arch/x86/include/asm/uaccess_64.h
index 20de34cc9aa6e7..a095165eb648eb 100644
--- a/arch/x86/include/asm/uaccess_64.h
+++ b/arch/x86/include/asm/uaccess_64.h
@@ -39,18 +39,27 @@ static inline unsigned long __untagged_addr(unsigned long addr)
 	(__force __typeof__(addr))__untagged_addr(__addr);		\
 })
 
+/*
+ * mm->context.untag_mask changes only when the target enables LAM or execs,
+ * always single-threaded. An unlocked remote reader can race that; READ_ONCE
+ * keeps the value whole and the untag is best-effort.
+ */
 static inline unsigned long __untagged_addr_remote(struct mm_struct *mm,
 						   unsigned long addr)
 {
-	mmap_assert_locked(mm);
-	return addr & (mm)->context.untag_mask;
+	return addr & READ_ONCE(mm->context.untag_mask);
 }
 
-#define untagged_addr_remote(mm, addr)	({				\
+#define untagged_addr_remote_unlocked(mm, addr)	({			\
 	unsigned long __addr = (__force unsigned long)(addr);		\
 	(__force __typeof__(addr))__untagged_addr_remote(mm, __addr);	\
 })
 
+#define untagged_addr_remote(mm, addr)	({				\
+	mmap_assert_locked(mm);						\
+	untagged_addr_remote_unlocked(mm, addr);			\
+})
+
 #endif
 
 #define valid_user_address(x) \
diff --git a/arch/x86/kernel/process_64.c b/arch/x86/kernel/process_64.c
index d44afbe005bb51..9fa659117f3826 100644
--- a/arch/x86/kernel/process_64.c
+++ b/arch/x86/kernel/process_64.c
@@ -814,7 +814,7 @@ static void enable_lam_func(void *__mm)
 static void mm_enable_lam(struct mm_struct *mm)
 {
 	mm->context.lam_cr3_mask = X86_CR3_LAM_U57;
-	mm->context.untag_mask =  ~GENMASK(62, 57);
+	WRITE_ONCE(mm->context.untag_mask, ~GENMASK(62, 57));
 
 	/*
 	 * Even though the process must still be single-threaded at this
@@ -952,7 +952,7 @@ long do_arch_prctl_64(struct task_struct *task, int option, unsigned long arg2)
 #endif
 #ifdef CONFIG_ADDRESS_MASKING
 	case ARCH_GET_UNTAG_MASK:
-		return put_user(task->mm->context.untag_mask,
+		return put_user(mm_untag_mask(task->mm),
 				(unsigned long __user *)arg2);
 	case ARCH_ENABLE_TAGGED_ADDR:
 		return prctl_enable_tagged_addr(task->mm, arg2);
diff --git a/arch/x86/kernel/uprobes.c b/arch/x86/kernel/uprobes.c
index 3af979fb41d38d..329efac0cfb37d 100644
--- a/arch/x86/kernel/uprobes.c
+++ b/arch/x86/kernel/uprobes.c
@@ -1036,7 +1036,7 @@ static int copy_from_vaddr(struct mm_struct *mm, unsigned long vaddr, void *dst,
 	struct vm_area_struct *vma;
 	struct page *page;
 
-	page = get_user_page_vma_remote(mm, vaddr, gup_flags, &vma);
+	page = get_user_page_lookup_vma(mm, vaddr, gup_flags, &vma);
 	if (IS_ERR(page))
 		return PTR_ERR(page);
 	uprobe_copy_from_page(page, vaddr, dst, len);
diff --git a/include/linux/mm.h b/include/linux/mm.h
index 485df9c2dbddb3..0f66d76e6ca719 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -3235,35 +3235,9 @@ long pin_user_pages_remote(struct mm_struct *mm,
 			   unsigned int gup_flags, struct page **pages,
 			   int *locked);
 
-/*
- * Retrieves a single page alongside its VMA. Does not support FOLL_NOWAIT.
- */
-static inline struct page *get_user_page_vma_remote(struct mm_struct *mm,
-						    unsigned long addr,
-						    int gup_flags,
-						    struct vm_area_struct **vmap)
-{
-	struct page *page;
-	struct vm_area_struct *vma;
-	int got;
-
-	if (WARN_ON_ONCE(unlikely(gup_flags & FOLL_NOWAIT)))
-		return ERR_PTR(-EINVAL);
-
-	got = get_user_pages_remote(mm, addr, 1, gup_flags, &page, NULL);
-
-	if (got < 0)
-		return ERR_PTR(got);
-
-	vma = vma_lookup(mm, addr);
-	if (WARN_ON_ONCE(!vma)) {
-		put_page(page);
-		return ERR_PTR(-EINVAL);
-	}
-
-	*vmap = vma;
-	return page;
-}
+struct page *get_user_page_lookup_vma(struct mm_struct *mm, unsigned long addr,
+				      int gup_flags,
+				      struct vm_area_struct **vmap);
 
 long get_user_pages(unsigned long start, unsigned long nr_pages,
 		    unsigned int gup_flags, struct page **pages);
diff --git a/include/linux/uaccess.h b/include/linux/uaccess.h
index eddbbb65ccc4f8..7e6e4c89184cd7 100644
--- a/include/linux/uaccess.h
+++ b/include/linux/uaccess.h
@@ -34,6 +34,13 @@
 })
 #endif
 
+#ifndef untagged_addr_remote_unlocked
+#define untagged_addr_remote_unlocked(mm, addr)	({	\
+	(void)(mm);					\
+	untagged_addr(addr);				\
+})
+#endif
+
 #ifdef masked_user_access_begin
  #define can_do_masked_user_access() 1
 # ifndef masked_user_write_access_begin
diff --git a/mm/gup.c b/mm/gup.c
index 0692119b790431..4910c93ba003e8 100644
--- a/mm/gup.c
+++ b/mm/gup.c
@@ -647,8 +647,9 @@ static inline bool can_follow_write_pud(pud_t pud, struct page *page,
 }
 
 static struct page *follow_huge_pud(struct vm_area_struct *vma,
-				    unsigned long addr, pud_t *pudp,
-				    int flags, unsigned long *page_mask)
+				    unsigned long addr, unsigned long end,
+				    pud_t *pudp, int flags,
+				    unsigned long *nr_pages)
 {
 	struct mm_struct *mm = vma->vm_mm;
 	struct page *page;
@@ -672,10 +673,13 @@ static struct page *follow_huge_pud(struct vm_area_struct *vma,
 		return ERR_PTR(-EMLINK);
 
 	ret = try_grab_folio(page_folio(page), 1, flags);
-	if (ret)
+	if (ret) {
 		page = ERR_PTR(ret);
-	else
-		*page_mask = HPAGE_PUD_NR - 1;
+	} else {
+		unsigned long off = (addr & ~PUD_MASK) >> PAGE_SHIFT;
+
+		*nr_pages = min(HPAGE_PUD_NR - off, (end - addr) >> PAGE_SHIFT);
+	}
 
 	return page;
 }
@@ -699,9 +703,9 @@ static inline bool can_follow_write_pmd(pmd_t pmd, struct page *page,
 }
 
 static struct page *follow_huge_pmd(struct vm_area_struct *vma,
-				    unsigned long addr, pmd_t *pmd,
-				    unsigned int flags,
-				    unsigned long *page_mask)
+				    unsigned long addr, unsigned long end,
+				    pmd_t *pmd, unsigned int flags,
+				    unsigned long *nr_pages)
 {
 	struct mm_struct *mm = vma->vm_mm;
 	pmd_t pmdval = *pmd;
@@ -738,23 +742,25 @@ static struct page *follow_huge_pmd(struct vm_area_struct *vma,
 #endif	/* CONFIG_TRANSPARENT_HUGEPAGE */
 
 	page += (addr & ~HPAGE_PMD_MASK) >> PAGE_SHIFT;
-	*page_mask = HPAGE_PMD_NR - 1;
+	*nr_pages = min(HPAGE_PMD_NR - ((addr & ~HPAGE_PMD_MASK) >> PAGE_SHIFT),
+			(end - addr) >> PAGE_SHIFT);
 
 	return page;
 }
 
 #else  /* CONFIG_PGTABLE_HAS_HUGE_LEAVES */
 static struct page *follow_huge_pud(struct vm_area_struct *vma,
-				    unsigned long addr, pud_t *pudp,
-				    int flags, unsigned long *page_mask)
+				    unsigned long addr, unsigned long end,
+				    pud_t *pudp, int flags,
+				    unsigned long *nr_pages)
 {
 	return NULL;
 }
 
 static struct page *follow_huge_pmd(struct vm_area_struct *vma,
-				    unsigned long addr, pmd_t *pmd,
-				    unsigned int flags,
-				    unsigned long *page_mask)
+				    unsigned long addr, unsigned long end,
+				    pmd_t *pmd, unsigned int flags,
+				    unsigned long *nr_pages)
 {
 	return NULL;
 }
@@ -799,8 +805,46 @@ static inline bool can_follow_write_pte(pte_t pte, struct page *page,
 	return !userfaultfd_pte_wp(vma, pte);
 }
 
+/*
+ * Count the pages, starting at @address and bounded by @end, that a PTE-mapped
+ * large @folio maps contiguously and that can be returned together with the
+ * page at @address: consecutive present PTEs mapping consecutive pages of
+ * @folio with a uniform write bit, within this VMA and a single page table.
+ * Returns at least 1.
+ *
+ * gup_must_unshare() and the write-fault check are per PTE. A writable run is
+ * always safe: a writable anon page is exclusive, and FOLL_WRITE is satisfied.
+ * A read-only run is only safe for a plain read; FOLL_WRITE would need a COW
+ * fault per page and FOLL_PIN would need a per-page gup_must_unshare() check,
+ * so those fall back to a single page.
+ */
+static unsigned long follow_pte_batch(struct vm_area_struct *vma,
+		unsigned long address, unsigned long end, struct folio *folio,
+		struct page *page, pte_t *ptep, pte_t pte, unsigned int flags)
+{
+	pte_t batch_pte = pte;
+	unsigned long max;
+
+	if (!pte_write(pte) && (flags & (FOLL_WRITE | FOLL_PIN)))
+		return 1;
+
+	/*
+	 * folio_pte_batch_flags() scans forward from @ptep, so the run must
+	 * stay within this page table: bound it by the PMD as well as @end and
+	 * the VMA, since a large folio can be PTE-mapped across a PMD boundary.
+	 */
+	max = min((pmd_addr_end(address, end) - address) >> PAGE_SHIFT,
+		  (vma->vm_end - address) >> PAGE_SHIFT);
+	if (max <= 1)
+		return 1;
+
+	return folio_pte_batch_flags(folio, vma, ptep, &batch_pte, max,
+				     FPB_RESPECT_WRITE);
+}
+
 static struct page *follow_page_pte(struct vm_area_struct *vma,
-		unsigned long address, pmd_t *pmd, unsigned int flags)
+		unsigned long address, unsigned long end, pmd_t *pmd,
+		unsigned int flags, unsigned long *nr_pages)
 {
 	struct mm_struct *mm = vma->vm_mm;
 	struct folio *folio;
@@ -885,6 +929,15 @@ static struct page *follow_page_pte(struct vm_area_struct *vma,
 		 */
 		folio_mark_accessed(folio);
 	}
+
+	/*
+	 * A PTE-mapped large folio can be handed back as a contiguous batch,
+	 * so the caller advances over the whole run in one step instead of
+	 * walking the page tables for every page.
+	 */
+	if (folio_test_large(folio))
+		*nr_pages = follow_pte_batch(vma, address, end, folio, page,
+					     ptep, pte, flags);
 out:
 	pte_unmap_unlock(ptep, ptl);
 	return page;
@@ -896,9 +949,9 @@ static struct page *follow_page_pte(struct vm_area_struct *vma,
 }
 
 static struct page *follow_pmd_mask(struct vm_area_struct *vma,
-				    unsigned long address, pud_t *pudp,
-				    unsigned int flags,
-				    unsigned long *page_mask)
+				    unsigned long address, unsigned long end,
+				    pud_t *pudp, unsigned int flags,
+				    unsigned long *nr_pages)
 {
 	pmd_t *pmd, pmdval;
 	spinlock_t *ptl;
@@ -912,7 +965,7 @@ static struct page *follow_pmd_mask(struct vm_area_struct *vma,
 	if (!pmd_present(pmdval))
 		return no_page_table(vma, flags, address);
 	if (likely(!pmd_leaf(pmdval)))
-		return follow_page_pte(vma, address, pmd, flags);
+		return follow_page_pte(vma, address, end, pmd, flags, nr_pages);
 
 	if (pmd_protnone(pmdval) && !gup_can_follow_protnone(vma, flags))
 		return no_page_table(vma, flags, address);
@@ -925,24 +978,24 @@ static struct page *follow_pmd_mask(struct vm_area_struct *vma,
 	}
 	if (unlikely(!pmd_leaf(pmdval))) {
 		spin_unlock(ptl);
-		return follow_page_pte(vma, address, pmd, flags);
+		return follow_page_pte(vma, address, end, pmd, flags, nr_pages);
 	}
 	if (pmd_trans_huge(pmdval) && (flags & FOLL_SPLIT_PMD)) {
 		spin_unlock(ptl);
 		split_huge_pmd(vma, pmd, address);
 		/* If pmd was left empty, stuff a page table in there quickly */
 		return pte_alloc(mm, pmd) ? ERR_PTR(-ENOMEM) :
-			follow_page_pte(vma, address, pmd, flags);
+			follow_page_pte(vma, address, end, pmd, flags, nr_pages);
 	}
-	page = follow_huge_pmd(vma, address, pmd, flags, page_mask);
+	page = follow_huge_pmd(vma, address, end, pmd, flags, nr_pages);
 	spin_unlock(ptl);
 	return page;
 }
 
 static struct page *follow_pud_mask(struct vm_area_struct *vma,
-				    unsigned long address, p4d_t *p4dp,
-				    unsigned int flags,
-				    unsigned long *page_mask)
+				    unsigned long address, unsigned long end,
+				    p4d_t *p4dp, unsigned int flags,
+				    unsigned long *nr_pages)
 {
 	pud_t *pudp, pud;
 	spinlock_t *ptl;
@@ -955,7 +1008,7 @@ static struct page *follow_pud_mask(struct vm_area_struct *vma,
 		return no_page_table(vma, flags, address);
 	if (pud_leaf(pud)) {
 		ptl = pud_lock(mm, pudp);
-		page = follow_huge_pud(vma, address, pudp, flags, page_mask);
+		page = follow_huge_pud(vma, address, end, pudp, flags, nr_pages);
 		spin_unlock(ptl);
 		if (page)
 			return page;
@@ -964,13 +1017,13 @@ static struct page *follow_pud_mask(struct vm_area_struct *vma,
 	if (unlikely(pud_bad(pud)))
 		return no_page_table(vma, flags, address);
 
-	return follow_pmd_mask(vma, address, pudp, flags, page_mask);
+	return follow_pmd_mask(vma, address, end, pudp, flags, nr_pages);
 }
 
 static struct page *follow_p4d_mask(struct vm_area_struct *vma,
-				    unsigned long address, pgd_t *pgdp,
-				    unsigned int flags,
-				    unsigned long *page_mask)
+				    unsigned long address, unsigned long end,
+				    pgd_t *pgdp, unsigned int flags,
+				    unsigned long *nr_pages)
 {
 	p4d_t *p4dp, p4d;
 
@@ -981,15 +1034,16 @@ static struct page *follow_p4d_mask(struct vm_area_struct *vma,
 	if (!p4d_present(p4d) || p4d_bad(p4d))
 		return no_page_table(vma, flags, address);
 
-	return follow_pud_mask(vma, address, p4dp, flags, page_mask);
+	return follow_pud_mask(vma, address, end, p4dp, flags, nr_pages);
 }
 
 /**
  * follow_page_mask - look up a page descriptor from a user-virtual address
  * @vma: vm_area_struct mapping @address
  * @address: virtual address to look up
+ * @end: virtual address at which to stop batching contiguous pages
  * @flags: flags modifying lookup behaviour
- * @page_mask: a pointer to output page_mask
+ * @nr_pages: output; number of contiguous pages the caller can read
  *
  * @flags can have FOLL_ flags set, defined in <linux/mm.h>
  *
@@ -998,15 +1052,17 @@ static struct page *follow_p4d_mask(struct vm_area_struct *vma,
  * trigger a fault with FAULT_FLAG_UNSHARE set. Note that unsharing is only
  * relevant with FOLL_PIN and !FOLL_WRITE.
  *
- * On output, @page_mask is set according to the size of the page.
+ * On output, @nr_pages holds how many contiguous pages the folio that includes
+ * the returned page has mapped into this process, so the caller can advance
+ * over a large folio in one step.
  *
  * Return: the mapped (struct page *), %NULL if no mapping exists, or
  * an error pointer if there is a mapping to something not represented
  * by a page descriptor (see also vm_normal_page()).
  */
 static struct page *follow_page_mask(struct vm_area_struct *vma,
-			      unsigned long address, unsigned int flags,
-			      unsigned long *page_mask)
+			      unsigned long address, unsigned long end,
+			      unsigned int flags, unsigned long *nr_pages)
 {
 	pgd_t *pgd;
 	struct mm_struct *mm = vma->vm_mm;
@@ -1014,13 +1070,13 @@ static struct page *follow_page_mask(struct vm_area_struct *vma,
 
 	vma_pgtable_walk_begin(vma);
 
-	*page_mask = 0;
+	*nr_pages = 1;
 	pgd = pgd_offset(mm, address);
 
 	if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
 		page = no_page_table(vma, flags, address);
 	else
-		page = follow_p4d_mask(vma, address, pgd, flags, page_mask);
+		page = follow_p4d_mask(vma, address, end, pgd, flags, nr_pages);
 
 	vma_pgtable_walk_end(vma);
 
@@ -1080,9 +1136,16 @@ 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
- * FOLL_NOWAIT, the mmap_lock may be released.  If it is, *@locked will be set
- * to 0 and -EBUSY returned.
+ * The caller holds either the mmap lock, or the per-VMA lock (with
+ * FOLL_VMA_LOCK), on entry. If @flags has FOLL_UNLOCKABLE but not FOLL_NOWAIT,
+ * the mmap_lock may be released. If it is, *@locked will be set 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 +1160,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 +1190,195 @@ 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 nr_pages;
+	struct page *page;
+	int locked = 1;
+	bool pfnmap;
+	int ret;
+
+	/*
+	 * Two lock modes are supported: the mmap lock, with neither flag, or
+	 * the per-VMA lock, with both FOLL_VMA_LOCK and FOLL_UNLOCKABLE. An
+	 * unlockable mmap fault would drop the lock and report it in *locked,
+	 * which this function does not relay, so reject a lone flag.
+	 */
+	VM_WARN_ON_ONCE(!(gup_flags & FOLL_VMA_LOCK) != !(gup_flags & FOLL_UNLOCKABLE));
+
+	/*
+	 * 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; let follow_page_mask() look for them, and treat only
+	 * its struct-page-less PFNs as unreachable. Other rejections
+	 * (secretmem, permissions, etc) result in immediate failure.
+	 */
+	ret = check_vma_flags(vma, gup_flags, VM_IO | VM_PFNMAP);
+	if (ret)
+		goto fail;
+	pfnmap = vma->vm_flags & (VM_IO | VM_PFNMAP);
+
+	for (;;) {
+		if (fatal_signal_pending(current)) {
+			ret = -EINTR;
+			goto fail;
+		}
+		cond_resched();
+
+		/* This helper hands back a single page; cap the batch at one. */
+		page = follow_page_mask(vma, addr, addr + PAGE_SIZE,
+					gup_flags | FOLL_TOUCH | FOLL_GET,
+					&nr_pages);
+		if (!IS_ERR_OR_NULL(page)) {
+			/* Match __get_user_pages(): flush for VIVT/aliasing caches. */
+			flush_anon_page(vma, page, addr);
+			flush_dcache_page(page);
+			return page;
+		}
+
+		/*
+		 * 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);
+}
+
+/*
+ * get_user_page_lookup_vma - fault in one page of a remote mm and hand back the
+ * page along with the VMA that covers it. The caller must hold the mmap_lock.
+ * Returns with the mmap_lock still held.
+ *
+ * Looks up the VMA, and gets a reference to the page through
+ * get_user_page_vma(), faulting in the page if needed.
+ *
+ * FOLL_NOWAIT and FOLL_UNLOCKABLE are rejected: both let the fault handler
+ * drop the mmap lock, which could invalidate the looked-up VMA.
+ */
+struct page *get_user_page_lookup_vma(struct mm_struct *mm, unsigned long addr,
+				      int gup_flags,
+				      struct vm_area_struct **vmap)
+{
+	struct vm_area_struct *vma;
+	struct page *page;
+
+	if (WARN_ON_ONCE(unlikely(gup_flags & (FOLL_NOWAIT | FOLL_UNLOCKABLE))))
+		return ERR_PTR(-EINVAL);
+
+	mmap_assert_locked(mm);
+
+	vma = vma_lookup(mm, addr);
+	if (!vma)
+		return ERR_PTR(-EFAULT);
+
+	page = get_user_page_vma(vma, addr, gup_flags | FOLL_REMOTE | FOLL_TOUCH);
+	if (IS_ERR(page))
+		return page;
+
+	*vmap = vma;
+	return page;
+}
+
 /*
  * Writing to file-backed mappings which require folio dirty tracking using GUP
  * is a fundamentally broken operation, as kernel write access to GUP mappings
@@ -1197,13 +1416,21 @@ static bool writable_file_mapping_allowed(struct vm_area_struct *vma,
 	return !vma_needs_dirty_tracking(vma);
 }
 
-static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags)
+int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags,
+		    vm_flags_t ignore_flags)
 {
 	vm_flags_t vm_flags = vma->vm_flags;
 	int write = (gup_flags & FOLL_WRITE);
 	int foreign = (gup_flags & FOLL_REMOTE);
 	bool vma_anon = vma_is_anonymous(vma);
 
+	/*
+	 * Opt out of the flag checks that read this local copy (the
+	 * VM_IO/VM_PFNMAP gate and the write/read/cow bits); checks that
+	 * re-read vma->vm_flags through helpers are unaffected.
+	 */
+	vm_flags &= ~ignore_flags;
+
 	if (vm_flags & (VM_IO | VM_PFNMAP))
 		return -EFAULT;
 
@@ -1358,7 +1585,6 @@ static long __get_user_pages(struct mm_struct *mm,
 {
 	long ret = 0, i = 0;
 	struct vm_area_struct *vma = NULL;
-	unsigned long page_mask = 0;
 
 	if (!nr_pages)
 		return 0;
@@ -1373,7 +1599,7 @@ static long __get_user_pages(struct mm_struct *mm,
 
 	do {
 		struct page *page;
-		unsigned int page_increm;
+		unsigned long page_increm;
 
 		/* first iteration or cross vma bound */
 		if (!vma || start >= vma->vm_end) {
@@ -1387,7 +1613,7 @@ static long __get_user_pages(struct mm_struct *mm,
 					ret = -ENOMEM;
 					goto out;
 				}
-				if (check_vma_flags(vma, gup_flags)) {
+				if (check_vma_flags(vma, gup_flags, 0)) {
 					ret = -EINVAL;
 					goto out;
 				}
@@ -1400,7 +1626,7 @@ static long __get_user_pages(struct mm_struct *mm,
 						pages ? &page : NULL);
 				if (ret)
 					goto out;
-				page_mask = 0;
+				page_increm = 1;
 				goto next_page;
 			}
 
@@ -1408,7 +1634,7 @@ static long __get_user_pages(struct mm_struct *mm,
 				ret = -EFAULT;
 				goto out;
 			}
-			ret = check_vma_flags(vma, gup_flags);
+			ret = check_vma_flags(vma, gup_flags, 0);
 			if (ret)
 				goto out;
 		}
@@ -1423,7 +1649,8 @@ static long __get_user_pages(struct mm_struct *mm,
 		}
 		cond_resched();
 
-		page = follow_page_mask(vma, start, gup_flags, &page_mask);
+		page = follow_page_mask(vma, start, start + nr_pages * PAGE_SIZE,
+					gup_flags, &page_increm);
 		if (!page || PTR_ERR(page) == -EMLINK) {
 			ret = faultin_page(vma, start, gup_flags,
 					   PTR_ERR(page) == -EMLINK, locked);
@@ -1456,7 +1683,6 @@ static long __get_user_pages(struct mm_struct *mm,
 			goto out;
 		}
 next_page:
-		page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
 		if (page_increm > nr_pages)
 			page_increm = nr_pages;
 
diff --git a/mm/internal.h b/mm/internal.h
index 181e79f1d6a207..706f7f08fd1815 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -1595,6 +1595,10 @@ struct vm_struct *__get_vm_area_node(unsigned long size,
  */
 int __must_check try_grab_folio(struct folio *folio, int refs,
 				unsigned int flags);
+int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags,
+		    vm_flags_t ignore_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 +1645,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,
diff --git a/mm/memory.c b/mm/memory.c
index ff338c2abe9231..aaf620017f3442 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -7015,86 +7015,244 @@ EXPORT_SYMBOL_GPL(generic_access_phys);
 #endif
 
 /*
- * Access another process' address space as given in mm.
+ * VM_IO / VM_PFNMAP memory, such as an ioremapped device mapping, maps
+ * PFNs that have no struct page, so get_user_page_vma() cannot fetch it
+ * even though the page tables are populated. It can still be reached
+ * through vma->vm_ops->access().
+ *
+ * Returns the number of bytes transferred, or <= 0 if @vma cannot be
+ * accessed this way.
  */
-static int __access_remote_vm(struct mm_struct *mm, unsigned long addr,
-			      void *buf, int len, unsigned int gup_flags)
+static int access_remote_vma_ops(struct vm_area_struct *vma, unsigned long addr,
+				 void *buf, int len, int write)
+{
+#ifdef CONFIG_HAVE_IOREMAP_PROT
+	if (vma->vm_ops && vma->vm_ops->access)
+		return vma->vm_ops->access(vma, addr, buf, len, write);
+#endif
+	return 0;
+}
+
+/*
+ * Lock @mm to reach the remote range [@addr, @addr + @len).
+ *
+ * Take the per-VMA lock when the whole range fits in a single VMA whose
+ * flags permit the access. The RCU freed page tables then keep page table
+ * memory from being reused with unexpected contents while the lock is held.
+ * Otherwise fall back to the mmap lock, which also covers multi-VMA ranges,
+ * stack expansion, and ->access() memory.
+ *
+ * Return whether the mmap lock is held. The per-VMA locked VMA, when one is
+ * taken, is stored in *@vmap; it is NULL on the mmap lock path. *@vmap is
+ * set to an ERR_PTR() when the mmap lock could not be taken, so callers must
+ * check IS_ERR(*@vmap) before using either result.
+ */
+static bool remote_access_lock(struct mm_struct *mm, unsigned long addr,
+			       int len, unsigned int gup_flags,
+			       struct vm_area_struct **vmap)
+{
+	struct vm_area_struct *vma = NULL;
+
+#if defined(CONFIG_PER_VMA_LOCK) && defined(CONFIG_MMU_GATHER_RCU_TABLE_FREE)
+	vma = lock_vma_under_rcu(mm, addr);
+	if (vma) {
+		/* addr + len must not wrap, and must fit within the one VMA. */
+		if (addr + len < addr || addr + len > vma->vm_end ||
+		    check_vma_flags(vma, gup_flags, 0)) {
+			vma_end_read(vma);
+			vma = NULL;
+		}
+	}
+#endif
+
+	if (!vma) {
+		if (mmap_read_lock_killable(mm)) {
+			*vmap = ERR_PTR(-EINTR);
+			return false;
+		}
+		*vmap = NULL;
+		return true;
+	}
+
+	*vmap = vma;
+	return false;
+}
+
+/* Release the lock taken by remote_access_lock(). */
+static void remote_access_unlock(struct mm_struct *mm,
+				 struct vm_area_struct *vma, bool have_mmap_lock)
+{
+	if (have_mmap_lock)
+		mmap_read_unlock(mm);
+	else if (vma)
+		vma_end_read(vma);
+}
+
+/*
+ * Per-page action for a remote VM walk. Handle up to @len bytes at @addr on
+ * @page, advancing *@buf past the bytes read from or written to it. @page is
+ * NULL for struct-page-less memory (VM_IO / VM_PFNMAP) reached under the mmap
+ * lock.
+ *
+ * Return the number of source bytes handled at @addr, 0 to end the walk (a
+ * string reached its NUL, or ->access() memory could not be reached), or a
+ * negative errno to abort.
+ */
+typedef int (*remote_vm_action)(struct vm_area_struct *vma, struct page *page,
+				unsigned long addr, void **buf, int len,
+				int write);
+
+/*
+ * Walk the remote range [@addr, @addr + @len) of @mm, handing each page to
+ * @action. Use the per-VMA lock when the range fits one VMA, and fall back to
+ * the mmap lock for multi-VMA ranges, stack expansion (when @can_expand_stack
+ * is set), or when the per-VMA lock cannot finish a fault.
+ *
+ * Each page is faulted in with get_user_page_vma() under whichever lock is
+ * held. Return the number of bytes @action consumed; *@err is a negative
+ * errno when the walk aborted, else 0.
+ */
+static int remote_vm_walk(struct mm_struct *mm, unsigned long addr, void *buf,
+			  int len, unsigned int gup_flags, bool can_expand_stack,
+			  remote_vm_action action, int *err)
 {
 	void *old_buf = buf;
 	int write = gup_flags & FOLL_WRITE;
+	bool have_mmap_lock;
+	struct vm_area_struct *vma;
 
-	if (mmap_read_lock_killable(mm))
-		return 0;
+	*err = 0;
+
+	/*
+	 * Set FOLL_REMOTE so check_vma_flags() applies the same protection key
+	 * rules as get_user_pages_remote() did: the current PKRU is not checked
+	 * against a VMA reached on @mm's behalf.
+	 */
+	gup_flags |= FOLL_REMOTE;
 
-	/* Untag the address before looking up the VMA */
-	addr = untagged_addr_remote(mm, addr);
+	addr = untagged_addr_remote_unlocked(mm, addr);
 
-	/* Avoid triggering the temporary warning in __get_user_pages */
-	if (!vma_lookup(mm, addr) && !expand_stack(mm, addr))
+	have_mmap_lock = remote_access_lock(mm, addr, len, gup_flags, &vma);
+	if (IS_ERR(vma)) {
+		*err = -EFAULT;
 		return 0;
+	}
 
-	/* ignore errors, just check how much was successfully transferred */
 	while (len) {
-		int bytes, offset;
-		void *maddr;
-		struct folio *folio;
-		struct vm_area_struct *vma = NULL;
-		struct page *page = get_user_page_vma_remote(mm, addr,
-							     gup_flags, &vma);
+		unsigned int foll_flags = gup_flags;
+		struct page *page;
+		int ret;
 
-		if (IS_ERR(page)) {
-			/* We might need to expand the stack to access it */
+		if (!vma || addr >= vma->vm_end) {
+			/* Any lookup here holds the mmap lock. */
+			VM_BUG_ON(!have_mmap_lock);
 			vma = vma_lookup(mm, addr);
-			if (!vma) {
+			if (!vma && can_expand_stack) {
+				/* expand_stack() drops the mmap lock if it fails */
 				vma = expand_stack(mm, addr);
-
-				/* mmap_lock was dropped on failure */
 				if (!vma)
-					return buf - old_buf;
-
-				/* Try again if stack expansion worked */
-				continue;
+					have_mmap_lock = false;
+			}
+			if (!vma) {
+				*err = -EFAULT;
+				break;
 			}
+		}
+
+		/*
+		 * FOLL_UNLOCKABLE lets the per-VMA fault retry, dropping the
+		 * lock, so the walk can fall back to the mmap lock.
+		 */
+		if (!have_mmap_lock)
+			foll_flags |= FOLL_VMA_LOCK | FOLL_UNLOCKABLE;
 
+		page = get_user_page_vma(vma, addr, foll_flags);
+		if (IS_ERR(page)) {
 			/*
-			 * Check if this is a VM_IO | VM_PFNMAP VMA, which
-			 * we can access using slightly different code.
+			 * get_user_page_vma() returns -EAGAIN, with the per-VMA
+			 * lock released, for anything it could not finish under
+			 * it; retake the mmap lock and retry. A different error
+			 * therefore only arrives under the mmap lock, where
+			 * struct-page-less memory can be reached via ->access().
 			 */
-			bytes = 0;
-#ifdef CONFIG_HAVE_IOREMAP_PROT
-			if (vma->vm_ops && vma->vm_ops->access)
-				bytes = vma->vm_ops->access(vma, addr, buf,
-							    len, write);
-#endif
-			if (bytes <= 0)
-				break;
-		} else {
-			folio = page_folio(page);
-			bytes = len;
-			offset = addr & (PAGE_SIZE-1);
-			if (bytes > PAGE_SIZE-offset)
-				bytes = PAGE_SIZE-offset;
-
-			maddr = kmap_local_folio(folio, folio_page_idx(folio, page) * PAGE_SIZE);
-			if (write) {
-				copy_to_user_page(vma, page, addr,
-						  maddr + offset, buf, bytes);
-				folio_mark_dirty_lock(folio);
-			} else {
-				copy_from_user_page(vma, page, addr,
-						    buf, maddr + offset, bytes);
+			if (PTR_ERR(page) == -EAGAIN) {
+				vma = NULL;
+				if (mmap_read_lock_killable(mm)) {
+					*err = -EFAULT;
+					break;
+				}
+				have_mmap_lock = true;
+				continue;
 			}
-			folio_release_kmap(folio, maddr);
+			if (WARN_ON_ONCE(!have_mmap_lock))
+				break;
+			page = NULL;
+		}
+
+		ret = action(vma, page, addr, &buf, len, write);
+		if (ret <= 0) {
+			if (ret < 0)
+				*err = ret;
+			break;
 		}
-		len -= bytes;
-		buf += bytes;
-		addr += bytes;
+		addr += ret;
+		len -= ret;
 	}
-	mmap_read_unlock(mm);
+
+	remote_access_unlock(mm, vma, have_mmap_lock);
 
 	return buf - old_buf;
 }
 
+/*
+ * Copy one page's worth of [@addr, @addr + @len) to or from *@buf. Reaches
+ * struct-page-less VM_IO / VM_PFNMAP memory through vma->vm_ops->access().
+ */
+static int access_vm_page(struct vm_area_struct *vma, struct page *page,
+			  unsigned long addr, void **buf, int len, int write)
+{
+	struct folio *folio;
+	int bytes, offset;
+	void *maddr;
+
+	if (!page) {
+		bytes = access_remote_vma_ops(vma, addr, *buf, len, write);
+		if (bytes > 0)
+			*buf += bytes;
+		return bytes;
+	}
+
+	bytes = len;
+	offset = addr & (PAGE_SIZE - 1);
+	if (bytes > PAGE_SIZE - offset)
+		bytes = PAGE_SIZE - offset;
+
+	folio = page_folio(page);
+	maddr = kmap_local_folio(folio, folio_page_idx(folio, page) * PAGE_SIZE);
+	if (write) {
+		copy_to_user_page(vma, page, addr, maddr + offset, *buf, bytes);
+		folio_mark_dirty_lock(folio);
+	} else {
+		copy_from_user_page(vma, page, addr, *buf, maddr + offset, bytes);
+	}
+	folio_release_kmap(folio, maddr);
+
+	*buf += bytes;
+	return bytes;
+}
+
+/*
+ * Access another process' address space as given in mm.
+ */
+static int __access_remote_vm(struct mm_struct *mm, unsigned long addr,
+			      void *buf, int len, unsigned int gup_flags)
+{
+	int err;
+
+	return remote_vm_walk(mm, addr, buf, len, gup_flags, true,
+			      access_vm_page, &err);
+}
+
 /**
  * access_remote_vm - access another process' address space
  * @mm:		the mm_struct of the target address space
@@ -7138,84 +7296,67 @@ EXPORT_SYMBOL_GPL(access_process_vm);
 
 #ifdef CONFIG_BPF_SYSCALL
 /*
- * Copy a string from another process's address space as given in mm.
- * If there is any error return -EFAULT.
+ * Copy a NUL-terminated string from @addr into *@buf, up to @len bytes,
+ * stopping at the NUL. strscpy() always NUL terminates, so recopy the last
+ * byte of a page when more pages follow. A string is never read from
+ * struct-page-less VM_IO / VM_PFNMAP memory.
  */
-static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,
-				void *buf, int len, unsigned int gup_flags)
+static int copy_vm_str(struct vm_area_struct *vma, struct page *page,
+		       unsigned long addr, void **buf, int len, int write)
 {
-	void *old_buf = buf;
-	int err = 0;
-
-	*(char *)buf = '\0';
+	struct folio *folio;
+	int bytes, offset, retval;
+	void *maddr;
 
-	if (mmap_read_lock_killable(mm))
+	if (!page)
 		return -EFAULT;
 
-	addr = untagged_addr_remote(mm, addr);
+	bytes = len;
+	offset = addr & (PAGE_SIZE - 1);
+	if (bytes > PAGE_SIZE - offset)
+		bytes = PAGE_SIZE - offset;
 
-	/* Avoid triggering the temporary warning in __get_user_pages */
-	if (!vma_lookup(mm, addr)) {
-		err = -EFAULT;
-		goto out;
+	folio = page_folio(page);
+	maddr = kmap_local_folio(folio, folio_page_idx(folio, page) * PAGE_SIZE);
+	retval = strscpy(*buf, maddr + offset, bytes);
+	if (retval >= 0) {
+		/* Found the end of the string. */
+		*buf += retval;
+		folio_release_kmap(folio, maddr);
+		return 0;
 	}
 
-	while (len) {
-		int bytes, offset, retval;
-		void *maddr;
-		struct folio *folio;
-		struct page *page;
-		struct vm_area_struct *vma = NULL;
+	*buf += bytes - 1;
+	if (bytes != len) {
+		copy_from_user_page(vma, page, addr + bytes - 1, *buf,
+				    maddr + (PAGE_SIZE - 1), 1);
+		*buf += 1;
+	}
+	folio_release_kmap(folio, maddr);
 
-		page = get_user_page_vma_remote(mm, addr, gup_flags, &vma);
-		if (IS_ERR(page)) {
-			/*
-			 * Treat as a total failure for now until we decide how
-			 * to handle the CONFIG_HAVE_IOREMAP_PROT case and
-			 * stack expansion.
-			 */
-			*(char *)buf = '\0';
-			err = -EFAULT;
-			goto out;
-		}
+	return bytes;
+}
 
-		folio = page_folio(page);
-		bytes = len;
-		offset = addr & (PAGE_SIZE - 1);
-		if (bytes > PAGE_SIZE - offset)
-			bytes = PAGE_SIZE - offset;
-
-		maddr = kmap_local_folio(folio, folio_page_idx(folio, page) * PAGE_SIZE);
-		retval = strscpy(buf, maddr + offset, bytes);
-		if (retval >= 0) {
-			/* Found the end of the string */
-			buf += retval;
-			folio_release_kmap(folio, maddr);
-			break;
-		}
+/*
+ * Copy a string from another process's address space as given in mm.
+ * If there is any error return -EFAULT.
+ */
+static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,
+				void *buf, int len, unsigned int gup_flags)
+{
+	int bytes, err;
 
-		buf += bytes - 1;
-		/*
-		 * Because strscpy always NUL terminates we need to
-		 * copy the last byte in the page if we are going to
-		 * load more pages
-		 */
-		if (bytes != len) {
-			addr += bytes - 1;
-			copy_from_user_page(vma, page, addr, buf, maddr + (PAGE_SIZE - 1), 1);
-			buf += 1;
-			addr += 1;
-		}
-		len -= bytes;
+	*(char *)buf = '\0';
 
-		folio_release_kmap(folio, maddr);
+	bytes = remote_vm_walk(mm, addr, buf, len, gup_flags, false,
+			       copy_vm_str, &err);
+	if (err) {
+		/* The contract guarantees a terminated buffer even on error. */
+		((char *)buf)[bytes] = '\0';
+		return err;
 	}
 
-out:
-	mmap_read_unlock(mm);
-	if (err)
-		return err;
-	return buf - old_buf;
+	return bytes;
 }
 
 /**
diff --git a/mm/rmap.c b/mm/rmap.c
index 1c77d5dc06e9fa..b36f2e219b8f49 100644
--- a/mm/rmap.c
+++ b/mm/rmap.c
@@ -2838,7 +2838,7 @@ struct page *make_device_exclusive(struct mm_struct *mm, unsigned long addr,
 	 * (non-device-exclusive) PTE and issue a MMU_NOTIFY_EXCLUSIVE.
 	 */
 retry:
-	page = get_user_page_vma_remote(mm, addr,
+	page = get_user_page_lookup_vma(mm, addr,
 					FOLL_GET | FOLL_WRITE | FOLL_SPLIT_PMD,
 					&vma);
 	if (IS_ERR(page))
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index e6df968f0971c8..6b917a4f73e00f 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -83,6 +83,7 @@ TEST_GEN_FILES += mrelease_test
 TEST_GEN_FILES += mremap_dontunmap
 TEST_GEN_FILES += mremap_test
 TEST_GEN_FILES += mseal_test
+TEST_GEN_FILES += mthp_gup_cow_test
 TEST_GEN_FILES += on-fault-limit
 TEST_GEN_FILES += pagemap_ioctl
 TEST_GEN_FILES += pfnmap
diff --git a/tools/testing/selftests/mm/mthp_gup_cow_test.c b/tools/testing/selftests/mm/mthp_gup_cow_test.c
new file mode 100644
index 00000000000000..52ee329d8641c0
--- /dev/null
+++ b/tools/testing/selftests/mm/mthp_gup_cow_test.c
@@ -0,0 +1,213 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Verify that the slow GUP path (pin_user_pages -> follow_page_mask ->
+ * follow_pte_batch) returns the correct pages for a PTE-mapped large folio
+ * (mTHP), including that COW copies produce the right content.
+ *
+ * Uses the CONFIG_GUP_TEST PIN_LONGTERM interface: START pins a range on the
+ * slow path, READ copies the pinned pages' bytes back so we can compare them
+ * against the pattern we wrote.
+ */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <linux/types.h>
+
+#define ARRAY_SIZE(a)	(sizeof(a) / sizeof((a)[0]))
+
+#define GUP_DEV "/sys/kernel/debug/gup_test"
+
+#define PIN_LONGTERM_TEST_START	_IOW('g', 7, struct pin_longterm_test)
+#define PIN_LONGTERM_TEST_STOP	_IO('g', 8)
+#define PIN_LONGTERM_TEST_READ	_IOW('g', 9, __u64)
+#define USE_WRITE	1
+#define USE_FAST	2
+
+struct pin_longterm_test {
+	__u64 addr;
+	__u64 size;
+	__u32 flags;
+};
+
+#define ORDER_KB	64
+#define NR_FOLIOS	16
+#define REGION		((size_t)ORDER_KB * 1024 * NR_FOLIOS)
+
+static long PS;
+static int fails;
+static int tap;
+
+static void ok(int cond, const char *desc)
+{
+	printf("%s %d %s\n", cond ? "ok" : "not ok", ++tap, desc);
+	if (!cond)
+		fails++;
+}
+
+/* Deterministic, per-page-distinct pattern so any mis-order or leak shows. */
+static void fill(char *base, size_t sz, uint32_t salt)
+{
+	for (size_t off = 0; off < sz; off += PS) {
+		uint32_t k = off / PS;
+		uint64_t v = ((uint64_t)salt << 32) ^ (k * 0x9E3779B1u + 0x1234);
+
+		for (size_t i = 0; i < PS; i += sizeof(v))
+			memcpy(base + off + i, &v, sizeof(v));
+	}
+}
+
+static int wsysfs(const char *path, const char *val)
+{
+	int fd = open(path, O_WRONLY);
+
+	if (fd < 0)
+		return -1;
+	int r = write(fd, val, strlen(val));
+
+	close(fd);
+	return r < 0 ? -1 : 0;
+}
+
+/* Force sub-PMD 64kB mTHP only, so faults produce PTE-mapped large folios. */
+static void setup_mthp(void)
+{
+	const char *thp = "/sys/kernel/mm/transparent_hugepage";
+	char p[256];
+	static const int kb[] = { 16, 32, 64, 128, 256, 512, 1024, 2048 };
+
+	wsysfs("/sys/kernel/mm/transparent_hugepage/enabled", "never");
+	for (unsigned int i = 0; i < ARRAY_SIZE(kb); i++) {
+		snprintf(p, sizeof(p), "%s/hugepages-%dkB/enabled", thp, kb[i]);
+		wsysfs(p, kb[i] == ORDER_KB ? "always" : "never");
+	}
+}
+
+/* Count how many pages sit in a contiguous >=ORDER_KB PFN run (via pagemap). */
+static int count_large_pages(char *base, size_t sz)
+{
+	int pm = open("/proc/self/pagemap", O_RDONLY);
+	size_t n = sz / PS, large = 0;
+	uint64_t *pfn = calloc(n, sizeof(*pfn));
+
+	if (pm < 0)
+		return -1;
+	for (size_t k = 0; k < n; k++) {
+		uint64_t ent;
+		off_t idx = ((uintptr_t)base + k * PS) / PS * sizeof(ent);
+
+		if (pread(pm, &ent, sizeof(ent), idx) != sizeof(ent) ||
+		    !(ent & (1ULL << 63)))
+			pfn[k] = 0;
+		else
+			pfn[k] = ent & ((1ULL << 55) - 1);
+	}
+	close(pm);
+	for (size_t k = 0; k < n; k++)
+		if (k + 1 < n && pfn[k] && pfn[k + 1] == pfn[k] + 1)
+			large++;
+	free(pfn);
+	return large;
+}
+
+/* Pin @base..@sz on the slow path, read the pinned bytes back, compare to exp. */
+static int pin_verify(int fd, char *base, size_t sz, uint32_t wr, char *exp)
+{
+	struct pin_longterm_test a = {
+		.addr = (uintptr_t)base, .size = sz,
+		.flags = wr ? USE_WRITE : 0,		/* USE_FAST unset => slow */
+	};
+	char *got = mmap(NULL, sz, PROT_READ | PROT_WRITE,
+			 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	__u64 ga = (uintptr_t)got;
+	int rc = -1;
+
+	if (got == MAP_FAILED)
+		return -1;
+	if (ioctl(fd, PIN_LONGTERM_TEST_START, &a)) {
+		fprintf(stderr, "START(%s) failed: %s\n",
+			wr ? "write" : "read", strerror(errno));
+		goto out;
+	}
+	if (ioctl(fd, PIN_LONGTERM_TEST_READ, &ga)) {
+		fprintf(stderr, "READ failed: %s\n", strerror(errno));
+		ioctl(fd, PIN_LONGTERM_TEST_STOP);
+		goto out;
+	}
+	ioctl(fd, PIN_LONGTERM_TEST_STOP);
+	rc = memcmp(got, exp, sz) ? 1 : 0;
+out:
+	munmap(got, sz);
+	return rc;
+}
+
+int main(void)
+{
+	PS = sysconf(_SC_PAGESIZE);
+	setup_mthp();
+
+	int fd = open(GUP_DEV, O_RDWR);
+
+	if (fd < 0) {
+		fprintf(stderr, "open %s: %s (CONFIG_GUP_TEST?)\n",
+			GUP_DEV, strerror(errno));
+		return 2;
+	}
+
+	char *r = mmap(NULL, REGION, PROT_READ | PROT_WRITE,
+		       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	if (r == MAP_FAILED) {
+		perror("mmap");
+		return 2;
+	}
+	fill(r, REGION, 0xA1);				/* pattern P */
+	char *expP = malloc(REGION);
+
+	memcpy(expP, r, REGION);
+
+	int large = count_large_pages(r, REGION);
+
+	printf("# %d/%zu pages in contiguous large-folio runs\n",
+	       large, REGION / PS);
+	if (large < (int)(REGION / PS) / 4)
+		printf("# WARN: little mTHP backing; batch path lightly covered\n");
+
+	/* A: read pin over writable mTHP -> batches -> content must equal P. */
+	ok(pin_verify(fd, r, REGION, 0, expP) == 0,
+	   "slow read-pin of mTHP returns correct contents");
+
+	/* B: write pin -> FOLL_WRITE batch path -> content must equal P. */
+	ok(pin_verify(fd, r, REGION, 1, expP) == 0,
+	   "slow write-pin of mTHP returns correct contents");
+
+	/* C: COW isolation. Child write-pins (unshares) then rewrites; parent P. */
+	pid_t pid = fork();
+
+	if (pid == 0) {
+		int cfd = open(GUP_DEV, O_RDWR);
+		int a = pin_verify(cfd, r, REGION, 1, expP);	/* COW copy == P */
+
+		fill(r, REGION, 0xB2);			/* child writes Q */
+		_exit(a == 0 ? 0 : 1);
+	}
+	int st = 0;
+
+	waitpid(pid, &st, 0);
+	ok(WIFEXITED(st) && WEXITSTATUS(st) == 0,
+	   "child write-pin after COW returns correct (copied) contents");
+	ok(memcmp(r, expP, REGION) == 0,
+	   "parent contents intact after child COW writes");
+	/* D: parent read-pin again after the COW split still correct. */
+	ok(pin_verify(fd, r, REGION, 0, expP) == 0,
+	   "parent slow read-pin after COW still correct");
+
+	printf("# totals: pass:%d fail:%d\n", tap - fails, fails);
+	return fails ? 1 : 0;
+}
diff --git a/tools/testing/selftests/mm/pfnmap.c b/tools/testing/selftests/mm/pfnmap.c
index 4f550822385a20..6ff5d1029517e3 100644
--- a/tools/testing/selftests/mm/pfnmap.c
+++ b/tools/testing/selftests/mm/pfnmap.c
@@ -31,6 +31,7 @@ static sigjmp_buf sigjmp_buf_env;
 static char *file = "/dev/mem";
 static off_t file_offset;
 static int fd;
+static int target_is_ram;
 
 static void signal_handler(int sig)
 {
@@ -113,6 +114,7 @@ static void pfnmap_init(void)
 		if (err)
 			ksft_exit_skip("Cannot find ram target in '/proc/iomem': %s\n",
 				       strerror(-err));
+		target_is_ram = 1;
 	} else {
 		file_offset = 0;
 	}
@@ -271,6 +273,70 @@ TEST_F(pfnmap, fork)
 	ASSERT_EQ(ret, 0);
 }
 
+TEST_F(pfnmap, procmem_cow_read)
+{
+	char *priv, *buf;
+	ssize_t rc;
+	int mem_fd;
+
+	/*
+	 * A COWed page in a VM_PFNMAP mapping has a struct page, so reading it
+	 * through /proc/self/mem -- __access_remote_vm() -> get_user_page_vma()
+	 * -- returns it directly, instead of routing to vma->vm_ops->access(),
+	 * which ioremaps the PFN and cannot reach a COWed RAM page.
+	 *
+	 * Map the file MAP_PRIVATE and writable, write to COW a page into anon
+	 * memory, then read the page back through /proc/self/mem.
+	 */
+	self->size2 = self->pagesize;
+	self->addr2 = mmap(NULL, self->size2, PROT_READ | PROT_WRITE,
+			   MAP_PRIVATE, fd, file_offset);
+	if (self->addr2 == MAP_FAILED)
+		SKIP(return, "Cannot create a writable private pfnmap mapping");
+	priv = self->addr2;
+
+	/* COW the page and stamp known bytes into the anon copy. */
+	priv[0] = 0x42;
+	priv[self->pagesize - 1] = 0x24;
+
+	buf = malloc(self->pagesize);
+	ASSERT_NE(buf, NULL);
+
+	mem_fd = open("/proc/self/mem", O_RDONLY);
+	ASSERT_GE(mem_fd, 0);
+	rc = pread(mem_fd, buf, self->pagesize, (off_t)(uintptr_t)priv);
+	close(mem_fd);
+
+	ASSERT_EQ(rc, (ssize_t)self->pagesize);
+	EXPECT_EQ(buf[0], 0x42);
+	EXPECT_EQ(buf[self->pagesize - 1], 0x24);
+
+	free(buf);
+}
+
+TEST_F(pfnmap, procmem_pfn_read)
+{
+	char buf[64];
+	ssize_t rc;
+	int mem_fd;
+
+	/*
+	 * A raw PFN of a VM_IO/VM_PFNMAP mapping has no struct page, so
+	 * __access_remote_vm() reaches it through vma->vm_ops->access()
+	 * (generic_access_phys()). That ioremaps the PFN, which is rejected for
+	 * RAM, so this only applies to genuine device memory.
+	 */
+	if (target_is_ram)
+		SKIP(return, "Target is System RAM; ->access() cannot ioremap RAM");
+
+	mem_fd = open("/proc/self/mem", O_RDONLY);
+	ASSERT_GE(mem_fd, 0);
+	rc = pread(mem_fd, buf, sizeof(buf), (off_t)(uintptr_t)self->addr1);
+	close(mem_fd);
+
+	ASSERT_EQ(rc, (ssize_t)sizeof(buf));
+}
+
 int main(int argc, char **argv)
 {
 	for (int i = 1; i < argc; i++) {
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 8c296dedf0474d..b49a3eb0c20508 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -289,6 +289,7 @@ fi
 # Dump pages 0, 19, and 4096, using pin_user_pages:
 CATEGORY="gup_test" run_test ./gup_test -ct -F 0x1 0 19 0x1000
 CATEGORY="gup_test" run_test ./gup_longterm
+CATEGORY="gup_test" run_test ./mthp_gup_cow_test
 
 CATEGORY="userfaultfd" run_test ./uffd-unit-tests
 uffd_stress_bin=./uffd-stress

3/2 2026/07/26 10:07 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 23777
							output: 30
							thoughts: 860
						

Thoughts:
**Analyzing a GUP and Remote VM Access Patch Series**

Okay, so I'm looking at this patch series that's overhauling `get_user_page_vma_remote` and related functions, hitting `mm/gup.c`, `mm/memory.c`, and some architecture-specific code like arm64, riscv, and x86. The core of it seems to be about refining the mechanisms for GUP (Get User Page) and remote VM access.

The main gist is the introduction of `get_user_page_lookup_vma` and `get_user_page_vma`, and a new `remote_vm_walk` mechanism, which will likely affect the efficiency of these operations. We see `remote_vm_walk` being used by `__access_remote_vm` and `__copy_remote_vm_str` to make a more structured way to access and copy data between VMs.  They've also beefed up the batching for contiguous pages in `follow_page_mask` and `follow_page_pte`, important for those PTE-mapped large folios (mTHP).

Diving into `mm/gup.c`, the new `follow_pte_batch` is a welcome addition, and the updates to the `follow_huge_*` functions suggest a focus on mTHP support, making them return `nr_pages`. I see that `faultin_page` is getting a bit of love too, handling `FOLL_VMA_LOCK`.  `get_user_page_vma` sounds like a nice abstraction for getting a page from a VMA, taking into account the different locking schemes. And `get_user_page_lookup_vma` is the replacement for `get_user_page_vma_remote`, which is a key change to track.

Now, over in `mm/memory.c`, the addition of `access_remote_vma_ops`, `remote_access_lock` and `remote_access_unlock` look like they're solidifying the remote VM access infrastructure. The real workhorse here is `remote_vm_walk`, which is now the core mechanism used by `__access_remote_vm` (along with `access_vm_page`) and `__copy_remote_vm_str` (with `copy_vm_str`) to make the operations much more structured.

This is a pretty fundamental shift in how we handle GUP and remote access. This needs some serious fuzzing to make sure everything is rock solid. I'm going to keep an eye on these symbols: `get_user_page_vma`, `get_user_page_lookup_vma`, `remote_vm_walk`, `access_vm_page`, `copy_vm_str`, `follow_pte_batch`, `faultin_page`, `__access_remote_vm`, and `__copy_remote_vm_str`.

Let's see if there are any specific configs we need to keep in mind. Since it's touching `mm/gup.c` and `mm/memory.c`, that means we're in core MM territory. I see `CONFIG_PER_VMA_LOCK` is mentioned in `remote_access_lock`, and `CONFIG_MMU_GATHER_RCU_TABLE_FREE` is another one to consider. And of course, `CONFIG_TRANSPARENT_HUGEPAGE` is definitely relevant given the changes to mTHP and the `follow_huge_*` functions.

I think I'll enable `CONFIG_PER_VMA_LOCK` and `CONFIG_TRANSPARENT_HUGEPAGE` during testing, just to cover all bases. I'll search for `remote_vm_walk` in `mm/memory.c` to see if it's under any specific config, but it looks like it's in the main body.

Time to peek into the architecture-specific stuff. Looks like `CONFIG_RISCV_ISA_SUPM` is relevant for RISC-V and `CONFIG_ADDRESS_MASKING` for x86.

Alright, I'll need to run this through some verification tools to be sure.



4/2 2026/07/26 10:07 tool grepper
0m Args:
{
  "Expression": "remote_vm_walk",
  "PathPrefix": "mm/memory.c"
}

Results:
{
  "Output": "mm/memory.c=7101=typedef int (*remote_vm_action)(struct vm_area_struct *vma, struct page *page,\n--\nmm/memory.c-7114- */\nmm/memory.c:7115:static int remote_vm_walk(struct mm_struct *mm, unsigned long addr, void *buf,\nmm/memory.c-7116-\t\t\t  int len, unsigned int gup_flags, bool can_expand_stack,\n--\nmm/memory.c=7247=static int __access_remote_vm(struct mm_struct *mm, unsigned long addr,\n--\nmm/memory.c-7251-\nmm/memory.c:7252:\treturn remote_vm_walk(mm, addr, buf, len, gup_flags, true,\nmm/memory.c-7253-\t\t\t      access_vm_page, \u0026err);\n--\nmm/memory.c=7344=static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,\n--\nmm/memory.c-7350-\nmm/memory.c:7351:\tbytes = remote_vm_walk(mm, addr, buf, len, gup_flags, false,\nmm/memory.c-7352-\t\t\t       copy_vm_str, \u0026err);\n"
}

5/2 2026/07/26 10:07 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 25924
							output: 30
							thoughts: 78
						

Thoughts:
Okay, here's my take on those considerations, from my perspective as someone deeply familiar with kernel internals:

**Prioritizing Fuzzing Targets in the VM Subsystem**

Alright, looking at the landscape, it's clear that `remote_vm_walk`, `get_user_page_vma`, `get_user_page_lookup_vma`, and `follow_pte_batch` are the real pressure points in the virtual memory subsystem. These functions are absolutely critical, serving as the gateways for user-space interactions and page table traversal. They're the kind of functions that, if they have vulnerabilities, could lead to widespread and devastating consequences. Therefore, these are *absolutely* the functions that I want to subject to intense fuzzing efforts. They're the low-hanging fruit where the reward is high, and bugs have the potential for dramatic impact.

And since we are talking about VM and memory-management, I should also check those obscure configurations: `CONFIG_PER_VMA_LOCK` and `CONFIG_TRANSPARENT_HUGEPAGE`. I am sure they'll add complexity to the functions I mentioned above and are critical points for security. We may need to add additional fuzzing vectors to target these specific configurations and find more bugs.



6/2 2026/07/26 10:07 tool grepper
0m Args:
{
  "Expression": "PER_VMA_LOCK",
  "PathPrefix": "mm/Kconfig"
}

Results:
{
  "Output": "mm/Kconfig=1433=config LRU_GEN_WALKS_MMU\n--\nmm/Kconfig-1437-\nmm/Kconfig:1438:config ARCH_SUPPORTS_PER_VMA_LOCK\nmm/Kconfig-1439-       def_bool n\nmm/Kconfig-1440-\nmm/Kconfig:1441:config PER_VMA_LOCK\nmm/Kconfig-1442-\tdef_bool y\nmm/Kconfig:1443:\tdepends on ARCH_SUPPORTS_PER_VMA_LOCK \u0026\u0026 MMU \u0026\u0026 SMP\nmm/Kconfig-1444-\thelp\n"
}

7/2 2026/07/26 10:07 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 26474
							output: 32
							thoughts: 45
						

Thoughts:
Okay, here's my interpretation of that thought process, from an expert's perspective:

**Checking the VMA Locking Configuration**

Alright, so the system I'm working with is likely configured to automatically leverage `PER_VMA_LOCK`, but only if the architecture supports it, which makes perfect sense. The code's already handling the conditional enabling based on `ARCH_SUPPORTS_PER_VMA_LOCK`, so that's a good starting point. Now, to understand how this interacts with the overall memory management strategy, I need to consider transparent hugepages. Let me just quickly check how `CONFIG_TRANSPARENT_HUGEPAGE` is set. That'll give me a clearer picture of potential lock contention and how these VMA locks are interacting with the hugepage allocations. I need to figure out how these two configs affect each other so I can optimize the memory setup for this workload.



8/2 2026/07/26 10:07 tool grepper
0m Args:
{
  "Expression": "TRANSPARENT_HUGEPAGE",
  "PathPrefix": "mm/Kconfig"
}

Results:
{
  "Output": "mm/Kconfig=827=config PERSISTENT_HUGE_ZERO_FOLIO\nmm/Kconfig-828-\tbool \"Allocate a PMD sized folio for zeroing\"\nmm/Kconfig:829:\tdepends on TRANSPARENT_HUGEPAGE\nmm/Kconfig-830-\thelp\n--\nmm/Kconfig=843=config MM_ID\n--\nmm/Kconfig-845-\nmm/Kconfig:846:menuconfig TRANSPARENT_HUGEPAGE\nmm/Kconfig-847-\tbool \"Transparent Hugepage Support\"\nmm/Kconfig:848:\tdepends on HAVE_ARCH_TRANSPARENT_HUGEPAGE \u0026\u0026 !PREEMPT_RT\nmm/Kconfig-849-\tselect COMPACTION\n--\nmm/Kconfig-861-\nmm/Kconfig:862:if TRANSPARENT_HUGEPAGE\nmm/Kconfig-863-\nmm/Kconfig=864=choice\nmm/Kconfig-865-\tprompt \"Transparent Hugepage Support sysfs defaults\"\nmm/Kconfig:866:\tdefault TRANSPARENT_HUGEPAGE_ALWAYS\nmm/Kconfig-867-\thelp\n--\nmm/Kconfig-869-\nmm/Kconfig:870:\tconfig TRANSPARENT_HUGEPAGE_ALWAYS\nmm/Kconfig-871-\t\tbool \"always\"\n--\nmm/Kconfig-876-\nmm/Kconfig:877:\tconfig TRANSPARENT_HUGEPAGE_MADVISE\nmm/Kconfig-878-\t\tbool \"madvise\"\n--\nmm/Kconfig-885-\nmm/Kconfig:886:\tconfig TRANSPARENT_HUGEPAGE_NEVER\nmm/Kconfig-887-\t\tbool \"never\"\n--\nmm/Kconfig=893=choice\nmm/Kconfig-894-\tprompt \"Shmem hugepage allocation defaults\"\nmm/Kconfig:895:\tdefault TRANSPARENT_HUGEPAGE_SHMEM_HUGE_NEVER\nmm/Kconfig-896-\thelp\n--\nmm/Kconfig-902-\nmm/Kconfig:903:\tconfig TRANSPARENT_HUGEPAGE_SHMEM_HUGE_NEVER\nmm/Kconfig-904-\t\tbool \"never\"\n--\nmm/Kconfig-912-\nmm/Kconfig:913:\tconfig TRANSPARENT_HUGEPAGE_SHMEM_HUGE_ALWAYS\nmm/Kconfig-914-\t\tbool \"always\"\n--\nmm/Kconfig-920-\nmm/Kconfig:921:\tconfig TRANSPARENT_HUGEPAGE_SHMEM_HUGE_WITHIN_SIZE\nmm/Kconfig-922-\t\tbool \"within_size\"\n--\nmm/Kconfig-928-\nmm/Kconfig:929:\tconfig TRANSPARENT_HUGEPAGE_SHMEM_HUGE_ADVISE\nmm/Kconfig-930-\t\tbool \"advise\"\n--\nmm/Kconfig=938=choice\nmm/Kconfig-939-\tprompt \"Tmpfs hugepage allocation defaults\"\nmm/Kconfig:940:\tdefault TRANSPARENT_HUGEPAGE_TMPFS_HUGE_NEVER\nmm/Kconfig-941-\thelp\n--\nmm/Kconfig-947-\nmm/Kconfig:948:\tconfig TRANSPARENT_HUGEPAGE_TMPFS_HUGE_NEVER\nmm/Kconfig-949-\t\tbool \"never\"\n--\nmm/Kconfig-956-\nmm/Kconfig:957:\tconfig TRANSPARENT_HUGEPAGE_TMPFS_HUGE_ALWAYS\nmm/Kconfig-958-\t\tbool \"always\"\n--\nmm/Kconfig-964-\nmm/Kconfig:965:\tconfig TRANSPARENT_HUGEPAGE_TMPFS_HUGE_WITHIN_SIZE\nmm/Kconfig-966-\t\tbool \"within_size\"\n--\nmm/Kconfig-972-\nmm/Kconfig:973:\tconfig TRANSPARENT_HUGEPAGE_TMPFS_HUGE_ADVISE\nmm/Kconfig-974-\t\tbool \"advise\"\n--\nmm/Kconfig=992=config NO_PAGE_MAPCOUNT\n--\nmm/Kconfig-1004-\nmm/Kconfig:1005:endif # TRANSPARENT_HUGEPAGE\nmm/Kconfig-1006-\n--\nmm/Kconfig=1014=config PGTABLE_HAS_HUGE_LEAVES\nmm/Kconfig:1015:\tdef_bool TRANSPARENT_HUGEPAGE || HUGETLB_PAGE\nmm/Kconfig-1016-\n--\nmm/Kconfig=1020=config HAVE_GIGANTIC_FOLIOS\nmm/Kconfig-1021-\tdef_bool (HUGETLB_PAGE \u0026\u0026 ARCH_HAS_GIGANTIC_PAGE) || \\\nmm/Kconfig:1022:\t\t (ZONE_DEVICE \u0026\u0026 HAVE_ARCH_TRANSPARENT_HUGEPAGE_PUD)\nmm/Kconfig-1023-\n--\nmm/Kconfig=1028=config ARCH_SUPPORTS_HUGE_PFNMAP\nmm/Kconfig-1029-\tdef_bool n\nmm/Kconfig:1030:\tdepends on TRANSPARENT_HUGEPAGE\nmm/Kconfig-1031-\nmm/Kconfig=1032=config ARCH_SUPPORTS_PMD_PFNMAP\nmm/Kconfig-1033-\tdef_bool y\nmm/Kconfig:1034:\tdepends on ARCH_SUPPORTS_HUGE_PFNMAP \u0026\u0026 HAVE_ARCH_TRANSPARENT_HUGEPAGE\nmm/Kconfig-1035-\nmm/Kconfig=1036=config ARCH_SUPPORTS_PUD_PFNMAP\nmm/Kconfig-1037-\tdef_bool y\nmm/Kconfig:1038:\tdepends on ARCH_SUPPORTS_HUGE_PFNMAP \u0026\u0026 HAVE_ARCH_TRANSPARENT_HUGEPAGE_PUD\nmm/Kconfig-1039-\n"
}

9/2 2026/07/26 10:07 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 28271
							output: 34
							thoughts: 100
						

Thoughts:
**Analyzing Kernel Configurations and Dependencies**

Okay, so I'm looking at this `TRANSPARENT_HUGEPAGE` config; that seems like a solid addition, generally speaking. I'm also seeing references to `GUP_TEST` in the selftest procedure, but the actual, core implementation of the related logic seems to be residing within `mm/gup.c` and `mm/memory.c`. I'll want to quickly verify if the `GUP_TEST` define is actually *required* for the functionality in those core files, or if it's strictly confined to the self-test procedures. Hmm, it looks like it is just for the selftest.

Now, let's turn my attention to `CONFIG_RISCV_ISA_SUPM`... I'll need to investigate that next.



10/2 2026/07/26 10:07 tool grepper
0m Args:
{
  "Expression": "RISCV_ISA_SUPM",
  "PathPrefix": "arch/riscv/Kconfig"
}

Results:
{
  "Output": "arch/riscv/Kconfig=553=config RISCV_ISA_C\n--\narch/riscv/Kconfig-563-\narch/riscv/Kconfig:564:config RISCV_ISA_SUPM\narch/riscv/Kconfig-565-\tbool \"Supm extension for userspace pointer masking\"\n"
}

11/2 2026/07/26 10:07 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 28675
							output: 32
							thoughts: 34
						

Thoughts:
Okay, here's my interpretation of those thoughts, tailored for an expert audience, presented in the first person:

**Navigating Processor Architectures**

Right, so I'm thinking about the RISC-V Instruction Set Architecture (ISA) here. Specifically, `RISCV_ISA_SUPM`—that's a pretty clear indicator we're dealing with RISC-V, and probably something to do with supervisor mode or some related privileged instruction set extension. I'll need to dig deeper on that if it comes up.

But now, I'm switching gears and my mind is automatically going to the x86 architecture. I need to understand this better. Now, a critical point for x86 is `CONFIG_ADDRESS_MASKING`. This suggests something about how memory addresses are handled, potentially related to PAE (Physical Address Extension), or perhaps a more fine-grained address space management scheme. I'll check that variable, and see exactly which configuration flags are in use.



12/2 2026/07/26 10:07 tool grepper
0m Args:
{
  "Expression": "ADDRESS_MASKING",
  "PathPrefix": "arch/x86/Kconfig"
}

Results:
{
  "Output": "arch/x86/Kconfig=2135=config RANDOMIZE_MEMORY_PHYSICAL_PADDING\n--\narch/x86/Kconfig-2149-\narch/x86/Kconfig:2150:config ADDRESS_MASKING\narch/x86/Kconfig-2151-\tbool \"Linear Address Masking support\"\n"
}

13/2 2026/07/26 10:07 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 29048
							output: 32
							thoughts: 517
						

Thoughts:
**Analyzing a Kernel Patch for Fuzzing Potential**

Okay, so this patch is implementing `ADDRESS_MASKING` for x86 – got it. It's touching `mm/gup.c` and `mm/memory.c`, which are critical kernel files, so that immediately flags it as interesting. I see a whole slew of new functions introduced in this patch: `get_user_page_vma`, `get_user_page_lookup_vma`, `remote_vm_walk`, `access_vm_page`, `copy_vm_str`, and `follow_pte_batch`. These are screaming for fuzzing, no doubt.

Let's make a list of the key symbols to focus on. Okay, so we've got the original list, but now we should also include these functions:
- `faultin_page`
- `__access_remote_vm`
- `__copy_remote_vm_str`

Now, the instructions are clear: prioritize non-static, non-inlined entry points. So, let's see if those new functions are static because static helpers are often inlined and don't offer distinct symbol addresses for fuzzing. Damn, looks like they are all static and should be avoided for fuzzing!

Okay, let's dig into the non-static entry points in `mm/memory.c`. I see `access_remote_vm` and `access_process_vm` (both `EXPORT_SYMBOL_GPL`).  And then `copy_from_user_page` and `copy_to_user_page` that weren't changed. Good. Wait, it looks like `__access_remote_vm` is called by `access_remote_vm` and `access_process_vm`. Looks like `__copy_remote_vm_str` is called by `copy_remote_vm_str`. Is this exported? I should check this...



14/2 2026/07/26 10:07 tool grepper
0m Args:
{
  "Expression": "copy_remote_vm_str",
  "PathPrefix": "mm/memory.c"
}

Results:
{
  "Output": "mm/memory.c=7304=static int copy_vm_str(struct vm_area_struct *vma, struct page *page,\n--\nmm/memory.c-7343- */\nmm/memory.c:7344:static int __copy_remote_vm_str(struct mm_struct *mm, unsigned long addr,\nmm/memory.c-7345-\t\t\t\tvoid *buf, int len, unsigned int gup_flags)\n--\nmm/memory.c-7362-/**\nmm/memory.c:7363: * copy_remote_vm_str - copy a string from another process's address space.\nmm/memory.c-7364- * @tsk:\tthe task of the target address space\n--\nmm/memory.c-7375- */\nmm/memory.c:7376:int copy_remote_vm_str(struct task_struct *tsk, unsigned long addr,\nmm/memory.c-7377-\t\t       void *buf, int len, unsigned int gup_flags)\n--\nmm/memory.c-7390-\nmm/memory.c:7391:\tret = __copy_remote_vm_str(mm, addr, buf, len, gup_flags);\nmm/memory.c-7392-\n--\nmm/memory.c-7396-}\nmm/memory.c:7397:EXPORT_SYMBOL_GPL(copy_remote_vm_str);\nmm/memory.c-7398-#endif /* CONFIG_BPF_SYSCALL */\n"
}

15/2 2026/07/26 10:07 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 30410
							output: 34
							thoughts: 179
						

Thoughts:
**Analyzing Memory Access Functions in the Kernel**

Okay, let's break this down. My initial scan of `mm/memory.c` has flagged `access_remote_vm`, `access_process_vm`, and `copy_remote_vm_str` as the key non-static entry points. These are the functions, based on their names, that likely handle remote virtual machine access and memory copying operations. I need to understand their interactions and behavior.

Now, shifting focus to `mm/gup.c`, I've identified several functions of interest. `get_user_page_vma` and `get_user_page_lookup_vma` are non-static and appear critical to the process of getting a user page and lookups. `__get_user_pages` is static which makes it a helper function within the `gup.c` file. I'll need to see how `get_user_pages`, `pin_user_pages`, `get_user_pages_remote`, and `pin_user_pages_remote` are tied to those functions, especially the ones that are also non-static. The 'remote' functions are probably crucial to my understanding of the architecture, as they deal with cross-VM access.

The core question now is, what's happening within `get_user_page_vma` and `get_user_page_lookup_vma`? I'll need to dig into those functions to get a clearer picture of the underlying mechanisms and how they interact with the overall memory management strategy.



16/2 2026/07/26 10:07 tool grepper
0m Args:
{
  "Expression": "get_user_page_vma",
  "PathPrefix": "mm/gup.c"
}

Results:
{
  "Output": "mm/gup.c=1150=static int faultin_page(struct vm_area_struct *vma,\n--\nmm/gup.c-1238-/*\nmm/gup.c:1239: * get_user_page_vma - get one page from @vma, whose lock the caller already\nmm/gup.c-1240- * holds: the mmap lock, or (with FOLL_VMA_LOCK) the per-VMA lock. Walks the\n--\nmm/gup.c-1252- */\nmm/gup.c:1253:struct page *get_user_page_vma(struct vm_area_struct *vma, unsigned long addr,\nmm/gup.c-1254-\t\t\t       unsigned int gup_flags)\n--\nmm/gup.c-1352- * Looks up the VMA, and gets a reference to the page through\nmm/gup.c:1353: * get_user_page_vma(), faulting in the page if needed.\nmm/gup.c-1354- *\n--\nmm/gup.c=1358=struct page *get_user_page_lookup_vma(struct mm_struct *mm, unsigned long addr,\n--\nmm/gup.c-1373-\nmm/gup.c:1374:\tpage = get_user_page_vma(vma, addr, gup_flags | FOLL_REMOTE | FOLL_TOUCH);\nmm/gup.c-1375-\tif (IS_ERR(page))\n"
}

17/2 2026/07/26 10:07 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 31366
							output: 31
							thoughts: 594
						

Thoughts:
**Refining the Focus for System Call Analysis**

Okay, so I'm diving into this analysis. First, I confirmed that `get_user_page_vma` and `get_user_page_lookup_vma` are indeed non-static – that's good for our purposes. Now, let's look at `follow_page_mask` - it's static, so it's not a direct entry point. However, `__get_user_pages` *is* static and calls it. `__get_user_pages` is called by a host of functions including `get_user_pages`, `get_user_pages_remote`, `pin_user_pages` and a few others.

My initial thought was to include those, but the instructions warn against including generic "hot path" functions to avoid skewing the test distributions. I need to be careful here. `get_user_pages` and `pin_user_pages` are definitely in that hot path category.

So I've refined the core focus to the following functions, avoiding the hot paths:

*   `get_user_page_vma`
*   `get_user_page_lookup_vma`
*   `access_remote_vm`
*   `access_process_vm`
*   `copy_remote_vm_str`

Those should provide a more targeted view.

Now, let's move on to the configurations. I need to note any relevant ones. I'm seeing:

*   `PER_VMA_LOCK` (dependent on `ARCH_SUPPORTS_PER_VMA_LOCK`, `MMU`, and `SMP`)
*   `TRANSPARENT_HUGEPAGE`
*   `RISCV_ISA_SUPM`
*   `ADDRESS_MASKING`

I have to make sure `PER_VMA_LOCK` is enabled properly... Ah, it's a `def_bool y` dependent on the other configs. That's fine, I can list it. `TRANSPARENT_HUGEPAGE` is a menuconfig, which means it will need to be explicitly set. `RISCV_ISA_SUPM` and `ADDRESS_MASKING` are just bools.

I will also quickly check `BPF_SYSCALL` with relation to `copy_remote_vm_str` because I recall a potential link there.



18/2 2026/07/26 10:08 tool grepper
0m Args:
{
  "Expression": "BPF_SYSCALL",
  "PathPrefix": "mm/memory.c"
}

Results:
{
  "Output": "mm/memory.c=7295=EXPORT_SYMBOL_GPL(access_process_vm);\nmm/memory.c-7296-\nmm/memory.c:7297:#ifdef CONFIG_BPF_SYSCALL\nmm/memory.c-7298-/*\n--\nmm/memory.c=7397=EXPORT_SYMBOL_GPL(copy_remote_vm_str);\nmm/memory.c:7398:#endif /* CONFIG_BPF_SYSCALL */\nmm/memory.c-7399-\n"
}

19/2 2026/07/26 10:08 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 32662
							output: 34
							thoughts: 49
						

Thoughts:
Okay, here's my take on those thoughts, tailored for an expert audience:

**Kernel Internals: Remote VM String Copy and I/O Remapping**

Right, so, as I see it, the inclusion of `BPF_SYSCALL` is definitely necessary for the `copy_remote_vm_str` function. It’s pretty clear from the context. We’re dealing with a system call involved in copying strings from a remote virtual machine, so the BPF hooks and capabilities are fundamentally required for proper operation and security in that scenario. Makes perfect sense.

Now, on the other hand, for handling those `access_remote_vma_ops` operations, I need to double-check that `HAVE_IOREMAP_PROT` is defined. This is a critical check to ensure we have the necessary hardware support for protected I/O remapping. We have to guarantee that the hardware capabilities exist before we even think about touching the remote VM's memory and trying to read or write it. I'll need to go over the relevant config options and ensure that the right checks are in place. This is where the intricacies of the architecture and hardware become critical, and I want to be 100% certain we're handling that remapping safely and reliably. Gotta cover all the bases!



20/2 2026/07/26 10:08 tool grepper
0m Args:
{
  "Expression": "HAVE_IOREMAP_PROT",
  "PathPrefix": "mm/memory.c"
}

Results:
{
  "Output": "mm/memory.c=6951=EXPORT_SYMBOL_GPL(follow_pfnmap_end);\nmm/memory.c-6952-\nmm/memory.c:6953:#ifdef CONFIG_HAVE_IOREMAP_PROT\nmm/memory.c-6954-/**\n--\nmm/memory.c=7026=static int access_remote_vma_ops(struct vm_area_struct *vma, unsigned long addr,\n--\nmm/memory.c-7028-{\nmm/memory.c:7029:#ifdef CONFIG_HAVE_IOREMAP_PROT\nmm/memory.c-7030-\tif (vma-\u003evm_ops \u0026\u0026 vma-\u003evm_ops-\u003eaccess)\n"
}

21/2 2026/07/26 10:08 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 33219
							output: 37
							thoughts: 37
						

Thoughts:
Okay, here's my take on those thoughts, framed as my own mental process, given my expertise:

**Checking Kernel Configuration Options**

Right, `HAVE_IOREMAP_PROT` is definitely worth adding to my mental checklist when reviewing this kernel configuration. It's a significant indicator of how the kernel handles memory-mapped I/O, and the presence or absence of that feature can have a profound impact on device driver development and performance. Knowing whether we have protection enabled during remapping is crucial for security and stability.

Now, let's pivot to the `MMU_GATHER_RCU_TABLE_FREE` option. I need to refresh my memory on the current status of that one. I'm recalling the implications of RCU with respect to freeing MMU tables. We need to evaluate whether enabling that option makes sense for the current use case, or if it might present problems with the architecture. Time to delve into the details and consider the memory management tradeoffs this entails.



22/2 2026/07/26 10:08 tool grepper
0m Args:
{
  "Expression": "MMU_GATHER_RCU_TABLE_FREE",
  "PathPrefix": "mm/memory.c"
}

Results:
{
  "Output": "mm/memory.c=7050=static bool remote_access_lock(struct mm_struct *mm, unsigned long addr,\n--\nmm/memory.c-7055-\nmm/memory.c:7056:#if defined(CONFIG_PER_VMA_LOCK) \u0026\u0026 defined(CONFIG_MMU_GATHER_RCU_TABLE_FREE)\nmm/memory.c-7057-\tvma = lock_vma_under_rcu(mm, addr);\n"
}

23/2 2026/07/26 10:08 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 33631
							output: 33
							thoughts: 133
						

Thoughts:
**My Configuration Audit**

Okay, let's refine this list of interesting kernel configurations. We've got a solid starting point: `TRANSPARENT_HUGEPAGE`, `BPF_SYSCALL`, `HAVE_IOREMAP_PROT`, `RISCV_ISA_SUPM`, and `ADDRESS_MASKING`. Good, good... but I think we should definitely add `MMU_GATHER_RCU_TABLE_FREE` to that list; it's got real potential for analysis.

Now, let's dig into `HAVE_IOREMAP_PROT` and `MMU_GATHER_RCU_TABLE_FREE` specifically. Need to determine if they're even *selectable* in the current configuration. Gotta check the dependencies and constraints. We'll be able to tell much more once we know whether these are in scope or not.



24/2 2026/07/26 10:08 tool grepper
0m Args:
{
  "Expression": "HAVE_IOREMAP_PROT",
  "PathPrefix": "arch/Kconfig"
}

Results:
{
  "Output": "arch/Kconfig=258=config USER_RETURN_NOTIFIER\n--\narch/Kconfig-264-\narch/Kconfig:265:config HAVE_IOREMAP_PROT\narch/Kconfig-266-\tbool\n"
}

25/2 2026/07/26 10:08 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 34060
							output: 36
							thoughts: 53
						

Thoughts:
**Considering Kernel Configuration Options**

Okay, so I'm looking at these kernel configuration options, and the first thing that jumps out is `HAVE_IOREMAP_PROT`. This one's a bit of a special case. It's a hidden bool, right? Architecture-specific. The whole point is, it's not something we expose for users to tweak directly. We shouldn't even list it; it's implicit. No point in cluttering the available choices with something that can't be explicitly enabled. That would just be confusing.

Next up, what about `MMU_GATHER_RCU_TABLE_FREE`? I need to think this one through carefully.



26/2 2026/07/26 10:08 tool grepper
0m Args:
{
  "Expression": "MMU_GATHER_RCU_TABLE_FREE",
  "PathPrefix": "arch/Kconfig"
}

Results:
{
  "Output": "arch/Kconfig=512=config MMU_GATHER_TABLE_FREE\n--\narch/Kconfig-514-\narch/Kconfig:515:config MMU_GATHER_RCU_TABLE_FREE\narch/Kconfig-516-\tbool\n"
}

27/2 2026/07/26 10:08 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 34377
							output: 30
							thoughts: 24
						

Thoughts:
Okay, here's my attempt at summarizing those thoughts from a first-person, expert perspective, keeping it concise and focusing on the underlying reasoning:

**Hidden Boolean and VMA Lock Considerations**

Alright, so I'm thinking about this data structure and I see this "hidden bool." It's intentionally hidden, and we shouldn't reveal it or include it in our listings. The design choice is clear – it's an internal detail, maybe an optimization, and exposing it would just clutter things for the user and potentially lead to misuse. We should maintain the abstraction level.

Then my mind jumps to the `PER_VMA_LOCK`.  I'm wondering if this is the correct choice or if this is the correct place for it. Thinking about the potential contention, the scope of protection, and the specific operations it's guarding. Is it really the best way to handle the concurrency challenges? What are the alternatives? Are there other locks that provide similar functionality, but may be more performant in the circumstances? Or should we explore a lock-free solution? We need to weigh this.



28/2 2026/07/26 10:08 tool grepper
0m Args:
{
  "Expression": "PER_VMA_LOCK",
  "PathPrefix": "mm/Kconfig"
}

Results:
{
  "Output": "mm/Kconfig=1433=config LRU_GEN_WALKS_MMU\n--\nmm/Kconfig-1437-\nmm/Kconfig:1438:config ARCH_SUPPORTS_PER_VMA_LOCK\nmm/Kconfig-1439-       def_bool n\nmm/Kconfig-1440-\nmm/Kconfig:1441:config PER_VMA_LOCK\nmm/Kconfig-1442-\tdef_bool y\nmm/Kconfig:1443:\tdepends on ARCH_SUPPORTS_PER_VMA_LOCK \u0026\u0026 MMU \u0026\u0026 SMP\nmm/Kconfig-1444-\thelp\n"
}

29/2 2026/07/26 10:08 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 34834
							output: 38
							thoughts: 309
						

Thoughts:
**My Assessment of the Kernel Patch's Implications**

Okay, so looking at this patch, I see that `PER_VMA_LOCK` is defined as a `def_bool y`, which means it's automatically enabled if its dependencies are met. I don't need to explicitly list it for configuration purposes.

Therefore, the key configurations to watch out for are: `TRANSPARENT_HUGEPAGE`, `BPF_SYSCALL`, `RISCV_ISA_SUPM`, and `ADDRESS_MASKING`. Those are the relevant knobs to consider when testing.

Now, let's review the critical symbols this patch touches. The ones that immediately stand out are `get_user_page_vma`, `get_user_page_lookup_vma`, `access_remote_vm`, `access_process_vm`, and `copy_remote_vm_str`. Yup, those all look like the right set of primitives to focus on.

My reasoning? This patch introduces a brand new remote VM walk mechanism via `remote_vm_walk` and is replacing `get_user_page_vma_remote` with `get_user_page_lookup_vma` and `get_user_page_vma`. It also brings batching support for PTE-mapped large folios (mTHP) in the slower GUP path. This indicates some *significant* core memory management changes. These changes are going to impact page fault handling, remote memory access operations (things like ptrace, accessing `/proc/pid/mem`), and even BPF string copying. This means fuzzing efforts should absolutely concentrate on the new entry points and the remote access APIs.

Just to be absolutely sure, let me quickly double-check if `access_remote_vm` is exported.



30/2 2026/07/26 10:08 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "EXPORT_SYMBOL_GPL\\(access_remote_vm\\)",
  "PathPrefix": "mm/memory.c"
}

Results:
{
  "error": "no matches"
}

31/2 2026/07/26 10:08 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 35576
							output: 30
							thoughts: 21
						

Thoughts:
Here's my thought process, as if I were considering this myself:

**Diving into the `mm/memory.c` Rabbit Hole**

Okay, before I go any further, I need to double-check the implementation of `access_remote_vm`. It's crucial, and the context of memory access is critical. Let's see how that function is defined within the `mm/memory.c` module. I need to understand how the system handles remote virtual machine memory access specifically – any potential vulnerabilities or race conditions there could be nasty. I'll need to look very closely at the code and see exactly what's happening.



32/2 2026/07/26 10:08 tool grepper
0m Args:
{
  "Expression": "access_remote_vm",
  "PathPrefix": "mm/memory.c"
}

Results:
{
  "Output": "mm/memory.c=7014=EXPORT_SYMBOL_GPL(generic_access_phys);\n--\nmm/memory.c-7025- */\nmm/memory.c:7026:static int access_remote_vma_ops(struct vm_area_struct *vma, unsigned long addr,\nmm/memory.c-7027-\t\t\t\t void *buf, int len, int write)\n--\nmm/memory.c=7211=static int access_vm_page(struct vm_area_struct *vma, struct page *page,\n--\nmm/memory.c-7218-\tif (!page) {\nmm/memory.c:7219:\t\tbytes = access_remote_vma_ops(vma, addr, *buf, len, write);\nmm/memory.c-7220-\t\tif (bytes \u003e 0)\n--\nmm/memory.c-7246- */\nmm/memory.c:7247:static int __access_remote_vm(struct mm_struct *mm, unsigned long addr,\nmm/memory.c-7248-\t\t\t      void *buf, int len, unsigned int gup_flags)\n--\nmm/memory.c-7256-/**\nmm/memory.c:7257: * access_remote_vm - access another process' address space\nmm/memory.c-7258- * @mm:\t\tthe mm_struct of the target address space\n--\nmm/memory.c-7267- */\nmm/memory.c:7268:int access_remote_vm(struct mm_struct *mm, unsigned long addr,\nmm/memory.c-7269-\t\tvoid *buf, int len, unsigned int gup_flags)\nmm/memory.c-7270-{\nmm/memory.c:7271:\treturn __access_remote_vm(mm, addr, buf, len, gup_flags);\nmm/memory.c-7272-}\n--\nmm/memory.c=7279=int access_process_vm(struct task_struct *tsk, unsigned long addr,\n--\nmm/memory.c-7288-\nmm/memory.c:7289:\tret = __access_remote_vm(mm, addr, buf, len, gup_flags);\nmm/memory.c-7290-\n"
}

33/2 2026/07/26 10:08 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 36352
							output: 249
							thoughts: 172
						

Thoughts:
**My Assessment of the Remote VM Access Implementation**

Okay, so I've been looking into this `access_remote_vm` situation. I see now; it's *not* exported, but it's also not static. That makes it a perfect symbol to zero in on, especially since `access_process_vm` *is* exported. Focusing on `access_remote_vm` seems like a solid approach.

I also considered `get_user_pages_remote` and `pin_user_pages_remote`. They're definitely relevant, but I have a hunch they're likely to be performance-critical paths, and I'd prefer to start with something a bit more fundamental. The patch touches `mm/gup.c` and `mm/memory.c`, which confirms my initial intuition. So, I'm sticking with my original selection of focus symbols for now – `access_remote_vm` seems like the most strategic starting point.

Before I commit to this approach, let me just quickly double-check the configuration options that are in play. I need to make sure I'm not overlooking anything critical. Let's see... `TRANSPARENT_HUGEPAGE`, `BPF_SYSCALL`, `RISCV_ISA_SUPM`, and `ADDRESS_MASKING`. Alright, I've got them. Looks like I've considered the relevant factors.

Now, I think I'm ready to move forward. I'll go ahead and call `set-results`.



34/2 2026/07/26 10:08 tool set-results
0m Error:
the following configs do not exist in the kernel tree: RISCV_ISA_SUPM

Args:
{
  "EnableConfigs": [
    "TRANSPARENT_HUGEPAGE",
    "BPF_SYSCALL",
    "RISCV_ISA_SUPM",
    "ADDRESS_MASKING"
  ],
  "FocusSymbols": [
    "get_user_page_vma",
    "get_user_page_lookup_vma",
    "access_remote_vm",
    "access_process_vm",
    "copy_remote_vm_str"
  ],
  "Reasoning": "The patch introduces a new remote VM walk mechanism (`remote_vm_walk`) and replaces `get_user_page_vma_remote` with `get_user_page_lookup_vma` and `get_user_page_vma`. It also adds support for batching contiguous pages for PTE-mapped large folios (mTHP) in the slow GUP path. These are core memory management changes that affect page faulting, remote memory access (e.g., ptrace, /proc/pid/mem), and BPF string copying. Fuzzing should focus on the new entry points and remote access APIs.",
  "WorthFuzzing": true
}

Results:
{
  "error": "the following configs do not exist in the kernel tree: RISCV_ISA_SUPM"
}

35/2 2026/07/26 10:08 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 37128
							output: 34
							thoughts: 28
						

Thoughts:
Here's my attempt at summarizing the thought process, tailored for an expert audience:

**Debugging a RISC-V Configuration Issue**

Okay, hold on a minute. `RISCV_ISA_SUPM` isn't showing up where I expect it to. That's a red flag. Before I start going down a rabbit hole of include paths or build system oddities, I need to verify that this is even a *defined* symbol. My immediate instinct is to check the core configuration file for the RISC-V architecture, specifically `arch/riscv/Kconfig`. That's the definitive source of truth for architectural features and options. Gotta see if it's declared there, correctly, or if there's a typo, condition, or dependency I'm missing. Let's get that Kconfig file open and do a quick search. Hopefully, this is a simple configuration misstep. If not, I'll need to dig deeper into the build system and potentially the headers themselves to figure out what's going on.



36/2 2026/07/26 10:08 tool grepper
0m Args:
{
  "Expression": "RISCV_ISA_SUPM",
  "PathPrefix": "arch/riscv/Kconfig"
}

Results:
{
  "Output": "arch/riscv/Kconfig=553=config RISCV_ISA_C\n--\narch/riscv/Kconfig-563-\narch/riscv/Kconfig:564:config RISCV_ISA_SUPM\narch/riscv/Kconfig-565-\tbool \"Supm extension for userspace pointer masking\"\n"
}

37/2 2026/07/26 10:08 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 37501
							output: 239
							thoughts: 102
						

Thoughts:
**Troubleshooting a Configuration Error**

Okay, so the tool is telling me this particular configuration option doesn't exist. That's a classic. My initial hunch is that it's either not part of the standard configuration tree, or the tool is using a more restrictive lookup than I expect. Let's start with the assumption that this is an architecture-specific issue. I'll remove `RISCV_ISA_SUPM` and `ADDRESS_MASKING` from my active configuration to try and isolate the problem. Those seem like potential culprits, being architecture-dependent and might not be present in the tool's default check. Wait a second, didn't I just see `ADDRESS_MASKING` in the x86 Kconfig files? I'll quickly double-check that, just to be sure. It's important to verify those details, no point assuming!



38/2 2026/07/26 10:08 tool set-results
0m Args:
{
  "EnableConfigs": [
    "TRANSPARENT_HUGEPAGE",
    "BPF_SYSCALL",
    "ADDRESS_MASKING"
  ],
  "FocusSymbols": [
    "get_user_page_vma",
    "get_user_page_lookup_vma",
    "access_remote_vm",
    "access_process_vm",
    "copy_remote_vm_str"
  ],
  "Reasoning": "The patch introduces a new remote VM walk mechanism (`remote_vm_walk`) and replaces `get_user_page_vma_remote` with `get_user_page_lookup_vma` and `get_user_page_vma`. It also adds support for batching contiguous pages for PTE-mapped large folios (mTHP) in the slow GUP path. These are core memory management changes that affect page faulting, remote memory access (e.g., ptrace, /proc/pid/mem), and BPF string copying. Fuzzing should focus on the new entry points and remote access APIs.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "TRANSPARENT_HUGEPAGE",
    "BPF_SYSCALL",
    "ADDRESS_MASKING"
  ],
  "FocusSymbols": [
    "get_user_page_vma",
    "get_user_page_lookup_vma",
    "access_remote_vm",
    "access_process_vm",
    "copy_remote_vm_str"
  ],
  "Reasoning": "The patch introduces a new remote VM walk mechanism (`remote_vm_walk`) and replaces `get_user_page_vma_remote` with `get_user_page_lookup_vma` and `get_user_page_vma`. It also adds support for batching contiguous pages for PTE-mapped large folios (mTHP) in the slow GUP path. These are core memory management changes that affect page faulting, remote memory access (e.g., ptrace, /proc/pid/mem), and BPF string copying. Fuzzing should focus on the new entry points and remote access APIs.",
  "WorthFuzzing": true
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)