AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/08/17 12:37 flow patch-triage
3m Args:
null

Results:
{
  "EnableConfigs": [
    "KASAN",
    "KASAN_GENERIC",
    "HOTPLUG_CPU"
  ],
  "FocusSymbols": [
    "arch_enter_lazy_mmu_mode",
    "arch_enter_lazy_mmu_mode_with_ptes",
    "arch_leave_lazy_mmu_mode",
    "arch_flush_lazy_mmu_mode",
    "lazy_mmu_online_cpu",
    "lazy_mmu_offline_cpu",
    "__kasan_poison_pte",
    "__kasan_unpoison_pte",
    "___ptep_modify_prot_start",
    "___ptep_modify_prot_commit"
  ],
  "KMSANReasoning": "The patch introduces a lazy MMU mode for the s390 architecture to buffer page table entry (PTE) updates. It also explicitly adds KASAN poisoning/unpoisoning to detect invalid direct accesses to PTEs while the lazy MMU mode is active, introducing a new KASAN bug type (`lazy-mmu-pte-access`). The changes are strictly confined to internal memory management and page table operations. No new structures are copied to user space, and all newly allocated structures (e.g., `struct ipte_range`) are properly initialized (e.g., via zero-allocation and explicit initialization of the `cache` array to `PTE_POISON`). There is no risk of uninitialized memory usage or kernel-to-user info-leaks introduced by this patch. Standard KASAN is perfectly suited and explicitly enhanced by this patch to catch any bugs related to these changes.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch introduces lazy MMU mode for s390 architecture, which batches PTE invalidations using the IPTE-range facility. It hooks into core memory management syscalls (madvise, mprotect, mremap, vmalloc) and CPU hotplug paths. It also adds KASAN instrumentation for direct PTE accesses in lazy MMU mode. These are reachable core kernel and architecture-specific memory management paths that should be fuzzed.",
  "WorthFuzzing": true
}

