[PATCH v2 5/9] kho: add read-only Kexec Handover mode
From: Pasha Tatashin
Date: Sun Oct 25 2026 - 13:42:35 EST
Kernels that inspect preserved KHO state without taking ownership of
preserved memory need a way to traverse the KHO FDT, radix tree, and
vmalloc chains in place while leaving the underlying physical pages and
metadata unmodified.
Add CONFIG_KEXEC_HANDOVER_READONLY and the 'kho_ro=<pa>' kernel
command-line parameter so both regular KHO and read-only KHO can be
compiled into the same kernel image and activated via the command line.
Map and validate the incoming KHO FDT once in an early_initcall, and
cache radix-tree node, leaf, folio, and vmalloc mappings in a unified
RCU-protected hash table (kho_ro_map_ht) for the lifetime of the
read-only KHO kernel.
Also export kho_is_ro(), kho_ro_lookup_folio_order(),
kho_ro_map_folio(), and kho_ro_map_vmalloc() so callers can check if
read-only KHO mode is active and map preserved folios and vmalloc
regions verified against the KHO radix tree.
Signed-off-by: Pasha Tatashin <pasha.tatashin@xxxxxxxxxx>
---
.../admin-guide/kernel-parameters.txt | 8 +
Documentation/core-api/kho/index.rst | 3 +
include/linux/kexec_handover.h | 44 +-
kernel/liveupdate/Kconfig | 10 +
kernel/liveupdate/Makefile | 1 +
kernel/liveupdate/kexec_handover_internal.h | 13 +
kernel/liveupdate/kexec_handover_ro.c | 429 ++++++++++++++++++
7 files changed, 501 insertions(+), 7 deletions(-)
create mode 100644 kernel/liveupdate/kexec_handover_ro.c
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 3b89b54db550..887a4b14cf40 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -3051,6 +3051,14 @@ Kernel parameters
bootmem areas. The form "nn%" defines scale factor
(in percents) of memory that was used during boot.
+ kho_ro= [KEXEC,EARLY]
+ Format: <phys_addr>
+ Enables read-only Kexec HandOver mode, pointing to the
+ page-aligned physical address of a preserved KHO FDT
+ blob, and disables regular KHO. The FDT and preserved
+ memory must not be part of this kernel's System RAM.
+ Requires CONFIG_KEXEC_HANDOVER_READONLY.
+
kmac= [MIPS] Korina ethernet MAC address.
Configure the RouterBoard 532 series on-chip
Ethernet adapter MAC address.
diff --git a/Documentation/core-api/kho/index.rst b/Documentation/core-api/kho/index.rst
index bce7fe281194..803e5dfc3caf 100644
--- a/Documentation/core-api/kho/index.rst
+++ b/Documentation/core-api/kho/index.rst
@@ -86,6 +86,9 @@ Public API
.. kernel-doc:: kernel/liveupdate/kexec_handover_common.c
:export:
+.. kernel-doc:: kernel/liveupdate/kexec_handover_ro.c
+ :export:
+
KHO Serialization Blocks API
============================
diff --git a/include/linux/kexec_handover.h b/include/linux/kexec_handover.h
index ef2b188c8ed0..bdb8dc3c3f54 100644
--- a/include/linux/kexec_handover.h
+++ b/include/linux/kexec_handover.h
@@ -13,6 +13,43 @@ struct kho_vmalloc;
struct folio;
struct page;
+#if defined(CONFIG_KEXEC_HANDOVER) || defined(CONFIG_KEXEC_HANDOVER_READONLY)
+int kho_retrieve_subtree(const char *name, phys_addr_t *phys, size_t *size);
+#else
+static inline int kho_retrieve_subtree(const char *name, phys_addr_t *phys,
+ size_t *size)
+{
+ return -EOPNOTSUPP;
+}
+#endif /* CONFIG_KEXEC_HANDOVER || CONFIG_KEXEC_HANDOVER_READONLY */
+
+#ifdef CONFIG_KEXEC_HANDOVER_READONLY
+bool kho_is_ro(void);
+int kho_ro_lookup_folio_order(phys_addr_t pa);
+void *kho_ro_map_folio(phys_addr_t pa, pgprot_t prot);
+const void *kho_ro_map_vmalloc(const struct kho_vmalloc *preservation);
+#else
+static inline bool kho_is_ro(void)
+{
+ return false;
+}
+
+static inline int kho_ro_lookup_folio_order(phys_addr_t pa)
+{
+ return -EOPNOTSUPP;
+}
+
+static inline void *kho_ro_map_folio(phys_addr_t pa, pgprot_t prot)
+{
+ return NULL;
+}
+
+static inline const void *kho_ro_map_vmalloc(const struct kho_vmalloc *preservation)
+{
+ return NULL;
+}
+#endif /* CONFIG_KEXEC_HANDOVER_READONLY */
+
#ifdef CONFIG_KEXEC_HANDOVER
bool kho_is_enabled(void);
bool is_kho_boot(void);
@@ -31,7 +68,6 @@ struct page *kho_restore_pages(phys_addr_t phys, unsigned long nr_pages);
void *kho_restore_vmalloc(const struct kho_vmalloc *preservation);
int kho_add_subtree(const char *name, void *blob, size_t size);
void kho_remove_subtree(void *blob);
-int kho_retrieve_subtree(const char *name, phys_addr_t *phys, size_t *size);
void kho_memory_init(void);
void kho_memory_init_early(void);
@@ -112,12 +148,6 @@ static inline int kho_add_subtree(const char *name, void *blob, size_t size)
static inline void kho_remove_subtree(void *blob) { }
-static inline int kho_retrieve_subtree(const char *name, phys_addr_t *phys,
- size_t *size)
-{
- return -EOPNOTSUPP;
-}
-
static inline void kho_memory_init(void) { }
static inline void kho_memory_init_early(void) { }
diff --git a/kernel/liveupdate/Kconfig b/kernel/liveupdate/Kconfig
index f50daee8eff0..d633e4c9197d 100644
--- a/kernel/liveupdate/Kconfig
+++ b/kernel/liveupdate/Kconfig
@@ -48,6 +48,16 @@ config KEXEC_HANDOVER_ENABLE_DEFAULT
The default behavior can still be overridden at boot time by
passing 'kho=off'.
+config KEXEC_HANDOVER_READONLY
+ bool "Read-only kexec handover"
+ depends on ARCH_SUPPORTS_KEXEC_HANDOVER
+ select LIBFDT
+ help
+ Enable read-only Kexec Handover ('kho_ro=<pa>'). This allows a
+ kernel to traverse preserved KHO subtrees, radix trees, and
+ metadata by mapping them in place without taking ownership of
+ or modifying the preserved KHO state.
+
config LIVEUPDATE
bool "Live Update Orchestrator"
depends on KEXEC_HANDOVER
diff --git a/kernel/liveupdate/Makefile b/kernel/liveupdate/Makefile
index f6bed6bcd380..74a584e6bb43 100644
--- a/kernel/liveupdate/Makefile
+++ b/kernel/liveupdate/Makefile
@@ -8,6 +8,7 @@ luo-y := \
luo_session.o
obj-$(CONFIG_KEXEC_HANDOVER) += kexec_handover.o kexec_handover_common.o
+obj-$(CONFIG_KEXEC_HANDOVER_READONLY) += kexec_handover_ro.o kexec_handover_common.o
obj-$(CONFIG_KEXEC_HANDOVER_DEBUGFS) += kexec_handover_debugfs.o
obj-$(CONFIG_LIVEUPDATE) += luo.o
diff --git a/kernel/liveupdate/kexec_handover_internal.h b/kernel/liveupdate/kexec_handover_internal.h
index dd79e5006562..39e85d0e2c7d 100644
--- a/kernel/liveupdate/kexec_handover_internal.h
+++ b/kernel/liveupdate/kexec_handover_internal.h
@@ -18,7 +18,20 @@ struct kho_vmalloc_walk_ops {
int (*entry)(phys_addr_t pa, void *priv);
};
+#ifdef CONFIG_KEXEC_HANDOVER
const void *kho_get_fdt(void);
+#else
+static inline const void *kho_get_fdt(void) { return NULL; }
+#endif
+
+#ifdef CONFIG_KEXEC_HANDOVER_READONLY
+const void *kho_ro_get_fdt(void);
+bool kho_ro_cmdline_enabled(void);
+#else
+static inline const void *kho_ro_get_fdt(void) { return NULL; }
+static inline bool kho_ro_cmdline_enabled(void) { return false; }
+#endif
+
bool kho_vmalloc_validate(const struct kho_vmalloc *preservation,
unsigned int *vm_flags);
int kho_vmalloc_walk(const struct kho_vmalloc *preservation,
diff --git a/kernel/liveupdate/kexec_handover_ro.c b/kernel/liveupdate/kexec_handover_ro.c
new file mode 100644
index 000000000000..fbf6d2ec8395
--- /dev/null
+++ b/kernel/liveupdate/kexec_handover_ro.c
@@ -0,0 +1,429 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * kexec_handover_ro.c - Read-Only Kexec Handover backend
+ *
+ * Copyright (c) 2026, Google LLC.
+ * Pasha Tatashin <pasha.tatashin@xxxxxxxxxx>
+ *
+ * Implements the read-only KHO backend (kho_ro=<pa>). In read-only mode, KHO
+ * is not enabled for outgoing or incoming page ownership transfer and makes no
+ * changes to boot or struct page initialization. Instead, when subsystems
+ * retrieve preserved state, this backend traverses the KHO FDT, radix tree,
+ * and preserved metadata by mapping physical pages read-only (PAGE_KERNEL_RO)
+ * without modifying or freeing the KHO metadata or reclaiming the preserved
+ * pages.
+ *
+ * The preserved KHO FDT, radix tree, and folio/vmalloc metadata are assumed to
+ * remain static for the lifetime of the read-only KHO kernel: the FDT is
+ * mapped at early_initcall time, and radix-tree nodes, leaves, and
+ * folio/vmalloc mappings are cached on first access. If a caller needs
+ * read-write access to preserved folio contents outside of KHO metadata, it
+ * can map them with PAGE_KERNEL via kho_ro_map_folio().
+ */
+
+#define pr_fmt(fmt) "KHO-RO: " fmt
+
+#include <linux/bitops.h>
+#include <linux/cleanup.h>
+#include <linux/hashtable.h>
+#include <linux/init.h>
+#include <linux/io.h>
+#include <linux/ioport.h>
+#include <linux/kexec_handover.h>
+#include <linux/kho/abi/kexec_handover.h>
+#include <linux/minmax.h>
+#include <linux/mm.h>
+#include <linux/moduleparam.h>
+#include <linux/mutex.h>
+#include <linux/rcupdate.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/vmalloc.h>
+
+#include <asm/setup.h>
+
+#include "kexec_handover_internal.h"
+
+#define KHO_RO_MAP_HASH_BITS 10
+
+/*
+ * In read-only KHO mode, the preserved KHO tree and memory must reside outside
+ * of the kernel's direct map so they do not conflict with the kernel that owns
+ * the KHO state. Virtual mappings used to access the tree and preserved memory
+ * are stored in an RCU-protected hash table (kho_ro_map_ht) that remains
+ * static for the lifetime of the kernel and is never unmapped, allowing fast
+ * lockless multi-threaded lookups under rcu_read_lock() during traversal while
+ * serializing insertions on cache misses with kho_ro_lock.
+ *
+ * Entries are keyed by (pa, size, prot):
+ * - Radix-tree nodes/leaves and preserved folios use their physical address,
+ * mapped byte size (PAGE_SIZE << order), and pgprot_t.
+ * - Preserved kho_vmalloc regions use preservation->first.phys with size = 0
+ * and PAGE_KERNEL_RO so a discontiguous vmalloc mapping never collides with
+ * a direct page/folio mapping (which always has size > 0).
+ */
+struct kho_ro_map_entry {
+ struct hlist_node node;
+ phys_addr_t pa;
+ size_t size;
+ pgprot_t prot;
+ void *vaddr;
+};
+
+static bool kho_ro __ro_after_init;
+static phys_addr_t kho_ro_fdt_phys __ro_after_init;
+static phys_addr_t kho_ro_mem_map_phys __ro_after_init;
+static const void *kho_ro_fdt __ro_after_init;
+static DEFINE_MUTEX(kho_ro_lock);
+static DEFINE_HASHTABLE(kho_ro_map_ht, KHO_RO_MAP_HASH_BITS);
+
+/**
+ * kho_is_ro - Check if KHO is operating in read-only mode.
+ *
+ * Return: true if read-only KHO mode is active, false otherwise.
+ */
+bool kho_is_ro(void)
+{
+ return kho_ro;
+}
+EXPORT_SYMBOL_GPL(kho_is_ro);
+
+static int __init kho_parse_ro(char *p)
+{
+ unsigned long long pa;
+
+ if (!p || kstrtoull(p, 0, &pa) || !pa || !PAGE_ALIGNED(pa))
+ return -EINVAL;
+
+ kho_ro_fdt_phys = pa;
+ kho_ro = true;
+ pr_info_once("Read-only mode enabled with FDT at %pa\n",
+ &kho_ro_fdt_phys);
+ return 0;
+}
+early_param("kho_ro", kho_parse_ro);
+
+static int __init kho_ro_early_parse(char *param, char *val,
+ const char *unused, void *arg)
+{
+ if (parameq(param, "kho_ro"))
+ kho_parse_ro(val);
+ return 0;
+}
+
+bool __init kho_ro_cmdline_enabled(void)
+{
+ static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;
+
+ if (!kho_ro) {
+ strscpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
+ parse_args("kho_ro", tmp_cmdline, NULL, 0, 0, 0, NULL,
+ kho_ro_early_parse);
+ }
+
+ return kho_ro;
+}
+
+static int kho_ro_map_range(unsigned long vaddr, phys_addr_t pa, size_t size,
+ pgprot_t prot)
+{
+ if (!pa || !size || !PAGE_ALIGNED(pa | size) || pa + size - 1 < pa)
+ return -EINVAL;
+
+ if (region_intersects(pa, size, IORESOURCE_SYSTEM_RAM,
+ IORES_DESC_NONE) != REGION_DISJOINT) {
+ pr_warn_once("Cannot map System RAM at %pa (size 0x%zx)\n",
+ &pa, size);
+ return -EINVAL;
+ }
+
+ return vmap_page_range(vaddr, vaddr + size, pa, prot);
+}
+
+static void *kho_ro_map(phys_addr_t pa, size_t size, pgprot_t prot)
+{
+ struct vm_struct *area;
+
+ area = get_vm_area_caller(size, VM_IOREMAP, __builtin_return_address(0));
+ if (!area)
+ return NULL;
+
+ area->phys_addr = pa;
+ if (kho_ro_map_range((unsigned long)area->addr, pa, size, prot)) {
+ vunmap(area->addr);
+ return NULL;
+ }
+
+ return area->addr;
+}
+
+static int __init kho_ro_init(void)
+{
+ phys_addr_t mem_map_phys;
+ const void *fdt;
+
+ if (!kho_ro)
+ return 0;
+
+ fdt = kho_ro_map(kho_ro_fdt_phys, PAGE_SIZE, PAGE_KERNEL_RO);
+ if (!fdt) {
+ pr_err("Failed to map FDT at %pa\n", &kho_ro_fdt_phys);
+ goto err_disable;
+ }
+
+ if (kho_check_fdt(fdt, kho_ro_fdt_phys))
+ goto err_unmap;
+
+ mem_map_phys = kho_get_mem_map_phys(fdt);
+ if (!mem_map_phys)
+ goto err_unmap;
+
+ kho_ro_fdt = fdt;
+ kho_ro_mem_map_phys = mem_map_phys;
+ return 0;
+
+err_unmap:
+ vunmap(fdt);
+err_disable:
+ pr_warn("Disabling read-only mode\n");
+ kho_ro = false;
+ return -EINVAL;
+}
+early_initcall(kho_ro_init);
+
+const void *kho_ro_get_fdt(void)
+{
+ return kho_ro_fdt;
+}
+
+static void *kho_ro_lookup_cached(phys_addr_t pa, size_t size, pgprot_t prot)
+{
+ struct kho_ro_map_entry *entry;
+ void *vaddr = NULL;
+
+ rcu_read_lock();
+ hash_for_each_possible_rcu(kho_ro_map_ht, entry, node, pa) {
+ if (entry->pa == pa && entry->size == size &&
+ pgprot_val(entry->prot) == pgprot_val(prot)) {
+ vaddr = entry->vaddr;
+ break;
+ }
+ }
+ rcu_read_unlock();
+
+ return vaddr;
+}
+
+static void *kho_ro_insert_cached(phys_addr_t pa, size_t size, pgprot_t prot,
+ void *vaddr)
+{
+ struct kho_ro_map_entry *entry;
+ void *existing;
+
+ guard(mutex)(&kho_ro_lock);
+ existing = kho_ro_lookup_cached(pa, size, prot);
+ if (existing)
+ return existing;
+
+ entry = kzalloc_obj(*entry);
+ if (!entry)
+ return NULL;
+
+ entry->pa = pa;
+ entry->size = size;
+ entry->prot = prot;
+ entry->vaddr = vaddr;
+ hash_add_rcu(kho_ro_map_ht, &entry->node, pa);
+
+ return vaddr;
+}
+
+static void *kho_ro_map_cached(phys_addr_t pa, size_t size, pgprot_t prot)
+{
+ void *vaddr, *cached;
+
+ vaddr = kho_ro_lookup_cached(pa, size, prot);
+ if (vaddr)
+ return vaddr;
+
+ vaddr = kho_ro_map(pa, size, prot);
+ if (!vaddr)
+ return NULL;
+
+ cached = kho_ro_insert_cached(pa, size, prot, vaddr);
+ if (cached != vaddr)
+ vunmap(vaddr);
+
+ return cached;
+}
+
+static void *kho_ro_map_radix(phys_addr_t pa)
+{
+ return kho_ro_map_cached(pa, PAGE_SIZE, PAGE_KERNEL_RO);
+}
+
+static bool kho_ro_radix_key_exists(unsigned long key)
+{
+ const struct kho_radix_leaf *leaf;
+
+ leaf = kho_radix_find_leaf(kho_ro_mem_map_phys, key, kho_ro_map_radix);
+ if (!leaf)
+ return false;
+
+ return test_bit(kho_radix_get_bitmap_index(key), leaf->bitmap);
+}
+
+/**
+ * kho_ro_lookup_folio_order - Look up the preserved order of a physical page in KHO.
+ * @pa: Page-aligned physical address of the preserved folio.
+ *
+ * Queries the preserved KHO memory radix tree in read-only mode to determine
+ * the order at which @pa was preserved.
+ *
+ * Return: Non-negative page order on success, -ENOENT if @pa is not preserved,
+ * or -EOPNOTSUPP if read-only KHO is not active.
+ */
+int kho_ro_lookup_folio_order(phys_addr_t pa)
+{
+ unsigned int order, max_order;
+
+ might_sleep();
+ if (!kho_ro)
+ return -EOPNOTSUPP;
+ if (!pa || !PAGE_ALIGNED(pa) || !kho_ro_mem_map_phys)
+ return -ENOENT;
+
+ max_order = min_t(unsigned int, __ffs64(pa >> PAGE_SHIFT),
+ KHO_RADIX_KEY_WIDTH - 2);
+
+ for (order = 0; order <= max_order; order++) {
+ if (kho_ro_radix_key_exists(kho_encode_radix_key(pa, order)))
+ return (int)order;
+ }
+
+ return -ENOENT;
+}
+EXPORT_SYMBOL_GPL(kho_ro_lookup_folio_order);
+
+/**
+ * kho_ro_map_folio - Map a preserved KHO folio into kernel virtual address space.
+ * @pa: Page-aligned physical address of the preserved folio.
+ * @prot: Page protection flags for the mapping (e.g. PAGE_KERNEL_RO or PAGE_KERNEL).
+ *
+ * Looks up @pa in the preserved KHO radix tree to determine its folio order,
+ * maps the entire folio (PAGE_SIZE << order) into kernel virtual address
+ * space with @prot permissions, and caches the mapping (keyed by @pa, size,
+ * and @prot) for the lifetime of the read-only KHO kernel.
+ *
+ * Return: Virtual pointer to the mapped folio on success, or NULL on failure.
+ */
+void *kho_ro_map_folio(phys_addr_t pa, pgprot_t prot)
+{
+ int order;
+
+ might_sleep();
+ order = kho_ro_lookup_folio_order(pa);
+ if (order < 0)
+ return NULL;
+
+ return kho_ro_map_cached(pa, PAGE_SIZE << order, prot);
+}
+EXPORT_SYMBOL_GPL(kho_ro_map_folio);
+
+struct kho_ro_vmalloc_ctx {
+ unsigned long vaddr;
+ size_t contig_size;
+ unsigned int order;
+};
+
+static const struct kho_vmalloc_chunk *kho_ro_vmalloc_get_chunk(phys_addr_t pa)
+{
+ if (kho_ro_lookup_folio_order(pa) != 0)
+ return NULL;
+
+ return kho_ro_map(pa, PAGE_SIZE, PAGE_KERNEL_RO);
+}
+
+static void kho_ro_vmalloc_put_chunk(const struct kho_vmalloc_chunk *chunk)
+{
+ vunmap(chunk);
+}
+
+static int kho_ro_vmalloc_map_entry(phys_addr_t pa, void *priv)
+{
+ struct kho_ro_vmalloc_ctx *ctx = priv;
+ int err;
+
+ if (kho_ro_lookup_folio_order(pa) != ctx->order)
+ return -EINVAL;
+
+ err = kho_ro_map_range(ctx->vaddr, pa, ctx->contig_size,
+ PAGE_KERNEL_RO);
+ if (err)
+ return err;
+
+ ctx->vaddr += ctx->contig_size;
+ return 0;
+}
+
+static const struct kho_vmalloc_walk_ops kho_ro_vmalloc_ops = {
+ .get_chunk = kho_ro_vmalloc_get_chunk,
+ .put_chunk = kho_ro_vmalloc_put_chunk,
+ .entry = kho_ro_vmalloc_map_entry,
+};
+
+/**
+ * kho_ro_map_vmalloc - Map a preserved vmalloc region read-only in place.
+ * @preservation: Pointer to the KHO vmalloc preservation metadata.
+ *
+ * Allocates a VM_IOREMAP virtual address range, validates each chunk header and
+ * preserved page order in the cached KHO radix tree, maps the preserved
+ * physical pages directly with read-only permissions (PAGE_KERNEL_RO) with
+ * zero RAM allocation or memcpy overhead, and caches the mapping for the
+ * lifetime of the read-only KHO kernel.
+ *
+ * Return: Read-only virtual pointer on success, or NULL on failure.
+ */
+const void *kho_ro_map_vmalloc(const struct kho_vmalloc *preservation)
+{
+ struct kho_ro_vmalloc_ctx ctx;
+ struct vm_struct *area;
+ unsigned int vm_flags;
+ size_t total_size;
+ void *cached;
+
+ might_sleep();
+ if (!kho_ro || !kho_vmalloc_validate(preservation, &vm_flags))
+ return NULL;
+
+ /* Use size 0 in the cache key to distinguish vmalloc from folios. */
+ cached = kho_ro_lookup_cached(preservation->first.phys, 0,
+ PAGE_KERNEL_RO);
+ if (cached)
+ return cached;
+
+ total_size = (size_t)preservation->total_pages * PAGE_SIZE;
+ area = get_vm_area_caller(total_size, VM_IOREMAP,
+ __builtin_return_address(0));
+ if (!area)
+ return NULL;
+
+ ctx = (struct kho_ro_vmalloc_ctx){
+ .vaddr = (unsigned long)area->addr,
+ .contig_size = PAGE_SIZE << preservation->order,
+ .order = preservation->order,
+ };
+
+ if (kho_vmalloc_walk(preservation, &kho_ro_vmalloc_ops, &ctx)) {
+ vunmap(area->addr);
+ return NULL;
+ }
+
+ cached = kho_ro_insert_cached(preservation->first.phys, 0,
+ PAGE_KERNEL_RO, area->addr);
+ if (cached != area->addr)
+ vunmap(area->addr);
+
+ return cached;
+}
+EXPORT_SYMBOL_GPL(kho_ro_map_vmalloc);
--
2.56.0.307.g5fe7bd680c-goog