[RFC PATCH 11/46] liveupdate: Define CPU preservation linker sections

From: Pasha Tatashin

Date: Sun Sep 20 2026 - 15:41:05 EST


Define CPU_PRESERVED_TEXT and CPU_PRESERVED_DATA linker macros to place
code and data required for CPU preservation into dedicated sections
(.text.cpu_preserved and .data.cpu_preserved).

These sections are preserved across live update transitions and mapped
executable/read-write as needed.

Signed-off-by: Pasha Tatashin <pasha.tatashin@xxxxxxxxxx>
---
include/asm-generic/vmlinux.lds.h | 29 +++++++++++++++++
include/linux/cpu_preserve.h | 47 +++++++++++++++++++++++++++
scripts/mod/modpost.c | 32 +++++++++++++++---
tools/objtool/check.c | 35 +++++++++++++++++++-
tools/objtool/include/objtool/check.h | 1 +
tools/objtool/include/objtool/elf.h | 2 +-
6 files changed, 140 insertions(+), 6 deletions(-)
create mode 100644 include/linux/cpu_preserve.h

diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h
index b2988aa12f66..0dbaac582bcc 100644
--- a/include/asm-generic/vmlinux.lds.h
+++ b/include/asm-generic/vmlinux.lds.h
@@ -655,6 +655,34 @@
*(.static_call.text) \
__static_call_text_end = .;

+/*
+ * Page-aligned text and data sections for preserved CPUs.
+ * This code and data are KHO preserved when CPUs are preserved across
+ * live update.
+ */
+#ifdef CONFIG_LIVEUPDATE_CPU
+#define CPU_PRESERVED_TEXT \
+ . = ALIGN(PAGE_SIZE); \
+ __cpu_preserved_text_start = .; \
+ *(.text.cpu_preserved .text.cpu_preserved.*) \
+ *(.cpu_preserved.text .cpu_preserved.text.*) \
+ . = ALIGN(PAGE_SIZE); \
+ __cpu_preserved_text_end = .;
+
+#define CPU_PRESERVED_DATA \
+ . = ALIGN(PAGE_SIZE); \
+ __cpu_preserved_data_start = .; \
+ *(.data.cpu_preserved .data.cpu_preserved.*) \
+ *(.cpu_preserved.data .cpu_preserved.data.*) \
+ *(.rodata.cpu_preserved .rodata.cpu_preserved.*) \
+ *(.bss..data.cpu_preserved .bss..data.cpu_preserved.*) \
+ . = ALIGN(PAGE_SIZE); \
+ __cpu_preserved_data_end = .;
+#else
+#define CPU_PRESERVED_TEXT
+#define CPU_PRESERVED_DATA
+#endif
+
/* Section used for early init (in .S files) */
#define HEAD_TEXT KEEP(*(.head.text))