1/1 2026/08/17 12:37 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 36c12673f35ddf2a14791164b5dcf9d4c3cf54c2\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Mon Aug 17 12:37:42 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/arch/s390/Kconfig b/arch/s390/Kconfig\nindex 84404e6778d50..7846332dcd0a6 100644\n--- a/arch/s390/Kconfig\n+++ b/arch/s390/Kconfig\n@@ -97,6 +97,7 @@ config S390\n \tselect ARCH_HAS_GCOV_PROFILE_ALL\n \tselect ARCH_HAS_GIGANTIC_PAGE\n \tselect ARCH_HAS_KCOV\n+\tselect ARCH_HAS_LAZY_MMU_MODE\n \tselect ARCH_HAS_MEMBARRIER_SYNC_CORE\n \tselect ARCH_HAS_MEM_ENCRYPT\n \tselect ARCH_HAS_NMI_SAFE_THIS_CPU_OPS\ndiff --git a/arch/s390/include/asm/lazy_mmu.h b/arch/s390/include/asm/lazy_mmu.h\nnew file mode 100644\nindex 0000000000000..98366e9de9bc3\n--- /dev/null\n+++ b/arch/s390/include/asm/lazy_mmu.h\n@@ -0,0 +1,9 @@\n+/* SPDX-License-Identifier: GPL-2.0 */\n+#ifndef __LAZY_MMU_H\n+#define __LAZY_MMU_H\n+\n+void lazy_mmu_online_boot_cpu(void);\n+int lazy_mmu_online_cpu(gfp_t gfp, unsigned int cpu);\n+void lazy_mmu_offline_cpu(unsigned int cpu);\n+\n+#endif /* __LAZY_MMU_H */\ndiff --git a/arch/s390/include/asm/lowcore.h b/arch/s390/include/asm/lowcore.h\nindex 3b3ecc647993f..dba236664da90 100644\n--- a/arch/s390/include/asm/lowcore.h\n+++ b/arch/s390/include/asm/lowcore.h\n@@ -163,7 +163,7 @@ struct lowcore {\n \t__s32\tpreempt_count;\t\t\t/* 0x03a8 */\n \t__u32\tspinlock_lockval;\t\t/* 0x03ac */\n \t__u32\tspinlock_index;\t\t\t/* 0x03b0 */\n-\t__u8\tpad_0x03b4[0x03b8-0x03b4];\t/* 0x03b4 */\n+\t__s32\tlazy_mmu_count;\t\t\t/* 0x03b4 */\n \t__u64\tpercpu_offset;\t\t\t/* 0x03b8 */\n \t__u8\tpercpu_register;\t\t/* 0x03c0 */\n \t__u8\tpad_0x03c1[0x0400-0x03c1];\t/* 0x03c1 */\ndiff --git a/arch/s390/include/asm/pgtable.h b/arch/s390/include/asm/pgtable.h\nindex 859ce7c7d4544..1e8a79989ed6e 100644\n--- a/arch/s390/include/asm/pgtable.h\n+++ b/arch/s390/include/asm/pgtable.h\n@@ -39,6 +39,64 @@ enum {\n \n extern atomic_long_t direct_pages_count[PG_DIRECT_MAP_MAX];\n \n+bool __lazy_mmu_ptep_test_and_clear_young(unsigned long addr, pte_t *ptep, int *res);\n+bool __lazy_mmu_ptep_get_and_clear(unsigned long addr, pte_t *ptep, pte_t *res);\n+bool __lazy_mmu_ptep_modify_prot_start(unsigned long addr, pte_t *ptep, pte_t *res);\n+bool __lazy_mmu_ptep_modify_prot_commit(unsigned long addr, pte_t *ptep, pte_t old_pte, pte_t pte);\n+bool __lazy_mmu_ptep_set_wrprotect(unsigned long addr, pte_t *ptep);\n+bool __lazy_mmu_set_pte(pte_t *ptep, pte_t pte);\n+bool __lazy_mmu_ptep_get(pte_t *ptep, pte_t *res);\n+\n+static __always_inline bool is_lazy_mmu_active(void)\n+{\n+\tif (__is_defined(__DECOMPRESSOR))\n+\t\treturn false;\n+\tif (!get_lowcore()-\u003elazy_mmu_count)\n+\t\treturn false;\n+\treturn true;\n+}\n+\n+static inline\n+bool lazy_mmu_ptep_test_and_clear_young(unsigned long addr, pte_t *ptep, int *res)\n+{\n+\tif (!is_lazy_mmu_active())\n+\t\treturn false;\n+\treturn __lazy_mmu_ptep_test_and_clear_young(addr, ptep, res);\n+}\n+\n+static inline\n+bool lazy_mmu_ptep_get_and_clear(unsigned long addr, pte_t *ptep, pte_t *res)\n+{\n+\tif (!is_lazy_mmu_active())\n+\t\treturn false;\n+\treturn __lazy_mmu_ptep_get_and_clear(addr, ptep, res);\n+}\n+\n+static inline\n+bool lazy_mmu_ptep_modify_prot_start(unsigned long addr, pte_t *ptep, pte_t *res)\n+{\n+\tif (!is_lazy_mmu_active())\n+\t\treturn false;\n+\treturn __lazy_mmu_ptep_modify_prot_start(addr, ptep, res);\n+}\n+\n+static inline\n+bool lazy_mmu_ptep_modify_prot_commit(unsigned long addr, pte_t *ptep,\n+\t\t\t\t      pte_t old_pte, pte_t pte)\n+{\n+\tif (!is_lazy_mmu_active())\n+\t\treturn false;\n+\treturn __lazy_mmu_ptep_modify_prot_commit(addr, ptep, old_pte, pte);\n+}\n+\n+static inline\n+bool lazy_mmu_ptep_set_wrprotect(unsigned long addr, pte_t *ptep)\n+{\n+\tif (!is_lazy_mmu_active())\n+\t\treturn false;\n+\treturn __lazy_mmu_ptep_set_wrprotect(addr, ptep);\n+}\n+\n static inline void update_page_count(int level, long count)\n {\n \tif (IS_ENABLED(CONFIG_PROC_FS))\n@@ -978,17 +1036,38 @@ static inline void set_pmd(pmd_t *pmdp, pmd_t pmd)\n \tWRITE_ONCE(*pmdp, pmd);\n }\n \n-static inline void set_pte(pte_t *ptep, pte_t pte)\n+static inline void __set_pte(pte_t *ptep, pte_t pte)\n {\n \tif (pte_present(pte))\n \t\tpte = clear_pte_bit(pte, __pgprot(_PAGE_UNUSED));\n \tWRITE_ONCE(*ptep, pte);\n }\n \n+static inline void set_pte(pte_t *ptep, pte_t pte)\n+{\n+\tif (!is_lazy_mmu_active() || !__lazy_mmu_set_pte(ptep, pte))\n+\t\t__set_pte(ptep, pte);\n+}\n+\n+#define ptep_get_lockless ptep_get_lockless\n+static inline __no_sanitize_address pte_t ptep_get_lockless(pte_t *ptep)\n+{\n+\treturn READ_ONCE(*ptep);\n+}\n+\n+static inline pte_t __ptep_get(pte_t *ptep)\n+{\n+\treturn READ_ONCE(*ptep);\n+}\n+\n #define ptep_get ptep_get\n static inline pte_t ptep_get(pte_t *ptep)\n {\n-\treturn READ_ONCE(*ptep);\n+\tpte_t res;\n+\n+\tif (!is_lazy_mmu_active() || !__lazy_mmu_ptep_get(ptep, \u0026res))\n+\t\tres = __ptep_get(ptep);\n+\treturn res;\n }\n \n #define pmdp_get pmdp_get\n@@ -1181,6 +1260,15 @@ static __always_inline void __ptep_ipte_range(unsigned long address, int nr,\n \t} while (nr != 255);\n }\n \n+void arch_enter_lazy_mmu_mode_with_ptes(struct mm_struct *mm,\n+\t\t\t\t\tunsigned long addr, unsigned long end,\n+\t\t\t\t\tpte_t *pte);\n+#define arch_enter_lazy_mmu_mode_with_ptes arch_enter_lazy_mmu_mode_with_ptes\n+\n+void arch_enter_lazy_mmu_mode(void);\n+void arch_leave_lazy_mmu_mode(void);\n+void arch_flush_lazy_mmu_mode(void);\n+\n /*\n  * This is hard to understand. ptep_get_and_clear and ptep_clear_flush\n  * both clear the TLB for the unmapped pte. The reason is that\n@@ -1201,10 +1289,16 @@ pte_t ptep_xchg_lazy(struct mm_struct *, unsigned long, pte_t *, pte_t);\n static inline bool ptep_test_and_clear_young(struct vm_area_struct *vma,\n \t\tunsigned long addr, pte_t *ptep)\n {\n-\tpte_t pte = ptep_get(ptep);\n+\tpte_t pte;\n+\tint res;\n \n-\tpte = ptep_xchg_direct(vma-\u003evm_mm, addr, ptep, pte_mkold(pte));\n-\treturn pte_young(pte);\n+\tif (!lazy_mmu_ptep_test_and_clear_young(addr, ptep, \u0026res)) {\n+\t\tpte = __ptep_get(ptep);\n+\t\tpte = pte_mkold(pte);\n+\t\tpte = ptep_xchg_direct(vma-\u003evm_mm, addr, ptep, pte);\n+\t\tres = pte_young(pte);\n+\t}\n+\treturn res;\n }\n \n #define __HAVE_ARCH_PTEP_CLEAR_YOUNG_FLUSH\n@@ -1220,7 +1314,8 @@ static inline pte_t ptep_get_and_clear(struct mm_struct *mm,\n {\n \tpte_t res;\n \n-\tres = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));\n+\tif (!lazy_mmu_ptep_get_and_clear(addr, ptep, \u0026res))\n+\t\tres = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));\n \tpage_table_check_pte_clear(mm, addr, res);\n \t/* At this point the reference through the mapping is still present */\n \tif (mm_is_protected(mm) \u0026\u0026 pte_present(res))\n@@ -1229,9 +1324,28 @@ static inline pte_t ptep_get_and_clear(struct mm_struct *mm,\n }\n \n #define __HAVE_ARCH_PTEP_MODIFY_PROT_TRANSACTION\n-pte_t ptep_modify_prot_start(struct vm_area_struct *, unsigned long, pte_t *);\n-void ptep_modify_prot_commit(struct vm_area_struct *, unsigned long,\n-\t\t\t     pte_t *, pte_t, pte_t);\n+pte_t ___ptep_modify_prot_start(struct vm_area_struct *, unsigned long, pte_t *);\n+void ___ptep_modify_prot_commit(struct vm_area_struct *, unsigned long,\n+\t\t\t\tpte_t *, pte_t, pte_t);\n+\n+static inline\n+pte_t ptep_modify_prot_start(struct vm_area_struct *vma,\n+\t\t\t     unsigned long addr, pte_t *ptep)\n+{\n+\tpte_t res;\n+\n+\tif (!lazy_mmu_ptep_modify_prot_start(addr, ptep, \u0026res))\n+\t\tres = ___ptep_modify_prot_start(vma, addr, ptep);\n+\treturn res;\n+}\n+\n+static inline\n+void ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,\n+\t\t\t     pte_t *ptep, pte_t old_pte, pte_t pte)\n+{\n+\tif (!lazy_mmu_ptep_modify_prot_commit(addr, ptep, old_pte, pte))\n+\t\t___ptep_modify_prot_commit(vma, addr, ptep, old_pte, pte);\n+}\n \n #define __HAVE_ARCH_PTEP_CLEAR_FLUSH\n static inline pte_t ptep_clear_flush(struct vm_area_struct *vma,\n@@ -1261,11 +1375,13 @@ static inline pte_t ptep_get_and_clear_full(struct mm_struct *mm,\n {\n \tpte_t res;\n \n-\tif (full) {\n-\t\tres = ptep_get(ptep);\n-\t\tset_pte(ptep, __pte(_PAGE_INVALID));\n-\t} else {\n-\t\tres = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));\n+\tif (!lazy_mmu_ptep_get_and_clear(addr, ptep, \u0026res)) {\n+\t\tif (full) {\n+\t\t\tres = __ptep_get(ptep);\n+\t\t\t__set_pte(ptep, __pte(_PAGE_INVALID));\n+\t\t} else {\n+\t\t\tres = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));\n+\t\t}\n \t}\n \tpage_table_check_pte_clear(mm, addr, res);\n \t/* At this point the reference through the mapping is still present */\n@@ -1291,10 +1407,15 @@ static inline pte_t ptep_get_and_clear_full(struct mm_struct *mm,\n static inline void ptep_set_wrprotect(struct mm_struct *mm,\n \t\t\t\t      unsigned long addr, pte_t *ptep)\n {\n-\tpte_t pte = ptep_get(ptep);\n+\tpte_t pte;\n \n-\tif (pte_write(pte))\n-\t\tptep_xchg_lazy(mm, addr, ptep, pte_wrprotect(pte));\n+\tif (!lazy_mmu_ptep_set_wrprotect(addr, ptep)) {\n+\t\tpte = __ptep_get(ptep);\n+\t\tif (pte_write(pte)) {\n+\t\t\tpte = pte_wrprotect(pte);\n+\t\t\tptep_xchg_lazy(mm, addr, ptep, pte);\n+\t\t}\n+\t}\n }\n \n /*\n@@ -1327,7 +1448,7 @@ static inline void flush_tlb_fix_spurious_fault(struct vm_area_struct *vma,\n \t * PTE does not have _PAGE_PROTECT set, to avoid unnecessary overhead.\n \t * A local RDP can be used to do the flush.\n \t */\n-\tif (cpu_has_rdp() \u0026\u0026 !(pte_val(ptep_get(ptep)) \u0026 _PAGE_PROTECT))\n+\tif (cpu_has_rdp() \u0026\u0026 !(pte_val(__ptep_get(ptep)) \u0026 _PAGE_PROTECT))\n \t\t__ptep_rdp(address, ptep, 1);\n }\n #define flush_tlb_fix_spurious_fault flush_tlb_fix_spurious_fault\ndiff --git a/arch/s390/kernel/setup.c b/arch/s390/kernel/setup.c\nindex b60284328fe3d..f5a3c9e1b6b88 100644\n--- a/arch/s390/kernel/setup.c\n+++ b/arch/s390/kernel/setup.c\n@@ -77,6 +77,7 @@\n #include \u003casm/maccess.h\u003e\n #include \u003casm/uv.h\u003e\n #include \u003casm/asm-offsets.h\u003e\n+#include \u003casm/lazy_mmu.h\u003e\n #include \"entry.h\"\n \n /*\n@@ -1012,5 +1013,6 @@ void __init setup_arch(char **cmdline_p)\n \n void __init arch_cpu_finalize_init(void)\n {\n+\tlazy_mmu_online_boot_cpu();\n \tsclp_init();\n }\ndiff --git a/arch/s390/kernel/smp.c b/arch/s390/kernel/smp.c\nindex 0ba7f89b81611..0a826bbaf1ddc 100644\n--- a/arch/s390/kernel/smp.c\n+++ b/arch/s390/kernel/smp.c\n@@ -59,6 +59,7 @@\n #include \u003casm/topology.h\u003e\n #include \u003casm/vdso.h\u003e\n #include \u003casm/maccess.h\u003e\n+#include \u003casm/lazy_mmu.h\u003e\n #include \"entry.h\"\n \n enum {\n@@ -866,6 +867,11 @@ int __cpu_up(unsigned int cpu, struct task_struct *tidle)\n \trc = pcpu_alloc_lowcore(pcpu, cpu);\n \tif (rc)\n \t\treturn rc;\n+\trc = lazy_mmu_online_cpu(GFP_KERNEL, cpu);\n+\tif (rc) {\n+\t\tpcpu_free_lowcore(pcpu, cpu);\n+\t\treturn rc;\n+\t}\n \t/*\n \t * Make sure global control register contents do not change\n \t * until new CPU has initialized control registers.\n@@ -921,6 +927,7 @@ void __cpu_die(unsigned int cpu)\n \tpcpu = per_cpu_ptr(\u0026pcpu_devices, cpu);\n \twhile (!pcpu_stopped(pcpu))\n \t\tcpu_relax();\n+\tlazy_mmu_offline_cpu(cpu);\n \tpcpu_free_lowcore(pcpu, cpu);\n \tcpumask_clear_cpu(cpu, mm_cpumask(\u0026init_mm));\n \tcpumask_clear_cpu(cpu, \u0026init_mm.context.cpu_attach_mask);\ndiff --git a/arch/s390/mm/Makefile b/arch/s390/mm/Makefile\nindex 193899c39ca7f..96cac3e99f516 100644\n--- a/arch/s390/mm/Makefile\n+++ b/arch/s390/mm/Makefile\n@@ -4,6 +4,7 @@\n #\n \n obj-y\t\t:= init.o fault.o extmem.o mmap.o vmem.o maccess.o\n+obj-y\t\t:= init.o fault.o extmem.o mmap.o vmem.o maccess.o lazy_mmu.o\n obj-y\t\t+= page-states.o pageattr.o pgtable.o pgalloc.o extable.o\n \n obj-$(CONFIG_CMM)\t\t+= cmm.o\ndiff --git a/arch/s390/mm/lazy_mmu.c b/arch/s390/mm/lazy_mmu.c\nnew file mode 100644\nindex 0000000000000..7e3a4115ea048\n--- /dev/null\n+++ b/arch/s390/mm/lazy_mmu.c\n@@ -0,0 +1,418 @@\n+// SPDX-License-Identifier: GPL-2.0\n+#include \u003clinux/pgtable.h\u003e\n+#include \u003clinux/kasan.h\u003e\n+#include \u003clinux/slab.h\u003e\n+#include \u003casm/facility.h\u003e\n+#include \u003casm/lazy_mmu.h\u003e\n+#include \u003ckunit/visibility.h\u003e\n+\n+#define PTE_POISON\t_PAGE_LARGE\n+\n+struct ipte_range {\n+\tstruct mm_struct *mm;\n+\tunsigned long base_addr;\n+\tunsigned long base_end;\n+\tpte_t *base_pte;\n+\tpte_t *start_pte;\n+\tpte_t *end_pte;\n+\tpte_t cache[PTRS_PER_PTE];\n+};\n+\n+static DEFINE_PER_CPU(struct ipte_range *, ipte_range);\n+\n+static int count_contiguous(pte_t *start, pte_t *end, bool *valid)\n+{\n+\tunsigned long page_invalid_bit;\n+\tpte_t *ptep, pte;\n+\n+\tpte = __ptep_get(start);\n+\tpage_invalid_bit = pte_val(pte) \u0026 _PAGE_INVALID;\n+\n+\tfor (ptep = start + 1; ptep \u003c end; ptep++) {\n+\t\tpte = __ptep_get(ptep);\n+\t\tif ((pte_val(pte) \u0026 _PAGE_INVALID) != page_invalid_bit)\n+\t\t\tbreak;\n+\t}\n+\n+\t*valid = !(page_invalid_bit);\n+\treturn ptep - start;\n+}\n+\n+static void __invalidate_pte_range(struct mm_struct *mm, unsigned long addr,\n+\t\t\t\t   int nr_ptes, pte_t *ptep)\n+{\n+\tatomic_inc(\u0026mm-\u003econtext.flush_count);\n+\tif (cpu_has_tlb_lc() \u0026\u0026 cpumask_equal(mm_cpumask(mm), cpumask_of(smp_processor_id())))\n+\t\t__ptep_ipte_range(addr, nr_ptes - 1, ptep, IPTE_LOCAL);\n+\telse\n+\t\t__ptep_ipte_range(addr, nr_ptes - 1, ptep, IPTE_GLOBAL);\n+\tatomic_dec(\u0026mm-\u003econtext.flush_count);\n+}\n+\n+static int invalidate_pte_range(struct mm_struct *mm, unsigned long addr,\n+\t\t\t\tpte_t *start, pte_t *end)\n+{\n+\tint nr_ptes;\n+\tbool valid;\n+\n+\tnr_ptes = count_contiguous(start, end, \u0026valid);\n+\tif (valid)\n+\t\t__invalidate_pte_range(mm, addr, nr_ptes, start);\n+\n+\treturn nr_ptes;\n+}\n+\n+static void set_pte_range(struct mm_struct *mm, unsigned long addr,\n+\t\t\t  pte_t *start, pte_t *end, pte_t *cache)\n+{\n+\tint nr_ptes, nr_total = end - start;\n+\tpte_t *ptep = start;\n+\tint i;\n+\n+\tkasan_unpoison_pte(start, nr_total);\n+\twhile (ptep \u003c end) {\n+\t\tnr_ptes = invalidate_pte_range(mm, addr, ptep, end);\n+\n+\t\tfor (i = 0; i \u003c nr_ptes; i++, ptep++, cache++) {\n+\t\t\t__set_pte(ptep, *cache);\n+\t\t\t*cache = __pte(PTE_POISON);\n+\t\t}\n+\n+\t\taddr += nr_ptes * PAGE_SIZE;\n+\t}\n+\tkasan_poison_pte(start, nr_total);\n+}\n+\n+static void enter_ipte_norange(void)\n+{\n+\tstruct ipte_range __maybe_unused *range;\n+\n+\tif (!test_facility(13))\n+\t\treturn;\n+\n+\tlocal_bh_disable();\n+\n+\trange = get_cpu_var(ipte_range);\n+\tget_lowcore()-\u003elazy_mmu_count++;\n+\n+\tlocal_bh_enable();\n+}\n+\n+static void enter_ipte_range(struct mm_struct *mm,\n+\t\t\t     unsigned long addr, unsigned long end, pte_t *pte)\n+{\n+\tstruct ipte_range *range;\n+\tunsigned int nr_ptes;\n+\n+\tif (!test_facility(13))\n+\t\treturn;\n+\n+\tlocal_bh_disable();\n+\n+\trange = get_cpu_var(ipte_range);\n+\tget_lowcore()-\u003elazy_mmu_count++;\n+\n+\tif (mm_is_protected(mm)) {\n+\t\tlocal_bh_enable();\n+\t\treturn;\n+\t}\n+\n+\trange-\u003emm = mm;\n+\trange-\u003ebase_addr = addr;\n+\trange-\u003ebase_end = end;\n+\trange-\u003ebase_pte = pte;\n+\n+\tnr_ptes = (range-\u003ebase_end - range-\u003ebase_addr) / PAGE_SIZE;\n+\tkasan_poison_pte(range-\u003ebase_pte, nr_ptes);\n+\n+\tlocal_bh_enable();\n+}\n+\n+static void leave_ipte_range(void)\n+{\n+\tpte_t *ptep, *start, *start_cache, *cache;\n+\tunsigned long start_addr, addr;\n+\tstruct ipte_range *range;\n+\tunsigned int nr_ptes;\n+\tint start_idx;\n+\n+\tif (!test_facility(13))\n+\t\treturn;\n+\n+\tlocal_bh_disable();\n+\n+\tlockdep_assert_preemption_disabled();\n+\trange = this_cpu_read(ipte_range);\n+\tif (!range-\u003emm)\n+\t\tgoto norange;\n+\tif (!range-\u003estart_pte)\n+\t\tgoto done;\n+\n+\tstart = range-\u003estart_pte;\n+\tstart_idx = range-\u003estart_pte - range-\u003ebase_pte;\n+\tstart_addr = range-\u003ebase_addr + start_idx * PAGE_SIZE;\n+\taddr = start_addr;\n+\tstart_cache = \u0026range-\u003ecache[start_idx];\n+\tcache = start_cache;\n+\tfor (ptep = start; ptep \u003c range-\u003eend_pte; ptep++, cache++, addr += PAGE_SIZE) {\n+\t\tif (pte_val(*cache) == PTE_POISON) {\n+\t\t\tif (start) {\n+\t\t\t\tset_pte_range(range-\u003emm, start_addr, start, ptep, start_cache);\n+\t\t\t\tstart = NULL;\n+\t\t\t}\n+\t\t} else if (!start) {\n+\t\t\tstart = ptep;\n+\t\t\tstart_addr = addr;\n+\t\t\tstart_cache = cache;\n+\t\t}\n+\t}\n+\tset_pte_range(range-\u003emm, start_addr, start, ptep, start_cache);\n+\n+\trange-\u003estart_pte = NULL;\n+\trange-\u003eend_pte = NULL;\n+\n+done:\n+\tnr_ptes = (range-\u003ebase_end - range-\u003ebase_addr) / PAGE_SIZE;\n+\tkasan_unpoison_pte(range-\u003ebase_pte, nr_ptes);\n+\n+\trange-\u003emm = NULL;\n+\trange-\u003ebase_addr = 0;\n+\trange-\u003ebase_end = 0;\n+\trange-\u003ebase_pte = NULL;\n+\n+norange:\n+\tget_lowcore()-\u003elazy_mmu_count--;\n+\tput_cpu_var(ipte_range);\n+\n+\tlocal_bh_enable();\n+}\n+\n+static void flush_lazy_mmu_mode(void)\n+{\n+\tunsigned long addr, end;\n+\tstruct ipte_range *range;\n+\tstruct mm_struct *mm;\n+\tpte_t *pte;\n+\n+\tif (!test_facility(13))\n+\t\treturn;\n+\n+\trange = get_cpu_var(ipte_range);\n+\tif (range-\u003emm) {\n+\t\tmm = range-\u003emm;\n+\t\taddr = range-\u003ebase_addr;\n+\t\tend = range-\u003ebase_end;\n+\t\tpte = range-\u003ebase_pte;\n+\n+\t\tleave_ipte_range();\n+\t\tenter_ipte_range(mm, addr, end, pte);\n+\t}\n+\tput_cpu_var(ipte_range);\n+}\n+\n+void arch_enter_lazy_mmu_mode(void)\n+{\n+\tenter_ipte_norange();\n+}\n+EXPORT_SYMBOL_IF_KUNIT(arch_enter_lazy_mmu_mode);\n+\n+void arch_enter_lazy_mmu_mode_with_ptes(struct mm_struct *mm,\n+\t\t\t\t\tunsigned long addr, unsigned long end,\n+\t\t\t\t\tpte_t *pte)\n+{\n+\tenter_ipte_range(mm, addr, end, pte);\n+}\n+EXPORT_SYMBOL_IF_KUNIT(arch_enter_lazy_mmu_mode_with_ptes);\n+\n+void arch_leave_lazy_mmu_mode(void)\n+{\n+\tleave_ipte_range();\n+}\n+EXPORT_SYMBOL_IF_KUNIT(arch_leave_lazy_mmu_mode);\n+\n+void arch_flush_lazy_mmu_mode(void)\n+{\n+\tflush_lazy_mmu_mode();\n+}\n+EXPORT_SYMBOL_IF_KUNIT(arch_flush_lazy_mmu_mode);\n+\n+static void __ipte_range_set_pte(struct ipte_range *range, pte_t *ptep, pte_t pte)\n+{\n+\tunsigned int idx = ptep - range-\u003ebase_pte;\n+\n+\tlockdep_assert_preemption_disabled();\n+\trange-\u003ecache[idx] = pte;\n+\n+\tif (!range-\u003estart_pte) {\n+\t\trange-\u003estart_pte = ptep;\n+\t\trange-\u003eend_pte = ptep + 1;\n+\t} else if (ptep \u003c range-\u003estart_pte) {\n+\t\trange-\u003estart_pte = ptep;\n+\t} else if (ptep + 1 \u003e range-\u003eend_pte) {\n+\t\trange-\u003eend_pte = ptep + 1;\n+\t}\n+}\n+\n+static pte_t __ipte_range_ptep_get(struct ipte_range *range, pte_t *ptep)\n+{\n+\tunsigned int idx = ptep - range-\u003ebase_pte;\n+\tpte_t pte;\n+\n+\tlockdep_assert_preemption_disabled();\n+\tif (pte_val(range-\u003ecache[idx]) == PTE_POISON) {\n+\t\tkasan_unpoison_pte(ptep, 1);\n+\t\tpte = __ptep_get(ptep);\n+\t\tkasan_poison_pte(ptep, 1);\n+\n+\t\treturn pte;\n+\t}\n+\n+\treturn range-\u003ecache[idx];\n+}\n+\n+static struct ipte_range *this_ipte_range(pte_t *ptep)\n+{\n+\tstruct ipte_range *range;\n+\tunsigned int nr_ptes;\n+\n+\trange = this_cpu_read(ipte_range);\n+\tif (ptep \u003c range-\u003ebase_pte)\n+\t\treturn NULL;\n+\tnr_ptes = (range-\u003ebase_end - range-\u003ebase_addr) / PAGE_SIZE;\n+\tif (ptep \u003e= range-\u003ebase_pte + nr_ptes)\n+\t\treturn NULL;\n+\n+\treturn range;\n+}\n+\n+bool __lazy_mmu_set_pte(pte_t *ptep, pte_t pte)\n+{\n+\tstruct ipte_range *range;\n+\n+\trange = this_ipte_range(ptep);\n+\tif (!range)\n+\t\treturn false;\n+\n+\t__ipte_range_set_pte(range, ptep, pte);\n+\n+\treturn true;\n+}\n+\n+bool __lazy_mmu_ptep_get(pte_t *ptep, pte_t *res)\n+{\n+\tstruct ipte_range *range;\n+\n+\trange = this_ipte_range(ptep);\n+\tif (!range)\n+\t\treturn false;\n+\n+\t*res = __ipte_range_ptep_get(range, ptep);\n+\n+\treturn true;\n+}\n+\n+bool __lazy_mmu_ptep_test_and_clear_young(unsigned long addr, pte_t *ptep, int *res)\n+{\n+\tstruct ipte_range *range;\n+\tpte_t pte, old;\n+\n+\trange = this_ipte_range(ptep);\n+\tif (!range)\n+\t\treturn false;\n+\n+\told = __ipte_range_ptep_get(range, ptep);\n+\tpte = pte_mkold(old);\n+\t__ipte_range_set_pte(range, ptep, pte);\n+\t*res = pte_young(old);\n+\n+\treturn true;\n+}\n+\n+bool __lazy_mmu_ptep_get_and_clear(unsigned long addr, pte_t *ptep, pte_t *res)\n+{\n+\tstruct ipte_range *range;\n+\tpte_t pte, old;\n+\n+\trange = this_ipte_range(ptep);\n+\tif (!range)\n+\t\treturn false;\n+\n+\told = __ipte_range_ptep_get(range, ptep);\n+\tpte = __pte(_PAGE_INVALID);\n+\t__ipte_range_set_pte(range, ptep, pte);\n+\t*res = old;\n+\n+\treturn true;\n+}\n+\n+bool __lazy_mmu_ptep_modify_prot_start(unsigned long addr, pte_t *ptep, pte_t *res)\n+{\n+\treturn __lazy_mmu_ptep_get_and_clear(addr, ptep, res);\n+}\n+\n+bool __lazy_mmu_ptep_modify_prot_commit(unsigned long addr, pte_t *ptep,\n+\t\t\t\t\tpte_t old_pte, pte_t pte)\n+{\n+\tstruct ipte_range *range;\n+\n+\trange = this_ipte_range(ptep);\n+\tif (!range)\n+\t\treturn false;\n+\n+\t__ipte_range_set_pte(range, ptep, pte);\n+\n+\treturn true;\n+}\n+\n+bool __lazy_mmu_ptep_set_wrprotect(unsigned long addr, pte_t *ptep)\n+{\n+\tstruct ipte_range *range;\n+\tpte_t pte;\n+\n+\trange = this_ipte_range(ptep);\n+\tif (!range)\n+\t\treturn false;\n+\n+\tpte = __ipte_range_ptep_get(range, ptep);\n+\tif (pte_write(pte)) {\n+\t\tpte = pte_wrprotect(pte);\n+\t\t__ipte_range_set_pte(range, ptep, pte);\n+\t}\n+\n+\treturn true;\n+}\n+\n+int lazy_mmu_online_cpu(gfp_t gfp, unsigned int cpu)\n+{\n+\tstruct ipte_range *range;\n+\tint i;\n+\n+\tif (!test_facility(13))\n+\t\treturn 0;\n+\n+\trange = kzalloc_obj(*range, gfp);\n+\tif (!range)\n+\t\treturn -ENOMEM;\n+\tfor (i = 0; i \u003c ARRAY_SIZE(range-\u003ecache); i++)\n+\t\trange-\u003ecache[i] = __pte(PTE_POISON);\n+\tper_cpu(ipte_range, cpu) = range;\n+\n+\treturn 0;\n+}\n+\n+void lazy_mmu_offline_cpu(unsigned int cpu)\n+{\n+\tstruct ipte_range *range;\n+\n+\tif (!test_facility(13))\n+\t\treturn;\n+\n+\trange = per_cpu(ipte_range, cpu);\n+\tper_cpu(ipte_range, cpu) = NULL;\n+\tkfree(range);\n+}\n+\n+void __init lazy_mmu_online_boot_cpu(void)\n+{\n+\tlazy_mmu_online_cpu(GFP_ATOMIC, 0);\n+}\ndiff --git a/arch/s390/mm/pgtable.c b/arch/s390/mm/pgtable.c\nindex 4acd8b140c4bd..df36523bcbbbb 100644\n--- a/arch/s390/mm/pgtable.c\n+++ b/arch/s390/mm/pgtable.c\n@@ -166,14 +166,14 @@ pte_t ptep_xchg_lazy(struct mm_struct *mm, unsigned long addr,\n }\n EXPORT_SYMBOL(ptep_xchg_lazy);\n \n-pte_t ptep_modify_prot_start(struct vm_area_struct *vma, unsigned long addr,\n-\t\t\t     pte_t *ptep)\n+pte_t ___ptep_modify_prot_start(struct vm_area_struct *vma, unsigned long addr,\n+\t\t\t\tpte_t *ptep)\n {\n \treturn ptep_flush_lazy(vma-\u003evm_mm, addr, ptep, 1);\n }\n \n-void ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,\n-\t\t\t     pte_t *ptep, pte_t old_pte, pte_t pte)\n+void ___ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,\n+\t\t\t\tpte_t *ptep, pte_t old_pte, pte_t pte)\n {\n \tset_pte(ptep, pte);\n }\ndiff --git a/fs/proc/task_mmu.c b/fs/proc/task_mmu.c\nindex 229d1fc3d7f1c..916785c3b2e0b 100644\n--- a/fs/proc/task_mmu.c\n+++ b/fs/proc/task_mmu.c\n@@ -2852,7 +2852,7 @@ static int pagemap_scan_pmd_entry(pmd_t *pmd, unsigned long start,\n \t\treturn 0;\n \t}\n \n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(vma-\u003evm_mm, start, end, start_pte);\n \n \tif ((p-\u003earg.flags \u0026 PM_SCAN_WP_MATCHING) \u0026\u0026 !p-\u003evec_out) {\n \t\t/* Fast path for performing exclusive WP */\ndiff --git a/include/linux/kasan.h b/include/linux/kasan.h\nindex bf233bde68c7e..1aa4d9dd2485a 100644\n--- a/include/linux/kasan.h\n+++ b/include/linux/kasan.h\n@@ -6,6 +6,7 @@\n #include \u003clinux/kasan-enabled.h\u003e\n #include \u003clinux/kasan-tags.h\u003e\n #include \u003clinux/kernel.h\u003e\n+#include \u003clinux/pgtable.h\u003e\n #include \u003clinux/static_key.h\u003e\n #include \u003clinux/types.h\u003e\n \n@@ -35,8 +36,6 @@ typedef unsigned int __bitwise kasan_vmalloc_flags_t;\n \n #if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)\n \n-#include \u003clinux/pgtable.h\u003e\n-\n /* Software KASAN implementations use shadow memory. */\n \n #ifdef CONFIG_KASAN_SW_TAGS\n@@ -134,6 +133,20 @@ static __always_inline void kasan_poison_slab(struct slab *slab)\n \t\t__kasan_poison_slab(slab);\n }\n \n+void __kasan_poison_pte(pte_t *pte, int nr);\n+static __always_inline void kasan_poison_pte(pte_t *pte, int nr)\n+{\n+\tif (kasan_enabled())\n+\t\t__kasan_poison_pte(pte, nr);\n+}\n+\n+void __kasan_unpoison_pte(pte_t *pte, int nr);\n+static __always_inline void kasan_unpoison_pte(pte_t *pte, int nr)\n+{\n+\tif (kasan_enabled())\n+\t\t__kasan_unpoison_pte(pte, nr);\n+}\n+\n void __kasan_unpoison_new_object(struct kmem_cache *cache, void *object);\n /**\n  * kasan_unpoison_new_object - Temporarily unpoison a new slab object.\n@@ -414,6 +427,8 @@ static inline bool kasan_unpoison_pages(struct page *page, unsigned int order,\n \treturn false;\n }\n static inline void kasan_poison_slab(struct slab *slab) {}\n+static inline void kasan_poison_pte(pte_t *pte, int nr) {}\n+static inline void kasan_unpoison_pte(pte_t *pte, int nr) {}\n static inline void kasan_unpoison_new_object(struct kmem_cache *cache,\n \t\t\t\t\tvoid *object) {}\n static inline void kasan_poison_new_object(struct kmem_cache *cache,\ndiff --git a/include/linux/pgtable.h b/include/linux/pgtable.h\nindex 2981e386da7b3..cc85daf30739e 100644\n--- a/include/linux/pgtable.h\n+++ b/include/linux/pgtable.h\n@@ -271,6 +271,50 @@ static inline void lazy_mmu_mode_enable(void)\n \t\tarch_enter_lazy_mmu_mode();\n }\n \n+#ifndef arch_enter_lazy_mmu_mode_with_ptes\n+static inline void arch_enter_lazy_mmu_mode_with_ptes(struct mm_struct *mm,\n+\t\tunsigned long addr, unsigned long end, pte_t *ptep)\n+{\n+\tarch_enter_lazy_mmu_mode();\n+}\n+#endif\n+\n+/**\n+ * lazy_mmu_mode_enable_with_ptes() - Enable the lazy MMU mode with a speedup hint.\n+ * @mm: Address space the pages are mapped into.\n+ * @addr: Start address of the range.\n+ * @end: End address of the range.\n+ * @ptep: Page table pointer for the first entry.\n+ *\n+ * Enters a new lazy MMU mode section; if the mode was not already enabled,\n+ * enables it and calls arch_enter_lazy_mmu_mode_with_ptes().\n+ *\n+ * PTEs that fall within the specified range might observe update speedups.\n+ * The PTEs must belong to the specified address space and be in the same PMD.\n+ *\n+ * There are no requirements on the order or range completeness of PTE\n+ * updates for the specified range.\n+ *\n+ * Must be paired with a call to lazy_mmu_mode_disable().\n+ *\n+ * Has no effect if called:\n+ * - While paused - see lazy_mmu_mode_pause()\n+ * - In interrupt context\n+ */\n+static inline void lazy_mmu_mode_enable_with_ptes(struct mm_struct *mm,\n+\t\tunsigned long addr, unsigned long end, pte_t *ptep)\n+{\n+\tstruct lazy_mmu_state *state = \u0026current-\u003elazy_mmu_state;\n+\n+\tif (in_interrupt() || state-\u003epause_count \u003e 0)\n+\t\treturn;\n+\n+\tVM_WARN_ON_ONCE(state-\u003eenable_count == U8_MAX);\n+\n+\tif (state-\u003eenable_count++ == 0)\n+\t\tarch_enter_lazy_mmu_mode_with_ptes(mm, addr, end, ptep);\n+}\n+\n /**\n  * lazy_mmu_mode_disable() - Disable the lazy MMU mode.\n  *\n@@ -387,6 +431,8 @@ static inline void lazy_mmu_mode_resume(void)\n }\n #else\n static inline void lazy_mmu_mode_enable(void) {}\n+static inline void lazy_mmu_mode_enable_with_ptes(struct mm_struct *mm,\n+\t\tunsigned long addr, unsigned long end, pte_t *ptep) {}\n static inline void lazy_mmu_mode_disable(void) {}\n static inline void lazy_mmu_mode_pause(void) {}\n static inline void lazy_mmu_mode_resume(void) {}\ndiff --git a/mm/kasan/common.c b/mm/kasan/common.c\nindex b7d05c2a6d93d..94d1060949891 100644\n--- a/mm/kasan/common.c\n+++ b/mm/kasan/common.c\n@@ -163,6 +163,20 @@ void __kasan_poison_slab(struct slab *slab)\n \t\t     KASAN_SLAB_REDZONE, false);\n }\n \n+void __kasan_poison_pte(pte_t *pte, int nr)\n+{\n+\tif (IS_ALIGNED(sizeof(*pte), KASAN_GRANULE_SIZE))\n+\t\tkasan_poison(pte, sizeof(*pte) * nr, KASAN_LAZY_MMU_PTE, false);\n+}\n+EXPORT_SYMBOL_GPL(__kasan_poison_pte);\n+\n+void __kasan_unpoison_pte(pte_t *pte, int nr)\n+{\n+\tif (IS_ALIGNED(sizeof(*pte), KASAN_GRANULE_SIZE))\n+\t\tkasan_unpoison(pte, sizeof(*pte) * nr, false);\n+}\n+EXPORT_SYMBOL_GPL(__kasan_unpoison_pte);\n+\n void __kasan_unpoison_new_object(struct kmem_cache *cache, void *object)\n {\n \tkasan_unpoison(object, cache-\u003eobject_size, false);\ndiff --git a/mm/kasan/kasan.h b/mm/kasan/kasan.h\nindex fc9169a547662..1a2d18cdb21d9 100644\n--- a/mm/kasan/kasan.h\n+++ b/mm/kasan/kasan.h\n@@ -144,12 +144,14 @@ static inline bool kasan_requires_meta(void)\n #define KASAN_PAGE_REDZONE\t0xFE  /* redzone for kmalloc_large allocation */\n #define KASAN_SLAB_REDZONE\t0xFC  /* redzone for slab object */\n #define KASAN_SLAB_FREE\t\t0xFB  /* freed slab object */\n+#define KASAN_LAZY_MMU_PTE\t0xFD  /* direct pte access in lazy mmu mode */\n #define KASAN_VMALLOC_INVALID\t0xF8  /* inaccessible space in vmap area */\n #else\n #define KASAN_PAGE_FREE\t\tKASAN_TAG_INVALID\n #define KASAN_PAGE_REDZONE\tKASAN_TAG_INVALID\n #define KASAN_SLAB_REDZONE\tKASAN_TAG_INVALID\n #define KASAN_SLAB_FREE\t\tKASAN_TAG_INVALID\n+#define KASAN_LAZY_MMU_PTE\tKASAN_TAG_INVALID\n #define KASAN_VMALLOC_INVALID\tKASAN_TAG_INVALID /* only used for SW_TAGS */\n #endif\n \ndiff --git a/mm/kasan/report_generic.c b/mm/kasan/report_generic.c\nindex f5b8e37b3805c..489d4a8d6902d 100644\n--- a/mm/kasan/report_generic.c\n+++ b/mm/kasan/report_generic.c\n@@ -113,6 +113,9 @@ static const char *get_shadow_bug_type(struct kasan_report_info *info)\n \tcase KASAN_SLAB_FREE_META:\n \t\tbug_type = \"slab-use-after-free\";\n \t\tbreak;\n+\tcase KASAN_LAZY_MMU_PTE:\n+\t\tbug_type = \"lazy-mmu-pte-access\";\n+\t\tbreak;\n \tcase KASAN_ALLOCA_LEFT:\n \tcase KASAN_ALLOCA_RIGHT:\n \t\tbug_type = \"alloca-out-of-bounds\";\ndiff --git a/mm/madvise.c b/mm/madvise.c\nindex 77552b03d318d..4419d16b39f96 100644\n--- a/mm/madvise.c\n+++ b/mm/madvise.c\n@@ -452,7 +452,7 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,\n \tif (!start_pte)\n \t\treturn 0;\n \tflush_tlb_batched_pending(mm);\n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);\n \tfor (; addr \u003c end; pte += nr, addr += nr * PAGE_SIZE) {\n \t\tnr = 1;\n \t\tptent = ptep_get(pte);\n@@ -507,7 +507,7 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,\n \t\t\t\tif (!start_pte)\n \t\t\t\t\tbreak;\n \t\t\t\tflush_tlb_batched_pending(mm);\n-\t\t\t\tlazy_mmu_mode_enable();\n+\t\t\t\tlazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);\n \t\t\t\tif (!err)\n \t\t\t\t\tnr = 0;\n \t\t\t\tcontinue;\n@@ -674,7 +674,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,\n \tif (!start_pte)\n \t\treturn 0;\n \tflush_tlb_batched_pending(mm);\n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);\n \tfor (; addr != end; pte += nr, addr += PAGE_SIZE * nr) {\n \t\tnr = 1;\n \t\tptent = ptep_get(pte);\n@@ -734,7 +734,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,\n \t\t\t\tif (!start_pte)\n \t\t\t\t\tbreak;\n \t\t\t\tflush_tlb_batched_pending(mm);\n-\t\t\t\tlazy_mmu_mode_enable();\n+\t\t\t\tlazy_mmu_mode_enable_with_ptes(mm, addr, end, pte);\n \t\t\t\tif (!err)\n \t\t\t\t\tnr = 0;\n \t\t\t\tcontinue;\ndiff --git a/mm/memory.c b/mm/memory.c\nindex 6b8280cfc1dbe..0cc07b043384b 100644\n--- a/mm/memory.c\n+++ b/mm/memory.c\n@@ -1272,7 +1272,7 @@ copy_pte_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,\n \tspin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);\n \torig_src_pte = src_pte;\n \torig_dst_pte = dst_pte;\n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(src_mm, addr, end, src_pte);\n \n \tdo {\n \t\tnr = 1;\n@@ -1922,7 +1922,7 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,\n \t\treturn addr;\n \n \tflush_tlb_batched_pending(mm);\n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);\n \tdo {\n \t\tbool any_skipped = false;\n \n@@ -2919,7 +2919,7 @@ static int remap_pte_range(struct mm_struct *mm, pmd_t *pmd,\n \tmapped_pte = pte = pte_alloc_map_lock(mm, pmd, addr, \u0026ptl);\n \tif (!pte)\n \t\treturn -ENOMEM;\n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(mm, addr, end, mapped_pte);\n \tdo {\n \t\tBUG_ON(!pte_none(ptep_get(pte)));\n \t\tif (!pfn_modify_allowed(pfn, prot)) {\n@@ -3330,7 +3330,7 @@ static int apply_to_pte_range(struct mm_struct *mm, pmd_t *pmd,\n \t\t\treturn -EINVAL;\n \t}\n \n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(mm, addr, end, mapped_pte);\n \n \tif (fn) {\n \t\tdo {\ndiff --git a/mm/mprotect.c b/mm/mprotect.c\nindex 9cbf932b028cf..3fc26418e837d 100644\n--- a/mm/mprotect.c\n+++ b/mm/mprotect.c\n@@ -337,7 +337,7 @@ static long change_pte_range(struct mmu_gather *tlb,\n \t\tis_private_single_threaded = vma_is_single_threaded_private(vma);\n \n \tflush_tlb_batched_pending(vma-\u003evm_mm);\n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(vma-\u003evm_mm, addr, end, pte);\n \tdo {\n \t\tnr_ptes = 1;\n \t\toldpte = ptep_get(pte);\ndiff --git a/mm/mremap.c b/mm/mremap.c\nindex e9c8b1d05832b..0dfe3de39ccc7 100644\n--- a/mm/mremap.c\n+++ b/mm/mremap.c\n@@ -260,7 +260,7 @@ static int move_ptes(struct pagetable_move_control *pmc,\n \tif (new_ptl != old_ptl)\n \t\tspin_lock_nested(new_ptl, SINGLE_DEPTH_NESTING);\n \tflush_tlb_batched_pending(vma-\u003evm_mm);\n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(mm, old_addr, old_end, old_ptep);\n \n \tfor (; old_addr \u003c old_end; old_ptep += nr_ptes, old_addr += nr_ptes * PAGE_SIZE,\n \t\tnew_ptep += nr_ptes, new_addr += nr_ptes * PAGE_SIZE) {\ndiff --git a/mm/vmalloc.c b/mm/vmalloc.c\nindex f4fa227a8d7f0..1c6ef8fd079e0 100644\n--- a/mm/vmalloc.c\n+++ b/mm/vmalloc.c\n@@ -109,7 +109,7 @@ static int vmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,\n \tif (!pte)\n \t\treturn -ENOMEM;\n \n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(\u0026init_mm, addr, end, pte);\n \n \tdo {\n \t\tif (unlikely(!pte_none(ptep_get(pte)))) {\n@@ -393,7 +393,7 @@ static void vunmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,\n \tunsigned long size = PAGE_SIZE;\n \n \tpte = pte_offset_kernel(pmd, addr);\n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(\u0026init_mm, addr, end, pte);\n \n \tdo {\n #ifdef CONFIG_HUGETLB_PAGE\n@@ -560,7 +560,7 @@ static int vmap_pages_pte_range(pmd_t *pmd, unsigned long addr,\n \tif (!pte)\n \t\treturn -ENOMEM;\n \n-\tlazy_mmu_mode_enable();\n+\tlazy_mmu_mode_enable_with_ptes(\u0026init_mm, addr, end, pte);\n \n \tdo {\n \t\tstruct page *page = pages[*nr];\n"
}

