* [PATCH 1/7] arm64: hibernate: pass HVC_SET_VECTORS args to the resume hvc
2026-08-25 20:58 [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework Bradley Morgan
@ 2026-08-25 20:58 ` Bradley Morgan
2026-08-25 20:58 ` [PATCH 2/7] arm64: head: correct comment for init_kernel_el() Bradley Morgan
` (6 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Bradley Morgan @ 2026-08-25 20:58 UTC (permalink / raw)
To: Will Deacon
Cc: Catalin Marinas, Mark Rutland, James Morse, Marc Zyngier,
Ard Biesheuvel, linux-arm-kernel, linux-kernel, brads, stable
swsusp_arch_suspend_exit() reinstalls the restored kernel's hyp stub
vectors with an hvc, but never passes the arguments. x0 is not set to
HVC_SET_VECTORS and x1 is not set to the vector address, so the stub
dispatch falls through and returns without writing vbar_el2. EL2 is
left pointing at the trans_pgd copy of the vectors, a page that
swsusp_free() releases right after resume.
Set the arguments up the same way __hyp_set_vectors() does.
Fixes: 788bfdd97434 ("arm64: trans_pgd: hibernate: Add trans_pgd_copy_el2_vectors")
Cc: stable@vger.kernel.org
Signed-off-by: Bradley Morgan <brads@mainlining.org>
---
arch/arm64/kernel/hibernate-asm.S | 2 ++
1 file changed, 2 insertions(+)
diff --git a/arch/arm64/kernel/hibernate-asm.S b/arch/arm64/kernel/hibernate-asm.S
index 0e1d9c3c6a93..2baefe7a82d3 100644
--- a/arch/arm64/kernel/hibernate-asm.S
+++ b/arch/arm64/kernel/hibernate-asm.S
@@ -89,6 +89,8 @@ alternative_insn "dc cvau, x4", "dc civac, x4", ARM64_WORKAROUND_CLEAN_CACHE
isb
cbz x24, 3f /* Do we need to re-initialise EL2? */
+ mov x1, x24
+ mov x0, #HVC_SET_VECTORS
hvc #0
3: ret
SYM_CODE_END(swsusp_arch_suspend_exit)
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH 2/7] arm64: head: correct comment for init_kernel_el()
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 ` Bradley Morgan
2026-08-25 20:58 ` [PATCH 3/7] arm64: hibernate: free MTE tag pages after saving Bradley Morgan
` (5 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Bradley Morgan @ 2026-08-25 20:58 UTC (permalink / raw)
To: Will Deacon
Cc: Catalin Marinas, Mark Rutland, James Morse, Marc Zyngier,
Ard Biesheuvel, linux-arm-kernel, linux-kernel, brads
The comment above init_kernel_el() still says the function configures
the CPU to execute at the highest reachable EL, but that stopped being
true a while back. Ever since commit ae4b7e38e9a94798 ("arm64: Allow
sticky E2H when entering EL1"), init_kernel_el() always drops to EL1,
and it is finalise_el2() that brings us back up to EL2 when we want it.
Update the comment to match what the code actually does.
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/kernel/head.S | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/arch/arm64/kernel/head.S b/arch/arm64/kernel/head.S
index 87a822e5c4ca..c6301557eee1 100644
--- a/arch/arm64/kernel/head.S
+++ b/arch/arm64/kernel/head.S
@@ -254,9 +254,9 @@ SYM_FUNC_END(__primary_switched)
.section ".idmap.text","a"
/*
- * Starting from EL2 or EL1, configure the CPU to execute at the highest
- * reachable EL supported by the kernel in a chosen default state. If dropping
- * from EL2 to EL1, configure EL2 before configuring EL1.
+ * Starting from EL2 or EL1, configure the CPU to execute at EL1.
+ * If dropping from EL2 to EL1, configure EL2 before configuring EL1.
+ * To use VHE we'll upgrade back to EL2 later in finalise_el2().
*
* Since we cannot always rely on ERET synchronizing writes to sysregs (e.g. if
* SCTLR_ELx.EOS is clear), we place an ISB prior to ERET.
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH 3/7] arm64: hibernate: free MTE tag pages after saving
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 ` Bradley Morgan
2026-08-25 20:58 ` [PATCH 4/7] arm64: hibernate: fix _cpu_resume() calling convention Bradley Morgan
` (4 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Bradley Morgan @ 2026-08-25 20:58 UTC (permalink / raw)
To: Will Deacon
Cc: Catalin Marinas, Mark Rutland, James Morse, Marc Zyngier,
Ard Biesheuvel, linux-arm-kernel, linux-kernel, brads
swsusp_arch_suspend() calls swsusp_mte_save_tags() to stash the
userspace MTE tags, but we only free them on a successful resume in
swsusp_mte_restore_tags(). If hibernation fails partway, the tags stay
allocated, and the next time we try to hibernate,
swsusp_mte_save_tags() warns about duplicate entries in the mte_pages
xarray.
Reproducing it is easy, just make a hibernate fail twice in a row.
| # echo test_resume > /sys/power/disk
| # echo disk > /sys/power/state
...
| PM: Cannot find swap device, try swapon -a
| PM: Cannot get swap writer
...
| # echo disk > /sys/power/state
...
| ------------[ cut here ]------------
| swsusp: save_tags: Duplicate entry
| WARNING: CPU: 0 PID: 123 at arch/arm64/kernel/hibernate.c:234 \
| swsusp_arch_suspend+0x3f0/0x5b0
...
Free the tag storage right after swsusp_save() has copied it into the
hibernation image. Where hibernation succeeds, the tags come back from
the image and get consumed by swsusp_mte_restore_tags() as before.
Fixes: ee11f332af96 ("arm64: mte: Save tags when hibernating")
Signed-off-by: Bradley Morgan <brads@mainlining.org>
---
arch/arm64/kernel/hibernate.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/arch/arm64/kernel/hibernate.c b/arch/arm64/kernel/hibernate.c
index 7bf117427777..8ac29058a839 100644
--- a/arch/arm64/kernel/hibernate.c
+++ b/arch/arm64/kernel/hibernate.c
@@ -241,14 +241,18 @@ static int save_tags(struct page *page, unsigned long pfn)
static void swsusp_mte_free_storage(void)
{
XA_STATE(xa_state, &mte_pages, 0);
+ int n = 0;
void *tags;
xa_lock(&mte_pages);
xas_for_each(&xa_state, tags, ULONG_MAX) {
mte_free_tag_storage(tags);
+ n++;
}
xa_unlock(&mte_pages);
+ pr_info("Freed %d MTE pages\n", n);
+
xa_destroy(&mte_pages);
}
@@ -355,6 +359,12 @@ int swsusp_arch_suspend(void)
sleep_cpu = smp_processor_id();
ret = swsusp_save();
+
+ /*
+ * Hibernation can still fail from here, so free the tags now,
+ * a second attempt would warn on duplicate entries otherwise.
+ */
+ swsusp_mte_free_storage();
} else {
/* Clean kernel core startup/idle code to PoC*/
dcache_clean_inval_poc((unsigned long)__mmuoff_data_start,
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH 4/7] arm64: hibernate: fix _cpu_resume() calling convention
2026-08-25 20:58 [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework Bradley Morgan
` (2 preceding siblings ...)
2026-08-25 20:58 ` [PATCH 3/7] arm64: hibernate: free MTE tag pages after saving Bradley Morgan
@ 2026-08-25 20:58 ` Bradley Morgan
2026-08-25 20:58 ` [PATCH 5/7] arm64: hibernate: use dcache_by_myline_op Bradley Morgan
` (3 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Bradley Morgan @ 2026-08-25 20:58 UTC (permalink / raw)
To: Will Deacon
Cc: Catalin Marinas, Mark Rutland, James Morse, Marc Zyngier,
Ard Biesheuvel, linux-arm-kernel, linux-kernel, brads
The hibernate path borrows bits of the normal idle suspend/resume code
and open codes the rest. It skips cpu_resume() and has
swsusp_arch_suspend_exit() branch straight into _cpu_resume() at the
kernel's native EL with the MMU on.
Commit 82e4958800c01daa ("arm64: head: Move all finalise_el2 calls to
after __enable_mmu") changed the calling convention for _cpu_resume()
to expect EL1 with the boot mode in x19, but we never updated
swsusp_arch_suspend_exit() to match.
So when swsusp_arch_suspend_exit() calls _cpu_resume(), x19 holds the
final struct pbe next pointer, which must be NULL since it marks the
end of the list. _cpu_resume() then passes that to finalise_el2() in
x0, and finalise_el2() only issues an HVC when the value is
BOOT_CPU_MODE_EL2 and we are at EL1, so it happens not to fire. That is
the right outcome, but it is pure luck rather than design.
Split _cpu_resume() so this is less fragile.
__cpu_resume_switched() is the shared part, used by both the idle and
hibernate code. It takes no arguments and issues no HVC. The name is
meant to match __primary_switched and __secondary_switched.
__cpu_resume_switched_finalise_el2() is for the idle path only. It
pulls the boot mode from x19 and calls finalise_el2() before
__cpu_resume_switched(). It is kept local to sleep.S so the odd calling
convention does not leak any further.
Fixes: 82e4958800c01daa ("arm64: head: Move all finalise_el2 calls to after __enable_mmu")
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 | 2 +-
arch/arm64/kernel/hibernate.c | 2 +-
arch/arm64/kernel/sleep.S | 9 ++++++---
3 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/arch/arm64/include/asm/suspend.h b/arch/arm64/include/asm/suspend.h
index e9ce68d50ba4..1b7570902264 100644
--- a/arch/arm64/include/asm/suspend.h
+++ b/arch/arm64/include/asm/suspend.h
@@ -41,7 +41,7 @@ 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(void);
+void __cpu_resume_switched(void);
int swsusp_arch_suspend(void);
int swsusp_arch_resume(void);
diff --git a/arch/arm64/kernel/hibernate.c b/arch/arm64/kernel/hibernate.c
index 8ac29058a839..30b02e39a397 100644
--- a/arch/arm64/kernel/hibernate.c
+++ b/arch/arm64/kernel/hibernate.c
@@ -114,7 +114,7 @@ int arch_hibernation_header_save(void *addr, unsigned int max_size)
arch_hdr_invariants(&hdr->invariants);
hdr->ttbr1_el1 = __pa_symbol(swapper_pg_dir);
- hdr->reenter_kernel = _cpu_resume;
+ hdr->reenter_kernel = __cpu_resume_switched;
/* We can't use __hyp_get_vectors() because kvm may still be loaded */
if (el2_reset_needed())
diff --git a/arch/arm64/kernel/sleep.S b/arch/arm64/kernel/sleep.S
index f093cdf71be1..da45ab63bd9c 100644
--- a/arch/arm64/kernel/sleep.S
+++ b/arch/arm64/kernel/sleep.S
@@ -107,16 +107,19 @@ SYM_CODE_START(cpu_resume)
adrp x1, swapper_pg_dir
adrp x2, idmap_pg_dir
bl __enable_mmu
- ldr x8, =_cpu_resume
+ ldr x8, =__cpu_resume_switched_finalise_el2
br x8
SYM_CODE_END(cpu_resume)
.ltorg
.popsection
-SYM_FUNC_START(_cpu_resume)
+SYM_FUNC_START_LOCAL(__cpu_resume_switched_finalise_el2)
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
@@ -152,4 +155,4 @@ SYM_FUNC_START(_cpu_resume)
ldp x29, lr, [x29]
mov x0, #0
ret
-SYM_FUNC_END(_cpu_resume)
+SYM_FUNC_END(__cpu_resume_switched)
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH 5/7] arm64: hibernate: use dcache_by_myline_op
2026-08-25 20:58 [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework Bradley Morgan
` (3 preceding siblings ...)
2026-08-25 20:58 ` [PATCH 4/7] arm64: hibernate: fix _cpu_resume() calling convention Bradley Morgan
@ 2026-08-25 20:58 ` Bradley Morgan
2026-08-25 20:58 ` [PATCH 6/7] arm64: hibernate: only clean to PoC Bradley Morgan
` (2 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Bradley Morgan @ 2026-08-25 20:58 UTC (permalink / raw)
To: Will Deacon
Cc: Catalin Marinas, Mark Rutland, James Morse, Marc Zyngier,
Ard Biesheuvel, linux-arm-kernel, linux-kernel, brads
swsusp_arch_suspend_exit() used to open code its cache maintenance, but
the dcache_by_myline_op macro does everything that open coded version
did, including the ARM64_WORKAROUND_CLEAN_CACHE handling. So use it.
Since this was first written the macro got split, with the trailing DSB
moved out into a wrapper, so use dcache_by_myline_op_nosync here and
keep relying on the DSB that already sits after the restore loop. That
keeps this a true no-op.
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/kernel/hibernate-asm.S | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/arch/arm64/kernel/hibernate-asm.S b/arch/arm64/kernel/hibernate-asm.S
index 2baefe7a82d3..f7613404f99e 100644
--- a/arch/arm64/kernel/hibernate-asm.S
+++ b/arch/arm64/kernel/hibernate-asm.S
@@ -66,16 +66,11 @@ SYM_CODE_START(swsusp_arch_suspend_exit)
copy_page x0, x1, x2, x3, x4, x5, x6, x7, x8, x9
+ /* Clean the restored page to the PoU */
+ mov x0, x10
add x1, x10, #PAGE_SIZE
- /* Clean the copied page to PoU - based on caches_clean_inval_pou() */
raw_dcache_line_size x2, x3
- sub x3, x2, #1
- bic x4, x10, x3
-2: /* clean D line / unified line */
-alternative_insn "dc cvau, x4", "dc civac, x4", ARM64_WORKAROUND_CLEAN_CACHE
- add x4, x4, x2
- cmp x4, x1
- b.lo 2b
+ dcache_by_myline_op_nosync cvau, x0, x1, x2, x3
ldr x19, [x19, #HIBERN_PBE_NEXT]
cbnz x19, 1b
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH 6/7] arm64: hibernate: only clean to PoC
2026-08-25 20:58 [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework Bradley Morgan
` (4 preceding siblings ...)
2026-08-25 20:58 ` [PATCH 5/7] arm64: hibernate: use dcache_by_myline_op Bradley Morgan
@ 2026-08-25 20:58 ` Bradley Morgan
2026-08-25 20:58 ` [PATCH 7/7] arm64: hibernate: use regular cpu_resume() logic Bradley Morgan
2026-08-28 10:32 ` [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework Ard Biesheuvel
7 siblings, 0 replies; 10+ messages in thread
From: Bradley Morgan @ 2026-08-25 20:58 UTC (permalink / raw)
To: Will Deacon
Cc: Catalin Marinas, Mark Rutland, James Morse, Marc Zyngier,
Ard Biesheuvel, linux-arm-kernel, linux-kernel, brads
The hibernate code has to clean parts of the kernel to the PoC so they
can be used with the MMU off. We only need to clean to the PoC, but we
have been using dcache_clean_inval_poc(), which does a clean plus
invalidate, just because that is what was available.
When hibernate was added in commit 82869ac57b5d ("arm64: kernel: Add
support for hibernate/suspend-to-disk"), the only cache maintenance
exposed outside the DMA code was __flush_dcache_area(), so that is what
got used. Commit d34fdb7081394cbf ("arm64: mm: convert __dma_* routines to use start, size")
later exposed dcache_clean_area_poc(), and commit fade9c2c6ee2baea
("arm64: Rename arm64-internal cache maintenance functions") renamed
things again, but the hibernate code never switched over either time.
The invalidate is pointless anyway, since the mappings we clean through
are cacheable and the CPU can just pull the memory straight back into
cache right after. Use dcache_clean_poc() instead.
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/kernel/hibernate.c | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/arch/arm64/kernel/hibernate.c b/arch/arm64/kernel/hibernate.c
index 30b02e39a397..60e264299a4d 100644
--- a/arch/arm64/kernel/hibernate.c
+++ b/arch/arm64/kernel/hibernate.c
@@ -367,18 +367,17 @@ int swsusp_arch_suspend(void)
swsusp_mte_free_storage();
} else {
/* Clean kernel core startup/idle code to PoC*/
- dcache_clean_inval_poc((unsigned long)__mmuoff_data_start,
- (unsigned long)__mmuoff_data_end);
- dcache_clean_inval_poc((unsigned long)__idmap_text_start,
- (unsigned long)__idmap_text_end);
+ 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_inval_poc(
- (unsigned long)__hyp_idmap_text_start,
- (unsigned long)__hyp_idmap_text_end);
- dcache_clean_inval_poc((unsigned long)__hyp_text_start,
- (unsigned long)__hyp_text_end);
+ 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();
--
2.47.3
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH 7/7] arm64: hibernate: use regular cpu_resume() logic
2026-08-25 20:58 [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework Bradley Morgan
` (5 preceding siblings ...)
2026-08-25 20:58 ` [PATCH 6/7] arm64: hibernate: only clean to PoC Bradley Morgan
@ 2026-08-25 20:58 ` Bradley Morgan
2026-08-28 10:32 ` [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework Ard Biesheuvel
7 siblings, 0 replies; 10+ messages in thread
From: Bradley Morgan @ 2026-08-25 20:58 UTC (permalink / raw)
To: Will Deacon
Cc: Catalin Marinas, Mark Rutland, James Morse, Marc Zyngier,
Ard Biesheuvel, linux-arm-kernel, linux-kernel, brads
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
^ permalink raw reply related [flat|nested] 10+ messages in thread* Re: [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework
2026-08-25 20:58 [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework Bradley Morgan
` (6 preceding siblings ...)
2026-08-25 20:58 ` [PATCH 7/7] arm64: hibernate: use regular cpu_resume() logic Bradley Morgan
@ 2026-08-28 10:32 ` Ard Biesheuvel
2026-08-28 11:31 ` Bradley Morgan
7 siblings, 1 reply; 10+ messages in thread
From: Ard Biesheuvel @ 2026-08-28 10:32 UTC (permalink / raw)
To: Bradley Morgan, Will Deacon
Cc: Catalin Marinas, Mark Rutland, James Morse, Marc Zyngier,
linux-arm-kernel, linux-kernel
Hi Bradley,
On Tue, 25 Aug 2026, at 22:58, Bradley Morgan wrote:
> Everything object compiles and passes checkpatch and sparse. I could not
> boot or run a hibernate cycle on real hardware here, so that still wants
> doing before anyone gets excited.
>
You can easily test hibernate under QEMU/mach-virt: just create a file on
the host, mkswap it and pass it to the VM as a virtio drive (e.g., /dev/vda)
You can then swapon it in the VM and do 'echo disk >/sys/power/state', which
will perform the hibernate and exit QEMU. If you then restart it with
'resume=/dev/vda' on the command line, it will perform the resume from
hibernate at boot.
I've had a go with your series on a NV capable system (Mac Mini M2). Without
the series applied, I can hibernate and resume 4 vCPUs without issue, so it
is not entirely clear to me what your series intends to fix.
With the series applied resume from hibernate still works - I did see one
intermittent failure where the secondaries failed to come up, but I only
saw it once and so I cannot say for certain whether your series triggered
it or not.
Tested-by: Ard Biesheuvel <ardb@kernel.org>
^ permalink raw reply [flat|nested] 10+ messages in thread* Re: [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework
2026-08-28 10:32 ` [PATCH 0/7] arm64: hibernate: dust off the suspend/cleanup rework Ard Biesheuvel
@ 2026-08-28 11:31 ` Bradley Morgan
0 siblings, 0 replies; 10+ messages in thread
From: Bradley Morgan @ 2026-08-28 11:31 UTC (permalink / raw)
To: Ard Biesheuvel, Will Deacon
Cc: Catalin Marinas, Mark Rutland, James Morse, Marc Zyngier,
linux-arm-kernel, linux-kernel
On 28 August 2026 11:32:27 BST, Ard Biesheuvel <ardb@kernel.org> wrote:
>Hi Bradley,
>
Ard!
>On Tue, 25 Aug 2026, at 22:58, Bradley Morgan wrote:
>> Everything object compiles and passes checkpatch and sparse. I could not
>> boot or run a hibernate cycle on real hardware here, so that still wants
>> doing before anyone gets excited.
>>
>
>You can easily test hibernate under QEMU/mach-virt: just create a file on
>the host, mkswap it and pass it to the VM as a virtio drive (e.g.,
>/dev/vda)
>You can then swapon it in the VM and do 'echo disk >/sys/power/state',
>which
>will perform the hibernate and exit QEMU. If you then restart it with
>'resume=/dev/vda' on the command line, it will perform the resume from
>hibernate at boot.
>
Ack. Tried on PKVM last night.
>I've had a go with your series on a NV capable system (Mac Mini M2).
>Without
>the series applied, I can hibernate and resume 4 vCPUs without issue, so
>it
>is not entirely clear to me what your series intends to fix.
>
>With the series applied resume from hibernate still works - I did see one
>intermittent failure where the secondaries failed to come up, but I only
>saw it once and so I cannot say for certain whether your series triggered
>it or not.
>
>Tested-by: Ard Biesheuvel <ardb@kernel.org>
>
For the whole series?
>
>
>
>
>
>
>
--- Thanks!
https://lore.kernel.org/all/EE579805-42F2-4C58-B752-F28779EEB717@grrlz.net/
^ permalink raw reply [flat|nested] 10+ messages in thread