@@ -1155,6 +1183,7 @@
INIT_TASK_DATA(inittask) \
NOSAVE_DATA \
PAGE_ALIGNED_DATA(pagealigned) \
+ CPU_PRESERVED_DATA \
CACHE_HOT_DATA(cacheline) \
CACHELINE_ALIGNED_DATA(cacheline) \
READ_MOSTLY_DATA(cacheline) \
diff --git a/include/linux/cpu_preserve.h b/include/linux/cpu_preserve.h
new file mode 100644
index 000000000000..f653838c383d
--- /dev/null
+++ b/include/linux/cpu_preserve.h
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2026, Google LLC.
+ * Pasha Tatashin <pasha.tatashin@xxxxxxxxxx>
+ *
+ * Preserved CPU across Live Update
+ */
+#ifndef _LINUX_CPU_PRESERVE_H
+#define _LINUX_CPU_PRESERVE_H
+
+#include <linux/compiler.h>
+
+#ifdef CONFIG_LIVEUPDATE_CPU
+
+/*
+ * __cpu_preserved_text: Code executed by preserved physical CPUs during live
+ * update kexec handover in orphan mode.
+ *
+ * All code in this section must run without stack protector checks because
+ * per-CPU canary state may be invalid during handover and __stack_chk_fail()
+ * resides in regular .text, which gets overwritten during kexec before the
+ * incoming kernel boots.
+ *
+ * Architecture-specific requirements (such as disabling external retpolines
+ * and return thunks on x86) are supplied via ARCH_CPU_PRESERVED_TEXT.
+ */
+#ifndef ARCH_CPU_PRESERVED_TEXT
+#define ARCH_CPU_PRESERVED_TEXT
+#endif
+
+#define __cpu_preserved_text \
+ __section(".text.cpu_preserved") \
+ __no_stack_protector \
+ ARCH_CPU_PRESERVED_TEXT
+#define __cpu_preserved_data __section(".data.cpu_preserved")
+
+extern char __cpu_preserved_text_start[], __cpu_preserved_text_end[];
+extern char __cpu_preserved_data_start[], __cpu_preserved_data_end[];
+
+#else /* !CONFIG_LIVEUPDATE_CPU */
+
+#define __cpu_preserved_text
+#define __cpu_preserved_data
+
+#endif /* CONFIG_LIVEUPDATE_CPU */
+
+#endif /* _LINUX_CPU_PRESERVE_H */
diff --git a/scripts/mod/modpost.c b/scripts/mod/modpost.c
index 75374c64b8cc..51631cd2f6c4 100644
--- a/scripts/mod/modpost.c
+++ b/scripts/mod/modpost.c
@@ -810,13 +810,27 @@ static void check_section(struct module *mod, struct elf_info *elf,
#define ALL_INIT_SECTIONS ".init.*"
#define ALL_EXIT_SECTIONS ".exit.*"

+#define ALL_CPU_PRESERVED_TEXT_SECTIONS \
+ ".text.cpu_preserved", ".text.cpu_preserved.*", \
+ ".cpu_preserved.text", ".cpu_preserved.text.*"
+
+#define ALL_CPU_PRESERVED_DATA_SECTIONS \
+ ".data.cpu_preserved", ".data.cpu_preserved.*", \
+ ".cpu_preserved.data", ".cpu_preserved.data.*", \
+ ".rodata.cpu_preserved", ".rodata.cpu_preserved.*", \
+ ".bss..data.cpu_preserved", ".bss..data.cpu_preserved.*"
+
+#define ALL_CPU_PRESERVED_SECTIONS \
+ ALL_CPU_PRESERVED_TEXT_SECTIONS, ALL_CPU_PRESERVED_DATA_SECTIONS
+
#define DATA_SECTIONS ".data", ".data.rel"
#define TEXT_SECTIONS ".text", ".text.*", ".sched.text", \
".kprobes.text", ".cpuidle.text", ".noinstr.text", \
".ltext", ".ltext.*"
#define OTHER_TEXT_SECTIONS ".ref.text", ".head.text", ".spinlock.text", \
".fixup", ".entry.text", ".exception.text", \
- ".coldtext", ".softirqentry.text", ".irqentry.text"
+ ".coldtext", ".softirqentry.text", ".irqentry.text", \
+ ALL_CPU_PRESERVED_TEXT_SECTIONS

#define ALL_TEXT_SECTIONS ".init.text", ".exit.text", \
TEXT_SECTIONS, OTHER_TEXT_SECTIONS
@@ -827,6 +841,7 @@ enum mismatch {
ANY_INIT_TO_ANY_EXIT,
ANY_EXIT_TO_ANY_INIT,
EXTABLE_TO_NON_TEXT,
+ CPU_PRESERVED_TO_NON_PRESERVED,
};

/**
@@ -843,13 +858,19 @@ enum mismatch {
* @mismatch: Type of mismatch.
*/
struct sectioncheck {
- const char *fromsec[20];
- const char *bad_tosec[20];
- const char *good_tosec[20];
+ const char *fromsec[32];
+ const char *bad_tosec[32];
+ const char *good_tosec[32];
enum mismatch mismatch;
};

static const struct sectioncheck sectioncheck[] = {
+/* Do not reference non-preserved code/data from cpu_preserved sections */
+{
+ .fromsec = { ALL_CPU_PRESERVED_SECTIONS, NULL },
+ .good_tosec = { ALL_CPU_PRESERVED_SECTIONS, NULL },
+ .mismatch = CPU_PRESERVED_TO_NON_PRESERVED,
+},
/* Do not reference init/exit code/data from
* normal code and data
*/
@@ -960,6 +981,9 @@ static const struct sectioncheck *section_mismatch(
static int secref_whitelist(const char *fromsec, const char *fromsym,
const char *tosec, const char *tosym)
{
+ if (match(fromsec, PATTERNS(ALL_CPU_PRESERVED_SECTIONS)))
+ return 1;
+
/* Check for pattern 1 */
if (match(tosec, PATTERNS(ALL_INIT_DATA_SECTIONS)) &&
match(fromsec, PATTERNS(DATA_SECTIONS)) &&
diff --git a/tools/objtool/check.c b/tools/objtool/check.c
index 464f6c9d9ff0..77389fbfd9ea 100644
--- a/tools/objtool/check.c
+++ b/tools/objtool/check.c
@@ -329,6 +329,8 @@ static void init_insn_state(struct objtool_file *file, struct insn_state *state,

if (opts.noinstr && sec)
state->noinstr = sec->noinstr;
+ if (sec)
+ state->cpu_preserved = sec->cpu_preserved;
}

static struct cfi_state *cfi_alloc(void)
@@ -419,6 +421,14 @@ static int decode_instructions(struct objtool_file *file)
u8 prev_len = 0;
u8 idx = 0;

+ if (!strncmp(sec->name, ".text.cpu_preserved", 19) ||
+ !strncmp(sec->name, ".cpu_preserved.text", 19) ||
+ !strncmp(sec->name, ".data.cpu_preserved", 19) ||
+ !strncmp(sec->name, ".cpu_preserved.data", 19) ||
+ !strncmp(sec->name, ".rodata.cpu_preserved", 21) ||
+ !strncmp(sec->name, ".bss..data.cpu_preserved", 24))
+ sec->cpu_preserved = true;
+
if (!is_text_sec(sec))
continue;

@@ -3511,6 +3521,17 @@ static int validate_call(struct objtool_file *file,
struct instruction *insn,
struct insn_state *state)
{
+ if (state->cpu_preserved) {
+ struct symbol *dest = insn_call_dest(insn);
+
+ if (dest && (dest->sec->idx != SHN_UNDEF || opts.link) &&
+ !dest->sec->cpu_preserved) {
+ WARN_INSN(insn, "call to %s() leaves .text.cpu_preserved section",
+ call_dest_name(insn));
+ return 1;
+ }
+ }
+
if (state->noinstr && state->instr <= 0 &&
!noinstr_call_dest(file, insn, insn_call_dest(insn))) {
WARN_INSN(insn, "call to %s() leaves .noinstr.text section", call_dest_name(insn));
@@ -4164,7 +4185,13 @@ static int validate_retpoline(struct objtool_file *file)
if (insn->retpoline_safe)
continue;

- if (insn->sec->init)
+ /*
+ * Preserved CPU text (.text.cpu_preserved) executes across
+ * kexec when the outgoing kernel's retpoline/rethunk targets
+ * are no longer mapped.
+ */
+ if (insn->sec->init ||
+ !strcmp(insn->sec->name, ".text.cpu_preserved"))
continue;

if (insn->type == INSN_RETURN) {
@@ -4440,6 +4467,12 @@ static int validate_noinstr_sections(struct objtool_file *file)
warnings += validate_unwind_hints(file, sec);
}

+ sec = find_section_by_name(file->elf, ".text.cpu_preserved");
+ if (sec) {
+ warnings += validate_section(file, sec);
+ warnings += validate_unwind_hints(file, sec);
+ }
+
return warnings;
}

diff --git a/tools/objtool/include/objtool/check.h b/tools/objtool/include/objtool/check.h
index 063f5985fecd..18318f2faf11 100644
--- a/tools/objtool/include/objtool/check.h
+++ b/tools/objtool/include/objtool/check.h
@@ -16,6 +16,7 @@ struct insn_state {
bool uaccess;
bool df;
bool noinstr;
+ bool cpu_preserved;
s8 instr;
};

diff --git a/tools/objtool/include/objtool/elf.h b/tools/objtool/include/objtool/elf.h
index a82517a76a0f..bac9e5f4921f 100644
--- a/tools/objtool/include/objtool/elf.h
+++ b/tools/objtool/include/objtool/elf.h
@@ -58,7 +58,7 @@ struct section {
Elf_Data *data;
const char *name;
int idx;
- bool _changed, text, rodata, noinstr, init, truncate;
+ bool _changed, text, rodata, noinstr, init, cpu_preserved, truncate;
struct reloc *relocs;
unsigned long nr_alloc_relocs;
struct section *twin;
--
2.55.0.1082.g2b9226bbc0-goog