2/1 2026/08/17 12:37 agent patch-evaluator
3m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "KASAN",
    "KASAN_GENERIC",
    "HOTPLUG_CPU"
  ],
  "FocusSymbols": [
    "arch_enter_lazy_mmu_mode",
    "arch_enter_lazy_mmu_mode_with_ptes",
    "arch_leave_lazy_mmu_mode",
    "arch_flush_lazy_mmu_mode",
    "lazy_mmu_online_cpu",
    "lazy_mmu_offline_cpu",
    "__kasan_poison_pte",
    "__kasan_unpoison_pte",
    "___ptep_modify_prot_start",
    "___ptep_modify_prot_commit"
  ],
  "Reasoning": "The patch introduces lazy MMU mode for s390 architecture, which batches PTE invalidations using the IPTE-range facility. It hooks into core memory management syscalls (madvise, mprotect, mremap, vmalloc) and CPU hotplug paths. It also adds KASAN instrumentation for direct PTE accesses in lazy MMU mode. These are reachable core kernel and architecture-specific memory management paths that should be fuzzed.",
  "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 36c12673f35ddf2a14791164b5dcf9d4c3cf54c2
Author: syz-cluster <triage@syzkaller.com>
Date:   Mon Aug 17 12:37:42 2026 +0000

    syz-cluster: applied patch under review

diff --git a/arch/s390/Kconfig b/arch/s390/Kconfig
index 84404e6778d50..7846332dcd0a6 100644
--- a/arch/s390/Kconfig
+++ b/arch/s390/Kconfig
@@ -97,6 +97,7 @@ config S390
 	select ARCH_HAS_GCOV_PROFILE_ALL
 	select ARCH_HAS_GIGANTIC_PAGE
 	select ARCH_HAS_KCOV
+	select ARCH_HAS_LAZY_MMU_MODE
 	select ARCH_HAS_MEMBARRIER_SYNC_CORE
 	select ARCH_HAS_MEM_ENCRYPT
 	select ARCH_HAS_NMI_SAFE_THIS_CPU_OPS
diff --git a/arch/s390/include/asm/lazy_mmu.h b/arch/s390/include/asm/lazy_mmu.h
new file mode 100644
index 0000000000000..98366e9de9bc3
--- /dev/null
+++ b/arch/s390/include/asm/lazy_mmu.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __LAZY_MMU_H
+#define __LAZY_MMU_H
+
+void lazy_mmu_online_boot_cpu(void);
+int lazy_mmu_online_cpu(gfp_t gfp, unsigned int cpu);
+void lazy_mmu_offline_cpu(unsigned int cpu);
+
+#endif /* __LAZY_MMU_H */
diff --git a/arch/s390/include/asm/lowcore.h b/arch/s390/include/asm/lowcore.h
index 3b3ecc647993f..dba236664da90 100644
--- a/arch/s390/include/asm/lowcore.h
+++ b/arch/s390/include/asm/lowcore.h
@@ -163,7 +163,7 @@ struct lowcore {
 	__s32	preempt_count;			/* 0x03a8 */
 	__u32	spinlock_lockval;		/* 0x03ac */
 	__u32	spinlock_index;			/* 0x03b0 */
-	__u8	pad_0x03b4[0x03b8-0x03b4];	/* 0x03b4 */
+	__s32	lazy_mmu_count;			/* 0x03b4 */
 	__u64	percpu_offset;			/* 0x03b8 */
 	__u8	percpu_register;		/* 0x03c0 */
 	__u8	pad_0x03c1[0x0400-0x03c1];	/* 0x03c1 */
diff --git a/arch/s390/include/asm/pgtable.h b/arch/s390/include/asm/pgtable.h
index 859ce7c7d4544..1e8a79989ed6e 100644
--- a/arch/s390/include/asm/pgtable.h
+++ b/arch/s390/include/asm/pgtable.h
@@ -39,6 +39,64 @@ enum {
 
 extern atomic_long_t direct_pages_count[PG_DIRECT_MAP_MAX];
 
+bool __lazy_mmu_ptep_test_and_clear_young(unsigned long addr, pte_t *ptep, int *res);
+bool __lazy_mmu_ptep_get_and_clear(unsigned long addr, pte_t *ptep, pte_t *res);
+bool __lazy_mmu_ptep_modify_prot_start(unsigned long addr, pte_t *ptep, pte_t *res);
+bool __lazy_mmu_ptep_modify_prot_commit(unsigned long addr, pte_t *ptep, pte_t old_pte, pte_t pte);
+bool __lazy_mmu_ptep_set_wrprotect(unsigned long addr, pte_t *ptep);
+bool __lazy_mmu_set_pte(pte_t *ptep, pte_t pte);
+bool __lazy_mmu_ptep_get(pte_t *ptep, pte_t *res);
+
+static __always_inline bool is_lazy_mmu_active(void)
+{
+	if (__is_defined(__DECOMPRESSOR))
+		return false;
+	if (!get_lowcore()->lazy_mmu_count)
+		return false;
+	return true;
+}
+
+static inline
+bool lazy_mmu_ptep_test_and_clear_young(unsigned long addr, pte_t *ptep, int *res)
+{
+	if (!is_lazy_mmu_active())
+		return false;
+	return __lazy_mmu_ptep_test_and_clear_young(addr, ptep, res);
+}
+
+static inline
+bool lazy_mmu_ptep_get_and_clear(unsigned long addr, pte_t *ptep, pte_t *res)
+{
+	if (!is_lazy_mmu_active())
+		return false;
+	return __lazy_mmu_ptep_get_and_clear(addr, ptep, res);
+}
+
+static inline
+bool lazy_mmu_ptep_modify_prot_start(unsigned long addr, pte_t *ptep, pte_t *res)
+{
+	if (!is_lazy_mmu_active())
+		return false;
+	return __lazy_mmu_ptep_modify_prot_start(addr, ptep, res);
+}
+
+static inline
+bool lazy_mmu_ptep_modify_prot_commit(unsigned long addr, pte_t *ptep,
+				      pte_t old_pte, pte_t pte)
+{
+	if (!is_lazy_mmu_active())
+		return false;
+	return __lazy_mmu_ptep_modify_prot_commit(addr, ptep, old_pte, pte);
+}
+
+static inline
+bool lazy_mmu_ptep_set_wrprotect(unsigned long addr, pte_t *ptep)
+{
+	if (!is_lazy_mmu_active())
+		return false;
+	return __lazy_mmu_ptep_set_wrprotect(addr, ptep);
+}
+
 static inline void update_page_count(int level, long count)
 {
 	if (IS_ENABLED(CONFIG_PROC_FS))
@@ -978,17 +1036,38 @@ static inline void set_pmd(pmd_t *pmdp, pmd_t pmd)
 	WRITE_ONCE(*pmdp, pmd);
 }
 
-static inline void set_pte(pte_t *ptep, pte_t pte)
+static inline void __set_pte(pte_t *ptep, pte_t pte)
 {
 	if (pte_present(pte))
 		pte = clear_pte_bit(pte, __pgprot(_PAGE_UNUSED));
 	WRITE_ONCE(*ptep, pte);
 }
 
+static inline void set_pte(pte_t *ptep, pte_t pte)
+{
+	if (!is_lazy_mmu_active() || !__lazy_mmu_set_pte(ptep, pte))
+		__set_pte(ptep, pte);
+}
+
+#define ptep_get_lockless ptep_get_lockless
+static inline __no_sanitize_address pte_t ptep_get_lockless(pte_t *ptep)
+{
+	return READ_ONCE(*ptep);
+}
+
+static inline pte_t __ptep_get(pte_t *ptep)
+{
+	return READ_ONCE(*ptep);
+}
+
 #define ptep_get ptep_get
 static inline pte_t ptep_get(pte_t *ptep)
 {
-	return READ_ONCE(*ptep);
+	pte_t res;
+
+	if (!is_lazy_mmu_active() || !__lazy_mmu_ptep_get(ptep, &res))
+		res = __ptep_get(ptep);
+	return res;
 }
 
 #define pmdp_get pmdp_get
@@ -1181,6 +1260,15 @@ static __always_inline void __ptep_ipte_range(unsigned long address, int nr,
 	} while (nr != 255);
 }
 
+void arch_enter_lazy_mmu_mode_with_ptes(struct mm_struct *mm,
+					unsigned long addr, unsigned long end,
+					pte_t *pte);
+#define arch_enter_lazy_mmu_mode_with_ptes arch_enter_lazy_mmu_mode_with_ptes
+
+void arch_enter_lazy_mmu_mode(void);
+void arch_leave_lazy_mmu_mode(void);
+void arch_flush_lazy_mmu_mode(void);
+
 /*
  * This is hard to understand. ptep_get_and_clear and ptep_clear_flush
  * both clear the TLB for the unmapped pte. The reason is that
@@ -1201,10 +1289,16 @@ pte_t ptep_xchg_lazy(struct mm_struct *, unsigned long, pte_t *, pte_t);
 static inline bool ptep_test_and_clear_young(struct vm_area_struct *vma,
 		unsigned long addr, pte_t *ptep)
 {
-	pte_t pte = ptep_get(ptep);
+	pte_t pte;
+	int res;
 
-	pte = ptep_xchg_direct(vma->vm_mm, addr, ptep, pte_mkold(pte));
-	return pte_young(pte);
+	if (!lazy_mmu_ptep_test_and_clear_young(addr, ptep, &res)) {
+		pte = __ptep_get(ptep);
+		pte = pte_mkold(pte);
+		pte = ptep_xchg_direct(vma->vm_mm, addr, ptep, pte);
+		res = pte_young(pte);
+	}
+	return res;
 }
 
 #define __HAVE_ARCH_PTEP_CLEAR_YOUNG_FLUSH
@@ -1220,7 +1314,8 @@ static inline pte_t ptep_get_and_clear(struct mm_struct *mm,
 {
 	pte_t res;
 
-	res = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));
+	if (!lazy_mmu_ptep_get_and_clear(addr, ptep, &res))
+		res = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));
 	page_table_check_pte_clear(mm, addr, res);
 	/* At this point the reference through the mapping is still present */
 	if (mm_is_protected(mm) && pte_present(res))
@@ -1229,9 +1324,28 @@ static inline pte_t ptep_get_and_clear(struct mm_struct *mm,
 }
 
 #define __HAVE_ARCH_PTEP_MODIFY_PROT_TRANSACTION
-pte_t ptep_modify_prot_start(struct vm_area_struct *, unsigned long, pte_t *);
-void ptep_modify_prot_commit(struct vm_area_struct *, unsigned long,
-			     pte_t *, pte_t, pte_t);
+pte_t ___ptep_modify_prot_start(struct vm_area_struct *, unsigned long, pte_t *);
+void ___ptep_modify_prot_commit(struct vm_area_struct *, unsigned long,
+				pte_t *, pte_t, pte_t);
+
+static inline
+pte_t ptep_modify_prot_start(struct vm_area_struct *vma,
+			     unsigned long addr, pte_t *ptep)
+{
+	pte_t res;
+
+	if (!lazy_mmu_ptep_modify_prot_start(addr, ptep, &res))
+		res = ___ptep_modify_prot_start(vma, addr, ptep);
+	return res;
+}
+
+static inline
+void ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,
+			     pte_t *ptep, pte_t old_pte, pte_t pte)
+{
+	if (!lazy_mmu_ptep_modify_prot_commit(addr, ptep, old_pte, pte))
+		___ptep_modify_prot_commit(vma, addr, ptep, old_pte, pte);
+}
 
 #define __HAVE_ARCH_PTEP_CLEAR_FLUSH
 static inline pte_t ptep_clear_flush(struct vm_area_struct *vma,
@@ -1261,11 +1375,13 @@ static inline pte_t ptep_get_and_clear_full(struct mm_struct *mm,
 {
 	pte_t res;
 
-	if (full) {
-		res = ptep_get(ptep);
-		set_pte(ptep, __pte(_PAGE_INVALID));
-	} else {
-		res = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));
+	if (!lazy_mmu_ptep_get_and_clear(addr, ptep, &res)) {
+		if (full) {
+			res = __ptep_get(ptep);
+			__set_pte(ptep, __pte(_PAGE_INVALID));
+		} else {
+			res = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));
+		}
 	}
 	page_table_check_pte_clear(mm, addr, res);
 	/* At this point the reference through the mapping is still present */
@@ -1291,10 +1407,15 @@ static inline pte_t ptep_get_and_clear_full(struct mm_struct *mm,
 static inline void ptep_set_wrprotect(struct mm_struct *mm,
 				      unsigned long addr, pte_t *ptep)
 {
-	pte_t pte = ptep_get(ptep);
+	pte_t pte;
 
-	if (pte_write(pte))
-		ptep_xchg_lazy(mm, addr, ptep, pte_wrprotect(pte));
+	if (!lazy_mmu_ptep_set_wrprotect(addr, ptep)) {
+		pte = __ptep_get(ptep);
+		if (pte_write(pte)) {
+			pte = pte_wrprotect(pte);
+			ptep_xchg_lazy(mm, addr, ptep, pte);
+		}
+	}
 }
 
 /*
@@ -1327,7 +1448,7 @@ static inline void flush_tlb_fix_spurious_fault(struct vm_area_struct *vma,
 	 * PTE does not have _PAGE_PROTECT set, to avoid unnecessary overhead.
 	 * A local RDP can be used to do the flush.
 	 */
-	if (cpu_has_rdp() && !(pte_val(ptep_get(ptep)) & _PAGE_PROTECT))
+	if (cpu_has_rdp() && !(pte_val(__ptep_get(ptep)) & _PAGE_PROTECT))
 		__ptep_rdp(address, ptep, 1);
 }
 #define flush_tlb_fix_spurious_fault flush_tlb_fix_spurious_fault
diff --git a/arch/s390/kernel/setup.c b/arch/s390/kernel/setup.c
index b60284328fe3d..f5a3c9e1b6b88 100644
--- a/arch/s390/kernel/setup.c
+++ b/arch/s390/kernel/setup.c
@@ -77,6 +77,7 @@
 #include <asm/maccess.h>
 #include <asm/uv.h>
 #include <asm/asm-offsets.h>
