Subject: [PATCH v2 1/3] powerpc/code-patching: Introduce open_patch_window()/close_patch_window()

From: Benjamin Gray <[email-masked]>
Date: Tue, 26 Mar 2024 09:48:46 +1100

The code patching capabilities have grown, and the specific quirks for
setting up and tearing down the patching window are getting duplicated.

This commit introduces an abstraction for working with this patching
window. It defines open_patch_window() to set up the writable alias
page, and close_patch_window() to remove it and flush the TLB. The
actual mechanism for providing this patching window is an implementation
detail that consumers don't need to worry about. Consumers are still
responsible for flushing/syncing any changes they make through this
alias page though.

Signed-off-by: Benjamin Gray <[email-masked]>

---

v1: https://lore.kernel.org/all/[email-masked]/

This design can be readily extended to remap the writable page to
another physical page without incurring all of the entry and exit
overhead. But that might have problems with spending too long in
an interrupts disabled context, so I've left it out for now.
---
 arch/powerpc/lib/code-patching.c | 113 +++++++++++++++++++++++++++++++
 1 file changed, 113 insertions(+)

diff --git a/arch/powerpc/lib/code-patching.c b/arch/powerpc/lib/code-patching.c
index df64343b9214..7c193e19e297 100644
--- a/arch/powerpc/lib/code-patching.c
+++ b/arch/powerpc/lib/code-patching.c
@@ -276,6 +276,119 @@ static void unmap_patch_area(unsigned long addr)
 	flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
 }
 
+/*
+ * Represents an active patching window.
+ */
+struct patch_window {
+	/*
+	 * Page aligned patching window address. The page is a writable alias of
+	 * the configured target page.
+	 */
+	unsigned long text_poke_addr;
+	/*
+	 * Pointer to the PTE for the patching window page.
+	 */
+	pte_t *ptep;
+	/*
+	 * (Temporary MM patching only) The original MM before creating the
+	 * patching window.
+	 */
+	struct mm_struct *orig_mm;
+	/*
+	 * (Temporary MM patching only) The patching window MM.
+	 */
+	struct mm_struct *patch_mm;
+	/*
+	 * (Temporary MM patching only) Lock for the patching window PTE.
+	 */
+	spinlock_t *ptl;
+};
+
+/*
+ * Calling this function opens a 'patching window' that maps a
+ * page starting at ctx->text_poke_addr (which is set by this function)
+ * as a writable alias to the page starting at addr.
+ *
+ * Upon success, callers must invoke the corresponding close_patch_window()
+ * function to return to the original state. Callers are also responsible
+ * for syncing any changes they make inside the window.
+ *
+ * Interrupts must be disabled for the entire duration of the patching. The PIDR
+ * is potentially changed during this time.
+ */
+static int open_patch_window(void *addr, struct patch_window *ctx)
+{
+	unsigned long pfn = get_patch_pfn(addr);
+
+	lockdep_assert_irqs_disabled();
+
+	ctx->text_poke_addr = (unsigned long)__this_cpu_read(cpu_patching_context.addr);
+
+	if (!mm_patch_enabled()) {
+		ctx->ptep = __this_cpu_read(cpu_patching_context.pte);
+		__set_pte_at(&init_mm, ctx->text_poke_addr,
+			     ctx->ptep, pfn_pte(pfn, PAGE_KERNEL), 0);
+
+		/* See ptesync comment in radix__set_pte_at() */
+		if (radix_enabled())
+			asm volatile("ptesync" ::: "memory");
+
+		return 0;
+	}
+
+	ctx->orig_mm = current->active_mm;
+	ctx->patch_mm = __this_cpu_read(cpu_patching_context.mm);
+
+	ctx->ptep = get_locked_pte(ctx->patch_mm, ctx->text_poke_addr, &ctx->ptl);
+	if (!ctx->ptep)
+		return -ENOMEM;
+
+	__set_pte_at(ctx->patch_mm, ctx->text_poke_addr,
+		     ctx->ptep, pfn_pte(pfn, PAGE_KERNEL), 0);
+
+	/* order PTE update before use, also serves as the hwsync */
+	asm volatile("ptesync" ::: "memory");
+
+	/*
+	 * Changing mm requires context synchronising instructions on both sides of
+	 * the context switch, as well as a hwsync between the last instruction for
+	 * which the address of an associated storage access was translated using
+	 * the current context. switch_mm_irqs_off() performs an isync after the
+	 * context switch.
+	 */
+	isync();
+	switch_mm_irqs_off(ctx->orig_mm, ctx->patch_mm, current);
+
+	WARN_ON(!mm_is_thread_local(ctx->patch_mm));
+
+	suspend_breakpoints();
+	return 0;
+}
+
+static void close_patch_window(struct patch_window *ctx)
+{
+	lockdep_assert_irqs_disabled();
+
+	if (!mm_patch_enabled()) {
+		pte_clear(&init_mm, ctx->text_poke_addr, ctx->ptep);
+		flush_tlb_kernel_range(ctx->text_poke_addr, ctx->text_poke_addr + PAGE_SIZE);
+		return;
+	}
+
+	isync();
+	switch_mm_irqs_off(ctx->patch_mm, ctx->orig_mm, current);
+	restore_breakpoints();
+
+	pte_clear(ctx->patch_mm, ctx->text_poke_addr, ctx->ptep);
+	/*
+	 * ptesync to order PTE update before TLB invalidation done
+	 * by radix__local_flush_tlb_page_psize (in _tlbiel_va)
+	 */
+	local_flush_tlb_page_psize(ctx->patch_mm, ctx->text_poke_addr, mmu_virtual_psize);
+
+	pte_unmap_unlock(ctx->ptep, ctx->ptl);
+}
+
 static int __do_patch_instruction_mm(u32 *addr, ppc_inst_t instr)
 {
 	int err;
-- 
2.44.0