[PATCH v1 5/8] kho: add read-only Kexec Handover mode

From: Pasha Tatashin

Date: Sun Oct 18 2026 - 22:15:43 EST


Add CONFIG_KEXEC_HANDOVER_READONLY and the 'kho_ro=<pa>' kernel
command-line parameter to support traversing a preserved KHO tree in
read-only mode without taking ownership of or modifying KHO metadata or
reclaiming preserved pages into the page allocator.

When booted with 'kho_ro=<pa>', preserved KHO metadata (FDT, radix-tree
nodes, and vmalloc chunks) lies outside the kernel's direct map and is
mapped read-only (PAGE_KERNEL_RO), with the FDT and radix-tree nodes
mapped lazily on first visit and kept cached. This allows
kho_retrieve_subtree(), kho_restore_vmalloc(), and
kho_ro_lookup_folio_order() to inspect the preserved KHO state while
leaving it intact.

Also export kho_ro_map_folio() and kho_ro_unmap() so callers (such as
kho_block, LUO, and file handlers) can map preserved folios verified
against the KHO radix tree with the desired page protections.

Signed-off-by: Pasha Tatashin <pasha.tatashin@xxxxxxxxxx>
---
.../admin-guide/kernel-parameters.txt | 6 +
include/linux/kexec_handover.h | 53 ++-
kernel/liveupdate/Kconfig | 16 +
kernel/liveupdate/Makefile | 1 +
kernel/liveupdate/kexec_handover_ro.c | 378 ++++++++++++++++++
5 files changed, 441 insertions(+), 13 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..594d0b468f32 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -3051,6 +3051,12 @@ 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. 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/include/linux/kexec_handover.h b/include/linux/kexec_handover.h
index ef2b188c8ed0..1f75c2f94be5 100644
--- a/include/linux/kexec_handover.h
+++ b/include/linux/kexec_handover.h
@@ -13,6 +13,46 @@ struct kho_vmalloc;
struct folio;
struct page;

+#if defined(CONFIG_KEXEC_HANDOVER) || defined(CONFIG_KEXEC_HANDOVER_READONLY)
+void *kho_restore_vmalloc(const struct kho_vmalloc *preservation);
+int kho_retrieve_subtree(const char *name, phys_addr_t *phys, size_t *size);
+#else
+static inline void *kho_restore_vmalloc(const struct kho_vmalloc *preservation)
+{
+ return NULL;
+}
+
+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);
+void kho_ro_unmap(const void *addr);
+#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 void kho_ro_unmap(const void *addr) { }
+#endif /* CONFIG_KEXEC_HANDOVER_READONLY */
+
#ifdef CONFIG_KEXEC_HANDOVER
bool kho_is_enabled(void);
bool is_kho_boot(void);
@@ -28,10 +68,8 @@ void kho_unpreserve_free(void *mem);
void kho_restore_free(void *mem);
struct folio *kho_restore_folio(phys_addr_t phys);
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);
@@ -100,11 +138,6 @@ static inline struct page *kho_restore_pages(phys_addr_t phys,
return NULL;
}