+#include <asm/lazy_mmu.h>
 #include "entry.h"
 
 /*
@@ -1012,5 +1013,6 @@ void __init setup_arch(char **cmdline_p)
 
 void __init arch_cpu_finalize_init(void)
 {
+	lazy_mmu_online_boot_cpu();
 	sclp_init();
 }
diff --git a/arch/s390/kernel/smp.c b/arch/s390/kernel/smp.c
index 0ba7f89b81611..0a826bbaf1ddc 100644
--- a/arch/s390/kernel/smp.c
+++ b/arch/s390/kernel/smp.c
@@ -59,6 +59,7 @@
 #include <asm/topology.h>
 #include <asm/vdso.h>
 #include <asm/maccess.h>
+#include <asm/lazy_mmu.h>
 #include "entry.h"
 
 enum {
@@ -866,6 +867,11 @@ int __cpu_up(unsigned int cpu, struct task_struct *tidle)
 	rc = pcpu_alloc_lowcore(pcpu, cpu);
 	if (rc)
 		return rc;
+	rc = lazy_mmu_online_cpu(GFP_KERNEL, cpu);
+	if (rc) {
+		pcpu_free_lowcore(pcpu, cpu);
+		return rc;
+	}
 	/*
 	 * Make sure global control register contents do not change
 	 * until new CPU has initialized control registers.
@@ -921,6 +927,7 @@ void __cpu_die(unsigned int cpu)
 	pcpu = per_cpu_ptr(&pcpu_devices, cpu);
 	while (!pcpu_stopped(pcpu))
 		cpu_relax();
+	lazy_mmu_offline_cpu(cpu);
 	pcpu_free_lowcore(pcpu, cpu);
 	cpumask_clear_cpu(cpu, mm_cpumask(&init_mm));
 	cpumask_clear_cpu(cpu, &init_mm.context.cpu_attach_mask);
diff --git a/arch/s390/mm/Makefile b/arch/s390/mm/Makefile
index 193899c39ca7f..96cac3e99f516 100644
--- a/arch/s390/mm/Makefile
+++ b/arch/s390/mm/Makefile
@@ -4,6 +4,7 @@
 #
 
 obj-y		:= init.o fault.o extmem.o mmap.o vmem.o maccess.o
+obj-y		:= init.o fault.o extmem.o mmap.o vmem.o maccess.o lazy_mmu.o
 obj-y		+= page-states.o pageattr.o pgtable.o pgalloc.o extable.o
 
 obj-$(CONFIG_CMM)		+= cmm.o
diff --git a/arch/s390/mm/lazy_mmu.c b/arch/s390/mm/lazy_mmu.c
new file mode 100644
index 0000000000000..7e3a4115ea048
--- /dev/null
+++ b/arch/s390/mm/lazy_mmu.c
@@ -0,0 +1,418 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/pgtable.h>
+#include <linux/kasan.h>
+#include <linux/slab.h>
+#include <asm/facility.h>
+#include <asm/lazy_mmu.h>
+#include <kunit/visibility.h>
+
+#define PTE_POISON	_PAGE_LARGE
+
+struct ipte_range {
+	struct mm_struct *mm;
+	unsigned long base_addr;
+	unsigned long base_end;
+	pte_t *base_pte;
+	pte_t *start_pte;
+	pte_t *end_pte;
+	pte_t cache[PTRS_PER_PTE];
+};
+
+static DEFINE_PER_CPU(struct ipte_range *, ipte_range);
+
+static int count_contiguous(pte_t *start, pte_t *end, bool *valid)
+{
+	unsigned long page_invalid_bit;
+	pte_t *ptep, pte;
+
+	pte = __ptep_get(start);
+	page_invalid_bit = pte_val(pte) & _PAGE_INVALID;
+
+	for (ptep = start + 1; ptep < end; ptep++) {
+		pte = __ptep_get(ptep);
+		if ((pte_val(pte) & _PAGE_INVALID) != page_invalid_bit)
+			break;
+	}
+
+	*valid = !(page_invalid_bit);
+	return ptep - start;
+}
+
+static void __invalidate_pte_range(struct mm_struct *mm, unsigned long addr,
+				   int nr_ptes, pte_t *ptep)
+{
+	atomic_inc(&mm->context.flush_count);
+	if (cpu_has_tlb_lc() && cpumask_equal(mm_cpumask(mm), cpumask_of(smp_processor_id())))
+		__ptep_ipte_range(addr, nr_ptes - 1, ptep, IPTE_LOCAL);
+	else
+		__ptep_ipte_range(addr, nr_ptes - 1, ptep, IPTE_GLOBAL);
+	atomic_dec(&mm->context.flush_count);
+}
+
+static int invalidate_pte_range(struct mm_struct *mm, unsigned long addr,
+				pte_t *start, pte_t *end)
+{
+	int nr_ptes;
+	bool valid;
+
+	nr_ptes = count_contiguous(start, end, &valid);
+	if (valid)
+		__invalidate_pte_range(mm, addr, nr_ptes, start);
+
+	return nr_ptes;
+}
+
+static void set_pte_range(struct mm_struct *mm, unsigned long addr,
+			  pte_t *start, pte_t *end, pte_t *cache)
+{
+	int nr_ptes, nr_total = end - start;
+	pte_t *ptep = start;
+	int i;
+
+	kasan_unpoison_pte(start, nr_total);
+	while (ptep < end) {
+		nr_ptes = invalidate_pte_range(mm, addr, ptep, end);
+
+		for (i = 0; i < nr_ptes; i++, ptep++, cache++) {
+			__set_pte(ptep, *cache);
+			*cache = __pte(PTE_POISON);
+		}
+
+		addr += nr_ptes * PAGE_SIZE;
+	}
+	kasan_poison_pte(start, nr_total);
+}
+
+static void enter_ipte_norange(void)
+{
+	struct ipte_range __maybe_unused *range;
+
+	if (!test_facility(13))
+		return;
+
+	local_bh_disable();
+
+	range = get_cpu_var(ipte_range);
+	get_lowcore()->lazy_mmu_count++;
+
+	local_bh_enable();
+}
+
+static void enter_ipte_range(struct mm_struct *mm,
+			     unsigned long addr, unsigned long end, pte_t *pte)
+{
+	struct ipte_range *range;
+	unsigned int nr_ptes;
+
+	if (!test_facility(13))
+		return;
+
+	local_bh_disable();
+
+	range = get_cpu_var(ipte_range);
+	get_lowcore()->lazy_mmu_count++;
+
+	if (mm_is_protected(mm)) {
+		local_bh_enable();
+		return;
+	}
+
+	range->mm = mm;
+	range->base_addr = addr;
+	range->base_end = end;
+	range->base_pte = pte;
+
+	nr_ptes = (range->base_end - range->base_addr) / PAGE_SIZE;
+	kasan_poison_pte(range->base_pte, nr_ptes);
+
+	local_bh_enable();
+}
+
+static void leave_ipte_range(void)
+{
+	pte_t *ptep, *start, *start_cache, *cache;
+	unsigned long start_addr, addr;
+	struct ipte_range *range;
+	unsigned int nr_ptes;
+	int start_idx;
+
+	if (!test_facility(13))
+		return;
+
+	local_bh_disable();
+
+	lockdep_assert_preemption_disabled();
+	range = this_cpu_read(ipte_range);
+	if (!range->mm)
+		goto norange;
+	if (!range->start_pte)
+		goto done;
+
+	start = range->start_pte;
+	start_idx = range->start_pte - range->base_pte;
+	start_addr = range->base_addr + start_idx * PAGE_SIZE;
+	addr = start_addr;
+	start_cache = &range->cache[start_idx];
+	cache = start_cache;
+	for (ptep = start; ptep < range->end_pte; ptep++, cache++, addr += PAGE_SIZE) {
+		if (pte_val(*cache) == PTE_POISON) {
+			if (start) {
+				set_pte_range(range->mm, start_addr, start, ptep, start_cache);
+				start = NULL;
+			}
+		} else if (!start) {
+			start = ptep;
+			start_addr = addr;
+			start_cache = cache;
+		}
+	}
+	set_pte_range(range->mm, start_addr, start, ptep, start_cache);
+
+	range->start_pte = NULL;
+	range->end_pte = NULL;
+
+done:
+	nr_ptes = (range->base_end - range->base_addr) / PAGE_SIZE;
+	kasan_unpoison_pte(range->base_pte, nr_ptes);
+
+	range->mm = NULL;
+	range->base_addr = 0;
+	range->base_end = 0;
+	range->base_pte = NULL;
+
+norange:
+	get_lowcore()->lazy_mmu_count--;
+	put_cpu_var(ipte_range);
+
+	local_bh_enable();
+}
+
+static void flush_lazy_mmu_mode(void)
+{
+	unsigned long addr, end;
+	struct ipte_range *range;
+	struct mm_struct *mm;
+	pte_t *pte;
+
+	if (!test_facility(13))
+		return;
+
+	range = get_cpu_var(ipte_range);
+	if (range->mm) {
+		mm = range->mm;
+		addr = range->base_addr;
+		end = range->base_end;
+		pte = range->base_pte;
+
+		leave_ipte_range();
+		enter_ipte_range(mm, addr, end, pte);
+	}
+	put_cpu_var(ipte_range);
+}
+
+void arch_enter_lazy_mmu_mode(void)
+{
+	enter_ipte_norange();
+}
+EXPORT_SYMBOL_IF_KUNIT(arch_enter_lazy_mmu_mode);
+
+void arch_enter_lazy_mmu_mode_with_ptes(struct mm_struct *mm,
+					unsigned long addr, unsigned long end,
+					pte_t *pte)
+{
+	enter_ipte_range(mm, addr, end, pte);
+}
+EXPORT_SYMBOL_IF_KUNIT(arch_enter_lazy_mmu_mode_with_ptes);
+
+void arch_leave_lazy_mmu_mode(void)
+{
+	leave_ipte_range();
+}
+EXPORT_SYMBOL_IF_KUNIT(arch_leave_lazy_mmu_mode);
+
+void arch_flush_lazy_mmu_mode(void)
+{
+	flush_lazy_mmu_mode();
+}
+EXPORT_SYMBOL_IF_KUNIT(arch_flush_lazy_mmu_mode);
+
+static void __ipte_range_set_pte(struct ipte_range *range, pte_t *ptep, pte_t pte)
+{
+	unsigned int idx = ptep - range->base_pte;
+
+	lockdep_assert_preemption_disabled();
+	range->cache[idx] = pte;
+
+	if (!range->start_pte) {
+		range->start_pte = ptep;
+		range->end_pte = ptep + 1;
+	} else if (ptep < range->start_pte) {
+		range->start_pte = ptep;
+	} else if (ptep + 1 > range->end_pte) {
+		range->end_pte = ptep + 1;
+	}
+}
+
+static pte_t __ipte_range_ptep_get(struct ipte_range *range, pte_t *ptep)
+{
+	unsigned int idx = ptep - range->base_pte;
+	pte_t pte;
+
+	lockdep_assert_preemption_disabled();
+	if (pte_val(range->cache[idx]) == PTE_POISON) {
+		kasan_unpoison_pte(ptep, 1);
+		pte = __ptep_get(ptep);
+		kasan_poison_pte(ptep, 1);
+
+		return pte;
+	}
+
+	return range->cache[idx];
+}
+
+static struct ipte_range *this_ipte_range(pte_t *ptep)
+{
+	struct ipte_range *range;
+	unsigned int nr_ptes;
+
+	range = this_cpu_read(ipte_range);
+	if (ptep < range->base_pte)
+		return NULL;
+	nr_ptes = (range->base_end - range->base_addr) / PAGE_SIZE;
+	if (ptep >= range->base_pte + nr_ptes)
+		return NULL;
+
+	return range;
+}
+
+bool __lazy_mmu_set_pte(pte_t *ptep, pte_t pte)
+{
+	struct ipte_range *range;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	__ipte_range_set_pte(range, ptep, pte);
+
+	return true;
+}
+
+bool __lazy_mmu_ptep_get(pte_t *ptep, pte_t *res)
+{
+	struct ipte_range *range;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	*res = __ipte_range_ptep_get(range, ptep);
+
+	return true;
+}
+
+bool __lazy_mmu_ptep_test_and_clear_young(unsigned long addr, pte_t *ptep, int *res)
+{
+	struct ipte_range *range;
+	pte_t pte, old;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	old = __ipte_range_ptep_get(range, ptep);
+	pte = pte_mkold(old);
+	__ipte_range_set_pte(range, ptep, pte);
+	*res = pte_young(old);
+
+	return true;
+}
+
+bool __lazy_mmu_ptep_get_and_clear(unsigned long addr, pte_t *ptep, pte_t *res)
+{
+	struct ipte_range *range;
+	pte_t pte, old;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	old = __ipte_range_ptep_get(range, ptep);
+	pte = __pte(_PAGE_INVALID);
+	__ipte_range_set_pte(range, ptep, pte);
+	*res = old;
+
+	return true;
+}
+
+bool __lazy_mmu_ptep_modify_prot_start(unsigned long addr, pte_t *ptep, pte_t *res)
+{
+	return __lazy_mmu_ptep_get_and_clear(addr, ptep, res);
+}
+
+bool __lazy_mmu_ptep_modify_prot_commit(unsigned long addr, pte_t *ptep,
+					pte_t old_pte, pte_t pte)
+{
+	struct ipte_range *range;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	__ipte_range_set_pte(range, ptep, pte);
+
+	return true;
+}
+
+bool __lazy_mmu_ptep_set_wrprotect(unsigned long addr, pte_t *ptep)
+{
+	struct ipte_range *range;
+	pte_t pte;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	pte = __ipte_range_ptep_get(range, ptep);
+	if (pte_write(pte)) {
+		pte = pte_wrprotect(pte);
+		__ipte_range_set_pte(range, ptep, pte);
+	}
+
+	return true;
+}
+
+int lazy_mmu_online_cpu(gfp_t gfp, unsigned int cpu)
+{
+	struct ipte_range *range;
+	int i;
+
+	if (!test_facility(13))
+		return 0;
+
+	range = kzalloc_obj(*range, gfp);
+	if (!range)
+		return -ENOMEM;
+	for (i = 0; i < ARRAY_SIZE(range->cache); i++)
+		range->cache[i] = __pte(PTE_POISON);
+	per_cpu(ipte_range, cpu) = range;
+
+	return 0;
+}
+
+void lazy_mmu_offline_cpu(unsigned int cpu)
+{
+	struct ipte_range *range;
+
+	if (!test_facility(13))
+		return;
+
+	range = per_cpu(ipte_range, cpu);
+	per_cpu(ipte_range, cpu) = NULL;
+	kfree(range);
+}
+
+void __init lazy_mmu_online_boot_cpu(void)
+{
+	lazy_mmu_online_cpu(GFP_ATOMIC, 0);
+}
diff --git a/arch/s390/mm/pgtable.c b/arch/s390/mm/pgtable.c
index 4acd8b140c4bd..df36523bcbbbb 100644
--- a/arch/s390/mm/pgtable.c
+++ b/arch/s390/mm/pgtable.c
@@ -166,14 +166,14 @@ pte_t ptep_xchg_lazy(struct mm_struct *mm, unsigned long addr,
 }
 EXPORT_SYMBOL(ptep_xchg_lazy);
 
-pte_t ptep_modify_prot_start(struct vm_area_struct *vma, unsigned long addr,
-			     pte_t *ptep)
+pte_t ___ptep_modify_prot_start(struct vm_area_struct *vma, unsigned long addr,
+				pte_t *ptep)
 {
 	return ptep_flush_lazy(vma->vm_mm, addr, ptep, 1);
 }
 
-void ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,
-			     pte_t *ptep, pte_t old_pte, pte_t pte)
+void ___ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,
+				pte_t *ptep, pte_t old_pte, pte_t pte)
 {
 	set_pte(ptep, pte);
 }
diff --git a/fs/proc/task_mmu.c b/fs/proc/task_mmu.c
index 229d1fc3d7f1c..916785c3b2e0b 100644
--- a/fs/proc/task_mmu.c
+++ b/fs/proc/task_mmu.c
@@ -2852,7 +2852,7 @@ static int pagemap_scan_pmd_entry(pmd_t *pmd, unsigned long start,
 		return 0;
 	}
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(vma->vm_mm, start, end, start_pte);
 
 	if ((p->arg.flags & PM_SCAN_WP_MATCHING) && !p->vec_out) {
 		/* Fast path for performing exclusive WP */
diff --git a/include/linux/kasan.h b/include/linux/kasan.h
index bf233bde68c7e..1aa4d9dd2485a 100644
--- a/include/linux/kasan.h
+++ b/include/linux/kasan.h
@@ -6,6 +6,7 @@
 #include <linux/kasan-enabled.h>
 #include <linux/kasan-tags.h>
 #include <linux/kernel.h>
+#include <linux/pgtable.h>
 #include <linux/static_key.h>
 #include <linux/types.h>
 
@@ -35,8 +36,6 @@ typedef unsigned int __bitwise kasan_vmalloc_flags_t;
 
 #if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
 
-#include <linux/pgtable.h>
-
 /* Software KASAN implementations use shadow memory. */
 
 #ifdef CONFIG_KASAN_SW_TAGS
@@ -134,6 +133,20 @@ static __always_inline void kasan_poison_slab(struct slab *slab)
 		__kasan_poison_slab(slab);
 }
 
+void __kasan_poison_pte(pte_t *pte, int nr);
+static __always_inline void kasan_poison_pte(pte_t *pte, int nr)
+{
+	if (kasan_enabled())
+		__kasan_poison_pte(pte, nr);
+}
+
+void __kasan_unpoison_pte(pte_t *pte, int nr);
+static __always_inline void kasan_unpoison_pte(pte_t *pte, int nr)
+{
+	if (kasan_enabled())
+		__kasan_unpoison_pte(pte, nr);
+}
+
 void __kasan_unpoison_new_object(struct kmem_cache *cache, void *object);
 /**
  * kasan_unpoison_new_object - Temporarily unpoison a new slab object.
@@ -414,6 +427,8 @@ static inline bool kasan_unpoison_pages(struct page *page, unsigned int order,
 	return false;
 }
 static inline void kasan_poison_slab(struct slab *slab) {}
+static inline void kasan_poison_pte(pte_t *pte, int nr) {}
+static inline void kasan_unpoison_pte(pte_t *pte, int nr) {}
 static inline void kasan_unpoison_new_object(struct kmem_cache *cache,
 					void *object) {}
 static inline void kasan_poison_new_object(struct kmem_cache *cache,
diff --git a/include/linux/pgtable.h b/include/linux/pgtable.h
index 2981e386da7b3..cc85daf30739e 100644
--- a/include/linux/pgtable.h
+++ b/include/linux/pgtable.h
@@ -271,6 +271,50 @@ static inline void lazy_mmu_mode_enable(void)
 		arch_enter_lazy_mmu_mode();
 }
 
+#ifndef arch_enter_lazy_mmu_mode_with_ptes
+static inline void arch_enter_lazy_mmu_mode_with_ptes(struct mm_struct *mm,
+		unsigned long addr, unsigned long end, pte_t *ptep)
+{
+	arch_enter_lazy_mmu_mode();
+}
+#endif
+
+/**
+ * lazy_mmu_mode_enable_with_ptes() - Enable the lazy MMU mode with a speedup hint.
+ * @mm: Address space the pages are mapped into.
+ * @addr: Start address of the range.
+ * @end: End address of the range.
+ * @ptep: Page table pointer for the first entry.
+ *
+ * Enters a new lazy MMU mode section; if the mode was not already enabled,
+ * enables it and calls arch_enter_lazy_mmu_mode_with_ptes().
+ *
+ * PTEs that fall within the specified range might observe update speedups.
+ * The PTEs must belong to the specified address space and be in the same PMD.
+ *
+ * There are no requirements on the order or range completeness of PTE
+ * updates for the specified range.
+ *
+ * Must be paired with a call to lazy_mmu_mode_disable().
+ *
+ * Has no effect if called:
+ * - While paused - see lazy_mmu_mode_pause()
+ * - In interrupt context
+ */
+static inline void lazy_mmu_mode_enable_with_ptes(struct mm_struct *mm,
+		unsigned long addr, unsigned long end, pte_t *ptep)
+{
+	struct lazy_mmu_state *state = &current->lazy_mmu_state;
+
+	if (in_interrupt() || state->pause_count > 0)
+		return;
+
+	VM_WARN_ON_ONCE(state->enable_count == U8_MAX);
+
+	if (state->enable_count++ == 0)
+		arch_enter_lazy_mmu_mode_with_ptes(mm, addr, end, ptep);
+}
+
 /**
  * lazy_mmu_mode_disable() - Disable the lazy MMU mode.
  *
@@ -387,6 +431,8 @@ static inline void lazy_mmu_mode_resume(void)
 }
 #else
 static inline void lazy_mmu_mode_enable(void) {}
+static inline void lazy_mmu_mode_enable_with_ptes(struct mm_struct *mm,
+		unsigned long addr, unsigned long end, pte_t *ptep) {}
 static inline void lazy_mmu_mode_disable(void) {}
 static inline void lazy_mmu_mode_pause(void) {}
 static inline void lazy_mmu_mode_resume(void) {}
diff --git a/mm/kasan/common.c b/mm/kasan/common.c
index b7d05c2a6d93d..94d1060949891 100644
--- a/mm/kasan/common.c
+++ b/mm/kasan/common.c
@@ -163,6 +163,20 @@ void __kasan_poison_slab(struct slab *slab)
 		     KASAN_SLAB_REDZONE, false);
 }
 
+void __kasan_poison_pte(pte_t *pte, int nr)
+{
+	if (IS_ALIGNED(sizeof(*pte), KASAN_GRANULE_SIZE))
+		kasan_poison(pte, sizeof(*pte) * nr, KASAN_LAZY_MMU_PTE, false);
+}
+EXPORT_SYMBOL_GPL(__kasan_poison_pte);
+
+void __kasan_unpoison_pte(pte_t *pte, int nr)
+{
+	if (IS_ALIGNED(sizeof(*pte), KASAN_GRANULE_SIZE))
+		kasan_unpoison(pte, sizeof(*pte) * nr, false);
+}
+EXPORT_SYMBOL_GPL(__kasan_unpoison_pte);
+
 void __kasan_unpoison_new_object(struct kmem_cache *cache, void *object)
 {
 	kasan_unpoison(object, cache->object_size, false);
diff --git a/mm/kasan/kasan.h b/mm/kasan/kasan.h
index fc9169a547662..1a2d18cdb21d9 100644
--- a/mm/kasan/kasan.h
+++ b/mm/kasan/kasan.h
@@ -144,12 +144,14 @@ static inline bool kasan_requires_meta(void)
 #define KASAN_PAGE_REDZONE	0xFE  /* redzone for kmalloc_large allocation */
 #define KASAN_SLAB_REDZONE	0xFC  /* redzone for slab object */
 #define KASAN_SLAB_FREE		0xFB  /* freed slab object */
+#define KASAN_LAZY_MMU_PTE	0xFD  /* direct pte access in lazy mmu mode */
 #define KASAN_VMALLOC_INVALID	0xF8  /* inaccessible space in vmap area */
 #else
 #define KASAN_PAGE_FREE		KASAN_TAG_INVALID
 #define KASAN_PAGE_REDZONE	KASAN_TAG_INVALID
 #define KASAN_SLAB_REDZONE	KASAN_TAG_INVALID
 #define KASAN_SLAB_FREE		KASAN_TAG_INVALID
+#define KASAN_LAZY_MMU_PTE	KASAN_TAG_INVALID
 #define KASAN_VMALLOC_INVALID	KASAN_TAG_INVALID /* only used for SW_TAGS */
 #endif
 
diff --git a/mm/kasan/report_generic.c b/mm/kasan/report_generic.c
index f5b8e37b3805c..489d4a8d6902d 100644
--- a/mm/kasan/report_generic.c
+++ b/mm/kasan/report_generic.c
@@ -113,6 +113,9 @@ static const char *get_shadow_bug_type(struct kasan_report_info *info)
 	case KASAN_SLAB_FREE_META:
 		bug_type = "slab-use-after-free";
 		break;
+	case KASAN_LAZY_MMU_PTE:
+		bug_type = "lazy-mmu-pte-access";
+		break;
 	case KASAN_ALLOCA_LEFT:
 	case KASAN_ALLOCA_RIGHT:
 		bug_type = "alloca-out-of-bounds";
diff --git a/mm/madvise.c b/mm/madvise.c
index 77552b03d318d..4419d16b39f96 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -452,7 +452,7 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
 	if (!start_pte)
 		return 0;
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);
 	for (; addr < end; pte += nr, addr += nr * PAGE_SIZE) {
 		nr = 1;
 		ptent = ptep_get(pte);
@@ -507,7 +507,7 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
 				if (!start_pte)
 					break;
 				flush_tlb_batched_pending(mm);
-				lazy_mmu_mode_enable();
+				lazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);
 				if (!err)
 					nr = 0;
 				continue;
