* [PATCH v1 1/8] kho: expose physical address of outgoing FDT
2026-10-19 2:11 [PATCH v1 0/8] kho: add read-only Kexec Handover mode Pasha Tatashin
@ 2026-10-19 2:11 ` Pasha Tatashin
2026-10-19 2:11 ` [PATCH v1 2/8] kho: move radix tree, vmalloc, and FDT helpers to internal header Pasha Tatashin
` (7 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Pasha Tatashin @ 2026-10-19 2:11 UTC (permalink / raw)
To: linux-kselftest, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
linux-mm, ljs, mhocko, skhan, linux-doc, vbabka, dapeng1.mi,
linux-kernel, corbet, surenb, pasha.tatashin, kexec, pratyush,
rdunlap, elver, graf, lirongqing, liam, jlayton
Expose the physical address of the outgoing KHO FDT via
/sys/kernel/debug/kho/out/fdt_phys (analogous to bootmem_phys) so
that userspace tooling can locate the root KHO FDT in physical memory.
Also add kho_debugfs_cleanup() and remove the KHO debugfs directory
if kho_init() fails after initializing debugfs, ensuring fdt_phys
does not retain a dangling pointer to a freed FDT.
This is going to be used by the upcoming KHO read-only mode.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
Documentation/admin-guide/mm/kho.rst | 3 +++
kernel/liveupdate/kexec_handover.c | 10 ++++++----
kernel/liveupdate/kexec_handover_debugfs.c | 22 ++++++++++++++++++++-
kernel/liveupdate/kexec_handover_internal.h | 7 +++++--
4 files changed, 35 insertions(+), 7 deletions(-)
diff --git a/Documentation/admin-guide/mm/kho.rst b/Documentation/admin-guide/mm/kho.rst
index 902d171a780a..2ba884c620c5 100644
--- a/Documentation/admin-guide/mm/kho.rst
+++ b/Documentation/admin-guide/mm/kho.rst
@@ -96,6 +96,9 @@ stabilized.
current KHO state in this file. Kexec user space tooling can use this
as input file for the KHO payload image.
+``/sys/kernel/debug/kho/out/fdt_phys``
+ Physical address of the outgoing KHO FDT blob.
+
``/sys/kernel/debug/kho/out/bootmem_len``
Lengths of KHO bootmem regions, which are physically contiguous
memory regions that will always stay available for future kexec
diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c
index 6f1f55fe3181..ce5983abf181 100644
--- a/kernel/liveupdate/kexec_handover.c
+++ b/kernel/liveupdate/kexec_handover.c
@@ -1851,17 +1851,17 @@ static __init int kho_init(void)
if (err)
goto err_free_fdt;
- err = kho_out_debugfs_init(&kho_out.dbg);
+ err = kho_out_debugfs_init(&kho_out.dbg, kho_out.fdt);
if (err)
- goto err_free_fdt;
+ goto err_cleanup_debugfs;
err = kho_out_fdt_setup();
if (err)
- goto err_free_fdt;
+ goto err_cleanup_debugfs;
err = kho_kexec_metadata_init(fdt);
if (err)
- goto err_free_fdt;
+ goto err_cleanup_debugfs;
if (fdt) {
kho_in_debugfs_init(&kho_in.dbg, fdt);
@@ -1893,6 +1893,8 @@ static __init int kho_init(void)
return 0;
+err_cleanup_debugfs:
+ kho_debugfs_cleanup();
err_free_fdt:
kho_unpreserve_free(kho_out.fdt);
err_free_kho_radix_tree:
diff --git a/kernel/liveupdate/kexec_handover_debugfs.c b/kernel/liveupdate/kexec_handover_debugfs.c
index 7d74477da0b7..db3830829398 100644
--- a/kernel/liveupdate/kexec_handover_debugfs.c
+++ b/kernel/liveupdate/kexec_handover_debugfs.c
@@ -95,6 +95,15 @@ static int bootmem_len_show(struct seq_file *m, void *v)
}
DEFINE_SHOW_ATTRIBUTE(bootmem_len);
+static int fdt_phys_show(struct seq_file *m, void *v)
+{
+ void *fdt = m->private;
+
+ seq_printf(m, "0x%llx\n", (unsigned long long)virt_to_phys(fdt));
+ return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(fdt_phys);
+
__init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt)
{
struct dentry *dir, *sub_fdt_dir;
@@ -172,7 +181,7 @@ __init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt)
}
}
-__init int kho_out_debugfs_init(struct kho_debugfs *dbg)
+__init int kho_out_debugfs_init(struct kho_debugfs *dbg, const void *fdt)
{
struct dentry *dir, *f, *sub_fdt_dir;
@@ -196,6 +205,11 @@ __init int kho_out_debugfs_init(struct kho_debugfs *dbg)
if (IS_ERR(f))
goto err_rmdir;
+ f = debugfs_create_file("fdt_phys", 0400, dir, (void *)fdt,
+ &fdt_phys_fops);
+ if (IS_ERR(f))
+ goto err_rmdir;
+
dbg->dir = dir;
dbg->sub_fdt_dir = sub_fdt_dir;
return 0;
@@ -212,3 +226,9 @@ __init int kho_debugfs_init(void)
return -ENOENT;
return 0;
}
+
+__init void kho_debugfs_cleanup(void)
+{
+ debugfs_remove_recursive(debugfs_root);
+ debugfs_root = NULL;
+}
diff --git a/kernel/liveupdate/kexec_handover_internal.h b/kernel/liveupdate/kexec_handover_internal.h
index e9685e1ef6d7..38181076f603 100644
--- a/kernel/liveupdate/kexec_handover_internal.h
+++ b/kernel/liveupdate/kexec_handover_internal.h
@@ -24,16 +24,19 @@ extern unsigned int kho_bootmem_cnt;
#ifdef CONFIG_KEXEC_HANDOVER_DEBUGFS
int kho_debugfs_init(void);
+void kho_debugfs_cleanup(void);
void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt);
-int kho_out_debugfs_init(struct kho_debugfs *dbg);
+int kho_out_debugfs_init(struct kho_debugfs *dbg, const void *fdt);
int kho_debugfs_blob_add(struct kho_debugfs *dbg, const char *name,
const void *blob, size_t size, bool root);
void kho_debugfs_blob_remove(struct kho_debugfs *dbg, void *blob);
#else
static inline int kho_debugfs_init(void) { return 0; }
+static inline void kho_debugfs_cleanup(void) { }
static inline void kho_in_debugfs_init(struct kho_debugfs *dbg,
const void *fdt) { }
-static inline int kho_out_debugfs_init(struct kho_debugfs *dbg) { return 0; }
+static inline int kho_out_debugfs_init(struct kho_debugfs *dbg,
+ const void *fdt) { return 0; }
static inline int kho_debugfs_blob_add(struct kho_debugfs *dbg,
const char *name, const void *blob,
size_t size, bool root) { return 0; }
--
2.56.0.176.g3575e65682-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v1 2/8] kho: move radix tree, vmalloc, and FDT helpers to internal header
2026-10-19 2:11 [PATCH v1 0/8] kho: add read-only Kexec Handover mode Pasha Tatashin
2026-10-19 2:11 ` [PATCH v1 1/8] kho: expose physical address of outgoing FDT Pasha Tatashin
@ 2026-10-19 2:11 ` Pasha Tatashin
2026-10-19 2:11 ` [PATCH v1 3/8] kho: validate preservation metadata in kho_restore_vmalloc() Pasha Tatashin
` (6 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Pasha Tatashin @ 2026-10-19 2:11 UTC (permalink / raw)
To: linux-kselftest, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
linux-mm, ljs, mhocko, skhan, linux-doc, vbabka, dapeng1.mi,
linux-kernel, corbet, surenb, pasha.tatashin, kexec, pratyush,
rdunlap, elver, graf, lirongqing, liam, jlayton
Move the shared KHO radix tree key encoding and indexing helpers,
vmalloc flag conversion helpers, and FDT validation and lookup helpers
from kexec_handover.c into kexec_handover_internal.h as static inline
functions.
The radix tree and vmalloc flag helpers are small bit-manipulation
routines, and the FDT helpers are small wrappers with a single call site
per translation unit, so it is natural to inline them in the internal
header.
This is going to be used by the upcoming KHO read-only mode to walk
preserved KHO radix trees, vmalloc preservation chains, and FDT
subtrees without duplicating the implementation.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
kernel/liveupdate/kexec_handover.c | 115 +----------------
kernel/liveupdate/kexec_handover_internal.h | 134 ++++++++++++++++++++
2 files changed, 136 insertions(+), 113 deletions(-)
diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c
index ce5983abf181..f427cdd9a969 100644
--- a/kernel/liveupdate/kexec_handover.c
+++ b/kernel/liveupdate/kexec_handover.c
@@ -113,29 +113,6 @@ static const void *kho_get_fdt(void)
return kho_in.fdt_phys ? phys_to_virt(kho_in.fdt_phys) : NULL;
}
-/**
- * kho_encode_radix_key - Encodes a physical address and order into a radix key.
- * @phys: The physical address of the page.
- * @order: The order of the page.
- *
- * This function combines a page's physical address and its order into a
- * single unsigned long, which is used as a key for all radix tree
- * operations.
- *
- * Return: The encoded unsigned long radix key.
- */
-static unsigned long kho_encode_radix_key(phys_addr_t phys, unsigned int order)
-{
- /* The physical address is encoded by shifting the PFN by its order. */
- unsigned long shift = PAGE_SHIFT + order;
- /* Order bit goes right before the shifted PFN. */
- unsigned long h = 1UL << (64 - shift);
- /* Shifted PFN. */
- unsigned long l = phys >> shift;
-
- return h | l;
-}
-
/**
* kho_decode_radix_key - Decodes a radix key back into a physical address and order.
* @key: The unsigned long key to decode.
@@ -161,20 +138,6 @@ static phys_addr_t kho_decode_radix_key(unsigned long key, unsigned int *order)
return phys;
}
-static unsigned long kho_radix_get_bitmap_index(unsigned long key)
-{
- return key % (1 << KHO_BITMAP_SIZE_LOG2);
-}
-
-static unsigned long kho_radix_get_table_index(unsigned long key,
- unsigned int level)
-{
- int s;
-
- s = ((level - 1) * KHO_TABLE_SIZE_LOG2) + KHO_BITMAP_SIZE_LOG2;
- return (key >> s) % (1 << KHO_TABLE_SIZE_LOG2);
-}
-
static void __ref *kho_radix_alloc_node(void)
{
struct kho_radix_node *node;
@@ -634,21 +597,6 @@ static int __init kho_preserved_memory_reserve(unsigned long key, void *data)
return 0;
}
-/* Returns physical address of the preserved memory map from FDT */
-static phys_addr_t __init kho_get_mem_map_phys(const void *fdt)
-{
- const void *mem_ptr;
- int len;
-
- mem_ptr = fdt_getprop(fdt, 0, KHO_FDT_MEMORY_MAP_PROP_NAME, &len);
- if (!mem_ptr || len != sizeof(u64)) {
- pr_err("failed to get preserved memory map\n");
- return 0;
- }
-
- return get_unaligned((const u64 *)mem_ptr);
-}
-
static void __init *kho_get_mem_map(const void *fdt)
{
phys_addr_t phys = kho_get_mem_map_phys(fdt);
@@ -1287,13 +1235,6 @@ void kho_unpreserve_pages(struct page *page, unsigned long nr_pages)
}
EXPORT_SYMBOL_GPL(kho_unpreserve_pages);
-/* vmalloc flags KHO supports */
-#define KHO_VMALLOC_SUPPORTED_FLAGS (VM_ALLOC | VM_ALLOW_HUGE_VMAP)
-
-/* KHO internal flags for vmalloc preservations */
-#define KHO_VMALLOC_ALLOC 0x0001
-#define KHO_VMALLOC_HUGE_VMAP 0x0002
-
static unsigned short vmalloc_flags_to_kho(unsigned int vm_flags)
{
unsigned short kho_flags = 0;
@@ -1306,18 +1247,6 @@ static unsigned short vmalloc_flags_to_kho(unsigned int vm_flags)
return kho_flags;
}
-static unsigned int kho_flags_to_vmalloc(unsigned short kho_flags)
-{
- unsigned int vm_flags = 0;
-
- if (kho_flags & KHO_VMALLOC_ALLOC)
- vm_flags |= VM_ALLOC;
- if (kho_flags & KHO_VMALLOC_HUGE_VMAP)
- vm_flags |= VM_ALLOW_HUGE_VMAP;
-
- return vm_flags;
-}
-
static struct kho_vmalloc_chunk *new_vmalloc_chunk(struct kho_vmalloc_chunk *cur)
{
struct kho_vmalloc_chunk *chunk;
@@ -1660,37 +1589,7 @@ EXPORT_SYMBOL_GPL(is_kho_boot);
*/
int kho_retrieve_subtree(const char *name, phys_addr_t *phys, size_t *size)
{
- const void *fdt = kho_get_fdt();
- const u64 *val;
- int offset, len;
-
- if (!fdt)
- return -ENOENT;
-
- if (!phys)
- return -EINVAL;
-
- offset = fdt_subnode_offset(fdt, 0, name);
- if (offset < 0)
- return -ENOENT;
-
- val = fdt_getprop(fdt, offset, KHO_SUB_TREE_PROP_NAME, &len);
- if (!val || len != sizeof(*val))
- return -EINVAL;
-
- *phys = (phys_addr_t)*val;
-
- val = fdt_getprop(fdt, offset, KHO_SUB_TREE_SIZE_PROP_NAME, &len);
- if (!val || len != sizeof(*val)) {
- pr_warn("broken KHO subnode '%s': missing or invalid blob-size property\n",
- name);
- return -EINVAL;
- }
-
- if (size)
- *size = (size_t)*val;
-
- return 0;
+ return __kho_retrieve_subtree(kho_get_fdt(), name, phys, size);
}
EXPORT_SYMBOL_GPL(kho_retrieve_subtree);
@@ -1981,18 +1880,8 @@ void __init kho_populate(phys_addr_t fdt_phys, u64 fdt_len,
pr_warn("setup: failed to memremap FDT (0x%llx)\n", fdt_phys);
goto report;
}
- err = fdt_check_header(fdt);
- if (err) {
- pr_warn("setup: handover FDT (0x%llx) is invalid: %d\n",
- fdt_phys, err);
+ if (kho_check_fdt(fdt, fdt_phys))
goto unmap_fdt;
- }
- err = fdt_node_check_compatible(fdt, 0, KHO_FDT_COMPATIBLE);
- if (err) {
- pr_warn("setup: handover FDT (0x%llx) is incompatible with '%s': %d\n",
- fdt_phys, KHO_FDT_COMPATIBLE, err);
- goto unmap_fdt;
- }
mem_map_phys = kho_get_mem_map_phys(fdt);
if (!mem_map_phys)
diff --git a/kernel/liveupdate/kexec_handover_internal.h b/kernel/liveupdate/kexec_handover_internal.h
index 38181076f603..094b4917a0f1 100644
--- a/kernel/liveupdate/kexec_handover_internal.h
+++ b/kernel/liveupdate/kexec_handover_internal.h
@@ -3,8 +3,142 @@
#define LINUX_KEXEC_HANDOVER_INTERNAL_H
#include <linux/kexec_handover.h>
+#include <linux/kho/abi/kexec_handover.h>
+#include <linux/libfdt.h>
#include <linux/list.h>
+#include <linux/mm.h>
+#include <linux/printk.h>
#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/vmalloc.h>
+
+/* vmalloc flags KHO supports */
+#define KHO_VMALLOC_SUPPORTED_FLAGS (VM_ALLOC | VM_ALLOW_HUGE_VMAP)
+
+/* KHO internal flags for vmalloc preservations */
+#define KHO_VMALLOC_ALLOC 0x0001
+#define KHO_VMALLOC_HUGE_VMAP 0x0002
+
+static inline unsigned int kho_flags_to_vmalloc(unsigned short kho_flags)
+{
+ unsigned int vm_flags = 0;
+
+ if (kho_flags & KHO_VMALLOC_ALLOC)
+ vm_flags |= VM_ALLOC;
+ if (kho_flags & KHO_VMALLOC_HUGE_VMAP)
+ vm_flags |= VM_ALLOW_HUGE_VMAP;
+
+ return vm_flags;
+}
+
+/**
+ * kho_encode_radix_key - Encodes a physical address and order into a radix key.
+ * @phys: The physical address of the page.
+ * @order: The order of the page.
+ *
+ * This function combines a page's physical address and its order into a
+ * single unsigned long, which is used as a key for all radix tree
+ * operations.
+ *
+ * Return: The encoded unsigned long radix key.
+ */
+static inline unsigned long kho_encode_radix_key(phys_addr_t phys,
+ unsigned int order)
+{
+ /* The physical address is encoded by shifting the PFN by its order. */
+ unsigned long shift = PAGE_SHIFT + order;
+ /* Order bit goes right before the shifted PFN. */
+ unsigned long h = 1UL << (64 - shift);
+ /* Shifted PFN. */
+ unsigned long l = phys >> shift;
+
+ return h | l;
+}
+
+static inline unsigned long kho_radix_get_bitmap_index(unsigned long key)
+{
+ return key % (1 << KHO_BITMAP_SIZE_LOG2);
+}
+
+static inline unsigned long kho_radix_get_table_index(unsigned long key,
+ unsigned int level)
+{
+ int s;
+
+ s = ((level - 1) * KHO_TABLE_SIZE_LOG2) + KHO_BITMAP_SIZE_LOG2;
+ return (key >> s) % (1 << KHO_TABLE_SIZE_LOG2);
+}
+
+static inline int kho_check_fdt(const void *fdt, phys_addr_t fdt_phys)
+{
+ int err;
+
+ err = fdt_check_header(fdt);
+ if (err) {
+ pr_warn("handover FDT (0x%llx) is invalid: %d\n",
+ (unsigned long long)fdt_phys, err);
+ return err;
+ }
+
+ err = fdt_node_check_compatible(fdt, 0, KHO_FDT_COMPATIBLE);
+ if (err) {
+ pr_warn("handover FDT (0x%llx) is incompatible with '%s': %d\n",
+ (unsigned long long)fdt_phys, KHO_FDT_COMPATIBLE, err);
+ return err;
+ }
+
+ return 0;
+}
+
+/* Returns physical address of the preserved memory map from FDT */
+static inline phys_addr_t kho_get_mem_map_phys(const void *fdt)
+{
+ const void *mem_ptr;
+ int len;
+
+ mem_ptr = fdt_getprop(fdt, 0, KHO_FDT_MEMORY_MAP_PROP_NAME, &len);
+ if (!mem_ptr || len != sizeof(u64)) {
+ pr_err("failed to get preserved memory map\n");
+ return 0;
+ }
+
+ return get_unaligned((const u64 *)mem_ptr);
+}
+
+static inline int __kho_retrieve_subtree(const void *fdt, const char *name,
+ phys_addr_t *phys, size_t *size)
+{
+ const u64 *val;
+ int offset, len;
+
+ if (!fdt)
+ return -ENOENT;
+
+ if (!phys)
+ return -EINVAL;
+
+ offset = fdt_subnode_offset(fdt, 0, name);
+ if (offset < 0)
+ return -ENOENT;
+
+ val = fdt_getprop(fdt, offset, KHO_SUB_TREE_PROP_NAME, &len);
+ if (!val || len != sizeof(*val))
+ return -EINVAL;
+
+ *phys = (phys_addr_t)get_unaligned(val);
+
+ val = fdt_getprop(fdt, offset, KHO_SUB_TREE_SIZE_PROP_NAME, &len);
+ if (!val || len != sizeof(*val)) {
+ pr_warn("broken KHO subnode '%s': missing or invalid blob-size property\n",
+ name);
+ return -EINVAL;
+ }
+
+ if (size)
+ *size = (size_t)get_unaligned(val);
+
+ return 0;
+}
#ifdef CONFIG_KEXEC_HANDOVER_DEBUGFS
#include <linux/debugfs.h>
--
2.56.0.176.g3575e65682-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v1 3/8] kho: validate preservation metadata in kho_restore_vmalloc()
2026-10-19 2:11 [PATCH v1 0/8] kho: add read-only Kexec Handover mode Pasha Tatashin
2026-10-19 2:11 ` [PATCH v1 1/8] kho: expose physical address of outgoing FDT Pasha Tatashin
2026-10-19 2:11 ` [PATCH v1 2/8] kho: move radix tree, vmalloc, and FDT helpers to internal header Pasha Tatashin
@ 2026-10-19 2:11 ` Pasha Tatashin
2026-10-19 2:11 ` [PATCH v1 4/8] kho: do not modify incoming serialized blocks Pasha Tatashin
` (5 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Pasha Tatashin @ 2026-10-19 2:11 UTC (permalink / raw)
To: linux-kselftest, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
linux-mm, ljs, mhocko, skhan, linux-doc, vbabka, dapeng1.mi,
linux-kernel, corbet, surenb, pasha.tatashin, kexec, pratyush,
rdunlap, elver, graf, lirongqing, liam, jlayton
kho_restore_vmalloc() currently dereferences preservation before
checking for NULL, does not reject unknown KHO vmalloc flags in
kho_flags_to_vmalloc(), and does not validate preservation->first.phys,
preservation->total_pages, preservation->order, or potential size
overflow before allocating the pages array and shifting by order.
Introduce kho_vmalloc_validate() in kexec_handover_internal.h to
validate the struct kho_vmalloc header and convert its flags before
restoring the vmalloc area.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
kernel/liveupdate/kexec_handover.c | 17 ++++++------
kernel/liveupdate/kexec_handover_internal.h | 30 +++++++++++++++++++++
2 files changed, 38 insertions(+), 9 deletions(-)
diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c
index f427cdd9a969..a9c52d998855 100644
--- a/kernel/liveupdate/kexec_handover.c
+++ b/kernel/liveupdate/kexec_handover.c
@@ -1384,28 +1384,27 @@ EXPORT_SYMBOL_GPL(kho_unpreserve_vmalloc);
*/
void *kho_restore_vmalloc(const struct kho_vmalloc *preservation)
{
- struct kho_vmalloc_chunk *chunk = KHOSER_LOAD_PTR(preservation->first);
kasan_vmalloc_flags_t kasan_flags = KASAN_VMALLOC_PROT_NORMAL;
- unsigned int align, order, shift, vm_flags;
- unsigned long total_pages, contig_pages;
+ unsigned long total_pages, contig_pages, align, idx = 0;
+ unsigned int order, shift, vm_flags;
+ struct kho_vmalloc_chunk *chunk;
unsigned long addr, size;
struct vm_struct *area;
struct page **pages;
- unsigned int idx = 0;
int err;
- vm_flags = kho_flags_to_vmalloc(preservation->flags);
- if (vm_flags & ~KHO_VMALLOC_SUPPORTED_FLAGS)
+ if (!kho_vmalloc_validate(preservation, &vm_flags))
return NULL;
+ chunk = KHOSER_LOAD_PTR(preservation->first);
total_pages = preservation->total_pages;
pages = kvmalloc_objs(*pages, total_pages);
if (!pages)
return NULL;
order = preservation->order;
- contig_pages = (1 << order);
+ contig_pages = 1UL << order;
shift = PAGE_SHIFT + order;
- align = 1 << shift;
+ align = 1UL << shift;
while (chunk) {
struct page *page;
@@ -1420,7 +1419,7 @@ void *kho_restore_vmalloc(const struct kho_vmalloc *preservation)
if (!page)
goto err_free_pages_array;
- for (int j = 0; j < contig_pages; j++)
+ for (unsigned long j = 0; j < contig_pages; j++)
pages[idx++] = page + j;
phys += contig_pages * PAGE_SIZE;
diff --git a/kernel/liveupdate/kexec_handover_internal.h b/kernel/liveupdate/kexec_handover_internal.h
index 094b4917a0f1..878e21bd8b00 100644
--- a/kernel/liveupdate/kexec_handover_internal.h
+++ b/kernel/liveupdate/kexec_handover_internal.h
@@ -7,6 +7,7 @@
#include <linux/libfdt.h>
#include <linux/list.h>
#include <linux/mm.h>
+#include <linux/overflow.h>
#include <linux/printk.h>
#include <linux/types.h>
#include <linux/unaligned.h>
@@ -23,6 +24,9 @@ static inline unsigned int kho_flags_to_vmalloc(unsigned short kho_flags)
{
unsigned int vm_flags = 0;
+ if (kho_flags & ~(KHO_VMALLOC_ALLOC | KHO_VMALLOC_HUGE_VMAP))
+ return ~KHO_VMALLOC_SUPPORTED_FLAGS;
+
if (kho_flags & KHO_VMALLOC_ALLOC)
vm_flags |= VM_ALLOC;
if (kho_flags & KHO_VMALLOC_HUGE_VMAP)
@@ -31,6 +35,32 @@ static inline unsigned int kho_flags_to_vmalloc(unsigned short kho_flags)
return vm_flags;
}
+static inline bool kho_vmalloc_validate(const struct kho_vmalloc *preservation,
+ unsigned int *vm_flags)
+{
+ size_t alloc_size;
+
+ if (!preservation || !preservation->first.phys ||
+ !PAGE_ALIGNED(preservation->first.phys) ||
+ !preservation->total_pages)
+ return false;
+
+ if (preservation->order >= BITS_PER_LONG - PAGE_SHIFT ||
+ (1UL << preservation->order) > preservation->total_pages ||
+ !IS_ALIGNED(preservation->total_pages, 1UL << preservation->order))
+ return false;
+
+ if (check_mul_overflow((size_t)preservation->total_pages, PAGE_SIZE,
+ &alloc_size))
+ return false;
+
+ *vm_flags = kho_flags_to_vmalloc(preservation->flags);
+ if (*vm_flags & ~KHO_VMALLOC_SUPPORTED_FLAGS)
+ return false;
+
+ return true;
+}
+
/**
* kho_encode_radix_key - Encodes a physical address and order into a radix key.
* @phys: The physical address of the page.
--
2.56.0.176.g3575e65682-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v1 4/8] kho: do not modify incoming serialized blocks
2026-10-19 2:11 [PATCH v1 0/8] kho: add read-only Kexec Handover mode Pasha Tatashin
` (2 preceding siblings ...)
2026-10-19 2:11 ` [PATCH v1 3/8] kho: validate preservation metadata in kho_restore_vmalloc() Pasha Tatashin
@ 2026-10-19 2:11 ` Pasha Tatashin
2026-10-19 2:11 ` [PATCH v1 5/8] kho: add read-only Kexec Handover mode Pasha Tatashin
` (4 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Pasha Tatashin @ 2026-10-19 2:11 UTC (permalink / raw)
To: linux-kselftest, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
linux-mm, ljs, mhocko, skhan, linux-doc, vbabka, dapeng1.mi,
linux-kernel, corbet, surenb, pasha.tatashin, kexec, pratyush,
rdunlap, elver, graf, lirongqing, liam, jlayton
When restoring an incoming block set via kho_block_set_restore(), the
physical chain of blocks is already linked and bs->head_pa is already
set. However, kho_block_add() unconditionally rewrites last->ser->next
and bs->head_pa, and kho_block_set_shrink_one() clears
new_last->ser->next even when operating on an incoming block set.
In the upcoming KHO read-only mode, incoming serialized blocks are
mapped read-only and must not be modified. Guard the ser->next and
bs->head_pa updates in kho_block_add() and kho_block_set_shrink_one()
with !bs->incoming.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
kernel/liveupdate/kho_block.c | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/kernel/liveupdate/kho_block.c b/kernel/liveupdate/kho_block.c
index 0d2a342ef422..a4e945f28fad 100644
--- a/kernel/liveupdate/kho_block.c
+++ b/kernel/liveupdate/kho_block.c
@@ -93,10 +93,12 @@ static int kho_block_add(struct kho_block_set *bs,
list_add_tail(&block->list, &bs->blocks);
bs->nblocks++;
- if (last)
- last->ser->next = virt_to_phys(ser);
- else
- bs->head_pa = virt_to_phys(ser);
+ if (!bs->incoming) {
+ if (last)
+ last->ser->next = virt_to_phys(ser);
+ else
+ bs->head_pa = virt_to_phys(ser);
+ }
return 0;
}
@@ -133,10 +135,10 @@ static void kho_block_set_shrink_one(struct kho_block_set *bs)
kfree(last);
new_last = list_last_entry_or_null(&bs->blocks, struct kho_block, list);
- if (new_last)
- new_last->ser->next = 0;
- else
+ if (!new_last)
bs->head_pa = 0;
+ else if (!bs->incoming)
+ new_last->ser->next = 0;
}
/**
--
2.56.0.176.g3575e65682-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v1 5/8] kho: add read-only Kexec Handover mode
2026-10-19 2:11 [PATCH v1 0/8] kho: add read-only Kexec Handover mode Pasha Tatashin
` (3 preceding siblings ...)
2026-10-19 2:11 ` [PATCH v1 4/8] kho: do not modify incoming serialized blocks Pasha Tatashin
@ 2026-10-19 2:11 ` Pasha Tatashin
2026-10-19 2:11 ` [PATCH v1 6/8] kho: support read-only KHO mode in kho_block Pasha Tatashin
` (3 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Pasha Tatashin @ 2026-10-19 2:11 UTC (permalink / raw)
To: linux-kselftest, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
linux-mm, ljs, mhocko, skhan, linux-doc, vbabka, dapeng1.mi,
linux-kernel, corbet, surenb, pasha.tatashin, kexec, pratyush,
rdunlap, elver, graf, lirongqing, liam, jlayton
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@soleen.com>
---
.../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@soleen.com>
+ *
+ * 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
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v1 6/8] kho: support read-only KHO mode in kho_block
2026-10-19 2:11 [PATCH v1 0/8] kho: add read-only Kexec Handover mode Pasha Tatashin
` (4 preceding siblings ...)
2026-10-19 2:11 ` [PATCH v1 5/8] kho: add read-only Kexec Handover mode Pasha Tatashin
@ 2026-10-19 2:11 ` Pasha Tatashin
2026-10-19 2:11 ` [PATCH v1 7/8] kho: test: support read-only KHO mode in test_kho Pasha Tatashin
` (2 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Pasha Tatashin @ 2026-10-19 2:11 UTC (permalink / raw)
To: linux-kselftest, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
linux-mm, ljs, mhocko, skhan, linux-doc, vbabka, dapeng1.mi,
linux-kernel, corbet, surenb, pasha.tatashin, kexec, pratyush,
rdunlap, elver, graf, lirongqing, liam, jlayton
In read-only KHO mode, incoming serialized kho_block chains lie outside
the kernel's direct map and must not be modified or freed back into the
page allocator.
Map incoming serialized blocks read-only via kho_ro_map_folio(...,
PAGE_KERNEL_RO) during cycle detection and restoration, and unmap them
via kho_ro_unmap() instead of calling kho_restore_free() when destroying
or shrinking a block set in read-only mode.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
kernel/liveupdate/Makefile | 5 +--
kernel/liveupdate/kho_block.c | 76 ++++++++++++++++++++++++++---------
2 files changed, 60 insertions(+), 21 deletions(-)
diff --git a/kernel/liveupdate/Makefile b/kernel/liveupdate/Makefile
index f8b09e04bea2..d3587048e619 100644
--- a/kernel/liveupdate/Makefile
+++ b/kernel/liveupdate/Makefile
@@ -1,14 +1,13 @@
# SPDX-License-Identifier: GPL-2.0
luo-y := \
- kho_block.o \
luo_core.o \
luo_file.o \
luo_flb.o \
luo_session.o
-obj-$(CONFIG_KEXEC_HANDOVER) += kexec_handover.o
-obj-$(CONFIG_KEXEC_HANDOVER_READONLY) += kexec_handover_ro.o
+obj-$(CONFIG_KEXEC_HANDOVER) += kexec_handover.o kho_block.o
+obj-$(CONFIG_KEXEC_HANDOVER_READONLY) += kexec_handover_ro.o kho_block.o
obj-$(CONFIG_KEXEC_HANDOVER_DEBUGFS) += kexec_handover_debugfs.o
obj-$(CONFIG_LIVEUPDATE) += luo.o
diff --git a/kernel/liveupdate/kho_block.c b/kernel/liveupdate/kho_block.c
index a4e945f28fad..a3cf5b964b26 100644
--- a/kernel/liveupdate/kho_block.c
+++ b/kernel/liveupdate/kho_block.c
@@ -60,10 +60,32 @@ static void *kho_block_entry(struct kho_block_set_it *it, u64 index)
return kho_block_entries(it->block) + (index * it->bs->entry_size);
}
+static struct kho_block_header_ser *kho_block_map_ser(phys_addr_t pa)
+{
+ if (!pa)
+ return NULL;
+
+ if (kho_is_ro())
+ return kho_ro_map_folio(pa, PAGE_KERNEL_RO);
+
+ return phys_to_virt(pa);
+}
+
+static void kho_block_unmap_ser(struct kho_block_header_ser *ser)
+{
+ if (kho_is_ro())
+ kho_ro_unmap(ser);
+}
+
/* Free serialized data */
static void kho_block_free_ser(struct kho_block_set *bs,
struct kho_block_header_ser *ser)
{
+ if (kho_is_ro()) {
+ kho_block_unmap_ser(ser);
+ return;
+ }
+
if (bs->incoming)
kho_restore_free(ser);
else
@@ -197,6 +219,20 @@ void kho_block_set_shrink(struct kho_block_set *bs, u64 count)
kho_block_set_shrink_one(bs);
}
+static bool kho_block_step_pa(phys_addr_t pa, phys_addr_t *next_pa)
+{
+ struct kho_block_header_ser *ser;
+
+ ser = kho_block_map_ser(pa);
+ if (!ser)
+ return false;
+
+ *next_pa = ser->next;
+ kho_block_unmap_ser(ser);
+
+ return true;
+}
+
/*
* kho_block_set_is_cyclic - Check for cycles in a linked list of blocks.
* Uses Floyd's cycle-finding algorithm to ensure sanity of the incoming list.
@@ -205,30 +241,26 @@ void kho_block_set_shrink(struct kho_block_set *bs, u64 count)
*/
static bool kho_block_set_is_cyclic(struct kho_block_set *bs)
{
- struct kho_block_header_ser *fast;
- struct kho_block_header_ser *slow;
+ phys_addr_t fast_pa = bs->head_pa;
+ phys_addr_t slow_pa = bs->head_pa;
int count = 0;
- fast = phys_to_virt(bs->head_pa);
- slow = fast;
-
- while (fast) {
+ while (fast_pa) {
if (count++ >= KHO_MAX_BLOCKS) {
pr_err("Block set is corrupted\n");
return true;
}
- if (!fast->next)
- break;
-
- fast = phys_to_virt(fast->next);
- if (!fast->next)
+ if (!kho_block_step_pa(fast_pa, &fast_pa))
+ return true;
+ if (!fast_pa)
break;
- fast = phys_to_virt(fast->next);
- slow = phys_to_virt(slow->next);
+ if (!kho_block_step_pa(fast_pa, &fast_pa) ||
+ !kho_block_step_pa(slow_pa, &slow_pa))
+ return true;
- if (slow == fast) {
+ if (slow_pa == fast_pa) {
pr_err("Block set is corrupted\n");
return true;
}
@@ -266,16 +298,23 @@ int kho_block_set_restore(struct kho_block_set *bs, u64 head_pa)
}
while (next_pa) {
- ser = phys_to_virt(next_pa);
+ ser = kho_block_map_ser(next_pa);
+ if (!ser) {
+ err = -ENOMEM;
+ goto err_destroy;
+ }
if (!ser->count || ser->count > bs->count_per_block) {
pr_warn("Block contains invalid entry count: %llu\n",
ser->count);
+ kho_block_unmap_ser(ser);
err = -EINVAL;
goto err_destroy;
}
err = kho_block_add(bs, ser);
- if (err)
+ if (err) {
+ kho_block_unmap_ser(ser);
goto err_destroy;
+ }
next_pa = ser->next;
}
@@ -285,9 +324,10 @@ int kho_block_set_restore(struct kho_block_set *bs, u64 head_pa)
kho_block_set_destroy(bs);
/* Free the remaining un-restored blocks in the physical chain */
- while (next_pa) {
- struct kho_block_header_ser *next_ser = phys_to_virt(next_pa);
+ while (!kho_is_ro() && next_pa) {
+ struct kho_block_header_ser *next_ser;
+ next_ser = phys_to_virt(next_pa);
next_pa = next_ser->next;
kho_block_free_ser(bs, next_ser);
}
--
2.56.0.176.g3575e65682-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v1 7/8] kho: test: support read-only KHO mode in test_kho
2026-10-19 2:11 [PATCH v1 0/8] kho: add read-only Kexec Handover mode Pasha Tatashin
` (5 preceding siblings ...)
2026-10-19 2:11 ` [PATCH v1 6/8] kho: support read-only KHO mode in kho_block Pasha Tatashin
@ 2026-10-19 2:11 ` Pasha Tatashin
2026-10-19 2:11 ` [PATCH v1 8/8] selftests: kho: add read-only KHO test to vmtest.sh Pasha Tatashin
2026-10-19 8:43 ` [PATCH v1 0/8] kho: add read-only Kexec Handover mode Mike Rapoport
8 siblings, 0 replies; 11+ messages in thread
From: Pasha Tatashin @ 2026-10-19 2:11 UTC (permalink / raw)
To: linux-kselftest, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
linux-mm, ljs, mhocko, skhan, linux-doc, vbabka, dapeng1.mi,
linux-kernel, corbet, surenb, pasha.tatashin, kexec, pratyush,
rdunlap, elver, graf, lirongqing, liam, jlayton
Allow CONFIG_TEST_KEXEC_HANDOVER to be enabled with
CONFIG_KEXEC_HANDOVER_READONLY and update test_kho to verify preserved
state when booted in read-only KHO mode.
In read-only mode, test_kho retrieves the test FDT subtree, maps the FDT
folio via kho_ro_map_folio(..., PAGE_KERNEL_RO), restores the preserved
vmalloc folio descriptor array via kho_restore_vmalloc(), verifies each
folio's preserved order in the KHO radix tree via
kho_ro_lookup_folio_order(), maps each folio via kho_ro_map_folio() (and
unmaps via kho_ro_unmap()), and verifies the checksum over all preserved
pages.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
lib/Kconfig.debug | 2 +-
lib/test_kho.c | 68 ++++++++++++++++++++++++++++++++++-------------
2 files changed, 51 insertions(+), 19 deletions(-)
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 134b15a44625..74223ef8bd1c 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -3381,7 +3381,7 @@ config TEST_OBJPOOL
config TEST_KEXEC_HANDOVER
bool "Test for Kexec HandOver"
default n
- depends on KEXEC_HANDOVER
+ depends on KEXEC_HANDOVER || KEXEC_HANDOVER_READONLY
help
This option enables test for Kexec HandOver (KHO).
The test consists of two parts: saving kernel data before kexec and
diff --git a/lib/test_kho.c b/lib/test_kho.c
index aa6a0956bb8b..1e0f257a5854 100644
--- a/lib/test_kho.c
+++ b/lib/test_kho.c
@@ -239,7 +239,7 @@ static int kho_test_restore_data(const void *fdt, int node)
phys_addr_t *folios_info;
const __wsum *old_csum;
__wsum csum = 0;
- int len;
+ int len, i;
node = fdt_path_offset(fdt, "/data");
@@ -259,11 +259,28 @@ static int kho_test_restore_data(const void *fdt, int node)
if (!folios_info)
return -EINVAL;
- for (int i = 0; i < *nr_folios; i++) {
+ for (i = 0; i < *nr_folios; i++) {
unsigned int order = folios_info[i] & ~PAGE_MASK;
phys_addr_t phys = folios_info[i] & PAGE_MASK;
- unsigned int size = PAGE_SIZE << order;
struct folio *folio;
+ size_t size;
+
+ if (order > MAX_PAGE_ORDER)
+ break;
+ size = PAGE_SIZE << order;
+
+ if (kho_is_ro()) {
+ void *addr;
+
+ if (kho_ro_lookup_folio_order(phys) != order)
+ break;
+ addr = kho_ro_map_folio(phys, PAGE_KERNEL_RO);
+ if (!addr)
+ break;
+ csum = csum_partial(addr, size, csum);
+ kho_ro_unmap(addr);
+ continue;
+ }
folio = kho_restore_folio(phys);
if (!folio)
@@ -278,50 +295,65 @@ static int kho_test_restore_data(const void *fdt, int node)
vfree(folios_info);
- if (csum != *old_csum)
+ if (i != *nr_folios || csum != *old_csum)
return -EINVAL;
return 0;
}
-static int kho_test_restore(phys_addr_t fdt_phys)
+static int kho_test_restore(phys_addr_t fdt_phys, size_t fdt_size)
{
- void *fdt = phys_to_virt(fdt_phys);
const unsigned int *magic;
- int node, len, err;
+ int node, len, err = -EINVAL;
+ void *fdt;
+
+ if (!fdt_size)
+ return -EINVAL;
+
+ if (kho_is_ro()) {
+ if (kho_ro_lookup_folio_order(fdt_phys) < get_order(fdt_size))
+ return -EINVAL;
+ fdt = kho_ro_map_folio(fdt_phys, PAGE_KERNEL_RO);
+ if (!fdt)
+ return -EINVAL;
+ } else {
+ fdt = phys_to_virt(fdt_phys);
+ }
node = fdt_path_offset(fdt, "/");
if (node < 0)
- return -EINVAL;
+ goto out_unmap;
if (fdt_node_check_compatible(fdt, node, KHO_TEST_COMPAT))
- return -EINVAL;
+ goto out_unmap;
magic = fdt_getprop(fdt, node, "magic", &len);
if (!magic || len != sizeof(*magic))
- return -EINVAL;
+ goto out_unmap;
if (*magic != KHO_TEST_MAGIC)
- return -EINVAL;
+ goto out_unmap;
err = kho_test_restore_data(fdt, node);
- if (err)
- return err;
- return 0;
+out_unmap:
+ if (kho_is_ro())
+ kho_ro_unmap(fdt);
+ return err;
}
static int __init kho_test_init(void)
{
phys_addr_t fdt_phys;
+ size_t fdt_size;
int err;
- if (!kho_is_enabled())
+ if (!kho_is_enabled() && !kho_is_ro())
return 0;
- err = kho_retrieve_subtree(KHO_TEST_FDT, &fdt_phys, NULL);
+ err = kho_retrieve_subtree(KHO_TEST_FDT, &fdt_phys, &fdt_size);
if (!err) {
- err = kho_test_restore(fdt_phys);
+ err = kho_test_restore(fdt_phys, fdt_size);
if (err)
pr_err("KHO restore failed\n");
else
@@ -330,7 +362,7 @@ static int __init kho_test_init(void)
return err;
}
- if (err != -ENOENT) {
+ if (err != -ENOENT || kho_is_ro()) {
pr_warn("failed to retrieve %s FDT: %d\n", KHO_TEST_FDT, err);
return err;
}
--
2.56.0.176.g3575e65682-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v1 8/8] selftests: kho: add read-only KHO test to vmtest.sh
2026-10-19 2:11 [PATCH v1 0/8] kho: add read-only Kexec Handover mode Pasha Tatashin
` (6 preceding siblings ...)
2026-10-19 2:11 ` [PATCH v1 7/8] kho: test: support read-only KHO mode in test_kho Pasha Tatashin
@ 2026-10-19 2:11 ` Pasha Tatashin
2026-10-19 8:43 ` [PATCH v1 0/8] kho: add read-only Kexec Handover mode Mike Rapoport
8 siblings, 0 replies; 11+ messages in thread
From: Pasha Tatashin @ 2026-10-19 2:11 UTC (permalink / raw)
To: linux-kselftest, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
linux-mm, ljs, mhocko, skhan, linux-doc, vbabka, dapeng1.mi,
linux-kernel, corbet, surenb, pasha.tatashin, kexec, pratyush,
rdunlap, elver, graf, lirongqing, liam, jlayton
Extend tools/testing/selftests/kho/vmtest.sh and init.c to run a second
QEMU test verifying read-only KHO ('kho_ro=<fdt_phys>') after the
standard KHO test on both x86_64 and aarch64:
- Build a second kernel configured with CONFIG_KEXEC_HANDOVER_READONLY=y
(and CONFIG_KEXEC_HANDOVER=n).
- Pass KHO_RO_CMDLINE ('nokaslr kho_bootmem=64M,64M,64M' on x86_64,
'cma=116M@0x40c00000' on aarch64) to the first kernel in the read-only
test run so KHO allocations are placed in predictable physical ranges.
- When booted with 'kho_ro_test', read /debugfs/kho/out/fdt_phys in
init.c and kexec into the read-only kernel with 'kho_ro=<fdt_phys>'
and architecture-specific memory parameters
('memmap=792M$16M memmap=24M$1000M' on x86_64,
'mem=128M cma=12M@0x40000000' on aarch64) so the preserved pages are
excluded from the boot memory map and mapped via kho_ro_map_folio().
- Verify 'KHO restore succeeded' from test_kho in read-only mode.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
tools/testing/selftests/kho/arm64.conf | 3 +
tools/testing/selftests/kho/init.c | 38 ++++++++-
tools/testing/selftests/kho/vmtest.sh | 103 +++++++++++++++++--------
tools/testing/selftests/kho/x86.conf | 1 +
4 files changed, 112 insertions(+), 33 deletions(-)
diff --git a/tools/testing/selftests/kho/arm64.conf b/tools/testing/selftests/kho/arm64.conf
index ee696807cd35..36dd8c46060c 100644
--- a/tools/testing/selftests/kho/arm64.conf
+++ b/tools/testing/selftests/kho/arm64.conf
@@ -4,6 +4,9 @@ CONFIG_SERIAL_AMBA_PL010=y
CONFIG_SERIAL_AMBA_PL010_CONSOLE=y
CONFIG_SERIAL_AMBA_PL011=y
CONFIG_SERIAL_AMBA_PL011_CONSOLE=y
+CONFIG_CMA=y
+CONFIG_DMA_CMA=y
"
KERNEL_IMAGE="Image"
KERNEL_CMDLINE="console=ttyAMA0"
+KHO_RO_CMDLINE="cma=116M@0x40c00000"
diff --git a/tools/testing/selftests/kho/init.c b/tools/testing/selftests/kho/init.c
index 88a41b6eba95..272338f186be 100644
--- a/tools/testing/selftests/kho/init.c
+++ b/tools/testing/selftests/kho/init.c
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0
#include <stdio.h>
+#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/syscall.h>
@@ -12,6 +13,15 @@
#define COMMAND_LINE_SIZE 2048
#define KERNEL_IMAGE "/kernel"
+#define FDT_PHYS_PATH "/debugfs/kho/out/fdt_phys"
+
+#if defined(__x86_64__)
+#define KHO_RO_MEM_PARAM "memmap=792M$16M memmap=24M$1000M"
+#elif defined(__aarch64__)
+#define KHO_RO_MEM_PARAM "mem=128M cma=12M@0x40000000"
+#else
+#define KHO_RO_MEM_PARAM ""
+#endif
static int mount_filesystems(void)
{
@@ -32,7 +42,8 @@ static long kexec_file_load(int kernel_fd, int initrd_fd,
static int kexec_load(void)
{
char cmdline[COMMAND_LINE_SIZE];
- ssize_t len;
+ ssize_t len, fdt_len;
+ char fdt_phys[64];
int fd, err;
fd = open("/proc/cmdline", O_RDONLY);
@@ -41,11 +52,34 @@ static int kexec_load(void)
len = read(fd, cmdline, sizeof(cmdline));
close(fd);
- if (len < 0)
+ if (len <= 0)
return -1;
/* replace \n with \0 */
cmdline[len - 1] = 0;
+
+ if (strstr(cmdline, "kho_ro_test")) {
+ fd = open(FDT_PHYS_PATH, O_RDONLY);
+ if (fd < 0)
+ return -1;
+
+ fdt_len = read(fd, fdt_phys, sizeof(fdt_phys) - 1);
+ close(fd);
+ if (fdt_len <= 0)
+ return -1;
+
+ if (fdt_phys[fdt_len - 1] == '\n')
+ fdt_len--;
+ fdt_phys[fdt_len] = 0;
+
+ len = (len - 1) + snprintf(cmdline + len - 1,
+ sizeof(cmdline) - (len - 1),
+ " kho_ro=%s " KHO_RO_MEM_PARAM,
+ fdt_phys) + 1;
+ if (len <= 0 || (size_t)len > sizeof(cmdline))
+ return -1;
+ }
+
fd = open(KERNEL_IMAGE, O_RDONLY);
if (fd < 0)
return -1;
diff --git a/tools/testing/selftests/kho/vmtest.sh b/tools/testing/selftests/kho/vmtest.sh
index 0014bd76e88d..b3659e4fd1c9 100755
--- a/tools/testing/selftests/kho/vmtest.sh
+++ b/tools/testing/selftests/kho/vmtest.sh
@@ -35,32 +35,25 @@ trap cleanup EXIT
function skip() {
local msg=${1:-""}
- ktap_test_skip "$msg"
+ while [[ "$KTAP_TESTNO" -le "$KSFT_NUM_TESTS" ]]; do
+ ktap_test_skip "$msg"
+ done
exit "$KSFT_SKIP"
}
-function fail() {
- local msg=${1:-""}
-
- ktap_test_fail "$msg"
- exit "$KSFT_FAIL"
-}
-
function build_kernel() {
local build_dir=$1
local make_cmd=$2
local arch_kconfig=$3
local kimage=$4
+ local kho_mode=${5:-"rw"}
local kho_config="$tmp_dir/kho.config"
local kconfig="$build_dir/.config"
# enable initrd, KHO and KHO test in kernel configuration
- tee "$kconfig" > "$kho_config" <<EOF
+ cat > "$kho_config" <<EOF
CONFIG_BLK_DEV_INITRD=y
-CONFIG_KEXEC_HANDOVER=y
-CONFIG_KEXEC_HANDOVER_DEBUG=y
-CONFIG_KEXEC_HANDOVER_DEBUGFS=y
CONFIG_TEST_KEXEC_HANDOVER=y
CONFIG_DEBUG_KERNEL=y
CONFIG_DEBUG_VM=y
@@ -70,20 +63,50 @@ CONFIG_DEFERRED_STRUCT_PAGE_INIT=y
$arch_kconfig
EOF
+ if [[ "$kho_mode" == "ro" ]]; then
+ cat >> "$kho_config" <<EOF
+# CONFIG_KEXEC_HANDOVER is not set
+CONFIG_KEXEC_HANDOVER_READONLY=y
+EOF
+ else
+ cat >> "$kho_config" <<EOF
+CONFIG_KEXEC_HANDOVER=y
+CONFIG_KEXEC_HANDOVER_DEBUG=y
+CONFIG_KEXEC_HANDOVER_DEBUGFS=y
+EOF
+ fi
+
make_cmd="$make_cmd -C $kernel_dir O=$build_dir"
- $make_cmd olddefconfig
+ local need_config=0
+ if [[ ! -f "$kconfig" ]]; then
+ need_config=1
+ else
+ while read -r opt ; do
+ [[ -z "$opt" ]] && continue
+ grep -q "^$opt$" "$kconfig" || need_config=1
+ done < "$kho_config"
+ fi
+
+ if [[ "$need_config" -eq 1 ]]; then
+ cp "$kho_config" "$kconfig"
+ $make_cmd olddefconfig
+ fi
# verify that kernel confiration has all necessary options
while read -r opt ; do
- grep "$opt" "$kconfig" &>/dev/null || skip "$opt is missing"
+ [[ -z "$opt" ]] && continue
+ grep -q "^$opt$" "$kconfig" || skip "$opt is missing"
done < "$kho_config"
$make_cmd "$kimage"
- $make_cmd headers_install INSTALL_HDR_PATH="$headers_dir"
+ if [[ "$kho_mode" != "ro" ]]; then
+ $make_cmd headers_install INSTALL_HDR_PATH="$headers_dir"
+ fi
}
function mkinitrd() {
local kernel=$1
+ local out_initrd=$2
"$CROSS_COMPILE"gcc -s -static -Os -nostdinc -nostdlib \
-fno-asynchronous-unwind-tables -fno-ident \
@@ -100,25 +123,31 @@ file /init $tmp_dir/init 0755 0 0
file /kernel $kernel 0644 0 0
EOF
- "$build_dir/usr/gen_init_cpio" "$tmp_dir/cpio_list" > "$initrd"
+ "$build_dir/usr/gen_init_cpio" "$tmp_dir/cpio_list" > "$out_initrd"
}
function run_qemu() {
local qemu_cmd=$1
local cmdline=$2
local kernel=$3
+ local initrd_path=$4
+ local test_name=$5
local serial="$tmp_dir/qemu.serial"
cmdline="$cmdline kho=on panic=-1"
- $qemu_cmd -m 1G -smp 2 -no-reboot -nographic -nodefaults \
- -accel kvm -accel hvf -accel tcg \
- -serial file:"$serial" \
- -append "$cmdline" \
- -kernel "$kernel" \
- -initrd "$initrd"
-
- grep "KHO restore succeeded" "$serial" &> /dev/null || fail "KHO failed"
+ rm -f "$serial"
+ if $qemu_cmd -m 1G -smp 2 -no-reboot -nographic -nodefaults \
+ -accel kvm -accel hvf -accel tcg \
+ -serial file:"$serial" \
+ -append "$cmdline" \
+ -kernel "$kernel" \
+ -initrd "$initrd_path" && \
+ grep "KHO restore succeeded" "$serial" &> /dev/null; then
+ ktap_test_pass "$test_name succeeded"
+ else
+ ktap_test_fail "$test_name failed"
+ fi
}
function target_to_arch() {
@@ -164,7 +193,7 @@ function main() {
done
ktap_print_header
- ktap_set_plan 1
+ ktap_set_plan 2
if [[ "$target" != "$(uname -m)" ]] && [[ -z "$CROSS_COMPILE" ]]; then
skip "Cross-platform testing needs to specify CROSS_COMPILE"
@@ -174,17 +203,29 @@ function main() {
local arch=$(target_to_arch "$target")
source "$test_dir/$arch.conf"
- # build the kernel and create initrd
+ # build the kernels and create initrds
# initrd includes the kernel image that will be kexec'ed
local make_cmd="make ARCH=$arch CROSS_COMPILE=$CROSS_COMPILE -j$jobs"
- build_kernel "$build_dir" "$make_cmd" "$QEMU_KCONFIG" "$KERNEL_IMAGE"
+ build_kernel "$build_dir" "$make_cmd" "$QEMU_KCONFIG" "$KERNEL_IMAGE" "rw"
- local kernel="$build_dir/arch/$arch/boot/$KERNEL_IMAGE"
- mkinitrd "$kernel"
+ local kernel="$tmp_dir/$KERNEL_IMAGE"
+ cp "$build_dir/arch/$arch/boot/$KERNEL_IMAGE" "$kernel"
+ mkinitrd "$kernel" "$initrd"
+
+ local build_dir_ro="${build_dir}-ro"
+ if [[ ! -d "$build_dir_ro" ]]; then
+ cp -a "$build_dir" "$build_dir_ro"
+ fi
+ build_kernel "$build_dir_ro" "$make_cmd" "$QEMU_KCONFIG" "$KERNEL_IMAGE" "ro"
+ local kernel_ro="$build_dir_ro/arch/$arch/boot/$KERNEL_IMAGE"
+ local initrd_ro="$tmp_dir/initrd-ro.cpio"
+ mkinitrd "$kernel_ro" "$initrd_ro"
- run_qemu "$QEMU_CMD" "$KERNEL_CMDLINE" "$kernel"
+ trap - ERR
- ktap_test_pass "KHO succeeded"
+ run_qemu "$QEMU_CMD" "$KERNEL_CMDLINE" "$kernel" "$initrd" "KHO"
+ run_qemu "$QEMU_CMD" "$KERNEL_CMDLINE $KHO_RO_CMDLINE kho_ro_test" \
+ "$kernel" "$initrd_ro" "KHO read-only"
}
main "$@"
diff --git a/tools/testing/selftests/kho/x86.conf b/tools/testing/selftests/kho/x86.conf
index b419e610ca22..2c097c5bd6c6 100644
--- a/tools/testing/selftests/kho/x86.conf
+++ b/tools/testing/selftests/kho/x86.conf
@@ -5,3 +5,4 @@ CONFIG_SERIAL_8250_CONSOLE=y
"
KERNEL_IMAGE="bzImage"
KERNEL_CMDLINE="console=ttyS0"
+KHO_RO_CMDLINE="nokaslr kho_bootmem=64M,64M,64M"
--
2.56.0.176.g3575e65682-goog
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH v1 0/8] kho: add read-only Kexec Handover mode
2026-10-19 2:11 [PATCH v1 0/8] kho: add read-only Kexec Handover mode Pasha Tatashin
` (7 preceding siblings ...)
2026-10-19 2:11 ` [PATCH v1 8/8] selftests: kho: add read-only KHO test to vmtest.sh Pasha Tatashin
@ 2026-10-19 8:43 ` Mike Rapoport
2026-10-19 14:23 ` Pasha Tatashin
8 siblings, 1 reply; 11+ messages in thread
From: Mike Rapoport @ 2026-10-19 8:43 UTC (permalink / raw)
To: Pasha Tatashin
Cc: linux-kselftest, kuba, enelsonmoore, bp, shuah, david, akpm,
linux-mm, ljs, mhocko, skhan, linux-doc, vbabka, dapeng1.mi,
linux-kernel, corbet, surenb, kexec, pratyush, rdunlap, elver,
graf, lirongqing, liam, jlayton
Hi Pasha,
Some brief comments before digging in.
On Sun, Oct 18, 2026 at 10:11:03PM -0400, Pasha Tatashin wrote:
> This series introduces a read-only Kexec Handover mode, enabled at boot
> via 'kho_ro=<pa>' pointing to the physical address of the KHO FDT.
>
> In standard KHO, the target kernel takes ownership of preserved memory
> during early boot, initializes struct pages for preserved folios, and
> reclaims KHO metadata and preserved pages back into the page allocator
> as they are restored.
>
> In some scenarios (such as a caretaker, inspection kernel, firmware), a
> kernel needs to inspect and consume preserved KHO subtrees, radix trees,
> and serialized metadata without taking ownership of or modifying the KHO
> state, leaving the preserved KHO tree intact for a subsequent kernel.
> In this mode, the KHO physical memory lies outside the kernel's direct
> map.
>
> To support this cleanly without duplicating KHO traversal logic:
> - Patch 1 exposes the physical address of the outgoing KHO FDT via
> /sys/kernel/debug/kho/out/fdt_phys so userspace tooling can pass it
> to a 'kho_ro=<pa>' kernel.
We already pass the physical address, why adding a boolean kho_ro is not
enough?
> - Patch 2 moves the shared radix-tree key encoding and indexing,
> vmalloc flag conversion, and FDT validation and subtree lookup helpers
> to kernel/liveupdate/kexec_handover_internal.h so they can be shared
> between standard and read-only KHO.
> - Patch 3 hardens kho_restore_vmalloc() by introducing
> kho_vmalloc_validate() in kernel/liveupdate/kexec_handover_internal.h
> to validate the preservation metadata header and flags before
> restoring the vmalloc area.
The diff for kexec_handover_internal.h
> kernel/liveupdate/kexec_handover_internal.h | 171 +++++++-
seems a wee too large. Maybe some of this should move to a .c file?
> - Patch 4 avoids modifying ser->next and bs->head_pa in kho_block_add()
> and kho_block_set_shrink_one() when operating on an incoming block
> set, as the chain is already linked and incoming blocks must not be
> modified in KHO read-only mode.
> - Patch 5 adds CONFIG_KEXEC_HANDOVER_READONLY and
> kernel/liveupdate/kexec_handover_ro.c. When booted with
> 'kho_ro=<pa>', preserved KHO metadata is mapped read-only
> (PAGE_KERNEL_RO), with the FDT and radix-tree nodes mapped lazily on
> first visit and kept cached, providing read-only implementations of
> kho_retrieve_subtree(), kho_restore_vmalloc(),
> kho_ro_lookup_folio_order(), kho_ro_map_folio(), and kho_ro_unmap().
What are the expected users of these APIs?
> - Patch 6 updates kho_block to map incoming serialized blocks read-only
> via kho_ro_map_folio() and unmap them via kho_ro_unmap() in read-only
> KHO mode.
> - Patches 7 and 8 extend test_kho and tools/testing/selftests/kho/vmtest.sh
> to test read-only KHO handover when preserved memory lies outside
> the boot kernel's initial memory range on both x86_64 and arm64.
>
> Pasha Tatashin (8):
> kho: expose physical address of outgoing FDT
> kho: move radix tree, vmalloc, and FDT helpers to internal header
> kho: validate preservation metadata in kho_restore_vmalloc()
> kho: do not modify incoming serialized blocks
> kho: add read-only Kexec Handover mode
> kho: support read-only KHO mode in kho_block
> kho: test: support read-only KHO mode in test_kho
> selftests: kho: add read-only KHO test to vmtest.sh
>
> .../admin-guide/kernel-parameters.txt | 6 +
> Documentation/admin-guide/mm/kho.rst | 3 +
> include/linux/kexec_handover.h | 53 ++-
> kernel/liveupdate/Kconfig | 16 +
> kernel/liveupdate/Makefile | 4 +-
> kernel/liveupdate/kexec_handover.c | 142 +------
> kernel/liveupdate/kexec_handover_debugfs.c | 22 +-
> kernel/liveupdate/kexec_handover_internal.h | 171 +++++++-
> kernel/liveupdate/kexec_handover_ro.c | 378 ++++++++++++++++++
> kernel/liveupdate/kho_block.c | 92 +++--
> lib/Kconfig.debug | 2 +-
> lib/test_kho.c | 68 +++-
> tools/testing/selftests/kho/arm64.conf | 3 +
> tools/testing/selftests/kho/init.c | 38 +-
> tools/testing/selftests/kho/vmtest.sh | 103 +++--
> tools/testing/selftests/kho/x86.conf | 1 +
> 16 files changed, 881 insertions(+), 221 deletions(-)
> create mode 100644 kernel/liveupdate/kexec_handover_ro.c
>
>
> base-commit: 27be83a12b246b6b3783e7d3ac5c378ff7520f25
> --
> 2.56.0.176.g3575e65682-goog
>
--
Sincerely yours,
Mike.
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH v1 0/8] kho: add read-only Kexec Handover mode
2026-10-19 8:43 ` [PATCH v1 0/8] kho: add read-only Kexec Handover mode Mike Rapoport
@ 2026-10-19 14:23 ` Pasha Tatashin
0 siblings, 0 replies; 11+ messages in thread
From: Pasha Tatashin @ 2026-10-19 14:23 UTC (permalink / raw)
To: Mike Rapoport
Cc: Pasha Tatashin, linux-kselftest, kuba, enelsonmoore, bp, shuah,
david, akpm, linux-mm, ljs, mhocko, skhan, linux-doc, vbabka,
dapeng1.mi, linux-kernel, corbet, surenb, kexec, pratyush,
rdunlap, elver, graf, lirongqing, liam, jlayton
On 10-19 11:43, Mike Rapoport wrote:
> Hi Pasha,
>
> Some brief comments before digging in.
>
> On Sun, Oct 18, 2026 at 10:11:03PM -0400, Pasha Tatashin wrote:
> > This series introduces a read-only Kexec Handover mode, enabled at boot
> > via 'kho_ro=<pa>' pointing to the physical address of the KHO FDT.
> >
> > In standard KHO, the target kernel takes ownership of preserved memory
> > during early boot, initializes struct pages for preserved folios, and
> > reclaims KHO metadata and preserved pages back into the page allocator
> > as they are restored.
> >
> > In some scenarios (such as a caretaker, inspection kernel, firmware), a
> > kernel needs to inspect and consume preserved KHO subtrees, radix trees,
> > and serialized metadata without taking ownership of or modifying the KHO
> > state, leaving the preserved KHO tree intact for a subsequent kernel.
> > In this mode, the KHO physical memory lies outside the kernel's direct
> > map.
> >
> > To support this cleanly without duplicating KHO traversal logic:
> > - Patch 1 exposes the physical address of the outgoing KHO FDT via
> > /sys/kernel/debug/kho/out/fdt_phys so userspace tooling can pass it
> > to a 'kho_ro=<pa>' kernel.
>
> We already pass the physical address, why adding a boolean kho_ro is not
> enough?
In standard KHO, we pass the physical address as part of the normal
kexec_file_load() reboot path. In that flow, the outgoing kernel shuts
down or serializes all devices into LUO state, releases machine control,
and hands over all machine resources to the next kernel, which becomes the
sole "Machine Owner" (MO) and owns all machine resources.
Here, we are using a model where a minimal "Linux Caretaker" (LC) kernel
runs concurrently while the MO is still active. We spin off LC on
dedicated CPUs that have been offlined from the MO scheduler, giving LC
read-only access to KHO so it can temporarily take care of VM while MO
undergoes a standard KHO-preserved kexec reboot (i.e. Live Update). Once
the new MO kernel boots up, it takes back the resources from LC and
retrieves everything normally from LUO/KHO.
LC is never the machine owner: it has no PCI support compiled in, does
not handle device interrupts, cannot handle page faults, etc. It only
exists as a temporary caretaker. LC is booted entirely inside a small
LUO-preserved memfd area (in our use case, LC's RAM is only ~128MB,
while the host machine has terabytes of memory). Because LC's direct map
only covers its own tiny memory footprint (for isolation reasons, and to
save on struct pages), the KHO FDT, radix tree, and preserved VM memory
all reside outside of LC's direct map.
LC is configured and launched directly from userspace while MO is still
running. Therefore, LC does not receive bootloader handoff data from an
outgoing kernel, as MO is not even shutting down yet. Userspace
therefore reads needs PA address of KHO FDT and explicitly passes
'kho_ro=<pa>' on LC's kernel command line.
>
> > - Patch 2 moves the shared radix-tree key encoding and indexing,
> > vmalloc flag conversion, and FDT validation and subtree lookup helpers
> > to kernel/liveupdate/kexec_handover_internal.h so they can be shared
> > between standard and read-only KHO.
> > - Patch 3 hardens kho_restore_vmalloc() by introducing
> > kho_vmalloc_validate() in kernel/liveupdate/kexec_handover_internal.h
> > to validate the preservation metadata header and flags before
> > restoring the vmalloc area.
>
> The diff for kexec_handover_internal.h
>
> > kernel/liveupdate/kexec_handover_internal.h | 171 +++++++-
>
> seems a wee too large. Maybe some of this should move to a .c file?
Agreed. I originally kept them in the header as static inlines because
they are mostly small bit manipulations and helper routines without loops,
but moving them into a dedicated kernel/liveupdate/kho_core.c is OK.
Also, for v2, I am planning to allow CONFIG_KEXEC_HANDOVER and
CONFIG_KEXEC_HANDOVER_READONLY to co-exist in the same build instead of
being mutually exclusive. That way we won't need two different kernels
for vmtests. In that model, common KHO API will use function vectors
dynamiclly decided at early boot depending on which mode is activated
('kho=on' vs 'kho_ro=<pa>').
> > - Patch 4 avoids modifying ser->next and bs->head_pa in kho_block_add()
> > and kho_block_set_shrink_one() when operating on an incoming block
> > set, as the chain is already linked and incoming blocks must not be
> > modified in KHO read-only mode.
> > - Patch 5 adds CONFIG_KEXEC_HANDOVER_READONLY and
> > kernel/liveupdate/kexec_handover_ro.c. When booted with
> > 'kho_ro=<pa>', preserved KHO metadata is mapped read-only
> > (PAGE_KERNEL_RO), with the FDT and radix-tree nodes mapped lazily on
> > first visit and kept cached, providing read-only implementations of
> > kho_retrieve_subtree(), kho_restore_vmalloc(),
> > kho_ro_lookup_folio_order(), kho_ro_map_folio(), and kho_ro_unmap().
>
> What are the expected users of these APIs?
The primary use case is achieving ultra-low disruption Live Update
(<30ms VM blackout time) while running a minimal VMM inside the caretaker
kernel. This VMM keeps VMs responsive by servicing software-only, fully
virtualized devices that do not require host hardware interrupts or global
machine state (such as virtio-console, pvpanic, virtio-rng, etc.).
Thanks,
Pasha
^ permalink raw reply [flat|nested] 11+ messages in thread