The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: Bradley Morgan <brads@mainlining.org>
To: Will Deacon <will@kernel.org>
Cc: Catalin Marinas <catalin.marinas@arm.com>,
	Mark Rutland <mark.rutland@arm.com>,
	James Morse <james.morse@arm.com>, Marc Zyngier <maz@kernel.org>,
	Ard Biesheuvel <ardb@kernel.org>,
	linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org, brads@mainlining.org
Subject: [PATCH 7/7] arm64: hibernate: use regular cpu_resume() logic
Date: Tue, 25 Aug 2026 20:58:39 +0000	[thread overview]
Message-ID: <20260825205839.14571-8-brads@mainlining.org> (raw)
In-Reply-To: <20260825205839.14571-1-brads@mainlining.org>

Right now the hibernate path borrows parts of the idle suspend/resume
code and writes the rest itself. It skips cpu_resume() and branches
straight into __cpu_resume_switched() at the kernel's native EL with
the MMU on. That split has been the source of a few subtle bugs, and it
makes the code a pain to maintain.

Rework it so a resume from hibernate looks like a resume from idle. We
enter cpu_resume() at the highest available EL with the MMU off, and
leave the resumed kernel responsible for restoring all its CPU state the
same way idle does. That makes the handoff cleaner and should be more
robust.

With this, swsusp_arch_suspend_exit() now cleans the whole kernel Image
to the PoC instead of leaving the resumed kernel to clean bits of
itself. That is nicer on its own, since we cannot forget to clean some
portion of the Image that gets added later, and the maintenance is
cheap next to the page copying that dominates resume time anyway.

It also means __cpu_resume_switched() no longer needs to be visible
outside sleep.S, so it can merge back with
__cpu_resume_switched_finalise_el2().