-static inline void *kho_restore_vmalloc(const struct kho_vmalloc *preservation)
-{
- return NULL;
-}
-
static inline int kho_add_subtree(const char *name, void *blob, size_t size)
{
return -EOPNOTSUPP;
@@ -112,12 +145,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..362b7a3e2629 100644
--- a/kernel/liveupdate/Kconfig
+++ b/kernel/liveupdate/Kconfig
@@ -48,6 +48,22 @@ 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 && !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 in read-only mode without taking ownership of or
+ modifying the KHO metadata.
+
+ Pages outside of KHO metadata can still be modified; for example,
+ a preserved file descriptor can be retrieved while remaining in
+ frozen mode (disallowing resize, hole punch, or faulting in new
+ memory), while the memory content of the file can still be
+ modified.
+
config LIVEUPDATE
bool "Live Update Orchestrator"
depends on KEXEC_HANDOVER
diff --git a/kernel/liveupdate/Makefile b/kernel/liveupdate/Makefile
index d9f469462556..f8b09e04bea2 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
+obj-$(CONFIG_KEXEC_HANDOVER_READONLY) += kexec_handover_ro.o
obj-$(CONFIG_KEXEC_HANDOVER_DEBUGFS) += kexec_handover_debugfs.o

obj-$(CONFIG_LIVEUPDATE) += luo.o
diff --git a/kernel/liveupdate/kexec_handover_ro.c b/kernel/liveupdate/kexec_handover_ro.c
new file mode 100644
index 000000000000..f5d5e9495778
--- /dev/null
+++ b/kernel/liveupdate/kexec_handover_ro.c
@@ -0,0 +1,378 @@
+// 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 KHO FDT and radix-tree page tables are mapped read-only lazily on
+ * first access and remain mapped for the lifetime of the kernel.
+ *
+ * Pages outside of KHO metadata can still be modified. For example, a
+ * preserved file descriptor can be retrieved while remaining in frozen mode:
+ * its layout cannot be changed (no resize, hole punch, or faulting in new
+ * memory), but the memory content of the file can still be modified. If a
+ * caller in LUO or a file handler needs read-write access to preserved pages,
+ * 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/err.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/libfdt.h>
+#include <linux/minmax.h>
+#include <linux/mm.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/vmalloc.h>
+
+#include "kexec_handover_internal.h"
+
+struct kho_ro_radix_node {
+ const struct kho_radix_node *phys_node;
+ void *children[1 << KHO_TABLE_SIZE_LOG2];
+};
+
+static bool kho_ro __ro_after_init;
+static phys_addr_t kho_ro_fdt_phys __ro_after_init;
+static DEFINE_MUTEX(kho_ro_lock);
+static const void *kho_ro_fdt;
+static struct kho_ro_radix_node *kho_ro_radix_root;
+
+/**
+ * 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;
+ int err;
+
+ if (!p)
+ return -EINVAL;
+
+ err = kstrtoull(p, 0, &pa);
+ if (err || !pa || !PAGE_ALIGNED(pa)) {
+ pr_err("Invalid kho_ro address '%s'\n", p);
+ return -EINVAL;
+ }
+
+ kho_ro_fdt_phys = pa;
+ kho_ro = true;
+ pr_info("Read-only mode enabled with FDT at 0x%llx\n", pa);
+ return 0;
+}
+early_param("kho_ro", kho_parse_ro);
+
+static void *kho_ro_map(phys_addr_t pa, size_t size, pgprot_t prot)
+{
+ struct vm_struct *area;
+ unsigned long vaddr;
+
+ if (!kho_ro || !pa || !size || !PAGE_ALIGNED(pa | size))
+ return NULL;
+
+ if (pa + size - 1 < pa)
+ return NULL;
+
+ 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 NULL;
+ }
+
+ area = get_vm_area_caller(size, VM_IOREMAP, __builtin_return_address(0));
+ if (!area)
+ return NULL;
+
+ vaddr = (unsigned long)area->addr;
+ area->phys_addr = pa;
+
+ if (ioremap_page_range(vaddr, vaddr + size, pa, prot)) {
+ free_vm_area(area);
+ return NULL;
+ }
+
+ return area->addr;
+}
+
+/**
+ * kho_ro_unmap - Unmap a KHO read-only mode virtual mapping.
+ * @addr: Virtual address previously returned by kho_ro_map_folio().
+ */
+void kho_ro_unmap(const void *addr)
+{
+ if (addr)
+ vunmap(addr);
+}
+EXPORT_SYMBOL_GPL(kho_ro_unmap);
+
+static const void *kho_ro_get_fdt(void)
+{
+ const void *fdt;
+
+ if (!kho_ro || !kho_ro_fdt_phys)
+ return NULL;
+
+ guard(mutex)(&kho_ro_lock);
+ if (kho_ro_fdt)
+ return kho_ro_fdt;
+
+ fdt = kho_ro_map(kho_ro_fdt_phys, PAGE_SIZE, PAGE_KERNEL_RO);
+ if (!fdt) {
+ pr_err_once("Failed to map FDT at 0x%llx\n",
+ (unsigned long long)kho_ro_fdt_phys);
+ return NULL;
+ }
+
+ if (!fdt_check_header(fdt) && fdt_totalsize(fdt) > PAGE_SIZE) {
+ pr_warn("handover FDT (0x%llx) totalsize %u exceeds PAGE_SIZE\n",
+ (unsigned long long)kho_ro_fdt_phys, fdt_totalsize(fdt));
+ kho_ro_unmap(fdt);
+ return NULL;
+ }
+
+ if (kho_check_fdt(fdt, kho_ro_fdt_phys)) {
+ kho_ro_unmap(fdt);
+ return NULL;
+ }
+
+ kho_ro_fdt = fdt;
+ return fdt;
+}
+
+static struct kho_ro_radix_node *kho_ro_alloc_radix_node(phys_addr_t pa)
+{
+ const struct kho_radix_node *phys_node;
+ struct kho_ro_radix_node *node;
+
+ phys_node = kho_ro_map(pa, PAGE_SIZE, PAGE_KERNEL_RO);
+ if (!phys_node)
+ return NULL;
+
+ node = kzalloc_obj(*node);
+ if (!node) {
+ kho_ro_unmap(phys_node);
+ return NULL;
+ }
+
+ node->phys_node = phys_node;
+ return node;
+}
+
+static bool kho_ro_radix_key_exists(struct kho_ro_radix_node *root,
+ unsigned long key)
+{
+ struct kho_ro_radix_node *node = root;
+ const struct kho_radix_leaf *leaf;
+ unsigned int i, idx;
+
+ if (!node || unlikely(fls64(key) > KHO_RADIX_KEY_WIDTH))
+ return false;
+
+ for (i = KHO_TREE_MAX_DEPTH - 1; i > 0; i--) {
+ idx = kho_radix_get_table_index(key, i);
+ if (!node->children[idx]) {
+ phys_addr_t next_pa = node->phys_node->table[idx];
+
+ if (i == 1)
+ node->children[idx] = kho_ro_map(next_pa, PAGE_SIZE,
+ PAGE_KERNEL_RO);
+ else
+ node->children[idx] = kho_ro_alloc_radix_node(next_pa);
+ if (!node->children[idx])
+ return false;
+ }
+
+ if (i == 1) {
+ leaf = node->children[idx];
+ return test_bit(kho_radix_get_bitmap_index(key),
+ leaf->bitmap);
+ }
+
+ node = node->children[idx];
+ }
+
+ return false;
+}
+
+/**
+ * kho_retrieve_subtree - Retrieve a preserved sub-blob by name from KHO root FDT.
+ * @name: Name of the subnode in the KHO root FDT.
+ * @phys: Output pointer for the physical address of the preserved sub-blob.
+ * @size: Optional output pointer for the size in bytes of the sub-blob.
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+int kho_retrieve_subtree(const char *name, phys_addr_t *phys, size_t *size)
+{
+ return __kho_retrieve_subtree(kho_ro_get_fdt(), name, phys, size);
+}
+EXPORT_SYMBOL_GPL(kho_retrieve_subtree);
+
+/**
+ * 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;
+ const void *fdt;
+
+ if (!kho_ro)
+ return -EOPNOTSUPP;
+ if (!pa || !PAGE_ALIGNED(pa))
+ return -ENOENT;
+
+ fdt = kho_ro_get_fdt();
+ if (!fdt)
+ return -ENOENT;
+
+ guard(mutex)(&kho_ro_lock);
+ if (!kho_ro_radix_root) {
+ kho_ro_radix_root = kho_ro_alloc_radix_node(kho_get_mem_map_phys(fdt));
+ if (!kho_ro_radix_root)
+ return -ENOENT;
+ }
+
+ max_order = min3((unsigned int)__ffs64(pa >> PAGE_SHIFT),
+ (unsigned int)(BITS_PER_LONG - PAGE_SHIFT - 1),
+ (unsigned int)(KHO_RADIX_KEY_WIDTH - 2));
+
+ for (order = 0; order <= max_order; order++) {
+ unsigned long key = kho_encode_radix_key(pa, order);
+
+ if (kho_ro_radix_key_exists(kho_ro_radix_root, key))
+ 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
+ * and maps the entire folio (PAGE_SIZE << order) into kernel virtual address
+ * space with @prot permissions. Pair with kho_ro_unmap().
+ *
+ * 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 = kho_ro_lookup_folio_order(pa);
+
+ if (order < 0)
+ return NULL;
+
+ return kho_ro_map(pa, PAGE_SIZE << order, prot);
+}
+EXPORT_SYMBOL_GPL(kho_ro_map_folio);
+
+/**
+ * kho_restore_vmalloc - Reconstruct a preserved vmalloc region in read-only mode.
+ * @preservation: Pointer to the KHO vmalloc preservation metadata.
+ *
+ * Allocates a new vmalloc buffer and copies the preserved physical pages into
+ * it via temporary read-only mappings so that the caller can inspect and later
+ * vfree() the buffer without modifying or reclaiming the underlying preserved
+ * KHO pages.
+ *
+ * Return: Virtual pointer to the newly allocated copy on success, or NULL on
+ * failure.
+ */
+void *kho_restore_vmalloc(const struct kho_vmalloc *preservation)
+{
+ size_t total_size, contig_size, copied = 0;
+ const struct kho_vmalloc_chunk *chunk;
+ unsigned int order, vm_flags;
+ phys_addr_t chunk_pa;
+ void *buf;
+
+ if (!kho_ro || !kho_vmalloc_validate(preservation, &vm_flags))
+ return NULL;
+
+ order = preservation->order;
+ contig_size = PAGE_SIZE << order;
+ total_size = preservation->total_pages * PAGE_SIZE;
+
+ buf = vmalloc(total_size);
+ if (!buf)
+ return NULL;
+
+ for (chunk_pa = preservation->first.phys; chunk_pa; ) {
+ unsigned int i;
+
+ if (kho_ro_lookup_folio_order(chunk_pa) != 0)
+ goto err_free;
+
+ chunk = kho_ro_map(chunk_pa, PAGE_SIZE, PAGE_KERNEL_RO);
+ if (!chunk)
+ goto err_free;
+
+ for (i = 0; i < ARRAY_SIZE(chunk->phys) && chunk->phys[i]; i++) {
+ phys_addr_t phys = chunk->phys[i];
+ const void *src;
+
+ if (copied + contig_size > total_size ||
+ kho_ro_lookup_folio_order(phys) != order)
+ goto err_unmap;
+
+ src = kho_ro_map(phys, contig_size, PAGE_KERNEL_RO);
+ if (!src)
+ goto err_unmap;
+
+ memcpy(buf + copied, src, contig_size);
+ kho_ro_unmap(src);
+ copied += contig_size;
+ }
+
+ chunk_pa = chunk->hdr.next.phys;
+ if (chunk_pa && i < ARRAY_SIZE(chunk->phys))
+ goto err_unmap;
+
+ kho_ro_unmap(chunk);
+ }
+
+ if (copied != total_size)
+ goto err_free;
+
+ return buf;
+
+err_unmap:
+ kho_ro_unmap(chunk);
+err_free:
+ vfree(buf);
+ return NULL;
+}
+EXPORT_SYMBOL_GPL(kho_restore_vmalloc);
--
2.56.0.176.g3575e65682-goog