@@ -674,7 +674,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 	if (!start_pte)
 		return 0;
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);
 	for (; addr != end; pte += nr, addr += PAGE_SIZE * nr) {
 		nr = 1;
 		ptent = ptep_get(pte);
@@ -734,7 +734,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 				if (!start_pte)
 					break;
 				flush_tlb_batched_pending(mm);
-				lazy_mmu_mode_enable();
+				lazy_mmu_mode_enable_with_ptes(mm, addr, end, pte);
 				if (!err)
 					nr = 0;
 				continue;
diff --git a/mm/memory.c b/mm/memory.c
index 6b8280cfc1dbe..0cc07b043384b 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -1272,7 +1272,7 @@ copy_pte_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
 	spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);
 	orig_src_pte = src_pte;
 	orig_dst_pte = dst_pte;
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(src_mm, addr, end, src_pte);
 
 	do {
 		nr = 1;
@@ -1922,7 +1922,7 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,
 		return addr;
 
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);
 	do {
 		bool any_skipped = false;
 
@@ -2919,7 +2919,7 @@ static int remap_pte_range(struct mm_struct *mm, pmd_t *pmd,
 	mapped_pte = pte = pte_alloc_map_lock(mm, pmd, addr, &ptl);
 	if (!pte)
 		return -ENOMEM;
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, addr, end, mapped_pte);
 	do {
 		BUG_ON(!pte_none(ptep_get(pte)));
 		if (!pfn_modify_allowed(pfn, prot)) {
@@ -3330,7 +3330,7 @@ static int apply_to_pte_range(struct mm_struct *mm, pmd_t *pmd,
 			return -EINVAL;
 	}
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, addr, end, mapped_pte);
 
 	if (fn) {
 		do {
diff --git a/mm/mprotect.c b/mm/mprotect.c
index 9cbf932b028cf..3fc26418e837d 100644
--- a/mm/mprotect.c
+++ b/mm/mprotect.c
@@ -337,7 +337,7 @@ static long change_pte_range(struct mmu_gather *tlb,
 		is_private_single_threaded = vma_is_single_threaded_private(vma);
 
 	flush_tlb_batched_pending(vma->vm_mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(vma->vm_mm, addr, end, pte);
 	do {
 		nr_ptes = 1;
 		oldpte = ptep_get(pte);
diff --git a/mm/mremap.c b/mm/mremap.c
index e9c8b1d05832b..0dfe3de39ccc7 100644
--- a/mm/mremap.c
+++ b/mm/mremap.c
@@ -260,7 +260,7 @@ static int move_ptes(struct pagetable_move_control *pmc,
 	if (new_ptl != old_ptl)
 		spin_lock_nested(new_ptl, SINGLE_DEPTH_NESTING);
 	flush_tlb_batched_pending(vma->vm_mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, old_addr, old_end, old_ptep);
 
 	for (; old_addr < old_end; old_ptep += nr_ptes, old_addr += nr_ptes * PAGE_SIZE,
 		new_ptep += nr_ptes, new_addr += nr_ptes * PAGE_SIZE) {
diff --git a/mm/vmalloc.c b/mm/vmalloc.c
index f4fa227a8d7f0..1c6ef8fd079e0 100644
--- a/mm/vmalloc.c
+++ b/mm/vmalloc.c
@@ -109,7 +109,7 @@ static int vmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 	if (!pte)
 		return -ENOMEM;
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(&init_mm, addr, end, pte);
 
 	do {
 		if (unlikely(!pte_none(ptep_get(pte)))) {
@@ -393,7 +393,7 @@ static void vunmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 	unsigned long size = PAGE_SIZE;
 
 	pte = pte_offset_kernel(pmd, addr);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(&init_mm, addr, end, pte);
 
 	do {
 #ifdef CONFIG_HUGETLB_PAGE
@@ -560,7 +560,7 @@ static int vmap_pages_pte_range(pmd_t *pmd, unsigned long addr,
 	if (!pte)
 		return -ENOMEM;
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(&init_mm, addr, end, pte);
 
 	do {
 		struct page *page = pages[*nr];

3/2 2026/08/17 12:37 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 16404
							output: 25
							thoughts: 344
						

Thoughts:
**My Initial Thoughts on the Lazy MMU Patch Series**

Okay, so we've got a new patch series here targeting the s390 architecture, and it seems to be about implementing a "lazy MMU" mode. Interesting! The list of modified files immediately catches my attention: `lazy_mmu.c`, of course, but also core memory management files like `pgtable.h`, `madvise.c`, `memory.c`, and even KASAN-related code. This suggests a pretty fundamental shift in how memory management is handled, potentially impacting a wide range of operations.

The core of the change seems to center around `lazy_mmu_mode_enable_with_ptes` calling `arch_enter_lazy_mmu_mode_with_ptes`. That makes sense; it's the entry point to activate this new mode. And it looks like the s390 implementation in `arch/s390/mm/lazy_mmu.c` is leveraging facility 13, the IPTE-range facility, to handle PTE invalidations in batches. That's a clever approach, assuming it's done efficiently.

Now, my immediate question is: Is this going to work in virtualized environments? The fact that the changes are in `core mm/` and `arch/s390/mm/` tells me it could be, but you never know. Thankfully, the text explicitly mentions that s390x QEMU/KVM supports facility 13 (IPTE-range), so that's a check in the affirmative. That's a huge relief.

Next, I need to understand the configuration implications. I see a `select ARCH_HAS_LAZY_MMU_MODE` in `arch/s390/Kconfig`. That's a clear indicator that there's probably a corresponding configuration option. I need to make sure I understand what this mode *enables* and *disables*. Time to grep the code for `ARCH_HAS_LAZY_MMU_MODE` or something similar to find the actual config name. I need to know exactly what needs to be enabled to take advantage of this new lazy MMU functionality.



4/2 2026/08/17 12:37 tool grepper
0m Args:
{
  "Expression": "ARCH_HAS_LAZY_MMU_MODE"
}

Results:
{
  "Output": "arch/arm64/Kconfig=2=config ARM64\n--\narch/arm64/Kconfig-36-\tselect ARCH_HAS_KEEPINITRD\narch/arm64/Kconfig:37:\tselect ARCH_HAS_LAZY_MMU_MODE\narch/arm64/Kconfig-38-\tselect ARCH_HAS_MEMBARRIER_SYNC_CORE\n--\narch/powerpc/platforms/Kconfig.cputype=83=config PPC_BOOK3S_64\n--\narch/powerpc/platforms/Kconfig.cputype-95-\tselect KASAN_VMALLOC if KASAN\narch/powerpc/platforms/Kconfig.cputype:96:\tselect ARCH_HAS_LAZY_MMU_MODE\narch/powerpc/platforms/Kconfig.cputype-97-\n--\narch/s390/Kconfig=75=config S390\n--\narch/s390/Kconfig-99-\tselect ARCH_HAS_KCOV\narch/s390/Kconfig:100:\tselect ARCH_HAS_LAZY_MMU_MODE\narch/s390/Kconfig-101-\tselect ARCH_HAS_MEMBARRIER_SYNC_CORE\n--\narch/sparc/Kconfig=70=config SPARC64\n--\narch/sparc/Kconfig-114-\tselect ARCH_SUPPORTS_SCHED_MC  if SMP\narch/sparc/Kconfig:115:\tselect ARCH_HAS_LAZY_MMU_MODE\narch/sparc/Kconfig-116-\tselect HAVE_GENERIC_VDSO\n--\narch/x86/Kconfig=805=config PARAVIRT_XXL\n--\narch/x86/Kconfig-807-\tdepends on X86_64\narch/x86/Kconfig:808:\tselect ARCH_HAS_LAZY_MMU_MODE\narch/x86/Kconfig-809-\n--\narch/x86/boot/compressed/misc.h-13-#undef CONFIG_PARAVIRT_SPINLOCKS\narch/x86/boot/compressed/misc.h:14:#undef CONFIG_ARCH_HAS_LAZY_MMU_MODE\narch/x86/boot/compressed/misc.h-15-#undef CONFIG_KASAN\n--\narch/x86/boot/startup/sme.c-26-#undef CONFIG_PARAVIRT_SPINLOCKS\narch/x86/boot/startup/sme.c:27:#undef CONFIG_ARCH_HAS_LAZY_MMU_MODE\narch/x86/boot/startup/sme.c-28-\n--\ninclude/linux/pgtable.h=190=static inline int pmd_dirty(pmd_t pmd)\n--\ninclude/linux/pgtable.h-247- */\ninclude/linux/pgtable.h:248:#ifdef CONFIG_ARCH_HAS_LAZY_MMU_MODE\ninclude/linux/pgtable.h-249-/**\n--\ninclude/linux/sched.h=826=struct task_struct {\n--\ninclude/linux/sched.h-1438-\ninclude/linux/sched.h:1439:#ifdef CONFIG_ARCH_HAS_LAZY_MMU_MODE\ninclude/linux/sched.h-1440-\tstruct lazy_mmu_state\t\tlazy_mmu_state;\n--\ninclude/linux/sched.h=1724=static inline char task_state_to_char(struct task_struct *tsk)\n--\ninclude/linux/sched.h-1728-\ninclude/linux/sched.h:1729:#ifdef CONFIG_ARCH_HAS_LAZY_MMU_MODE\ninclude/linux/sched.h-1730-/**\n--\nmm/Kconfig=1490=config FIND_NORMAL_PAGE\n--\nmm/Kconfig-1492-\nmm/Kconfig:1493:config ARCH_HAS_LAZY_MMU_MODE\nmm/Kconfig-1494-\tbool\n--\nmm/Kconfig=1500=config LAZY_MMU_MODE_KUNIT_TEST\nmm/Kconfig-1501-\ttristate \"KUnit tests for the lazy MMU mode\" if !KUNIT_ALL_TESTS\nmm/Kconfig:1502:\tdepends on ARCH_HAS_LAZY_MMU_MODE\nmm/Kconfig-1503-\tdepends on KUNIT\n"
}

5/2 2026/08/17 12:37 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 18369
							output: 26
							thoughts: 635
						

Thoughts:
**Analyzing the Lazy MMU Patch**

Okay, so I'm looking at this patch introducing `CONFIG_LAZY_MMU_MODE_KUNIT_TEST`. The initial clue is the addition of `lazy_mmu.c` to the `arch/s390/mm/Makefile`. It's compiled unconditionally, so it's always included when the kernel is built for s390.

Next, I need to check for any interaction with KASAN, and yep, there are changes to `include/linux/kasan.h` and `mm/kasan/common.c`, specifically adding `__kasan_poison_pte`, `__kasan_unpoison_pte`, and `KASAN_LAZY_MMU_PTE`. This means `CONFIG_KASAN` is going to be relevant when testing.

Now, let's dive into the core of `arch/s390/mm/lazy_mmu.c`. The patch introduces these functions: `arch_enter_lazy_mmu_mode`, `arch_enter_lazy_mmu_mode_with_ptes`, `arch_leave_lazy_mmu_mode`, `arch_flush_lazy_mmu_mode`, `__lazy_mmu_set_pte`, `__lazy_mmu_ptep_get`, `__lazy_mmu_ptep_test_and_clear_young`, `__lazy_mmu_ptep_get_and_clear`, `__lazy_mmu_ptep_modify_prot_start`, `__lazy_mmu_ptep_modify_prot_commit`, and `__lazy_mmu_ptep_set_wrprotect`.

These functions are key because they are invoked from `arch/s390/include/asm/pgtable.h` inline functions like `lazy_mmu_ptep_test_and_clear_young`, `set_pte`, `ptep_get`, etc. These are *very* hot path functions. The `__lazy_mmu_*` variants are the actual implementations. The prompt wants me to avoid hot-path functions. While the `__lazy_mmu_*` functions are exported and not inlined, they are called on *every* PTE operation when the lazy MMU is active. Technically they are not static, and not inlined but they are hot-path. 

However, `arch_enter_lazy_mmu_mode_with_ptes`, `arch_leave_lazy_mmu_mode`, and `arch_flush_lazy_mmu_mode` are good entry points that should be tested because they are the entry points to the lazy MMU mode.
I'll keep `arch/s390/mm/lazy_mmu.c` open as I continue to examine the exact functions involved.



6/2 2026/08/17 12:38 tool grepper
0m Args:
{
  "Expression": "^void arch_enter_lazy_mmu_mode"
}

Results:
{
  "Output": "arch/s390/include/asm/pgtable.h=1249=static __always_inline void __ptep_ipte_range(unsigned long address, int nr,\n--\narch/s390/include/asm/pgtable.h-1262-\narch/s390/include/asm/pgtable.h:1263:void arch_enter_lazy_mmu_mode_with_ptes(struct mm_struct *mm,\narch/s390/include/asm/pgtable.h-1264-\t\t\t\t\tunsigned long addr, unsigned long end,\n--\narch/s390/include/asm/pgtable.h-1267-\narch/s390/include/asm/pgtable.h:1268:void arch_enter_lazy_mmu_mode(void);\narch/s390/include/asm/pgtable.h-1269-void arch_leave_lazy_mmu_mode(void);\n--\narch/s390/mm/lazy_mmu.c=190=static void flush_lazy_mmu_mode(void)\n--\narch/s390/mm/lazy_mmu.c-212-\narch/s390/mm/lazy_mmu.c:213:void arch_enter_lazy_mmu_mode(void)\narch/s390/mm/lazy_mmu.c-214-{\n--\narch/s390/mm/lazy_mmu.c=217=EXPORT_SYMBOL_IF_KUNIT(arch_enter_lazy_mmu_mode);\narch/s390/mm/lazy_mmu.c-218-\narch/s390/mm/lazy_mmu.c:219:void arch_enter_lazy_mmu_mode_with_ptes(struct mm_struct *mm,\narch/s390/mm/lazy_mmu.c-220-\t\t\t\t\tunsigned long addr, unsigned long end,\n--\narch/sparc/include/asm/tlbflush_64.h=41=void flush_tlb_pending(void);\narch/sparc/include/asm/tlbflush_64.h:42:void arch_enter_lazy_mmu_mode(void);\narch/sparc/include/asm/tlbflush_64.h-43-void arch_flush_lazy_mmu_mode(void);\n--\narch/sparc/mm/tlb.c=25=void flush_tlb_pending(void)\n--\narch/sparc/mm/tlb.c-54-\narch/sparc/mm/tlb.c:55:void arch_enter_lazy_mmu_mode(void)\narch/sparc/mm/tlb.c-56-{\n"
}

7/2 2026/08/17 12:38 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 20359
							output: 74
							thoughts: 407
						

Thoughts:
**My Assessment of Potential Fuzzing Targets**

Okay, so I'm thinking about where to inject fuzzing into this lazy MMU implementation. The goal, as always, is to find good candidates that are both impactful and won't lead to skewed test distributions by focusing on the insanely hot paths.

Initially, I considered the core PTE manipulation functions: `__lazy_mmu_set_pte`, `__lazy_mmu_ptep_get`, `__lazy_mmu_ptep_test_and_clear_young`, and the others. These are the workhorses – direct operations on page table entries. But then the prompt reminded me to avoid generic hot-path functions, and these are *definitely* hot path, called repeatedly for every single page access. So, scratch those.

However, `arch_enter_lazy_mmu_mode_with_ptes` caught my eye again. While not directly operating on individual pages, it's called during operations that *manage* virtual memory ranges – things like `madvise`, `mprotect`, `mremap`, `vmalloc`, and what looks like `task_mmu.c`. These are syscalls, not the stuff that's happening on every single memory access. And, looking at the code, `arch_enter_lazy_mmu_mode_with_ptes` seems to be invoked once per VMA range operation. That makes it a solid candidate. It balances impact with avoiding the truly insane hot path.

Now, I should take a look at the code to confirm my initial thoughts. I'll need to go through the `arch/s390/mm/lazy_mmu.c` source file to see a complete picture of its exported and non-static functions. That'll provide a better overall context.



8/2 2026/08/17 12:38 tool grepper
0m Args:
{
  "Expression": "^[a-zA-Z_][a-zA-Z0-9_]* [a-zA-Z_][a-zA-Z0-9_]*\\(.*\\)",
  "PathPrefix": "arch/s390/mm/lazy_mmu.c"
}

Results:
{
  "Output": "arch/s390/mm/lazy_mmu.c=11=struct ipte_range {\n--\narch/s390/mm/lazy_mmu.c-20-\narch/s390/mm/lazy_mmu.c:21:static DEFINE_PER_CPU(struct ipte_range *, ipte_range);\narch/s390/mm/lazy_mmu.c-22-\n--\narch/s390/mm/lazy_mmu.c=190=static void flush_lazy_mmu_mode(void)\n--\narch/s390/mm/lazy_mmu.c-212-\narch/s390/mm/lazy_mmu.c:213:void arch_enter_lazy_mmu_mode(void)\narch/s390/mm/lazy_mmu.c-214-{\n--\narch/s390/mm/lazy_mmu.c=225=EXPORT_SYMBOL_IF_KUNIT(arch_enter_lazy_mmu_mode_with_ptes);\narch/s390/mm/lazy_mmu.c-226-\narch/s390/mm/lazy_mmu.c:227:void arch_leave_lazy_mmu_mode(void)\narch/s390/mm/lazy_mmu.c-228-{\n--\narch/s390/mm/lazy_mmu.c=231=EXPORT_SYMBOL_IF_KUNIT(arch_leave_lazy_mmu_mode);\narch/s390/mm/lazy_mmu.c-232-\narch/s390/mm/lazy_mmu.c:233:void arch_flush_lazy_mmu_mode(void)\narch/s390/mm/lazy_mmu.c-234-{\n--\narch/s390/mm/lazy_mmu.c=273=static struct ipte_range *this_ipte_range(pte_t *ptep)\n--\narch/s390/mm/lazy_mmu.c-287-\narch/s390/mm/lazy_mmu.c:288:bool __lazy_mmu_set_pte(pte_t *ptep, pte_t pte)\narch/s390/mm/lazy_mmu.c-289-{\n--\narch/s390/mm/lazy_mmu.c-300-\narch/s390/mm/lazy_mmu.c:301:bool __lazy_mmu_ptep_get(pte_t *ptep, pte_t *res)\narch/s390/mm/lazy_mmu.c-302-{\n--\narch/s390/mm/lazy_mmu.c-313-\narch/s390/mm/lazy_mmu.c:314:bool __lazy_mmu_ptep_test_and_clear_young(unsigned long addr, pte_t *ptep, int *res)\narch/s390/mm/lazy_mmu.c-315-{\n--\narch/s390/mm/lazy_mmu.c-330-\narch/s390/mm/lazy_mmu.c:331:bool __lazy_mmu_ptep_get_and_clear(unsigned long addr, pte_t *ptep, pte_t *res)\narch/s390/mm/lazy_mmu.c-332-{\n--\narch/s390/mm/lazy_mmu.c-347-\narch/s390/mm/lazy_mmu.c:348:bool __lazy_mmu_ptep_modify_prot_start(unsigned long addr, pte_t *ptep, pte_t *res)\narch/s390/mm/lazy_mmu.c-349-{\n--\narch/s390/mm/lazy_mmu.c=353=bool __lazy_mmu_ptep_modify_prot_commit(unsigned long addr, pte_t *ptep,\n--\narch/s390/mm/lazy_mmu.c-366-\narch/s390/mm/lazy_mmu.c:367:bool __lazy_mmu_ptep_set_wrprotect(unsigned long addr, pte_t *ptep)\narch/s390/mm/lazy_mmu.c-368-{\n--\narch/s390/mm/lazy_mmu.c-384-\narch/s390/mm/lazy_mmu.c:385:int lazy_mmu_online_cpu(gfp_t gfp, unsigned int cpu)\narch/s390/mm/lazy_mmu.c-386-{\n--\narch/s390/mm/lazy_mmu.c-402-\narch/s390/mm/lazy_mmu.c:403:void lazy_mmu_offline_cpu(unsigned int cpu)\narch/s390/mm/lazy_mmu.c-404-{\n"
}

9/2 2026/08/17 12:38 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 22538
							output: 37
							thoughts: 260
						

Thoughts:
**My Initial Assessment of the Lazy MMU Mode Implementation**

Okay, so I'm looking at this proposed lazy MMU mode implementation. From the initial scan, `arch_enter_lazy_mmu_mode`, `arch_enter_lazy_mmu_mode_with_ptes`, `arch_leave_lazy_mmu_mode`, and `arch_flush_lazy_mmu_mode` all seem like promising candidates for modification. The CPU online/offline functions, `lazy_mmu_online_cpu` and `lazy_mmu_offline_cpu`, also appear relevant. However, I need to be cautious about functions like `__lazy_mmu_set_pte` and similar lower-level PTE manipulation routines. While they are called from `set_pte`, which is a notoriously hot path, modifying them could introduce significant performance regressions. I'll exclude them from initial consideration to avoid any immediate impact.

Now, I need to check for any configuration dependencies. Given the context, `CONFIG_KASAN` is obviously needed to enable KASAN-related changes, but are there more specific KASAN configurations involved, such as `CONFIG_KASAN_GENERIC` or `CONFIG_KASAN_SW_TAGS` that I need to be aware of? The patch touches `mm/kasan/common.c` and `include/linux/kasan.h`, which suggests KASAN integration is crucial to the design. The addition of `__kasan_poison_pte` and `__kasan_unpoison_pte` functions reinforces this.

I'll specifically need to examine `mm/kasan/common.c` in detail to confirm that the relevant sections are indeed compiled only when `CONFIG_KASAN` is defined. This is a critical point to ensure we don't introduce KASAN-specific code into non-KASAN builds.



10/2 2026/08/17 12:38 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "^obj-\\$\\(CONFIG_KASAN\\)",
  "PathPrefix": "mm/kasan/Makefile"
}

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

11/2 2026/08/17 12:38 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 23256
							output: 32
							thoughts: 12
						

Thoughts:
Here's a summary of my immediate thought process, as if I were considering the provided text:

**Diving into the KASAN Makefile**

Okay, time to dig into the `mm/kasan/Makefile`. I know this is where the core build configuration for Kernel Address Sanitizer (KASAN) within the memory management subsystem resides. I need to refresh my memory on what's defined there, and how they hook into the broader build process. Specifically, I'm thinking I should:

*   **Quickly scan for key variables:** I expect to find flags related to compiler options, object file inclusion, and potentially some conditional compilation based on the target architecture or specific KASAN features.
*   **Look for dependencies:** I need to understand how this Makefile interacts with other `Makefiles` in the memory management (`mm`) directory and potentially higher up in the kernel's build hierarchy. What does it depend on, and what does it provide?
*   **Focus on the build targets:** Are there specific targets defined for different KASAN components, or is it a more monolithic build approach? This will help me understand how modular the KASAN implementation is.
*   **Identify potential entry points:** Look for where the KASAN code gets integrated during kernel compilation, which could be critical for understanding how it fits into the overall memory management scheme.

Essentially, I want to form a mental model of KASAN's build process within the memory management framework. Let's see what's in there.



12/2 2026/08/17 12:38 tool read-file
0m Args:
{
  "File": "mm/kasan/Makefile",
  "FirstLine": 1,
  "LineCount": 20
}

Results:
{
  "Contents": "   1:\t# SPDX-License-Identifier: GPL-2.0\n   2:\tKASAN_SANITIZE := n\n   3:\tUBSAN_SANITIZE := n\n   4:\tKCOV_INSTRUMENT := n\n   5:\t\n   6:\t# Disable ftrace to avoid recursion.\n   7:\tCFLAGS_REMOVE_common.o = $(CC_FLAGS_FTRACE)\n   8:\tCFLAGS_REMOVE_generic.o = $(CC_FLAGS_FTRACE)\n   9:\tCFLAGS_REMOVE_init.o = $(CC_FLAGS_FTRACE)\n  10:\tCFLAGS_REMOVE_quarantine.o = $(CC_FLAGS_FTRACE)\n  11:\tCFLAGS_REMOVE_report.o = $(CC_FLAGS_FTRACE)\n  12:\tCFLAGS_REMOVE_report_generic.o = $(CC_FLAGS_FTRACE)\n  13:\tCFLAGS_REMOVE_report_hw_tags.o = $(CC_FLAGS_FTRACE)\n  14:\tCFLAGS_REMOVE_report_sw_tags.o = $(CC_FLAGS_FTRACE)\n  15:\tCFLAGS_REMOVE_shadow.o = $(CC_FLAGS_FTRACE)\n  16:\tCFLAGS_REMOVE_hw_tags.o = $(CC_FLAGS_FTRACE)\n  17:\tCFLAGS_REMOVE_sw_tags.o = $(CC_FLAGS_FTRACE)\n  18:\t\n  19:\t# Function splitter causes unnecessary splits in __asan_load1/__asan_store1\n  20:\t# see: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63533\n"
}

13/2 2026/08/17 12:38 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 24020
							output: 33
							thoughts: 6
						

Thoughts:
Okay, I'm ready. Let's see what we've got. Here's a summary:

**Digging Deeper: The Urge to Understand**

Alright, so the initial thought is clear: I need to delve further. It's that immediate, almost involuntary reaction I get when presented with something new, something that sparks my curiosity. My experience has taught me that initial surface impressions are rarely enough. There's always a hidden complexity, a nuance, a deeper layer of meaning waiting to be uncovered. Reading more, analyzing more, investigating more – that's the only way to truly grasp the subject at hand and assess its significance within the broader framework of my expertise. This urge, this compulsion to understand thoroughly, is fundamental to how I approach my work.



14/2 2026/08/17 12:38 tool read-file
0m Args:
{
  "File": "mm/kasan/Makefile",
  "FirstLine": 21,
  "LineCount": 20
}

Results:
{
  "Contents": "  21:\tCC_FLAGS_KASAN_RUNTIME := $(call cc-option, -fno-conserve-stack)\n  22:\tCC_FLAGS_KASAN_RUNTIME += -fno-stack-protector\n  23:\t# Disable branch tracing to avoid recursion.\n  24:\tCC_FLAGS_KASAN_RUNTIME += -DDISABLE_BRANCH_PROFILING\n  25:\t\n  26:\tCFLAGS_common.o := $(CC_FLAGS_KASAN_RUNTIME)\n  27:\tCFLAGS_generic.o := $(CC_FLAGS_KASAN_RUNTIME)\n  28:\tCFLAGS_init.o := $(CC_FLAGS_KASAN_RUNTIME)\n  29:\tCFLAGS_quarantine.o := $(CC_FLAGS_KASAN_RUNTIME)\n  30:\tCFLAGS_report.o := $(CC_FLAGS_KASAN_RUNTIME)\n  31:\tCFLAGS_report_generic.o := $(CC_FLAGS_KASAN_RUNTIME)\n  32:\tCFLAGS_report_hw_tags.o := $(CC_FLAGS_KASAN_RUNTIME)\n  33:\tCFLAGS_report_sw_tags.o := $(CC_FLAGS_KASAN_RUNTIME)\n  34:\tCFLAGS_shadow.o := $(CC_FLAGS_KASAN_RUNTIME)\n  35:\tCFLAGS_hw_tags.o := $(CC_FLAGS_KASAN_RUNTIME)\n  36:\tCFLAGS_sw_tags.o := $(CC_FLAGS_KASAN_RUNTIME)\n  37:\t\n  38:\tCFLAGS_KASAN_TEST := $(CFLAGS_KASAN)\n  39:\tifndef CONFIG_CC_HAS_KASAN_MEMINTRINSIC_PREFIX\n  40:\t# If compiler instruments memintrinsics by prefixing them with __asan/__hwasan,\n"
}

15/2 2026/08/17 12:38 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 24680
							output: 33
							thoughts: 6
						

Thoughts:
Okay, I'm ready. Let's see what we've got. Here's a summary:

**Digging Deeper: The Urge to Understand**

Alright, so the initial thought is clear: I need to delve further. It's that immediate, almost involuntary reaction I get when presented with something new, something that sparks my curiosity. My experience has taught me that initial surface impressions are rarely enough. There's always a hidden complexity, a nuance, a deeper layer of meaning waiting to be uncovered. Reading more, analyzing more, investigating more – that's the only way to truly grasp the subject at hand and assess its significance within the broader framework of my expertise. This urge, this compulsion to understand thoroughly, is fundamental to how I approach my work.



16/2 2026/08/17 12:39 tool read-file
0m Args:
{
  "File": "mm/kasan/Makefile",
  "FirstLine": 41,
  "LineCount": 20
}

Results:
{
  "Contents": "  41:\t# we need to treat them normally (as builtins), otherwise the compiler won't\n  42:\t# recognize them as instrumentable. If it doesn't instrument them, we need to\n  43:\t# pass -fno-builtin, so the compiler doesn't inline them.\n  44:\tCFLAGS_KASAN_TEST += -fno-builtin\n  45:\tendif\n  46:\t\n  47:\tCFLAGS_REMOVE_kasan_test_c.o += $(call cc-option, -Wvla-larger-than=1)\n  48:\tCFLAGS_kasan_test_c.o := $(CFLAGS_KASAN_TEST)\n  49:\tRUSTFLAGS_kasan_test_rust.o := $(RUSTFLAGS_KASAN)\n  50:\t\n  51:\tobj-y := common.o report.o\n  52:\tobj-$(CONFIG_KASAN_GENERIC) += init.o generic.o report_generic.o shadow.o quarantine.o\n  53:\tobj-$(CONFIG_KASAN_HW_TAGS) += hw_tags.o report_hw_tags.o tags.o report_tags.o\n  54:\tobj-$(CONFIG_KASAN_SW_TAGS) += init.o report_sw_tags.o shadow.o sw_tags.o tags.o report_tags.o\n  55:\t\n  56:\tkasan_test-objs := kasan_test_c.o\n  57:\tifdef CONFIG_RUST\n  58:\t\tkasan_test-objs += kasan_test_rust.o\n  59:\tendif\n  60:\t\n"
}

17/2 2026/08/17 12:39 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 25305
							output: 34
							thoughts: 49
						

Thoughts:
**My KASAN Compilation Deduction**

Okay, so I'm tracing through this compilation process and something is immediately clear: `common.o` in the kernel build is unconditionally compiled *if* the KASAN memory safety feature is enabled. I know from experience that `mm/kasan` is the specific subsystem responsible for KASAN, so that's where to start. The crucial piece here is figuring out the condition that gets us *into* `mm/kasan`. Based on this, it's pretty obvious that `CONFIG_KASAN` is the deciding factor.

Time to confirm my suspicions and verify the dependencies. I should check the `mm/Makefile` to see how the build system handles the `mm/kasan` directory and what conditions are specified. A quick look at the Makefile should tell me if the `CONFIG_KASAN` option truly dictates the entry into `mm/kasan` and, therefore, the inclusion of `common.o`. Let me dive in and verify this.



18/2 2026/08/17 12:39 tool grepper
0m Args:
{
  "Expression": "^obj-\\$\\(CONFIG_KASAN\\)",
  "PathPrefix": "mm/Makefile"
}

Results:
{
  "Output": "mm/Makefile=91=obj-$(CONFIG_PAGE_POISONING) += page_poison.o\nmm/Makefile:92:obj-$(CONFIG_KASAN)\t+= kasan/\nmm/Makefile-93-obj-$(CONFIG_KFENCE) += kfence/\n"
}

19/2 2026/08/17 12:39 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 25682
							output: 37
							thoughts: 1155
						

Thoughts:
**Analyzing the KASAN and Lazy MMU Mode Integration**

Okay, so I'm looking at these changes related to KASAN and the lazy MMU mode, specifically for s390. First off, I'm thinking, `CONFIG_KASAN` is definitely a prerequisite, that's clear. But are there any other crucial configurations to consider? The patch adds `select ARCH_HAS_LAZY_MMU_MODE` to `arch/s390/Kconfig`. Right, lazy MMU mode gets enabled if `ARCH_HAS_LAZY_MMU_MODE` is set. Now, `ARCH_HAS_LAZY_MMU_MODE` is a boolean config defined in `mm/Kconfig`, it seems to be selected by `S390` itself. Therefore, on the s390 architecture, the lazy MMU mode is inherently enabled.

Now, aside from `CONFIG_KASAN`, what other configurations should be highlighted? The patch touches `mm/kasan/report_generic.c`, which is only compiled if `CONFIG_KASAN_GENERIC` is enabled. Seems like `KASAN_GENERIC` is going to be another relevant configuration.

Digging deeper, I examined `arch_enter_lazy_mmu_mode_with_ptes` in `arch/s390/mm/lazy_mmu.c`. It's calling `enter_ipte_range`, which, in turn, is using `test_facility(13)`. Facility 13 represents the IPTE-range facility, and thankfully, it's available on z10 and later systems and is supported in QEMU.

So, the key functions to keep in mind are: `arch_enter_lazy_mmu_mode`, `arch_enter_lazy_mmu_mode_with_ptes`, `arch_leave_lazy_mmu_mode`, `arch_flush_lazy_mmu_mode`, `lazy_mmu_online_cpu`, and `lazy_mmu_offline_cpu`.

Just double-checking, I see `lazy_mmu_online_cpu` and `lazy_mmu_offline_cpu` aren't static functions. And, the other `arch_*` functions are exported using `EXPORT_SYMBOL_IF_KUNIT`. Okay, these are not static, either.

Now, are there any other potential entry points I should be aware of? Yes! `lazy_mmu_mode_enable_with_ptes` is called in various places: `fs/proc/task_mmu.c`, `mm/madvise.c`, `mm/memory.c`, `mm/mprotect.c`, `mm/mremap.c`, and `mm/vmalloc.c`. Interesting.

All of these are inline functions in `include/linux/pgtable.h` that call `arch_enter_lazy_mmu_mode_with_ptes`, making `arch_enter_lazy_mmu_mode_with_ptes` the main entry point to focus on.

Circling back to the configurations, is `KASAN_GENERIC` definitely the right one? Let's confirm by looking at `mm/kasan/report_generic.c` again. The code includes a case for `KASAN_LAZY_MMU_PTE`, which triggers the "lazy-mmu-pte-access" bug type. This code path is compiled only if `CONFIG_KASAN_GENERIC` is enabled. Therefore, `KASAN_GENERIC` is definitely a relevant configuration to consider.

Now, what about `KASAN_SW_TAGS` or `KASAN_HW_TAGS`? The patch modifies `mm/kasan/kasan.h` to introduce `KASAN_LAZY_MMU_PTE`, which is used in `report_generic.c`. But there's no modification of `report_sw_tags.c` or `report_hw_tags.c`. So it looks like `KASAN_GENERIC` is the most pertinent one here.

Finally, are there any other related configurations? `CONFIG_SMP` is required due to the `smp.c` changes, but that's usually enabled. Now, regarding `CONFIG_HOTPLUG_CPU`, is it needed for `__cpu_die` and `__cpu_up` within `smp.c`? Okay, time to check `arch/s390/kernel/smp.c` to confirm.



20/2 2026/08/17 12:39 tool grepper
0m Args:
{
  "Expression": "__cpu_up",
  "PathPrefix": "arch/s390/kernel/smp.c"
}

Results:
{
  "Output": "arch/s390/kernel/smp.c=824=static void smp_start_secondary(void *cpuvoid)\n--\narch/s390/kernel/smp.c-855-/* Upping and downing of CPUs */\narch/s390/kernel/smp.c:856:int __cpu_up(unsigned int cpu, struct task_struct *tidle)\narch/s390/kernel/smp.c-857-{\n"
}

21/2 2026/08/17 12:39 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 28007
							output: 37
							thoughts: 39
						

Thoughts:
Okay, here's my interpretation of those statements, as if they were my own immediate thoughts as an embedded systems expert:

**CPU Hotplug & SMP Compilation Considerations**

Right, so these macros, `__cpu_up` and `__cpu_die`, are clearly related to the handling of CPUs within the kernel. The first one, `__cpu_up`, is only going to be compiled into the system when `CONFIG_SMP` is enabled. That makes perfect sense; `__cpu_up` is likely related to bringing a CPU online, which is a feature primarily associated with Symmetric Multi-Processing. If you only have a single CPU, you typically don't need to dynamically 'bring it up'.

Now, looking at `__cpu_die`, this one's conditional on `CONFIG_HOTPLUG_CPU`. This immediately tells me that this macro is for taking a CPU offline, which is a core concept to CPU hotplug. If hotplugging is disabled at compile time, then there's no way to bring a CPU offline (aside from powering the system down or otherwise). I would expect the functionality of `__cpu_die` to be for the reverse of `__cpu_up`. It would likely involve a series of steps to safely take a CPU off the system, such as migrating any relevant processes off that CPU, flushing caches, and potentially stopping execution on that CPU.

Essentially, these preprocessor directives tell me how the code is structured to handle SMP and CPU hotplug. The inclusion or exclusion of these functions is directly tied to the defined compile-time configuration of the system. I would next look at these functions themselves to figure out the exact mechanisms and the architecture that they handle to best understand CPU handling for that system.



22/2 2026/08/17 12:39 tool grepper
0m Args:
{
  "Expression": "__cpu_die",
  "PathPrefix": "arch/s390/kernel/smp.c"
}

Results:
{
  "Output": "arch/s390/kernel/smp.c=899=int __cpu_disable(void)\n--\narch/s390/kernel/smp.c-921-\narch/s390/kernel/smp.c:922:void __cpu_die(unsigned int cpu)\narch/s390/kernel/smp.c-923-{\n"
}

23/2 2026/08/17 12:39 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 28555
							output: 39
							thoughts: 113
						

Thoughts:
**My Configuration Testing Plan**

Okay, so I'm thinking, `HOTPLUG_CPU` could really help me nail down the CPU hotplug code paths during testing. That seems like a good avenue to explore further.

Let's see, I need to keep track of the configurations involved. I'm focusing on:

*   `KASAN`
*   `KASAN_GENERIC`
*   `HOTPLUG_CPU`

Wait a minute, `KASAN_GENERIC` is dependent on `KASAN` right? Of course, that's important to remember. Now, I have to ensure the full test suite can even be executed.

Is `KASAN` even supported on the s390 architecture? Let me check quickly... Ah, yes, according to the `arch/s390/Kconfig` file, it *does* have `select ARCH_HAS_KASAN_GENERIC`. Great. Now I can continue my planning.



24/2 2026/08/17 12:39 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "ARCH_HAS_KASAN_GENERIC",
  "PathPrefix": "arch/s390/Kconfig"
}

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

25/2 2026/08/17 12:39 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 28937
							output: 32
							thoughts: 17
						

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

**Kernel AddressSanitizer (KASAN) Considerations**

Okay, so I'm diving into this kernel code and need to understand the memory safety context. The first thing that pops into my mind is whether KASAN, or something analogous, is enabled. I need to quickly establish if we're running with this runtime memory error detection, which would fundamentally alter how I interpret potential issues. I'll need to check the preprocessor definitions. I'll be looking for `HAVE_ARCH_KASAN`, or something equivalent. That's the baseline, the most direct indicator, but it could be named differently depending on the architecture. Also, I'll keep in mind that other instrumentation could be enabled, and I'll keep this in the back of my head to check for in the surrounding code.



26/2 2026/08/17 12:40 tool grepper
0m Args:
{
  "Expression": "KASAN",
  "PathPrefix": "arch/s390/Kconfig"
}

Results:
{
  "Output": "arch/s390/Kconfig=38=config ARCH_SUPPORTS_UPROBES\n--\narch/s390/Kconfig-40-\narch/s390/Kconfig:41:config KASAN_SHADOW_OFFSET\narch/s390/Kconfig-42-\thex\narch/s390/Kconfig:43:\tdepends on KASAN\narch/s390/Kconfig-44-\tdefault 0x1C000000000000\n--\narch/s390/Kconfig=75=config S390\n--\narch/s390/Kconfig-183-\tselect HAVE_ARCH_JUMP_LABEL_RELATIVE\narch/s390/Kconfig:184:\tselect HAVE_ARCH_KASAN\narch/s390/Kconfig:185:\tselect HAVE_ARCH_KASAN_VMALLOC\narch/s390/Kconfig-186-\tselect HAVE_ARCH_KCSAN\n--\narch/s390/Kconfig-260-\tselect IRQ_MSI_LIB\t\tif PCI\narch/s390/Kconfig:261:\tselect KASAN_VMALLOC if KASAN\narch/s390/Kconfig-262-\tselect LOCK_MM_AND_FIND_VMA\n--\narch/s390/Kconfig=688=config KERNEL_IMAGE_BASE\narch/s390/Kconfig-689-\thex \"Kernel image base address\"\narch/s390/Kconfig:690:\trange 0x100000 0x1FFFFFE0000000 if !KASAN\narch/s390/Kconfig:691:\trange 0x100000 0x1BFFFFE0000000 if KASAN\narch/s390/Kconfig:692:\tdefault 0x3FFE0000000 if !KASAN\narch/s390/Kconfig:693:\tdefault 0x7FFFE0000000 if KASAN\narch/s390/Kconfig-694-\thelp\n"
}

27/2 2026/08/17 12:40 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 29747
							output: 35
							thoughts: 419
						

Thoughts:
**My Configuration Assessment for Testing `LAZY_MMU_MODE`**

Okay, so `HAVE_ARCH_KASAN` is definitely enabled, good start. Now, let's look at `LAZY_MMU_MODE_KUNIT_TEST`. I found it in `mm/Kconfig`, which is a good sign. The prompt wants me to identify any *specific* CONFIG options required to properly test this feature. Since it's a KUnit test, my initial instinct is to enable it. Even though syzkaller doesn't typically run KUnit tests unless explicitly configured, having it enabled *could* be beneficial, you never know. It's a KUnit test, so, I will just enable it.

Now, as per the prompt, I also need to make sure `KASAN`, `KASAN_GENERIC`, and `HOTPLUG_CPU` are included to thoroughly test this new feature, and ensure coverage.

Next, I need to identify the key functions to focus on for this testing. Looking at the implementation, I want to keep a close eye on: `arch_enter_lazy_mmu_mode`, `arch_enter_lazy_mmu_mode_with_ptes`, `arch_leave_lazy_mmu_mode`, `arch_flush_lazy_mmu_mode`, `lazy_mmu_online_cpu`, and `lazy_mmu_offline_cpu`. These seem like the core entry/exit points and management functions for `LAZY_MMU_MODE`.

Wait, there are more, and not static either. I see `__kasan_poison_pte` and `__kasan_unpoison_pte` in `mm/kasan/common.c`. These are exported and are called by the inline functions, `kasan_poison_pte` and `kasan_unpoison_pte`. They are *excellent* candidates for testing! I will keep these on the list, too. I'll need to double check `__kasan_poison_pte`.



28/2 2026/08/17 12:40 tool grepper
0m Args:
{
  "Expression": "__kasan_poison_pte",
  "PathPrefix": "mm/kasan/common.c"
}

Results:
{
  "Output": "mm/kasan/common.c=155=void __kasan_poison_slab(struct slab *slab)\n--\nmm/kasan/common.c-165-\nmm/kasan/common.c:166:void __kasan_poison_pte(pte_t *pte, int nr)\nmm/kasan/common.c-167-{\n--\nmm/kasan/common.c-170-}\nmm/kasan/common.c:171:EXPORT_SYMBOL_GPL(__kasan_poison_pte);\nmm/kasan/common.c-172-\n"
}

29/2 2026/08/17 12:40 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 30802
							output: 42
							thoughts: 562
						

Thoughts:
**Lazy MMU Functionality Assessment**

Okay, so I'm evaluating the functions within `arch/s390/mm/lazy_mmu.c`. I've confirmed that these functions, like the ones dealing with PTE manipulation, are indeed exported. Now, let's see if there are other functions in this file that are relevant. Looking at the codebase, I see a set of functions: `__lazy_mmu_set_pte`, `__lazy_mmu_ptep_get`, `__lazy_mmu_ptep_test_and_clear_young`, `__lazy_mmu_ptep_get_and_clear`, `__lazy_mmu_ptep_modify_prot_start`, `__lazy_mmu_ptep_modify_prot_commit`, and `__lazy_mmu_ptep_set_wrprotect`.

These aren't static, but they *are* called from inline functions within `pgtable.h`. The question becomes whether these are hot path functions. It seems they are; they're called during every PTE operation when the lazy MMU mode is active. However, wait, lazy MMU mode is only enabled for specific operations like `madvise`, `mprotect`, `mremap`, `vmalloc`, and operations within `task_mmu.c`. It's not a global, always-on feature. `is_lazy_mmu_active()` checks `get_lowcore()->lazy_mmu_count`. So, these particular functions are only executed when `lazy_mmu_count > 0`. This is clearly part of the new feature.

The prompt instructs me to avoid listing generic hot-path functions, preferring non-static, non-inlined API entrypoints over internal static helper functions. But, since these are the core implementations of the lazy MMU mode PTE operations, and they are non-static, they are potential candidates.

However, the primary entry points are `arch_enter_lazy_mmu_mode_with_ptes`. After careful consideration, I'll include the following functions: `arch_enter_lazy_mmu_mode`, `arch_enter_lazy_mmu_mode_with_ptes`, `arch_leave_lazy_mmu_mode`, `arch_flush_lazy_mmu_mode`, `lazy_mmu_online_cpu`, `lazy_mmu_offline_cpu`, `__kasan_poison_pte`, and `__kasan_unpoison_pte`. These represent the primary control and management points of the lazy MMU mode.

Moving on, I'll examine `arch/s390/mm/pgtable.c`. I find `___ptep_modify_prot_start` and `___ptep_modify_prot_commit`. These were introduced by the patch as well.



30/2 2026/08/17 12:40 tool grepper
0m Args:
{
  "Expression": "___ptep_modify_prot_start",
  "PathPrefix": "arch/s390/mm/pgtable.c"
}

Results:
{
  "Output": "arch/s390/mm/pgtable.c=167=EXPORT_SYMBOL(ptep_xchg_lazy);\narch/s390/mm/pgtable.c-168-\narch/s390/mm/pgtable.c:169:pte_t ___ptep_modify_prot_start(struct vm_area_struct *vma, unsigned long addr,\narch/s390/mm/pgtable.c-170-\t\t\t\tpte_t *ptep)\n"
}

31/2 2026/08/17 12:40 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 32161
							output: 268
							thoughts: 1286
						

Thoughts:
**Analyzing the Configuration and Focus for Fuzzing**

Okay, let's break this down. First, yes, these considerations are not static; they evolve with the analysis. I need to make sure I'm capturing everything, so I'll review the configs one more time.

Alright, the core configurations I need to consider are `KASAN`, `KASAN_GENERIC`, and `HOTPLUG_CPU`. I initially looked at `LAZY_MMU_MODE_KUNIT_TEST`, but since syzkaller doesn't typically run KUnit tests by default, I'll skip that one for now. It doesn't hurt to include it, but it's not essential.

Now, a quick check regarding `LAZY_MMU_MODE_KUNIT_TEST`. Does this patch actually *add* that config? Let's verify by checking `mm/Kconfig` against the patch. The diff doesn't show any modifications to `mm/Kconfig`. Hang on... the grep I did for `ARCH_HAS_LAZY_MMU_MODE` showed `LAZY_MMU_MODE_KUNIT_TEST` in `mm/Kconfig`, but the patch diff doesn't include `mm/Kconfig`. Ah, the patch diff I was provided: (follows the patch diff provided in the prompt). Okay, so `mm/Kconfig` isn't modified. It must have been added in an earlier patch or is already present in the tree. The prompt says, "The changes have ALREADY been applied and committed as the HEAD commit in your workspace." This means that `ARCH_HAS_LAZY_MMU_MODE` is *already* in `mm/Kconfig`. The current patch *does* add `select ARCH_HAS_LAZY_MMU_MODE` to `arch/s390/Kconfig`. Thus, `ARCH_HAS_LAZY_MMU_MODE` is automatically selected on s390, so there's no need to explicitly specify it.

Regarding `KASAN`, it's a standard config. `KASAN_GENERIC` is also standard. And `HOTPLUG_CPU` is also standard.

Therefore, the final output will be:

**WorthFuzzing: true**

**FocusSymbols:**

-   `arch_enter_lazy_mmu_mode`
-   `arch_enter_lazy_mmu_mode_with_ptes`
-   `arch_leave_lazy_mmu_mode`
-   `arch_flush_lazy_mmu_mode`
-   `lazy_mmu_online_cpu`
-   `lazy_mmu_offline_cpu`
-   `__kasan_poison_pte`
-   `__kasan_unpoison_pte`
-   `___ptep_modify_prot_start`
-   `___ptep_modify_prot_commit`

**EnableConfigs:**

-   `KASAN`
-   `KASAN_GENERIC`
-   `HOTPLUG_CPU`

**Reasoning:**

The patch introduces lazy MMU mode for the s390 architecture, leveraging the IPTE-range facility for batched PTE invalidations. It's hooking into key memory management syscalls (madvise, mprotect, mremap, vmalloc) and CPU hotplug paths. Crucially, it includes KASAN instrumentation for direct PTE access in lazy MMU mode. These are fundamental kernel and architecture-specific memory management paths ripe for fuzzing.



32/2 2026/08/17 12:41 tool set-results
0m Args:
{
  "EnableConfigs": [
    "KASAN",
    "KASAN_GENERIC",
    "HOTPLUG_CPU"
  ],
  "FocusSymbols": [
    "arch_enter_lazy_mmu_mode",
    "arch_enter_lazy_mmu_mode_with_ptes",
    "arch_leave_lazy_mmu_mode",
    "arch_flush_lazy_mmu_mode",
    "lazy_mmu_online_cpu",
    "lazy_mmu_offline_cpu",
    "__kasan_poison_pte",
    "__kasan_unpoison_pte",
    "___ptep_modify_prot_start",
    "___ptep_modify_prot_commit"
  ],
  "Reasoning": "The patch introduces lazy MMU mode for s390 architecture, which batches PTE invalidations using the IPTE-range facility. It hooks into core memory management syscalls (madvise, mprotect, mremap, vmalloc) and CPU hotplug paths. It also adds KASAN instrumentation for direct PTE accesses in lazy MMU mode. These are reachable core kernel and architecture-specific memory management paths that should be fuzzed.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "KASAN",
    "KASAN_GENERIC",
    "HOTPLUG_CPU"
  ],
  "FocusSymbols": [
    "arch_enter_lazy_mmu_mode",
    "arch_enter_lazy_mmu_mode_with_ptes",
    "arch_leave_lazy_mmu_mode",
    "arch_flush_lazy_mmu_mode",
    "lazy_mmu_online_cpu",
    "lazy_mmu_offline_cpu",
    "__kasan_poison_pte",
    "__kasan_unpoison_pte",
    "___ptep_modify_prot_start",
    "___ptep_modify_prot_commit"
  ],
  "Reasoning": "The patch introduces lazy MMU mode for s390 architecture, which batches PTE invalidations using the IPTE-range facility. It hooks into core memory management syscalls (madvise, mprotect, mremap, vmalloc) and CPU hotplug paths. It also adds KASAN instrumentation for direct PTE accesses in lazy MMU mode. These are reachable core kernel and architecture-specific memory management paths that should be fuzzed.",
  "WorthFuzzing": true
}

33/1 2026/08/17 12:41 action If
0m Args:
{
  "WorthFuzzing": true
}

Results:
null

34/2 2026/08/17 12:41 agent kmsan-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "KMSANReasoning": "The patch introduces a lazy MMU mode for the s390 architecture to buffer page table entry (PTE) updates. It also explicitly adds KASAN poisoning/unpoisoning to detect invalid direct accesses to PTEs while the lazy MMU mode is active, introducing a new KASAN bug type (`lazy-mmu-pte-access`). The changes are strictly confined to internal memory management and page table operations. No new structures are copied to user space, and all newly allocated structures (e.g., `struct ipte_range`) are properly initialized (e.g., via zero-allocation and explicit initialization of the `cache` array to `PTE_POISON`). There is no risk of uninitialized memory usage or kernel-to-user info-leaks introduced by this patch. Standard KASAN is perfectly suited and explicitly enhanced by this patch to catch any bugs related to these changes.",
  "NeedsKMSAN": false
}

Instruction:
You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.

CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
  a comprehensive suite of debugging tools and sanitizers, including KASAN
  (out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
  (locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
  or page allocations) and kernel-to-user memory info-leaks.

Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.

Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.

Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
   attributes, ioctl output arguments, socket options, or BPF buffers) where fields
   or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
   or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
   could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
   uninitialized bytes of existing buffers.

Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
  dereferences, locking deadlocks, or use-after-free bugs (these are already caught
  by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
  or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.

Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
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 36c12673f35ddf2a14791164b5dcf9d4c3cf54c2
Author: syz-cluster <triage@syzkaller.com>
Date:   Mon Aug 17 12:37:42 2026 +0000

    syz-cluster: applied patch under review

diff --git a/arch/s390/Kconfig b/arch/s390/Kconfig
index 84404e6778d50..7846332dcd0a6 100644
--- a/arch/s390/Kconfig
+++ b/arch/s390/Kconfig
@@ -97,6 +97,7 @@ config S390
 	select ARCH_HAS_GCOV_PROFILE_ALL
 	select ARCH_HAS_GIGANTIC_PAGE
 	select ARCH_HAS_KCOV
+	select ARCH_HAS_LAZY_MMU_MODE
 	select ARCH_HAS_MEMBARRIER_SYNC_CORE
 	select ARCH_HAS_MEM_ENCRYPT
 	select ARCH_HAS_NMI_SAFE_THIS_CPU_OPS
diff --git a/arch/s390/include/asm/lazy_mmu.h b/arch/s390/include/asm/lazy_mmu.h
new file mode 100644
index 0000000000000..98366e9de9bc3
--- /dev/null
+++ b/arch/s390/include/asm/lazy_mmu.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __LAZY_MMU_H
+#define __LAZY_MMU_H
+
+void lazy_mmu_online_boot_cpu(void);
+int lazy_mmu_online_cpu(gfp_t gfp, unsigned int cpu);
+void lazy_mmu_offline_cpu(unsigned int cpu);
+
+#endif /* __LAZY_MMU_H */
diff --git a/arch/s390/include/asm/lowcore.h b/arch/s390/include/asm/lowcore.h
index 3b3ecc647993f..dba236664da90 100644
--- a/arch/s390/include/asm/lowcore.h
+++ b/arch/s390/include/asm/lowcore.h
@@ -163,7 +163,7 @@ struct lowcore {
 	__s32	preempt_count;			/* 0x03a8 */
 	__u32	spinlock_lockval;		/* 0x03ac */
 	__u32	spinlock_index;			/* 0x03b0 */
-	__u8	pad_0x03b4[0x03b8-0x03b4];	/* 0x03b4 */
+	__s32	lazy_mmu_count;			/* 0x03b4 */
 	__u64	percpu_offset;			/* 0x03b8 */
 	__u8	percpu_register;		/* 0x03c0 */
 	__u8	pad_0x03c1[0x0400-0x03c1];	/* 0x03c1 */
diff --git a/arch/s390/include/asm/pgtable.h b/arch/s390/include/asm/pgtable.h
index 859ce7c7d4544..1e8a79989ed6e 100644
--- a/arch/s390/include/asm/pgtable.h
+++ b/arch/s390/include/asm/pgtable.h
@@ -39,6 +39,64 @@ enum {
 
 extern atomic_long_t direct_pages_count[PG_DIRECT_MAP_MAX];
 
+bool __lazy_mmu_ptep_test_and_clear_young(unsigned long addr, pte_t *ptep, int *res);
+bool __lazy_mmu_ptep_get_and_clear(unsigned long addr, pte_t *ptep, pte_t *res);
+bool __lazy_mmu_ptep_modify_prot_start(unsigned long addr, pte_t *ptep, pte_t *res);
+bool __lazy_mmu_ptep_modify_prot_commit(unsigned long addr, pte_t *ptep, pte_t old_pte, pte_t pte);
+bool __lazy_mmu_ptep_set_wrprotect(unsigned long addr, pte_t *ptep);
+bool __lazy_mmu_set_pte(pte_t *ptep, pte_t pte);
+bool __lazy_mmu_ptep_get(pte_t *ptep, pte_t *res);
+
+static __always_inline bool is_lazy_mmu_active(void)
+{
+	if (__is_defined(__DECOMPRESSOR))
+		return false;
+	if (!get_lowcore()->lazy_mmu_count)
+		return false;
+	return true;
+}
+
+static inline
+bool lazy_mmu_ptep_test_and_clear_young(unsigned long addr, pte_t *ptep, int *res)
+{
+	if (!is_lazy_mmu_active())
+		return false;
+	return __lazy_mmu_ptep_test_and_clear_young(addr, ptep, res);
+}
+
+static inline
+bool lazy_mmu_ptep_get_and_clear(unsigned long addr, pte_t *ptep, pte_t *res)
+{
+	if (!is_lazy_mmu_active())
+		return false;
+	return __lazy_mmu_ptep_get_and_clear(addr, ptep, res);
+}
+
+static inline
+bool lazy_mmu_ptep_modify_prot_start(unsigned long addr, pte_t *ptep, pte_t *res)
+{
+	if (!is_lazy_mmu_active())
+		return false;
+	return __lazy_mmu_ptep_modify_prot_start(addr, ptep, res);
+}
+
+static inline
+bool lazy_mmu_ptep_modify_prot_commit(unsigned long addr, pte_t *ptep,
+				      pte_t old_pte, pte_t pte)
+{
+	if (!is_lazy_mmu_active())
+		return false;
+	return __lazy_mmu_ptep_modify_prot_commit(addr, ptep, old_pte, pte);
+}
+
+static inline
+bool lazy_mmu_ptep_set_wrprotect(unsigned long addr, pte_t *ptep)
+{
+	if (!is_lazy_mmu_active())
+		return false;
+	return __lazy_mmu_ptep_set_wrprotect(addr, ptep);
+}
+
 static inline void update_page_count(int level, long count)
 {
 	if (IS_ENABLED(CONFIG_PROC_FS))
@@ -978,17 +1036,38 @@ static inline void set_pmd(pmd_t *pmdp, pmd_t pmd)
 	WRITE_ONCE(*pmdp, pmd);
 }
 
-static inline void set_pte(pte_t *ptep, pte_t pte)
+static inline void __set_pte(pte_t *ptep, pte_t pte)
 {
 	if (pte_present(pte))
 		pte = clear_pte_bit(pte, __pgprot(_PAGE_UNUSED));
 	WRITE_ONCE(*ptep, pte);
 }
 
+static inline void set_pte(pte_t *ptep, pte_t pte)
+{
+	if (!is_lazy_mmu_active() || !__lazy_mmu_set_pte(ptep, pte))
+		__set_pte(ptep, pte);
+}
+
+#define ptep_get_lockless ptep_get_lockless
+static inline __no_sanitize_address pte_t ptep_get_lockless(pte_t *ptep)
+{
+	return READ_ONCE(*ptep);
+}
+
+static inline pte_t __ptep_get(pte_t *ptep)
+{
+	return READ_ONCE(*ptep);
+}
+
 #define ptep_get ptep_get
 static inline pte_t ptep_get(pte_t *ptep)
 {
-	return READ_ONCE(*ptep);
+	pte_t res;
+
+	if (!is_lazy_mmu_active() || !__lazy_mmu_ptep_get(ptep, &res))
+		res = __ptep_get(ptep);
+	return res;
 }
 
 #define pmdp_get pmdp_get
@@ -1181,6 +1260,15 @@ static __always_inline void __ptep_ipte_range(unsigned long address, int nr,
 	} while (nr != 255);
 }
 
+void arch_enter_lazy_mmu_mode_with_ptes(struct mm_struct *mm,
+					unsigned long addr, unsigned long end,
+					pte_t *pte);
+#define arch_enter_lazy_mmu_mode_with_ptes arch_enter_lazy_mmu_mode_with_ptes
+
+void arch_enter_lazy_mmu_mode(void);
+void arch_leave_lazy_mmu_mode(void);
+void arch_flush_lazy_mmu_mode(void);
+
 /*
  * This is hard to understand. ptep_get_and_clear and ptep_clear_flush
  * both clear the TLB for the unmapped pte. The reason is that
@@ -1201,10 +1289,16 @@ pte_t ptep_xchg_lazy(struct mm_struct *, unsigned long, pte_t *, pte_t);
 static inline bool ptep_test_and_clear_young(struct vm_area_struct *vma,
 		unsigned long addr, pte_t *ptep)
 {
-	pte_t pte = ptep_get(ptep);
+	pte_t pte;
+	int res;
 
-	pte = ptep_xchg_direct(vma->vm_mm, addr, ptep, pte_mkold(pte));
-	return pte_young(pte);
+	if (!lazy_mmu_ptep_test_and_clear_young(addr, ptep, &res)) {
+		pte = __ptep_get(ptep);
+		pte = pte_mkold(pte);
+		pte = ptep_xchg_direct(vma->vm_mm, addr, ptep, pte);
+		res = pte_young(pte);
+	}
+	return res;
 }
 
 #define __HAVE_ARCH_PTEP_CLEAR_YOUNG_FLUSH
@@ -1220,7 +1314,8 @@ static inline pte_t ptep_get_and_clear(struct mm_struct *mm,
 {
 	pte_t res;
 
-	res = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));
+	if (!lazy_mmu_ptep_get_and_clear(addr, ptep, &res))
+		res = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));
 	page_table_check_pte_clear(mm, addr, res);
 	/* At this point the reference through the mapping is still present */
 	if (mm_is_protected(mm) && pte_present(res))
@@ -1229,9 +1324,28 @@ static inline pte_t ptep_get_and_clear(struct mm_struct *mm,
 }
 
 #define __HAVE_ARCH_PTEP_MODIFY_PROT_TRANSACTION
-pte_t ptep_modify_prot_start(struct vm_area_struct *, unsigned long, pte_t *);
-void ptep_modify_prot_commit(struct vm_area_struct *, unsigned long,
-			     pte_t *, pte_t, pte_t);
+pte_t ___ptep_modify_prot_start(struct vm_area_struct *, unsigned long, pte_t *);
+void ___ptep_modify_prot_commit(struct vm_area_struct *, unsigned long,
+				pte_t *, pte_t, pte_t);
+
+static inline
+pte_t ptep_modify_prot_start(struct vm_area_struct *vma,
+			     unsigned long addr, pte_t *ptep)
+{
+	pte_t res;
+
+	if (!lazy_mmu_ptep_modify_prot_start(addr, ptep, &res))
+		res = ___ptep_modify_prot_start(vma, addr, ptep);
+	return res;
+}
+
+static inline
+void ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,
+			     pte_t *ptep, pte_t old_pte, pte_t pte)
+{
+	if (!lazy_mmu_ptep_modify_prot_commit(addr, ptep, old_pte, pte))
+		___ptep_modify_prot_commit(vma, addr, ptep, old_pte, pte);
+}
 
 #define __HAVE_ARCH_PTEP_CLEAR_FLUSH
 static inline pte_t ptep_clear_flush(struct vm_area_struct *vma,
@@ -1261,11 +1375,13 @@ static inline pte_t ptep_get_and_clear_full(struct mm_struct *mm,
 {
 	pte_t res;
 
-	if (full) {
-		res = ptep_get(ptep);
-		set_pte(ptep, __pte(_PAGE_INVALID));
-	} else {
-		res = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));
+	if (!lazy_mmu_ptep_get_and_clear(addr, ptep, &res)) {
+		if (full) {
+			res = __ptep_get(ptep);
+			__set_pte(ptep, __pte(_PAGE_INVALID));
+		} else {
+			res = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));
+		}
 	}
 	page_table_check_pte_clear(mm, addr, res);
 	/* At this point the reference through the mapping is still present */
@@ -1291,10 +1407,15 @@ static inline pte_t ptep_get_and_clear_full(struct mm_struct *mm,
 static inline void ptep_set_wrprotect(struct mm_struct *mm,
 				      unsigned long addr, pte_t *ptep)
 {
-	pte_t pte = ptep_get(ptep);
+	pte_t pte;
 
-	if (pte_write(pte))
-		ptep_xchg_lazy(mm, addr, ptep, pte_wrprotect(pte));
+	if (!lazy_mmu_ptep_set_wrprotect(addr, ptep)) {
+		pte = __ptep_get(ptep);
+		if (pte_write(pte)) {
+			pte = pte_wrprotect(pte);
+			ptep_xchg_lazy(mm, addr, ptep, pte);
+		}
+	}
 }
 
 /*
@@ -1327,7 +1448,7 @@ static inline void flush_tlb_fix_spurious_fault(struct vm_area_struct *vma,
 	 * PTE does not have _PAGE_PROTECT set, to avoid unnecessary overhead.
 	 * A local RDP can be used to do the flush.
 	 */
-	if (cpu_has_rdp() && !(pte_val(ptep_get(ptep)) & _PAGE_PROTECT))
+	if (cpu_has_rdp() && !(pte_val(__ptep_get(ptep)) & _PAGE_PROTECT))
 		__ptep_rdp(address, ptep, 1);
 }
 #define flush_tlb_fix_spurious_fault flush_tlb_fix_spurious_fault
diff --git a/arch/s390/kernel/setup.c b/arch/s390/kernel/setup.c
index b60284328fe3d..f5a3c9e1b6b88 100644
--- a/arch/s390/kernel/setup.c
+++ b/arch/s390/kernel/setup.c
@@ -77,6 +77,7 @@
 #include <asm/maccess.h>
 #include <asm/uv.h>
 #include <asm/asm-offsets.h>
+#include <asm/lazy_mmu.h>
 #include "entry.h"
 
 /*
@@ -1012,5 +1013,6 @@ void __init setup_arch(char **cmdline_p)
 
 void __init arch_cpu_finalize_init(void)
 {
+	lazy_mmu_online_boot_cpu();
 	sclp_init();
 }
diff --git a/arch/s390/kernel/smp.c b/arch/s390/kernel/smp.c
index 0ba7f89b81611..0a826bbaf1ddc 100644
--- a/arch/s390/kernel/smp.c
+++ b/arch/s390/kernel/smp.c
@@ -59,6 +59,7 @@
 #include <asm/topology.h>
 #include <asm/vdso.h>
 #include <asm/maccess.h>
+#include <asm/lazy_mmu.h>
 #include "entry.h"
 
 enum {
@@ -866,6 +867,11 @@ int __cpu_up(unsigned int cpu, struct task_struct *tidle)
 	rc = pcpu_alloc_lowcore(pcpu, cpu);
 	if (rc)
 		return rc;
+	rc = lazy_mmu_online_cpu(GFP_KERNEL, cpu);
+	if (rc) {
+		pcpu_free_lowcore(pcpu, cpu);
+		return rc;
+	}
 	/*
 	 * Make sure global control register contents do not change
 	 * until new CPU has initialized control registers.
@@ -921,6 +927,7 @@ void __cpu_die(unsigned int cpu)
 	pcpu = per_cpu_ptr(&pcpu_devices, cpu);
 	while (!pcpu_stopped(pcpu))
 		cpu_relax();
+	lazy_mmu_offline_cpu(cpu);
 	pcpu_free_lowcore(pcpu, cpu);
 	cpumask_clear_cpu(cpu, mm_cpumask(&init_mm));
 	cpumask_clear_cpu(cpu, &init_mm.context.cpu_attach_mask);
diff --git a/arch/s390/mm/Makefile b/arch/s390/mm/Makefile
index 193899c39ca7f..96cac3e99f516 100644
--- a/arch/s390/mm/Makefile
+++ b/arch/s390/mm/Makefile
@@ -4,6 +4,7 @@
 #
 
 obj-y		:= init.o fault.o extmem.o mmap.o vmem.o maccess.o
+obj-y		:= init.o fault.o extmem.o mmap.o vmem.o maccess.o lazy_mmu.o
 obj-y		+= page-states.o pageattr.o pgtable.o pgalloc.o extable.o
 
 obj-$(CONFIG_CMM)		+= cmm.o
diff --git a/arch/s390/mm/lazy_mmu.c b/arch/s390/mm/lazy_mmu.c
new file mode 100644
index 0000000000000..7e3a4115ea048
--- /dev/null
+++ b/arch/s390/mm/lazy_mmu.c
@@ -0,0 +1,418 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/pgtable.h>
+#include <linux/kasan.h>
+#include <linux/slab.h>
+#include <asm/facility.h>
+#include <asm/lazy_mmu.h>
+#include <kunit/visibility.h>
+
+#define PTE_POISON	_PAGE_LARGE
+
+struct ipte_range {
+	struct mm_struct *mm;
+	unsigned long base_addr;
+	unsigned long base_end;
+	pte_t *base_pte;
+	pte_t *start_pte;
+	pte_t *end_pte;
+	pte_t cache[PTRS_PER_PTE];
+};
+
+static DEFINE_PER_CPU(struct ipte_range *, ipte_range);
+
+static int count_contiguous(pte_t *start, pte_t *end, bool *valid)
+{
+	unsigned long page_invalid_bit;
+	pte_t *ptep, pte;
+
+	pte = __ptep_get(start);
+	page_invalid_bit = pte_val(pte) & _PAGE_INVALID;
+
+	for (ptep = start + 1; ptep < end; ptep++) {
+		pte = __ptep_get(ptep);
+		if ((pte_val(pte) & _PAGE_INVALID) != page_invalid_bit)
+			break;
+	}
+
+	*valid = !(page_invalid_bit);
+	return ptep - start;
+}
+
+static void __invalidate_pte_range(struct mm_struct *mm, unsigned long addr,
+				   int nr_ptes, pte_t *ptep)
+{
+	atomic_inc(&mm->context.flush_count);
+	if (cpu_has_tlb_lc() && cpumask_equal(mm_cpumask(mm), cpumask_of(smp_processor_id())))
+		__ptep_ipte_range(addr, nr_ptes - 1, ptep, IPTE_LOCAL);
+	else
+		__ptep_ipte_range(addr, nr_ptes - 1, ptep, IPTE_GLOBAL);
+	atomic_dec(&mm->context.flush_count);
+}
+
+static int invalidate_pte_range(struct mm_struct *mm, unsigned long addr,
+				pte_t *start, pte_t *end)
+{
+	int nr_ptes;
+	bool valid;
+
+	nr_ptes = count_contiguous(start, end, &valid);
+	if (valid)
+		__invalidate_pte_range(mm, addr, nr_ptes, start);
+
+	return nr_ptes;
+}
+
+static void set_pte_range(struct mm_struct *mm, unsigned long addr,
+			  pte_t *start, pte_t *end, pte_t *cache)
+{
+	int nr_ptes, nr_total = end - start;
+	pte_t *ptep = start;
+	int i;
+
+	kasan_unpoison_pte(start, nr_total);
+	while (ptep < end) {
+		nr_ptes = invalidate_pte_range(mm, addr, ptep, end);
+
+		for (i = 0; i < nr_ptes; i++, ptep++, cache++) {
+			__set_pte(ptep, *cache);
+			*cache = __pte(PTE_POISON);
+		}
+
+		addr += nr_ptes * PAGE_SIZE;
+	}
+	kasan_poison_pte(start, nr_total);
+}
+
+static void enter_ipte_norange(void)
+{
+	struct ipte_range __maybe_unused *range;
+
+	if (!test_facility(13))
+		return;
+
+	local_bh_disable();
+
+	range = get_cpu_var(ipte_range);
+	get_lowcore()->lazy_mmu_count++;
+
+	local_bh_enable();
+}
+
+static void enter_ipte_range(struct mm_struct *mm,
+			     unsigned long addr, unsigned long end, pte_t *pte)
+{
+	struct ipte_range *range;
+	unsigned int nr_ptes;
+
+	if (!test_facility(13))
+		return;
+
+	local_bh_disable();
+
+	range = get_cpu_var(ipte_range);
+	get_lowcore()->lazy_mmu_count++;
+
+	if (mm_is_protected(mm)) {
+		local_bh_enable();
+		return;
+	}
+
+	range->mm = mm;
+	range->base_addr = addr;
+	range->base_end = end;
+	range->base_pte = pte;
+
+	nr_ptes = (range->base_end - range->base_addr) / PAGE_SIZE;
+	kasan_poison_pte(range->base_pte, nr_ptes);
+
+	local_bh_enable();
+}
+
+static void leave_ipte_range(void)
+{
+	pte_t *ptep, *start, *start_cache, *cache;
+	unsigned long start_addr, addr;
+	struct ipte_range *range;
+	unsigned int nr_ptes;
+	int start_idx;
+
+	if (!test_facility(13))
+		return;
+
+	local_bh_disable();
+
+	lockdep_assert_preemption_disabled();
+	range = this_cpu_read(ipte_range);
+	if (!range->mm)
+		goto norange;
+	if (!range->start_pte)
+		goto done;
+
+	start = range->start_pte;
+	start_idx = range->start_pte - range->base_pte;
+	start_addr = range->base_addr + start_idx * PAGE_SIZE;
+	addr = start_addr;
+	start_cache = &range->cache[start_idx];
+	cache = start_cache;
+	for (ptep = start; ptep < range->end_pte; ptep++, cache++, addr += PAGE_SIZE) {
+		if (pte_val(*cache) == PTE_POISON) {
+			if (start) {
+				set_pte_range(range->mm, start_addr, start, ptep, start_cache);
+				start = NULL;
+			}
+		} else if (!start) {
+			start = ptep;
+			start_addr = addr;
+			start_cache = cache;
+		}
+	}
+	set_pte_range(range->mm, start_addr, start, ptep, start_cache);
+
+	range->start_pte = NULL;
+	range->end_pte = NULL;
+
+done:
+	nr_ptes = (range->base_end - range->base_addr) / PAGE_SIZE;
+	kasan_unpoison_pte(range->base_pte, nr_ptes);
+
+	range->mm = NULL;
+	range->base_addr = 0;
+	range->base_end = 0;
+	range->base_pte = NULL;
+
+norange:
+	get_lowcore()->lazy_mmu_count--;
+	put_cpu_var(ipte_range);
+
+	local_bh_enable();
+}
+
+static void flush_lazy_mmu_mode(void)
+{
+	unsigned long addr, end;
+	struct ipte_range *range;
+	struct mm_struct *mm;
+	pte_t *pte;
+
+	if (!test_facility(13))
+		return;
+
+	range = get_cpu_var(ipte_range);
+	if (range->mm) {
+		mm = range->mm;
+		addr = range->base_addr;
+		end = range->base_end;
+		pte = range->base_pte;
+
+		leave_ipte_range();
+		enter_ipte_range(mm, addr, end, pte);
+	}
+	put_cpu_var(ipte_range);
+}
+
+void arch_enter_lazy_mmu_mode(void)
+{
+	enter_ipte_norange();
+}
+EXPORT_SYMBOL_IF_KUNIT(arch_enter_lazy_mmu_mode);
+
+void arch_enter_lazy_mmu_mode_with_ptes(struct mm_struct *mm,
+					unsigned long addr, unsigned long end,
+					pte_t *pte)
+{
+	enter_ipte_range(mm, addr, end, pte);
+}
+EXPORT_SYMBOL_IF_KUNIT(arch_enter_lazy_mmu_mode_with_ptes);
+
+void arch_leave_lazy_mmu_mode(void)
+{
+	leave_ipte_range();
+}
+EXPORT_SYMBOL_IF_KUNIT(arch_leave_lazy_mmu_mode);
+
+void arch_flush_lazy_mmu_mode(void)
+{
+	flush_lazy_mmu_mode();
+}
+EXPORT_SYMBOL_IF_KUNIT(arch_flush_lazy_mmu_mode);
+
+static void __ipte_range_set_pte(struct ipte_range *range, pte_t *ptep, pte_t pte)
+{
+	unsigned int idx = ptep - range->base_pte;
+
+	lockdep_assert_preemption_disabled();
+	range->cache[idx] = pte;
+
+	if (!range->start_pte) {
+		range->start_pte = ptep;
+		range->end_pte = ptep + 1;
+	} else if (ptep < range->start_pte) {
+		range->start_pte = ptep;
+	} else if (ptep + 1 > range->end_pte) {
+		range->end_pte = ptep + 1;
+	}
+}
+
+static pte_t __ipte_range_ptep_get(struct ipte_range *range, pte_t *ptep)
+{
+	unsigned int idx = ptep - range->base_pte;
+	pte_t pte;
+
+	lockdep_assert_preemption_disabled();
+	if (pte_val(range->cache[idx]) == PTE_POISON) {
+		kasan_unpoison_pte(ptep, 1);
+		pte = __ptep_get(ptep);
+		kasan_poison_pte(ptep, 1);
+
+		return pte;
+	}
+
+	return range->cache[idx];
+}
+
+static struct ipte_range *this_ipte_range(pte_t *ptep)
+{
+	struct ipte_range *range;
+	unsigned int nr_ptes;
+
+	range = this_cpu_read(ipte_range);
+	if (ptep < range->base_pte)
+		return NULL;
+	nr_ptes = (range->base_end - range->base_addr) / PAGE_SIZE;
+	if (ptep >= range->base_pte + nr_ptes)
+		return NULL;
+
+	return range;
+}
+
+bool __lazy_mmu_set_pte(pte_t *ptep, pte_t pte)
+{
+	struct ipte_range *range;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	__ipte_range_set_pte(range, ptep, pte);
+
+	return true;
+}
+
+bool __lazy_mmu_ptep_get(pte_t *ptep, pte_t *res)
+{
+	struct ipte_range *range;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	*res = __ipte_range_ptep_get(range, ptep);
+
+	return true;
+}
+
+bool __lazy_mmu_ptep_test_and_clear_young(unsigned long addr, pte_t *ptep, int *res)
+{
+	struct ipte_range *range;
+	pte_t pte, old;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	old = __ipte_range_ptep_get(range, ptep);
+	pte = pte_mkold(old);
+	__ipte_range_set_pte(range, ptep, pte);
+	*res = pte_young(old);
+
+	return true;
+}
+
+bool __lazy_mmu_ptep_get_and_clear(unsigned long addr, pte_t *ptep, pte_t *res)
+{
+	struct ipte_range *range;
+	pte_t pte, old;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	old = __ipte_range_ptep_get(range, ptep);
+	pte = __pte(_PAGE_INVALID);
+	__ipte_range_set_pte(range, ptep, pte);
+	*res = old;
+
+	return true;
+}
+
+bool __lazy_mmu_ptep_modify_prot_start(unsigned long addr, pte_t *ptep, pte_t *res)
+{
+	return __lazy_mmu_ptep_get_and_clear(addr, ptep, res);
+}
+
+bool __lazy_mmu_ptep_modify_prot_commit(unsigned long addr, pte_t *ptep,
+					pte_t old_pte, pte_t pte)
+{
+	struct ipte_range *range;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	__ipte_range_set_pte(range, ptep, pte);
+
+	return true;
+}
+
+bool __lazy_mmu_ptep_set_wrprotect(unsigned long addr, pte_t *ptep)
+{
+	struct ipte_range *range;
+	pte_t pte;
+
+	range = this_ipte_range(ptep);
+	if (!range)
+		return false;
+
+	pte = __ipte_range_ptep_get(range, ptep);
+	if (pte_write(pte)) {
+		pte = pte_wrprotect(pte);
+		__ipte_range_set_pte(range, ptep, pte);
+	}
+
+	return true;
+}
+
+int lazy_mmu_online_cpu(gfp_t gfp, unsigned int cpu)
+{
+	struct ipte_range *range;
+	int i;
+
+	if (!test_facility(13))
+		return 0;
+
+	range = kzalloc_obj(*range, gfp);
+	if (!range)
+		return -ENOMEM;
+	for (i = 0; i < ARRAY_SIZE(range->cache); i++)
+		range->cache[i] = __pte(PTE_POISON);
+	per_cpu(ipte_range, cpu) = range;
+
+	return 0;
+}
+
+void lazy_mmu_offline_cpu(unsigned int cpu)
+{
+	struct ipte_range *range;
+
+	if (!test_facility(13))
+		return;
+
+	range = per_cpu(ipte_range, cpu);
+	per_cpu(ipte_range, cpu) = NULL;
+	kfree(range);
+}
+
+void __init lazy_mmu_online_boot_cpu(void)
+{
+	lazy_mmu_online_cpu(GFP_ATOMIC, 0);
+}
diff --git a/arch/s390/mm/pgtable.c b/arch/s390/mm/pgtable.c
index 4acd8b140c4bd..df36523bcbbbb 100644
--- a/arch/s390/mm/pgtable.c
+++ b/arch/s390/mm/pgtable.c
@@ -166,14 +166,14 @@ pte_t ptep_xchg_lazy(struct mm_struct *mm, unsigned long addr,
 }
 EXPORT_SYMBOL(ptep_xchg_lazy);
 
-pte_t ptep_modify_prot_start(struct vm_area_struct *vma, unsigned long addr,
-			     pte_t *ptep)
+pte_t ___ptep_modify_prot_start(struct vm_area_struct *vma, unsigned long addr,
+				pte_t *ptep)
 {
 	return ptep_flush_lazy(vma->vm_mm, addr, ptep, 1);
 }
 
-void ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,
-			     pte_t *ptep, pte_t old_pte, pte_t pte)
+void ___ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,
+				pte_t *ptep, pte_t old_pte, pte_t pte)
 {
 	set_pte(ptep, pte);
 }
diff --git a/fs/proc/task_mmu.c b/fs/proc/task_mmu.c
index 229d1fc3d7f1c..916785c3b2e0b 100644
--- a/fs/proc/task_mmu.c
+++ b/fs/proc/task_mmu.c
@@ -2852,7 +2852,7 @@ static int pagemap_scan_pmd_entry(pmd_t *pmd, unsigned long start,
 		return 0;
 	}
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(vma->vm_mm, start, end, start_pte);
 
 	if ((p->arg.flags & PM_SCAN_WP_MATCHING) && !p->vec_out) {
 		/* Fast path for performing exclusive WP */
diff --git a/include/linux/kasan.h b/include/linux/kasan.h
index bf233bde68c7e..1aa4d9dd2485a 100644
--- a/include/linux/kasan.h
+++ b/include/linux/kasan.h
@@ -6,6 +6,7 @@
 #include <linux/kasan-enabled.h>
 #include <linux/kasan-tags.h>
 #include <linux/kernel.h>
+#include <linux/pgtable.h>
 #include <linux/static_key.h>
 #include <linux/types.h>
 
@@ -35,8 +36,6 @@ typedef unsigned int __bitwise kasan_vmalloc_flags_t;
 
 #if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
 
-#include <linux/pgtable.h>
-
 /* Software KASAN implementations use shadow memory. */
 
 #ifdef CONFIG_KASAN_SW_TAGS
@@ -134,6 +133,20 @@ static __always_inline void kasan_poison_slab(struct slab *slab)
 		__kasan_poison_slab(slab);
 }
 
+void __kasan_poison_pte(pte_t *pte, int nr);
+static __always_inline void kasan_poison_pte(pte_t *pte, int nr)
+{
+	if (kasan_enabled())
+		__kasan_poison_pte(pte, nr);
+}
+
+void __kasan_unpoison_pte(pte_t *pte, int nr);
+static __always_inline void kasan_unpoison_pte(pte_t *pte, int nr)
+{
+	if (kasan_enabled())
+		__kasan_unpoison_pte(pte, nr);
+}
+
 void __kasan_unpoison_new_object(struct kmem_cache *cache, void *object);
 /**
  * kasan_unpoison_new_object - Temporarily unpoison a new slab object.
@@ -414,6 +427,8 @@ static inline bool kasan_unpoison_pages(struct page *page, unsigned int order,
 	return false;
 }
 static inline void kasan_poison_slab(struct slab *slab) {}
+static inline void kasan_poison_pte(pte_t *pte, int nr) {}
+static inline void kasan_unpoison_pte(pte_t *pte, int nr) {}
 static inline void kasan_unpoison_new_object(struct kmem_cache *cache,
 					void *object) {}
 static inline void kasan_poison_new_object(struct kmem_cache *cache,
diff --git a/include/linux/pgtable.h b/include/linux/pgtable.h
index 2981e386da7b3..cc85daf30739e 100644
--- a/include/linux/pgtable.h
+++ b/include/linux/pgtable.h
@@ -271,6 +271,50 @@ static inline void lazy_mmu_mode_enable(void)
 		arch_enter_lazy_mmu_mode();
 }
 
+#ifndef arch_enter_lazy_mmu_mode_with_ptes
+static inline void arch_enter_lazy_mmu_mode_with_ptes(struct mm_struct *mm,
+		unsigned long addr, unsigned long end, pte_t *ptep)
+{
+	arch_enter_lazy_mmu_mode();
+}
+#endif
+
+/**
+ * lazy_mmu_mode_enable_with_ptes() - Enable the lazy MMU mode with a speedup hint.
+ * @mm: Address space the pages are mapped into.
+ * @addr: Start address of the range.
+ * @end: End address of the range.
+ * @ptep: Page table pointer for the first entry.
+ *
+ * Enters a new lazy MMU mode section; if the mode was not already enabled,
+ * enables it and calls arch_enter_lazy_mmu_mode_with_ptes().
+ *
+ * PTEs that fall within the specified range might observe update speedups.
+ * The PTEs must belong to the specified address space and be in the same PMD.
+ *
+ * There are no requirements on the order or range completeness of PTE
+ * updates for the specified range.
+ *
+ * Must be paired with a call to lazy_mmu_mode_disable().
+ *
+ * Has no effect if called:
+ * - While paused - see lazy_mmu_mode_pause()
+ * - In interrupt context
+ */
+static inline void lazy_mmu_mode_enable_with_ptes(struct mm_struct *mm,
+		unsigned long addr, unsigned long end, pte_t *ptep)
+{
+	struct lazy_mmu_state *state = &current->lazy_mmu_state;
+
+	if (in_interrupt() || state->pause_count > 0)
+		return;
+
+	VM_WARN_ON_ONCE(state->enable_count == U8_MAX);
+
+	if (state->enable_count++ == 0)
+		arch_enter_lazy_mmu_mode_with_ptes(mm, addr, end, ptep);
+}
+
 /**
  * lazy_mmu_mode_disable() - Disable the lazy MMU mode.
  *
@@ -387,6 +431,8 @@ static inline void lazy_mmu_mode_resume(void)
 }
 #else
 static inline void lazy_mmu_mode_enable(void) {}
+static inline void lazy_mmu_mode_enable_with_ptes(struct mm_struct *mm,
+		unsigned long addr, unsigned long end, pte_t *ptep) {}
 static inline void lazy_mmu_mode_disable(void) {}
 static inline void lazy_mmu_mode_pause(void) {}
 static inline void lazy_mmu_mode_resume(void) {}
diff --git a/mm/kasan/common.c b/mm/kasan/common.c
index b7d05c2a6d93d..94d1060949891 100644
--- a/mm/kasan/common.c
+++ b/mm/kasan/common.c
@@ -163,6 +163,20 @@ void __kasan_poison_slab(struct slab *slab)
 		     KASAN_SLAB_REDZONE, false);
 }
 
+void __kasan_poison_pte(pte_t *pte, int nr)
+{
+	if (IS_ALIGNED(sizeof(*pte), KASAN_GRANULE_SIZE))
+		kasan_poison(pte, sizeof(*pte) * nr, KASAN_LAZY_MMU_PTE, false);
+}
+EXPORT_SYMBOL_GPL(__kasan_poison_pte);
+
+void __kasan_unpoison_pte(pte_t *pte, int nr)
+{
+	if (IS_ALIGNED(sizeof(*pte), KASAN_GRANULE_SIZE))
+		kasan_unpoison(pte, sizeof(*pte) * nr, false);
+}
+EXPORT_SYMBOL_GPL(__kasan_unpoison_pte);
+
 void __kasan_unpoison_new_object(struct kmem_cache *cache, void *object)
 {
 	kasan_unpoison(object, cache->object_size, false);
diff --git a/mm/kasan/kasan.h b/mm/kasan/kasan.h
index fc9169a547662..1a2d18cdb21d9 100644
--- a/mm/kasan/kasan.h
+++ b/mm/kasan/kasan.h
@@ -144,12 +144,14 @@ static inline bool kasan_requires_meta(void)
 #define KASAN_PAGE_REDZONE	0xFE  /* redzone for kmalloc_large allocation */
 #define KASAN_SLAB_REDZONE	0xFC  /* redzone for slab object */
 #define KASAN_SLAB_FREE		0xFB  /* freed slab object */
+#define KASAN_LAZY_MMU_PTE	0xFD  /* direct pte access in lazy mmu mode */
 #define KASAN_VMALLOC_INVALID	0xF8  /* inaccessible space in vmap area */
 #else
 #define KASAN_PAGE_FREE		KASAN_TAG_INVALID
 #define KASAN_PAGE_REDZONE	KASAN_TAG_INVALID
 #define KASAN_SLAB_REDZONE	KASAN_TAG_INVALID
 #define KASAN_SLAB_FREE		KASAN_TAG_INVALID
+#define KASAN_LAZY_MMU_PTE	KASAN_TAG_INVALID
 #define KASAN_VMALLOC_INVALID	KASAN_TAG_INVALID /* only used for SW_TAGS */
 #endif
 
diff --git a/mm/kasan/report_generic.c b/mm/kasan/report_generic.c
index f5b8e37b3805c..489d4a8d6902d 100644
--- a/mm/kasan/report_generic.c
+++ b/mm/kasan/report_generic.c
@@ -113,6 +113,9 @@ static const char *get_shadow_bug_type(struct kasan_report_info *info)
 	case KASAN_SLAB_FREE_META:
 		bug_type = "slab-use-after-free";
 		break;
+	case KASAN_LAZY_MMU_PTE:
+		bug_type = "lazy-mmu-pte-access";
+		break;
 	case KASAN_ALLOCA_LEFT:
 	case KASAN_ALLOCA_RIGHT:
 		bug_type = "alloca-out-of-bounds";
diff --git a/mm/madvise.c b/mm/madvise.c
index 77552b03d318d..4419d16b39f96 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -452,7 +452,7 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
 	if (!start_pte)
 		return 0;
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);
 	for (; addr < end; pte += nr, addr += nr * PAGE_SIZE) {
 		nr = 1;
 		ptent = ptep_get(pte);
@@ -507,7 +507,7 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
 				if (!start_pte)
 					break;
 				flush_tlb_batched_pending(mm);
-				lazy_mmu_mode_enable();
+				lazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);
 				if (!err)
 					nr = 0;
 				continue;
@@ -674,7 +674,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 	if (!start_pte)
 		return 0;
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);
 	for (; addr != end; pte += nr, addr += PAGE_SIZE * nr) {
 		nr = 1;
 		ptent = ptep_get(pte);
@@ -734,7 +734,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 				if (!start_pte)
 					break;
 				flush_tlb_batched_pending(mm);
-				lazy_mmu_mode_enable();
+				lazy_mmu_mode_enable_with_ptes(mm, addr, end, pte);
 				if (!err)
 					nr = 0;
 				continue;
diff --git a/mm/memory.c b/mm/memory.c
index 6b8280cfc1dbe..0cc07b043384b 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -1272,7 +1272,7 @@ copy_pte_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
 	spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);
 	orig_src_pte = src_pte;
 	orig_dst_pte = dst_pte;
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(src_mm, addr, end, src_pte);
 
 	do {
 		nr = 1;
@@ -1922,7 +1922,7 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,
 		return addr;
 
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, addr, end, start_pte);
 	do {
 		bool any_skipped = false;
 
@@ -2919,7 +2919,7 @@ static int remap_pte_range(struct mm_struct *mm, pmd_t *pmd,
 	mapped_pte = pte = pte_alloc_map_lock(mm, pmd, addr, &ptl);
 	if (!pte)
 		return -ENOMEM;
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, addr, end, mapped_pte);
 	do {
 		BUG_ON(!pte_none(ptep_get(pte)));
 		if (!pfn_modify_allowed(pfn, prot)) {
@@ -3330,7 +3330,7 @@ static int apply_to_pte_range(struct mm_struct *mm, pmd_t *pmd,
 			return -EINVAL;
 	}
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, addr, end, mapped_pte);
 
 	if (fn) {
 		do {
diff --git a/mm/mprotect.c b/mm/mprotect.c
index 9cbf932b028cf..3fc26418e837d 100644
--- a/mm/mprotect.c
+++ b/mm/mprotect.c
@@ -337,7 +337,7 @@ static long change_pte_range(struct mmu_gather *tlb,
 		is_private_single_threaded = vma_is_single_threaded_private(vma);
 
 	flush_tlb_batched_pending(vma->vm_mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(vma->vm_mm, addr, end, pte);
 	do {
 		nr_ptes = 1;
 		oldpte = ptep_get(pte);
diff --git a/mm/mremap.c b/mm/mremap.c
index e9c8b1d05832b..0dfe3de39ccc7 100644
--- a/mm/mremap.c
+++ b/mm/mremap.c
@@ -260,7 +260,7 @@ static int move_ptes(struct pagetable_move_control *pmc,
 	if (new_ptl != old_ptl)
 		spin_lock_nested(new_ptl, SINGLE_DEPTH_NESTING);
 	flush_tlb_batched_pending(vma->vm_mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(mm, old_addr, old_end, old_ptep);
 
 	for (; old_addr < old_end; old_ptep += nr_ptes, old_addr += nr_ptes * PAGE_SIZE,
 		new_ptep += nr_ptes, new_addr += nr_ptes * PAGE_SIZE) {
diff --git a/mm/vmalloc.c b/mm/vmalloc.c
index f4fa227a8d7f0..1c6ef8fd079e0 100644
--- a/mm/vmalloc.c
+++ b/mm/vmalloc.c
@@ -109,7 +109,7 @@ static int vmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 	if (!pte)
 		return -ENOMEM;
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(&init_mm, addr, end, pte);
 
 	do {
 		if (unlikely(!pte_none(ptep_get(pte)))) {
@@ -393,7 +393,7 @@ static void vunmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 	unsigned long size = PAGE_SIZE;
 
 	pte = pte_offset_kernel(pmd, addr);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(&init_mm, addr, end, pte);
 
 	do {
 #ifdef CONFIG_HUGETLB_PAGE
@@ -560,7 +560,7 @@ static int vmap_pages_pte_range(pmd_t *pmd, unsigned long addr,
 	if (!pte)
 		return -ENOMEM;
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_with_ptes(&init_mm, addr, end, pte);
 
 	do {
 		struct page *page = pages[*nr];

35/3 2026/08/17 12:41 llm kmsan-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 16401
							output: 214
							thoughts: 1376
						

Thoughts:
**KMSAN Fuzzing Session Justification: A Detailed Examination**

Okay, so the question is whether this patch series warrants a dedicated KMSAN fuzzing run. KMSAN's all about catching reads of uninitialized memory and kernel-to-user info-leaks. Standard KASAN handles the usual suspects like out-of-bounds accesses, use-after-free, and locking bugs. This patch introduces a "lazy MMU mode" for the s390 architecture. Let's dig in.

The patch's got changes across the board, from `lazy_mmu.h` to modifications in various kernel areas related to memory management and KASAN. It looks like it's implementing lazy PTE updates. Interesting – it's buffering those updates. The code uses `lazy_mmu_mode_enable_with_ptes()` which takes a `pte_t *ptep`. Ah, and there's KASAN poisoning/unpoisoning of PTEs (`KASAN_LAZY_MMU_PTE`). Wait, this is *explicitly* a KASAN feature. It's adding a new KASAN tag and a new bug type: `"lazy-mmu-pte-access"`. This isn't about uninitialized memory at all, it's about catching *invalid* accesses to PTEs during lazy mode using KASAN.

Alright, let's play devil's advocate and check for uninitialized memory risks. Let me see... `struct ipte_range` is allocated in `lazy_mmu_online_cpu` using `kzalloc_obj(*range, gfp)`. Wait, what is `kzalloc_obj`? Is that a typo? It's not a standard kernel function. Let's see if I can find it with `grep`. Oh! It's probably a macro that's a wrapper for `kzalloc(sizeof(*range), gfp)`. Actually, it doesn't matter, it's a zero-allocation!

Let's look more closely at the code in `arch/s390/mm/lazy_mmu.c`:
```c
	range = kzalloc_obj(*range, gfp);
	if (!range)
		return -ENOMEM;
	for (i = 0; i < ARRAY_SIZE(range->cache); i++)
		range->cache[i] = __pte(PTE_POISON);
```
Yes, definitely a zero-allocation, as expected. The PTE cache is then initialized with poison values.

Is anything here passed to user space? Nope. It's purely internal MMU page table management. PTEs aren't being sent directly to userspace. In fact, this patch is *adding* KASAN checks to catch *direct* PTE accesses in lazy mode. Since it's built-in, KASAN is the right tool here. KMSAN's purpose, catching uninitialized memory problems, isn't what this patch does.

Let's double-check just to be sure... the `struct ipte_range` has a `pte_t cache[PTRS_PER_PTE];`. It's initialized to `PTE_POISON` in `lazy_mmu_online_cpu`. `enter_ipte_range` sets the base address, end, and PTE, and `__lazy_mmu_set_pte` writes to `range->cache[idx]`. Nothing is copied to userspace and nothing is read uninitialized.

Therefore, no. `NeedsKMSAN = false`.

The patch's all about lazy PTE updates on s390, explicitly adding KASAN poisoning/unpoisoning to catch direct PTE accesses, introducing a new KASAN bug type. All of the changes are internal, page table related, and no structures are copied to userspace. Newly allocated structures are fully initialized (via zero-allocation and explicit `PTE_POISON`). There's no uninitialized memory risk, no kernel-to-user info leaks. KASAN is the perfect tool for checking this. `kzalloc_obj`, which is used, does zero allocation, or is implemented as such.
```
```
I'm calling `set-results` now.
```



36/3 2026/08/17 12:41 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch introduces a lazy MMU mode for the s390 architecture to buffer page table entry (PTE) updates. It also explicitly adds KASAN poisoning/unpoisoning to detect invalid direct accesses to PTEs while the lazy MMU mode is active, introducing a new KASAN bug type (`lazy-mmu-pte-access`). The changes are strictly confined to internal memory management and page table operations. No new structures are copied to user space, and all newly allocated structures (e.g., `struct ipte_range`) are properly initialized (e.g., via zero-allocation and explicit initialization of the `cache` array to `PTE_POISON`). There is no risk of uninitialized memory usage or kernel-to-user info-leaks introduced by this patch. Standard KASAN is perfectly suited and explicitly enhanced by this patch to catch any bugs related to these changes.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch introduces a lazy MMU mode for the s390 architecture to buffer page table entry (PTE) updates. It also explicitly adds KASAN poisoning/unpoisoning to detect invalid direct accesses to PTEs while the lazy MMU mode is active, introducing a new KASAN bug type (`lazy-mmu-pte-access`). The changes are strictly confined to internal memory management and page table operations. No new structures are copied to user space, and all newly allocated structures (e.g., `struct ipte_range`) are properly initialized (e.g., via zero-allocation and explicit initialization of the `cache` array to `PTE_POISON`). There is no risk of uninitialized memory usage or kernel-to-user info-leaks introduced by this patch. Standard KASAN is perfectly suited and explicitly enhanced by this patch to catch any bugs related to these changes.",
  "NeedsKMSAN": false
}

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)