Since this was first written, two things landed upstream and are folded
in here. Commit 684bde100117931f ("arm64: hibernate: mask DAIF before
restoring hibernated kernel") made swsusp_arch_resume() mask all DAIF
exceptions before calling hibernate_exit() and mark it unreachable(),
and that masking is kept around the new hibernate_exit() call. Commit
2c92eff008a2 ("arm64: Provide dcache_by_myline_op_nosync helper") split
dcache_by_myline_op into a _nosync form, so use that and rely on the
existing DSB after the maintenance. swsusp_arch_resume() also keeps its
__nocfi annotation from commit e2f8216ca2d8 ("arm64: Set __nocfi on
swsusp_arch_resume()").

Signed-off-by: Bradley Morgan <brads@mainlining.org>
Cc: Ard Biesheuvel <ardb@kernel.org>
Cc: Catalin Marinas <catalin.marinas@arm.com>
Cc: James Morse <james.morse@arm.com>
Cc: Marc Zyngier <maz@kernel.org>
Cc: Will Deacon <will@kernel.org>
---
 arch/arm64/include/asm/suspend.h  |  1 -
 arch/arm64/kernel/hibernate-asm.S | 78 ++++++++++++++++++++-----------
 arch/arm64/kernel/hibernate.c     | 55 ++++++++--------------
 arch/arm64/kernel/sleep.S         |  7 +--
 4 files changed, 71 insertions(+), 70 deletions(-)

diff --git a/arch/arm64/include/asm/suspend.h b/arch/arm64/include/asm/suspend.h
index 1b7570902264..3aba69df06b9 100644
--- a/arch/arm64/include/asm/suspend.h
+++ b/arch/arm64/include/asm/suspend.h
@@ -41,7 +41,6 @@ extern int cpu_suspend(unsigned long arg, int (*fn)(unsigned long));
 extern void cpu_resume(void);
 int __cpu_suspend_enter(struct sleep_stack_data *state);
 void __cpu_suspend_exit(void);
-void __cpu_resume_switched(void);
 
 int swsusp_arch_suspend(void);
 int swsusp_arch_resume(void);
diff --git a/arch/arm64/kernel/hibernate-asm.S b/arch/arm64/kernel/hibernate-asm.S
index f7613404f99e..edfab9ce4ed1 100644
--- a/arch/arm64/kernel/hibernate-asm.S
+++ b/arch/arm64/kernel/hibernate-asm.S
@@ -29,40 +29,48 @@
  *
  * This 'safe' page is mapped via ttbr0, and executed from there. This function
  * switches to a copy of the linear map in ttbr1, performs the restore, then
- * switches ttbr1 to the original kernel's swapper_pg_dir.
+ * reenters the original kernel with the MMU off.
  *
  * All of memory gets written to, including code. We need to clean the kernel
- * text to the Point of Coherence (PoC) before secondary cores can be booted.
+ * text to the Point of Coherence (PoC) before reentering it.
  * Because the kernel modules and executable pages mapped to user space are
  * also written as data, we clean all pages we touch to the Point of
  * Unification (PoU).
  *
- * x0: physical address of temporary page tables
- * x1: physical address of swapper page tables
- * x2: address of cpu_resume
- * x3: linear map address of restore_pblist in the current kernel
- * x4: physical address of __hyp_stub_vectors, or 0
- * x5: physical address of a  zero page that remains zero after resume
+ * x0: physical address of temporary zero page
+ * x1: physical address of temporary page tables
+ * x2: linear map address of restore_pblist in the current kernel
+ * x3: linear map address of the start of the resumed kernel image
+ * x4: linear map address of the end of the resumed kernel image
+ * x5: physical address to reenter the resumed kernel
+ * x6: whether to return to el2
  */
 .pushsection    ".hibernate_exit.text", "ax"
 SYM_CODE_START(swsusp_arch_suspend_exit)
 	/*
-	 * We execute from ttbr0, change ttbr1 to our copied linear map tables
-	 * with a break-before-make via the zero page
+	 * Save the arguments, the low numbered registers get clobbered below.
 	 */
-	break_before_make_ttbr_switch	x5, x0, x6, x8
-
-	mov	x21, x1
-	mov	x30, x2
+	mov	x22, x2
+	mov	x23, x3
 	mov	x24, x4
 	mov	x25, x5
+	mov	x26, x6
 
-	/* walk the restore_pblist and use copy_page() to over-write memory */
-	mov	x19, x3
+	/*
+	 * We run from an idmap in TTBR0 here, install the temporary linear
+	 * map in TTBR1.
+	 */
+	break_before_make_ttbr_switch	x0, x1, x3, x4
 
-1:	ldr	x10, [x19, #HIBERN_PBE_ORIG]
-	mov	x0, x10
-	ldr	x1, [x19, #HIBERN_PBE_ADDR]
+	/*
+	 * Walk the restore_pblist and restore each page, cleaning to the PoU
+	 * in case it holds userspace or module code.
+	 */
+.L__next_pbe:
+	ldr	x0,  [x22, #HIBERN_PBE_ORIG]
+	ldr	x1,  [x22, #HIBERN_PBE_ADDR]
+
+	mov	x10, x0
 
 	copy_page	x0, x1, x2, x3, x4, x5, x6, x7, x8, x9
 
@@ -72,21 +80,35 @@ SYM_CODE_START(swsusp_arch_suspend_exit)
 	raw_dcache_line_size x2, x3
 	dcache_by_myline_op_nosync cvau, x0, x1, x2, x3
 
-	ldr	x19, [x19, #HIBERN_PBE_NEXT]
-	cbnz	x19, 1b
-	dsb	ish		/* wait for PoU cleaning to finish */
+	ldr	x22, [x22, #HIBERN_PBE_NEXT]
+	cbnz	x22, .L__next_pbe
 
-	/* switch to the restored kernels page tables */
-	break_before_make_ttbr_switch	x25, x21, x6, x8
+	/* Clean the kernel image to the PoC, it gets used with the MMU off */
+	dcache_by_myline_op_nosync cvac, x23, x24, x2, x3
+
+	/* Complete all D-cache maintenance above */
+	dsb	ish
 
 	ic	ialluis
 	dsb	ish
 	isb
 
-	cbz	x24, 3f		/* Do we need to re-initialise EL2? */
-	mov	x1, x24
-	mov	x0, #HVC_SET_VECTORS
+	cbz	x26, .L__no_el2
+
+	/* Enter the resumed kernel at EL2 with the MMU disabled. */
+	mov	x0, #HVC_SOFT_RESTART
+	mov	x1, x25
 	hvc	#0
-3:	ret
+
+	/* HVC_SOFT_RESTART should never return */
+	b	.
+
+.L__no_el2:
+	/* Enter the resumed kernel at EL1 with the MMU disabled. */
+	mov_q	x0, INIT_SCTLR_EL1_MMU_OFF
+	pre_disable_mmu_workaround
+	msr	sctlr_el1, x0
+	isb
+	br	x25
 SYM_CODE_END(swsusp_arch_suspend_exit)
 .popsection
diff --git a/arch/arm64/kernel/hibernate.c b/arch/arm64/kernel/hibernate.c
index 60e264299a4d..c3556d1c4cef 100644
--- a/arch/arm64/kernel/hibernate.c
+++ b/arch/arm64/kernel/hibernate.c
@@ -69,15 +69,9 @@ struct arch_hibernate_hdr_invariants {
 static struct arch_hibernate_hdr {
 	struct arch_hibernate_hdr_invariants invariants;
 
-	/* These are needed to find the relocated kernel if built with kaslr */
-	phys_addr_t	ttbr1_el1;
-	void		(*reenter_kernel)(void);
-
-	/*
-	 * We need to know where the __hyp_stub_vectors are after restore to
-	 * re-configure el2.
-	 */
-	phys_addr_t	__hyp_stub_vectors;
+	phys_addr_t	kernel_start;
+	phys_addr_t	kernel_end;
+	phys_addr_t	reenter_kernel;
 
 	u64		sleep_cpu_mpidr;
 } resume_hdr;
@@ -113,14 +107,10 @@ int arch_hibernation_header_save(void *addr, unsigned int max_size)
 		return -EOVERFLOW;
 
 	arch_hdr_invariants(&hdr->invariants);
-	hdr->ttbr1_el1		= __pa_symbol(swapper_pg_dir);
-	hdr->reenter_kernel	= __cpu_resume_switched;
 
-	/* We can't use __hyp_get_vectors() because kvm may still be loaded */
-	if (el2_reset_needed())
-		hdr->__hyp_stub_vectors = __pa_symbol(__hyp_stub_vectors);
-	else
-		hdr->__hyp_stub_vectors = 0;
+	hdr->kernel_start	= __pa_symbol(_text);
+	hdr->kernel_end		= __pa_symbol(_end);
+	hdr->reenter_kernel	= __pa_symbol(cpu_resume);
 
 	/* Save the mpidr of the cpu we called cpu_suspend() on... */
 	if (sleep_cpu < 0) {
@@ -366,20 +356,6 @@ int swsusp_arch_suspend(void)
 		 */
 		swsusp_mte_free_storage();
 	} else {
-		/* Clean kernel core startup/idle code to PoC*/
-		dcache_clean_poc((unsigned long)__mmuoff_data_start,
-				 (unsigned long)__mmuoff_data_end);
-		dcache_clean_poc((unsigned long)__idmap_text_start,
-				 (unsigned long)__idmap_text_end);
-
-		/* Clean kvm setup code to PoC? */
-		if (el2_reset_needed()) {
-			dcache_clean_poc((unsigned long)__hyp_idmap_text_start,
-					 (unsigned long)__hyp_idmap_text_end);
-			dcache_clean_poc((unsigned long)__hyp_text_start,
-					 (unsigned long)__hyp_text_end);
-		}
-
 		swsusp_mte_restore_tags();
 
 		/* make the crash dump kernel image protected again */
@@ -420,8 +396,12 @@ int __nocfi swsusp_arch_resume(void)
 	size_t exit_size;
 	pgd_t *tmp_pg_dir;
 	phys_addr_t el2_vectors;
-	void __noreturn (*hibernate_exit)(phys_addr_t, phys_addr_t, void *,
-					  void *, phys_addr_t, phys_addr_t);
+
+	void (*hibernate_exit)(phys_addr_t zero_page, phys_addr_t temp_pgd,
+			       void *pblist, void *kstart, void *kend,
+			       phys_addr_t cpu_resume,
+			       unsigned long el2) __noreturn;
+
 	struct trans_pgd_info trans_info = {
 		.trans_alloc_page	= hibernate_page_alloc,
 		.trans_alloc_arg	= (__force void *)GFP_ATOMIC,
@@ -487,11 +467,14 @@ int __nocfi swsusp_arch_resume(void)
 	 *   same state : with all DAIF exceptions masked.
 	 */
 	local_daif_save();
-	hibernate_exit(virt_to_phys(tmp_pg_dir), resume_hdr.ttbr1_el1,
-		       resume_hdr.reenter_kernel, restore_pblist,
-		       resume_hdr.__hyp_stub_vectors, virt_to_phys(zero_page));
+	hibernate_exit(virt_to_phys(zero_page),
+		       virt_to_phys(tmp_pg_dir),
+		       restore_pblist,
+		       phys_to_virt(resume_hdr.kernel_start),
+		       phys_to_virt(resume_hdr.kernel_end),
+		       resume_hdr.reenter_kernel,
+		       el2_reset_needed());
 	unreachable();
-
 	return 0;
 }
 
diff --git a/arch/arm64/kernel/sleep.S b/arch/arm64/kernel/sleep.S
index da45ab63bd9c..8c63ffdf6d75 100644
--- a/arch/arm64/kernel/sleep.S
+++ b/arch/arm64/kernel/sleep.S
@@ -107,19 +107,16 @@ SYM_CODE_START(cpu_resume)
 	adrp	x1, swapper_pg_dir
 	adrp	x2, idmap_pg_dir
 	bl	__enable_mmu
-	ldr	x8, =__cpu_resume_switched_finalise_el2
+	ldr	x8, =__cpu_resume_switched
 	br	x8
 SYM_CODE_END(cpu_resume)
 	.ltorg
 	.popsection
 
-SYM_FUNC_START_LOCAL(__cpu_resume_switched_finalise_el2)
+SYM_FUNC_START_LOCAL(__cpu_resume_switched)
 	mov	x0, x19
 	bl	finalise_el2
-	b	__cpu_resume_switched
-SYM_FUNC_END(__cpu_resume_switched_finalise_el2)
 
-SYM_FUNC_START(__cpu_resume_switched)
 	mrs	x1, mpidr_el1
 	adr_l	x8, mpidr_hash		// x8 = struct mpidr_hash virt address
 
-- 
2.47.3


      parent reply	other threads:[~2026-08-25 20:58 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-25 20:58 [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework Bradley Morgan
2026-08-25 20:58 ` [PATCH 1/7] arm64: hibernate: pass HVC_SET_VECTORS args to the resume hvc Bradley Morgan
2026-08-25 20:58 ` [PATCH 2/7] arm64: head: correct comment for init_kernel_el() Bradley Morgan
2026-08-25 20:58 ` [PATCH 3/7] arm64: hibernate: free MTE tag pages after saving Bradley Morgan
2026-08-25 20:58 ` [PATCH 4/7] arm64: hibernate: fix _cpu_resume() calling convention Bradley Morgan
2026-08-25 20:58 ` [PATCH 5/7] arm64: hibernate: use dcache_by_myline_op Bradley Morgan
2026-08-25 20:58 ` [PATCH 6/7] arm64: hibernate: only clean to PoC Bradley Morgan
2026-08-25 20:58 ` Bradley Morgan [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260825205839.14571-8-brads@mainlining.org \
    --to=brads@mainlining.org \
    --cc=ardb@kernel.org \
    --cc=catalin.marinas@arm.com \
    --cc=james.morse@arm.com \
    --cc=linux-arm-kernel@lists.infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=mark.rutland@arm.com \
    --cc=maz@kernel.org \
    --cc=will@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox