* [PATCH v2 1/9] kho: expose physical address of outgoing FDT
2026-10-25 17:40 [PATCH v2 0/9] kho: add read-only Kexec Handover mode Pasha Tatashin
@ 2026-10-25 17:40 ` Pasha Tatashin
2026-10-25 17:56 ` sashiko-bot
2026-10-25 17:40 ` [PATCH v2 2/9] kho: move radix tree and FDT helpers to common file Pasha Tatashin
` (7 subsequent siblings)
8 siblings, 1 reply; 19+ messages in thread
From: Pasha Tatashin @ 2026-10-25 17:40 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.307.g5fe7bd680c-goog
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 1/9] kho: expose physical address of outgoing FDT
2026-10-25 17:40 ` [PATCH v2 1/9] kho: expose physical address of outgoing FDT Pasha Tatashin
@ 2026-10-25 17:56 ` sashiko-bot
0 siblings, 0 replies; 19+ messages in thread
From: sashiko-bot @ 2026-10-25 17:56 UTC (permalink / raw)
To: Pasha Tatashin, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
ljs, mhocko, skhan, vbabka, dapeng1.mi, corbet, surenb, 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>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261025174017.3870481-1-pasha.tatashin@soleen.com?part=1
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 2/9] kho: move radix tree and FDT helpers to common file
2026-10-25 17:40 [PATCH v2 0/9] kho: add read-only Kexec Handover mode Pasha Tatashin
2026-10-25 17:40 ` [PATCH v2 1/9] kho: expose physical address of outgoing FDT Pasha Tatashin
@ 2026-10-25 17:40 ` Pasha Tatashin
2026-10-25 17:56 ` sashiko-bot
2026-10-25 17:40 ` [PATCH v2 3/9] kho: validate preservation metadata in kho_restore_vmalloc() Pasha Tatashin
` (6 subsequent siblings)
8 siblings, 1 reply; 19+ messages in thread
From: Pasha Tatashin @ 2026-10-25 17:40 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
Read-only KHO mode needs to traverse preserved KHO radix trees and FDT
subtrees without duplicating the parsing and key-encoding implementation
in kexec_handover.c.
Move the shared KHO radix tree key encoding and indexing helpers, FDT
validation and lookup helpers, and kho_retrieve_subtree() from
kexec_handover.c into a shared compilation unit
kernel/liveupdate/kexec_handover_common.c with prototypes in
kexec_handover_internal.h, and factor kho_radix_find_leaf() for use by
kho_radix_del_key() and the upcoming read-only KHO backend.
While factoring kho_check_fdt() from kho_populate(), drop the "setup: "
prefix from the FDT validation warnings and verify that
fdt_totalsize(fdt) does not exceed PAGE_SIZE, which is safe for
kho_populate() as both the x86 and device-tree kexec paths allocate and
pass a single-page FDT. Also use get_unaligned() in
kho_retrieve_subtree() when reading 64-bit FDT properties, matching
kho_get_mem_map_phys(), since FDT property values are 4-byte aligned.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
Documentation/core-api/kho/index.rst | 3 +
kernel/liveupdate/Makefile | 2 +-
kernel/liveupdate/kexec_handover.c | 167 ++--------------
kernel/liveupdate/kexec_handover_common.c | 211 ++++++++++++++++++++
kernel/liveupdate/kexec_handover_internal.h | 13 ++
5 files changed, 240 insertions(+), 156 deletions(-)
create mode 100644 kernel/liveupdate/kexec_handover_common.c
diff --git a/Documentation/core-api/kho/index.rst b/Documentation/core-api/kho/index.rst
index e78a2bbe2be1..bce7fe281194 100644
--- a/Documentation/core-api/kho/index.rst
+++ b/Documentation/core-api/kho/index.rst
@@ -83,6 +83,9 @@ Public API
.. kernel-doc:: kernel/liveupdate/kexec_handover.c
:export:
+.. kernel-doc:: kernel/liveupdate/kexec_handover_common.c
+ :export:
+
KHO Serialization Blocks API
============================
diff --git a/kernel/liveupdate/Makefile b/kernel/liveupdate/Makefile
index d9f469462556..f6bed6bcd380 100644
--- a/kernel/liveupdate/Makefile
+++ b/kernel/liveupdate/Makefile
@@ -7,7 +7,7 @@ luo-y := \
luo_flb.o \
luo_session.o
-obj-$(CONFIG_KEXEC_HANDOVER) += kexec_handover.o
+obj-$(CONFIG_KEXEC_HANDOVER) += kexec_handover.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.c b/kernel/liveupdate/kexec_handover.c
index ce5983abf181..c24f25bfc96a 100644
--- a/kernel/liveupdate/kexec_handover.c
+++ b/kernel/liveupdate/kexec_handover.c
@@ -108,73 +108,11 @@ struct kho_in {
static struct kho_in kho_in = {
};
-static const void *kho_get_fdt(void)
+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.
- * @order: An output parameter, a pointer to an unsigned int where the decoded
- * page order will be stored.
- *
- * This function reverses the encoding performed by kho_encode_radix_key(),
- * extracting the original physical address and page order from a given key.
- *
- * Return: The decoded physical address.
- */
-static phys_addr_t kho_decode_radix_key(unsigned long key, unsigned int *order)
-{
- /* fls64() indexes starting from 1. */
- unsigned int order_bit = fls64(key) - 1;
- phys_addr_t phys;
-
- /* order bit goes right before the shifted PFN. */
- *order = 64 - (PAGE_SHIFT + order_bit);
- /* The order bit is discarded by the shift */
- phys = key << (PAGE_SHIFT + *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;
@@ -283,6 +221,11 @@ int kho_radix_add_key(struct kho_radix_tree *tree, unsigned long key)
}
EXPORT_SYMBOL_GPL(kho_radix_add_key);
+static void *kho_phys_to_virt(phys_addr_t pa)
+{
+ return phys_to_virt(pa);
+}
+
/**
* kho_radix_del_key - Removes the key from the radix tree.
* @tree: The KHO radix tree.
@@ -294,9 +237,7 @@ EXPORT_SYMBOL_GPL(kho_radix_add_key);
*/
void kho_radix_del_key(struct kho_radix_tree *tree, unsigned long key)
{
- struct kho_radix_node *node = tree->root;
struct kho_radix_leaf *leaf;
- unsigned int i, idx;
if (WARN_ON_ONCE(!tree->root))
return;
@@ -309,24 +250,12 @@ void kho_radix_del_key(struct kho_radix_tree *tree, unsigned long key)
guard(mutex)(&tree->lock);
- /* Go from high levels to low levels */
- for (i = KHO_TREE_MAX_DEPTH - 1; i > 0; i--) {
- idx = kho_radix_get_table_index(key, i);
-
- /*
- * Attempting to delete a page that has not been preserved,
- * return with a warning.
- */
- if (WARN_ON(!node->table[idx]))
- return;
-
- node = phys_to_virt(node->table[idx]);
- }
+ leaf = kho_radix_find_leaf(virt_to_phys(tree->root), key,
+ kho_phys_to_virt);
+ if (WARN_ON(!leaf))
+ return;
- /* Handle the leaf level bitmap (level 0) */
- leaf = (struct kho_radix_leaf *)node;
- idx = kho_radix_get_bitmap_index(key);
- __clear_bit(idx, leaf->bitmap);
+ __clear_bit(kho_radix_get_bitmap_index(key), leaf->bitmap);
}
EXPORT_SYMBOL_GPL(kho_radix_del_key);
@@ -634,21 +563,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);
@@ -1647,53 +1561,6 @@ bool is_kho_boot(void)
}
EXPORT_SYMBOL_GPL(is_kho_boot);
-/**
- * kho_retrieve_subtree - retrieve a preserved sub blob by its name.
- * @name: the name of the sub blob passed to kho_add_subtree().
- * @phys: if found, the physical address of the sub blob is stored in @phys.
- * @size: if not NULL and found, the size of the sub blob is stored in @size.
- *
- * Retrieve a preserved sub blob named @name and store its physical
- * address in @phys and optionally its size in @size.
- *
- * Return: 0 on success, error code on failure
- */
-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;
-}
-EXPORT_SYMBOL_GPL(kho_retrieve_subtree);
-
static void __init kho_mem_retrieve(void)
{
const struct kho_radix_walk_cb cb = {
@@ -1981,18 +1848,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);
- 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);
+ if (kho_check_fdt(fdt, fdt_phys))
goto unmap_fdt;
- }
mem_map_phys = kho_get_mem_map_phys(fdt);
if (!mem_map_phys)
diff --git a/kernel/liveupdate/kexec_handover_common.c b/kernel/liveupdate/kexec_handover_common.c
new file mode 100644
index 000000000000..f2784f5c523c
--- /dev/null
+++ b/kernel/liveupdate/kexec_handover_common.c
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * kexec_handover_common.c - Shared Kexec Handover helpers
+ *
+ * Copyright (C) 2025 Microsoft Corporation, Mike Rapoport <rppt@kernel.org>
+ * Copyright (c) 2026, Google LLC.
+ * Pasha Tatashin <pasha.tatashin@soleen.com>
+ */
+
+#define pr_fmt(fmt) "KHO: " fmt
+
+#include <linux/bitops.h>
+#include <linux/export.h>
+#include <linux/init.h>
+#include <linux/kexec_handover.h>
+#include <linux/kho/abi/kexec_handover.h>
+#include <linux/libfdt.h>
+#include <linux/mm.h>
+#include <linux/printk.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+
+#include "kexec_handover_internal.h"
+
+/**
+ * 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.
+ */
+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.
+ * @order: An output parameter, a pointer to an unsigned int where the decoded
+ * page order will be stored.
+ *
+ * This function reverses the encoding performed by kho_encode_radix_key(),
+ * extracting the original physical address and page order from a given key.
+ *
+ * Return: The decoded physical address.
+ */
+phys_addr_t kho_decode_radix_key(unsigned long key, unsigned int *order)
+{
+ /* fls64() indexes starting from 1. */
+ unsigned int order_bit = fls64(key) - 1;
+ phys_addr_t phys;
+
+ /* order bit goes right before the shifted PFN. */
+ *order = 64 - (PAGE_SHIFT + order_bit);
+ /* The order bit is discarded by the shift */
+ phys = key << (PAGE_SHIFT + *order);
+
+ return phys;
+}
+
+unsigned long kho_radix_get_bitmap_index(unsigned long key)
+{
+ return key % (1 << KHO_BITMAP_SIZE_LOG2);
+}
+
+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);
+}
+
+/**
+ * kho_radix_find_leaf - Find the radix tree leaf for a given key.
+ * @root_pa: Physical address of the radix tree root node.
+ * @key: The encoded radix key to look up.
+ * @map: Callback to map a physical page address to a virtual pointer.
+ *
+ * Traverses the intermediate radix tree levels from @root_pa using @map to
+ * translate physical node addresses and returns the mapped leaf node for
+ * @key.
+ *
+ * Return: Virtual pointer to the leaf node on success, or NULL if @key is
+ * not present or a mapping fails.
+ */
+struct kho_radix_leaf *kho_radix_find_leaf(phys_addr_t root_pa,
+ unsigned long key,
+ void *(*map)(phys_addr_t pa))
+{
+ phys_addr_t pa = root_pa;
+ unsigned int i, idx;
+
+ if (!pa || unlikely(fls64(key) > KHO_RADIX_KEY_WIDTH))
+ return NULL;
+
+ for (i = KHO_TREE_MAX_DEPTH - 1; i > 0; i--) {
+ const struct kho_radix_node *node = map(pa);
+
+ if (!node)
+ return NULL;
+
+ idx = kho_radix_get_table_index(key, i);
+ pa = (phys_addr_t)READ_ONCE(node->table[idx]);
+ if (!pa)
+ return NULL;
+ }
+
+ return map(pa);
+}
+
+int __init 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;
+ }
+
+ if (fdt_totalsize(fdt) > PAGE_SIZE) {
+ pr_warn("handover FDT (0x%llx) totalsize %u exceeds PAGE_SIZE\n",
+ (unsigned long long)fdt_phys, fdt_totalsize(fdt));
+ return -EINVAL;
+ }
+
+ 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 */
+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);
+}
+
+/**
+ * kho_retrieve_subtree - retrieve a preserved sub blob by its name.
+ * @name: the name of the sub blob passed to kho_add_subtree().
+ * @phys: if found, the physical address of the sub blob is stored in @phys.
+ * @size: if not NULL and found, the size of the sub blob is stored in @size.
+ *
+ * Retrieve a preserved sub blob named @name and store its physical
+ * address in @phys and optionally its size in @size.
+ *
+ * Return: 0 on success, error code on failure
+ */
+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)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;
+}
+EXPORT_SYMBOL_GPL(kho_retrieve_subtree);
diff --git a/kernel/liveupdate/kexec_handover_internal.h b/kernel/liveupdate/kexec_handover_internal.h
index 38181076f603..e73a2e310917 100644
--- a/kernel/liveupdate/kexec_handover_internal.h
+++ b/kernel/liveupdate/kexec_handover_internal.h
@@ -2,10 +2,23 @@
#ifndef LINUX_KEXEC_HANDOVER_INTERNAL_H
#define LINUX_KEXEC_HANDOVER_INTERNAL_H
+#include <linux/init.h>
#include <linux/kexec_handover.h>
+#include <linux/kho/abi/kexec_handover.h>
#include <linux/list.h>
#include <linux/types.h>
+const void *kho_get_fdt(void);
+unsigned long kho_encode_radix_key(phys_addr_t phys, unsigned int order);
+phys_addr_t kho_decode_radix_key(unsigned long key, unsigned int *order);
+unsigned long kho_radix_get_bitmap_index(unsigned long key);
+unsigned long kho_radix_get_table_index(unsigned long key, unsigned int level);
+struct kho_radix_leaf *kho_radix_find_leaf(phys_addr_t root_pa,
+ unsigned long key,
+ void *(*map)(phys_addr_t pa));
+int __init kho_check_fdt(const void *fdt, phys_addr_t fdt_phys);
+phys_addr_t __init kho_get_mem_map_phys(const void *fdt);
+
#ifdef CONFIG_KEXEC_HANDOVER_DEBUGFS
#include <linux/debugfs.h>
--
2.56.0.307.g5fe7bd680c-goog
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 2/9] kho: move radix tree and FDT helpers to common file
2026-10-25 17:40 ` [PATCH v2 2/9] kho: move radix tree and FDT helpers to common file Pasha Tatashin
@ 2026-10-25 17:56 ` sashiko-bot
0 siblings, 0 replies; 19+ messages in thread
From: sashiko-bot @ 2026-10-25 17:56 UTC (permalink / raw)
To: Pasha Tatashin, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
ljs, mhocko, skhan, vbabka, dapeng1.mi, corbet, surenb, kexec,
pratyush, rdunlap, elver, graf, lirongqing, liam, jlayton
> Read-only KHO mode needs to traverse preserved KHO radix trees and FDT
> subtrees without duplicating the parsing and key-encoding implementation
> in kexec_handover.c.
>
> Move the shared KHO radix tree key encoding and indexing helpers, FDT
> validation and lookup helpers, and kho_retrieve_subtree() from
> kexec_handover.c into a shared compilation unit
> kernel/liveupdate/kexec_handover_common.c with prototypes in
> kexec_handover_internal.h, and factor kho_radix_find_leaf() for use by
> kho_radix_del_key() and the upcoming read-only KHO backend.
>
> While factoring kho_check_fdt() from kho_populate(), drop the "setup: "
> prefix from the FDT validation warnings and verify that
> fdt_totalsize(fdt) does not exceed PAGE_SIZE, which is safe for
> kho_populate() as both the x86 and device-tree kexec paths allocate and
> pass a single-page FDT. Also use get_unaligned() in
> kho_retrieve_subtree() when reading 64-bit FDT properties, matching
> kho_get_mem_map_phys(), since FDT property values are 4-byte aligned.
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261025174017.3870481-1-pasha.tatashin@soleen.com?part=2
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 3/9] kho: validate preservation metadata in kho_restore_vmalloc()
2026-10-25 17:40 [PATCH v2 0/9] kho: add read-only Kexec Handover mode Pasha Tatashin
2026-10-25 17:40 ` [PATCH v2 1/9] kho: expose physical address of outgoing FDT Pasha Tatashin
2026-10-25 17:40 ` [PATCH v2 2/9] kho: move radix tree and FDT helpers to common file Pasha Tatashin
@ 2026-10-25 17:40 ` Pasha Tatashin
2026-10-25 17:56 ` sashiko-bot
2026-10-25 17:40 ` [PATCH v2 4/9] kho: do not modify incoming serialized blocks Pasha Tatashin
` (5 subsequent siblings)
8 siblings, 1 reply; 19+ messages in thread
From: Pasha Tatashin @ 2026-10-25 17:40 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, and does
not validate preservation->first.phys, preservation->total_pages, or
preservation->order before allocating the pages array and shifting by
order.
Move kho_flags_to_vmalloc() to kexec_handover_common.c and introduce
kho_vmalloc_validate() and kho_vmalloc_walk() so both regular KHO and
the upcoming read-only KHO mode can share vmalloc header validation and
chunk-list traversal. Converting kho_restore_vmalloc() to
kho_vmalloc_walk() also restores each chunk page before reading its
contents and rejects non-terminal chunks that are not completely full.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
kernel/liveupdate/kexec_handover.c | 99 ++++++++++-----------
kernel/liveupdate/kexec_handover_common.c | 87 ++++++++++++++++++
kernel/liveupdate/kexec_handover_internal.h | 14 +++
3 files changed, 150 insertions(+), 50 deletions(-)
diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c
index c24f25bfc96a..67efbdd39439 100644
--- a/kernel/liveupdate/kexec_handover.c
+++ b/kernel/liveupdate/kexec_handover.c
@@ -1204,10 +1204,6 @@ 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;
@@ -1220,18 +1216,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;
@@ -1357,6 +1341,45 @@ void kho_unpreserve_vmalloc(struct kho_vmalloc *preservation)
}
EXPORT_SYMBOL_GPL(kho_unpreserve_vmalloc);
+struct kho_vmalloc_restore_ctx {
+ struct page **pages;
+ unsigned long contig_pages;
+ unsigned long idx;
+};
+
+static const struct kho_vmalloc_chunk *kho_get_chunk(phys_addr_t pa)
+{
+ struct page *page = kho_restore_pages(pa, 1);
+
+ return page ? page_to_virt(page) : NULL;
+}
+
+static void kho_put_chunk(const struct kho_vmalloc_chunk *chunk)
+{
+ free_page((unsigned long)chunk);
+}
+
+static int kho_vmalloc_restore_entry(phys_addr_t pa, void *priv)
+{
+ struct kho_vmalloc_restore_ctx *ctx = priv;
+ struct page *page;
+
+ page = kho_restore_pages(pa, ctx->contig_pages);
+ if (!page)
+ return -EINVAL;
+
+ for (unsigned long j = 0; j < ctx->contig_pages; j++)
+ ctx->pages[ctx->idx++] = page + j;
+
+ return 0;
+}
+
+static const struct kho_vmalloc_walk_ops kho_vmalloc_restore_ops = {
+ .get_chunk = kho_get_chunk,
+ .put_chunk = kho_put_chunk,
+ .entry = kho_vmalloc_restore_entry,
+};
+
/**
* kho_restore_vmalloc - recreates and populates an area in vmalloc address
* space from the preserved memory.
@@ -1369,18 +1392,16 @@ 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;
+ struct kho_vmalloc_restore_ctx ctx;
+ unsigned long total_pages, align;
+ unsigned int order, shift, vm_flags;
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;
total_pages = preservation->total_pages;
@@ -1388,37 +1409,15 @@ void *kho_restore_vmalloc(const struct kho_vmalloc *preservation)
if (!pages)
return NULL;
order = preservation->order;
- contig_pages = (1 << order);
shift = PAGE_SHIFT + order;
- align = 1 << shift;
-
- while (chunk) {
- struct page *page;
+ align = 1UL << shift;
- for (int i = 0; i < ARRAY_SIZE(chunk->phys) && chunk->phys[i]; i++) {
- phys_addr_t phys = chunk->phys[i];
-
- if (idx + contig_pages > total_pages)
- goto err_free_pages_array;
-
- page = kho_restore_pages(phys, contig_pages);
- if (!page)
- goto err_free_pages_array;
-
- for (int j = 0; j < contig_pages; j++)
- pages[idx++] = page + j;
-
- phys += contig_pages * PAGE_SIZE;
- }
-
- page = kho_restore_pages(virt_to_phys(chunk), 1);
- if (!page)
- goto err_free_pages_array;
- chunk = KHOSER_LOAD_PTR(chunk->hdr.next);
- __free_page(page);
- }
+ ctx = (struct kho_vmalloc_restore_ctx){
+ .pages = pages,
+ .contig_pages = 1UL << order,
+ };
- if (idx != total_pages)
+ if (kho_vmalloc_walk(preservation, &kho_vmalloc_restore_ops, &ctx))
goto err_free_pages_array;
area = __get_vm_area_node(total_pages * PAGE_SIZE, align, shift,
diff --git a/kernel/liveupdate/kexec_handover_common.c b/kernel/liveupdate/kexec_handover_common.c
index f2784f5c523c..dc25cb4c08af 100644
--- a/kernel/liveupdate/kexec_handover_common.c
+++ b/kernel/liveupdate/kexec_handover_common.c
@@ -19,9 +19,96 @@
#include <linux/printk.h>
#include <linux/types.h>
#include <linux/unaligned.h>
+#include <linux/vmalloc.h>
#include "kexec_handover_internal.h"
+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;
+}
+
+bool kho_vmalloc_validate(const struct kho_vmalloc *preservation,
+ unsigned int *vm_flags)
+{
+ 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 (preservation->flags & ~(KHO_VMALLOC_ALLOC | KHO_VMALLOC_HUGE_VMAP))
+ return false;
+
+ *vm_flags = kho_flags_to_vmalloc(preservation->flags);
+ return true;
+}
+
+/**
+ * kho_vmalloc_walk - Walk the chunk list of a preserved vmalloc region.
+ * @preservation: Validated KHO vmalloc preservation metadata.
+ * @ops: Callbacks to acquire/release chunks and process each entry.
+ * @priv: Caller context passed to @ops->entry.
+ *
+ * Traverses the linked list of struct kho_vmalloc_chunk pages starting from
+ * @preservation->first.phys, invoking @ops->entry for each contiguous physical
+ * range and verifying that non-terminal chunks are full and that the total
+ * number of pages matches @preservation->total_pages.
+ *
+ * Return: 0 on success, or a negative errno on failure.
+ */
+int kho_vmalloc_walk(const struct kho_vmalloc *preservation,
+ const struct kho_vmalloc_walk_ops *ops, void *priv)
+{
+ unsigned long contig_pages = 1UL << preservation->order;
+ unsigned long total_pages = preservation->total_pages;
+ unsigned long nr_pages = 0;
+ phys_addr_t chunk_pa;
+
+ for (chunk_pa = preservation->first.phys; chunk_pa; ) {
+ const struct kho_vmalloc_chunk *chunk;
+ phys_addr_t next_pa;
+ unsigned int i;
+
+ chunk = ops->get_chunk(chunk_pa);
+ if (!chunk)
+ return -EINVAL;
+
+ for (i = 0; i < ARRAY_SIZE(chunk->phys) && chunk->phys[i]; i++) {
+ if (nr_pages + contig_pages > total_pages ||
+ ops->entry(chunk->phys[i], priv))
+ goto err_put_chunk;
+
+ nr_pages += contig_pages;
+ }
+
+ next_pa = chunk->hdr.next.phys;
+ if (next_pa && i < ARRAY_SIZE(chunk->phys))
+ goto err_put_chunk;
+
+ ops->put_chunk(chunk);
+ chunk_pa = next_pa;
+ continue;
+
+err_put_chunk:
+ ops->put_chunk(chunk);
+ return -EINVAL;
+ }
+
+ return nr_pages == total_pages ? 0 : -EINVAL;
+}
+
/**
* kho_encode_radix_key - Encodes a physical address and order into a radix key.
* @phys: The physical address of the page.
diff --git a/kernel/liveupdate/kexec_handover_internal.h b/kernel/liveupdate/kexec_handover_internal.h
index e73a2e310917..dd79e5006562 100644
--- a/kernel/liveupdate/kexec_handover_internal.h
+++ b/kernel/liveupdate/kexec_handover_internal.h
@@ -8,7 +8,21 @@
#include <linux/list.h>
#include <linux/types.h>
+/* KHO internal flags for vmalloc preservations */
+#define KHO_VMALLOC_ALLOC 0x0001
+#define KHO_VMALLOC_HUGE_VMAP 0x0002
+
+struct kho_vmalloc_walk_ops {
+ const struct kho_vmalloc_chunk *(*get_chunk)(phys_addr_t pa);
+ void (*put_chunk)(const struct kho_vmalloc_chunk *chunk);
+ int (*entry)(phys_addr_t pa, void *priv);
+};
+
const void *kho_get_fdt(void);
+bool kho_vmalloc_validate(const struct kho_vmalloc *preservation,
+ unsigned int *vm_flags);
+int kho_vmalloc_walk(const struct kho_vmalloc *preservation,
+ const struct kho_vmalloc_walk_ops *ops, void *priv);
unsigned long kho_encode_radix_key(phys_addr_t phys, unsigned int order);
phys_addr_t kho_decode_radix_key(unsigned long key, unsigned int *order);
unsigned long kho_radix_get_bitmap_index(unsigned long key);
--
2.56.0.307.g5fe7bd680c-goog
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 3/9] kho: validate preservation metadata in kho_restore_vmalloc()
2026-10-25 17:40 ` [PATCH v2 3/9] kho: validate preservation metadata in kho_restore_vmalloc() Pasha Tatashin
@ 2026-10-25 17:56 ` sashiko-bot
0 siblings, 0 replies; 19+ messages in thread
From: sashiko-bot @ 2026-10-25 17:56 UTC (permalink / raw)
To: Pasha Tatashin, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
ljs, mhocko, skhan, vbabka, dapeng1.mi, corbet, surenb, 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, and does
> not validate preservation->first.phys, preservation->total_pages, or
> preservation->order before allocating the pages array and shifting by
> order.
>
> Move kho_flags_to_vmalloc() to kexec_handover_common.c and introduce
> kho_vmalloc_validate() and kho_vmalloc_walk() so both regular KHO and
> the upcoming read-only KHO mode can share vmalloc header validation and
> chunk-list traversal. Converting kho_restore_vmalloc() to
> kho_vmalloc_walk() also restores each chunk page before reading its
> contents and rejects non-terminal chunks that are not completely full.
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261025174017.3870481-1-pasha.tatashin@soleen.com?part=3
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 4/9] kho: do not modify incoming serialized blocks
2026-10-25 17:40 [PATCH v2 0/9] kho: add read-only Kexec Handover mode Pasha Tatashin
` (2 preceding siblings ...)
2026-10-25 17:40 ` [PATCH v2 3/9] kho: validate preservation metadata in kho_restore_vmalloc() Pasha Tatashin
@ 2026-10-25 17:40 ` Pasha Tatashin
2026-10-25 17:56 ` sashiko-bot
2026-10-25 17:40 ` [PATCH v2 5/9] kho: add read-only Kexec Handover mode Pasha Tatashin
` (4 subsequent siblings)
8 siblings, 1 reply; 19+ messages in thread
From: Pasha Tatashin @ 2026-10-25 17:40 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.307.g5fe7bd680c-goog
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 4/9] kho: do not modify incoming serialized blocks
2026-10-25 17:40 ` [PATCH v2 4/9] kho: do not modify incoming serialized blocks Pasha Tatashin
@ 2026-10-25 17:56 ` sashiko-bot
0 siblings, 0 replies; 19+ messages in thread
From: sashiko-bot @ 2026-10-25 17:56 UTC (permalink / raw)
To: Pasha Tatashin, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
ljs, mhocko, skhan, vbabka, dapeng1.mi, corbet, surenb, 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>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261025174017.3870481-1-pasha.tatashin@soleen.com?part=4
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 5/9] kho: add read-only Kexec Handover mode
2026-10-25 17:40 [PATCH v2 0/9] kho: add read-only Kexec Handover mode Pasha Tatashin
` (3 preceding siblings ...)
2026-10-25 17:40 ` [PATCH v2 4/9] kho: do not modify incoming serialized blocks Pasha Tatashin
@ 2026-10-25 17:40 ` Pasha Tatashin
2026-10-25 17:57 ` sashiko-bot
2026-10-25 17:40 ` [PATCH v2 6/9] kho: support read-only KHO mode in KHO core Pasha Tatashin
` (3 subsequent siblings)
8 siblings, 1 reply; 19+ messages in thread
From: Pasha Tatashin @ 2026-10-25 17:40 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
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@soleen.com>
---
.../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@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 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
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 5/9] kho: add read-only Kexec Handover mode
2026-10-25 17:40 ` [PATCH v2 5/9] kho: add read-only Kexec Handover mode Pasha Tatashin
@ 2026-10-25 17:57 ` sashiko-bot
0 siblings, 0 replies; 19+ messages in thread
From: sashiko-bot @ 2026-10-25 17:57 UTC (permalink / raw)
To: Pasha Tatashin, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
ljs, mhocko, skhan, vbabka, dapeng1.mi, corbet, surenb, kexec,
pratyush, rdunlap, elver, graf, lirongqing, liam, jlayton
> 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@soleen.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261025174017.3870481-1-pasha.tatashin@soleen.com?part=5
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 6/9] kho: support read-only KHO mode in KHO core
2026-10-25 17:40 [PATCH v2 0/9] kho: add read-only Kexec Handover mode Pasha Tatashin
` (4 preceding siblings ...)
2026-10-25 17:40 ` [PATCH v2 5/9] kho: add read-only Kexec Handover mode Pasha Tatashin
@ 2026-10-25 17:40 ` Pasha Tatashin
2026-10-25 17:57 ` sashiko-bot
2026-10-25 17:40 ` [PATCH v2 7/9] kho: support read-only KHO mode in kho_block Pasha Tatashin
` (2 subsequent siblings)
8 siblings, 1 reply; 19+ messages in thread
From: Pasha Tatashin @ 2026-10-25 17:40 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 booted with 'kho_ro=<pa>', skip standard KHO bootmem and FDT
ingestion in kho_populate() before early_param parsing runs, disable
standard KHO in kho_memory_init() once early parameters are parsed, and
use kho_ro_get_fdt() in kho_retrieve_subtree().
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
kernel/liveupdate/kexec_handover.c | 8 ++++++++
kernel/liveupdate/kexec_handover_common.c | 2 +-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c
index 67efbdd39439..cc56caeecba7 100644
--- a/kernel/liveupdate/kexec_handover.c
+++ b/kernel/liveupdate/kexec_handover.c
@@ -1819,6 +1819,11 @@ void __init kho_memory_init_early(void)
void __init kho_memory_init(void)
{
+ if (kho_is_ro()) {
+ kho_enable = false;
+ return;
+ }
+
if (is_kdump_kernel()) {
kho_enable = false;
pr_info("disabled in the kdump kernel\n");
@@ -1841,6 +1846,9 @@ void __init kho_populate(phys_addr_t fdt_phys, u64 fdt_len,
bool populated = false;
int err;
+ if (kho_ro_cmdline_enabled())
+ return;
+
/* Validate the input FDT */
fdt = early_memremap(fdt_phys, fdt_len);
if (!fdt) {
diff --git a/kernel/liveupdate/kexec_handover_common.c b/kernel/liveupdate/kexec_handover_common.c
index dc25cb4c08af..a9bc8d45d3ba 100644
--- a/kernel/liveupdate/kexec_handover_common.c
+++ b/kernel/liveupdate/kexec_handover_common.c
@@ -263,7 +263,7 @@ phys_addr_t __init kho_get_mem_map_phys(const void *fdt)
*/
int kho_retrieve_subtree(const char *name, phys_addr_t *phys, size_t *size)
{
- const void *fdt = kho_get_fdt();
+ const void *fdt = kho_is_ro() ? kho_ro_get_fdt() : kho_get_fdt();
const u64 *val;
int offset, len;
--
2.56.0.307.g5fe7bd680c-goog
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 6/9] kho: support read-only KHO mode in KHO core
2026-10-25 17:40 ` [PATCH v2 6/9] kho: support read-only KHO mode in KHO core Pasha Tatashin
@ 2026-10-25 17:57 ` sashiko-bot
0 siblings, 0 replies; 19+ messages in thread
From: sashiko-bot @ 2026-10-25 17:57 UTC (permalink / raw)
To: Pasha Tatashin, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
ljs, mhocko, skhan, vbabka, dapeng1.mi, corbet, surenb, kexec,
pratyush, rdunlap, elver, graf, lirongqing, liam, jlayton
> When booted with 'kho_ro=<pa>', skip standard KHO bootmem and FDT
> ingestion in kho_populate() before early_param parsing runs, disable
> standard KHO in kho_memory_init() once early parameters are parsed, and
> use kho_ro_get_fdt() in kho_retrieve_subtree().
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261025174017.3870481-1-pasha.tatashin@soleen.com?part=6
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 7/9] kho: support read-only KHO mode in kho_block
2026-10-25 17:40 [PATCH v2 0/9] kho: add read-only Kexec Handover mode Pasha Tatashin
` (5 preceding siblings ...)
2026-10-25 17:40 ` [PATCH v2 6/9] kho: support read-only KHO mode in KHO core Pasha Tatashin
@ 2026-10-25 17:40 ` Pasha Tatashin
2026-10-25 17:57 ` sashiko-bot
2026-10-25 17:40 ` [PATCH v2 8/9] kho: test: support read-only KHO mode in test_kho Pasha Tatashin
2026-10-25 17:40 ` [PATCH v2 9/9] selftests: kho: add read-only KHO test to vmtest.sh Pasha Tatashin
8 siblings, 1 reply; 19+ messages in thread
From: Pasha Tatashin @ 2026-10-25 17:40 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 skip 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/kho_block.c | 62 ++++++++++++++++++++++++++---------
1 file changed, 46 insertions(+), 16 deletions(-)
diff --git a/kernel/liveupdate/kho_block.c b/kernel/liveupdate/kho_block.c
index a4e945f28fad..5b08bd8a9623 100644
--- a/kernel/liveupdate/kho_block.c
+++ b/kernel/liveupdate/kho_block.c
@@ -60,10 +60,24 @@ 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);
+}
+
/* Free serialized data */
static void kho_block_free_ser(struct kho_block_set *bs,
struct kho_block_header_ser *ser)
{
+ if (kho_is_ro())
+ return;
+
if (bs->incoming)
kho_restore_free(ser);
else
@@ -197,6 +211,19 @@ 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;
+
+ 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 +232,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,7 +289,11 @@ 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 = -EINVAL;
+ goto err_destroy;
+ }
if (!ser->count || ser->count > bs->count_per_block) {
pr_warn("Block contains invalid entry count: %llu\n",
ser->count);
@@ -283,11 +310,14 @@ int kho_block_set_restore(struct kho_block_set *bs, u64 head_pa)
err_destroy:
kho_block_set_destroy(bs);
+ if (kho_is_ro())
+ return err;
/* 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);
+ 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.307.g5fe7bd680c-goog
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 7/9] kho: support read-only KHO mode in kho_block
2026-10-25 17:40 ` [PATCH v2 7/9] kho: support read-only KHO mode in kho_block Pasha Tatashin
@ 2026-10-25 17:57 ` sashiko-bot
0 siblings, 0 replies; 19+ messages in thread
From: sashiko-bot @ 2026-10-25 17:57 UTC (permalink / raw)
To: Pasha Tatashin, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
ljs, mhocko, skhan, vbabka, dapeng1.mi, corbet, surenb, 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 skip calling
> kho_restore_free() when destroying or shrinking a block set in read-only
> mode.
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261025174017.3870481-1-pasha.tatashin@soleen.com?part=7
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 8/9] kho: test: support read-only KHO mode in test_kho
2026-10-25 17:40 [PATCH v2 0/9] kho: add read-only Kexec Handover mode Pasha Tatashin
` (6 preceding siblings ...)
2026-10-25 17:40 ` [PATCH v2 7/9] kho: support read-only KHO mode in kho_block Pasha Tatashin
@ 2026-10-25 17:40 ` Pasha Tatashin
2026-10-25 17:57 ` sashiko-bot
2026-10-25 17:40 ` [PATCH v2 9/9] selftests: kho: add read-only KHO test to vmtest.sh Pasha Tatashin
8 siblings, 1 reply; 19+ messages in thread
From: Pasha Tatashin @ 2026-10-25 17:40 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
test_kho currently depends on CONFIG_KEXEC_HANDOVER and restores
preserved folios via phys_to_virt() and kho_restore_folio(), which
prevents testing preserved KHO state when booted in read-only KHO mode.
Allow CONFIG_TEST_KEXEC_HANDOVER to be enabled with
CONFIG_KEXEC_HANDOVER_READONLY and update test_kho to verify preserved
state when kho_is_ro() is active. In read-only mode, retrieve the test
FDT subtree, map the FDT folio via kho_ro_map_folio(...,
PAGE_KERNEL_RO), map the preserved vmalloc folio descriptor array via
kho_ro_map_vmalloc(), verify each folio's preserved order in the KHO
radix tree via kho_ro_lookup_folio_order(), map each folio via
kho_ro_map_folio(), and verify the checksum over all preserved pages.
Also guard kho_test_exit() when no outgoing test FDT was allocated.
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
---
lib/Kconfig.debug | 2 +-
lib/test_kho.c | 71 ++++++++++++++++++++++++++++++++++-------------
2 files changed, 53 insertions(+), 20 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..29029dcf5815 100644
--- a/lib/test_kho.c
+++ b/lib/test_kho.c
@@ -236,10 +236,10 @@ static int kho_test_restore_data(const void *fdt, int node)
{
const struct kho_vmalloc *folios_info_phys;
const unsigned int *nr_folios;
- phys_addr_t *folios_info;
+ const phys_addr_t *folios_info;
const __wsum *old_csum;
__wsum csum = 0;
- int len;
+ int len, i;
node = fdt_path_offset(fdt, "/data");
@@ -255,15 +255,34 @@ static int kho_test_restore_data(const void *fdt, int node)
if (!folios_info_phys || len != sizeof(*folios_info_phys))
return -EINVAL;
- folios_info = kho_restore_vmalloc(folios_info_phys);
+ if (kho_is_ro())
+ folios_info = kho_ro_map_vmalloc(folios_info_phys);
+ else
+ folios_info = kho_restore_vmalloc(folios_info_phys);
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);
+ continue;
+ }
folio = kho_restore_folio(phys);
if (!folio)
@@ -276,19 +295,33 @@ static int kho_test_restore_data(const void *fdt, int node)
folio_put(folio);
}
- vfree(folios_info);
+ if (!kho_is_ro())
+ 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;
+ 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)
@@ -304,24 +337,21 @@ static int kho_test_restore(phys_addr_t fdt_phys)
if (*magic != KHO_TEST_MAGIC)
return -EINVAL;
- err = kho_test_restore_data(fdt, node);
- if (err)
- return err;
-
- return 0;
+ return kho_test_restore_data(fdt, node);
}
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 +360,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;
}
@@ -355,6 +385,9 @@ static void kho_test_cleanup(void)
static void __exit kho_test_exit(void)
{
+ if (!kho_test_state.fdt)
+ return;
+
kho_remove_subtree(folio_address(kho_test_state.fdt));
kho_test_cleanup();
}
--
2.56.0.307.g5fe7bd680c-goog
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 8/9] kho: test: support read-only KHO mode in test_kho
2026-10-25 17:40 ` [PATCH v2 8/9] kho: test: support read-only KHO mode in test_kho Pasha Tatashin
@ 2026-10-25 17:57 ` sashiko-bot
0 siblings, 0 replies; 19+ messages in thread
From: sashiko-bot @ 2026-10-25 17:57 UTC (permalink / raw)
To: Pasha Tatashin, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
ljs, mhocko, skhan, vbabka, dapeng1.mi, corbet, surenb, kexec,
pratyush, rdunlap, elver, graf, lirongqing, liam, jlayton
> test_kho currently depends on CONFIG_KEXEC_HANDOVER and restores
> preserved folios via phys_to_virt() and kho_restore_folio(), which
> prevents testing preserved KHO state when booted in read-only KHO mode.
>
> Allow CONFIG_TEST_KEXEC_HANDOVER to be enabled with
> CONFIG_KEXEC_HANDOVER_READONLY and update test_kho to verify preserved
> state when kho_is_ro() is active. In read-only mode, retrieve the test
> FDT subtree, map the FDT folio via kho_ro_map_folio(...,
> PAGE_KERNEL_RO), map the preserved vmalloc folio descriptor array via
> kho_ro_map_vmalloc(), verify each folio's preserved order in the KHO
> radix tree via kho_ro_lookup_folio_order(), map each folio via
> kho_ro_map_folio(), and verify the checksum over all preserved pages.
> Also guard kho_test_exit() when no outgoing test FDT was allocated.
>
> Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261025174017.3870481-1-pasha.tatashin@soleen.com?part=8
^ permalink raw reply [flat|nested] 19+ messages in thread
* [PATCH v2 9/9] selftests: kho: add read-only KHO test to vmtest.sh
2026-10-25 17:40 [PATCH v2 0/9] kho: add read-only Kexec Handover mode Pasha Tatashin
` (7 preceding siblings ...)
2026-10-25 17:40 ` [PATCH v2 8/9] kho: test: support read-only KHO mode in test_kho Pasha Tatashin
@ 2026-10-25 17:40 ` Pasha Tatashin
2026-10-25 17:57 ` sashiko-bot
8 siblings, 1 reply; 19+ messages in thread
From: Pasha Tatashin @ 2026-10-25 17:40 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
vmtest.sh currently only tests standard KHO restoration and does not
exercise read-only KHO mode ('kho_ro=<fdt_phys>') in QEMU.
Extend tools/testing/selftests/kho/vmtest.sh and init.c to enable both
CONFIG_KEXEC_HANDOVER=y and CONFIG_KEXEC_HANDOVER_READONLY=y in the
kernel configuration and run a second QEMU test verifying read-only KHO
after the standard KHO test on both x86_64 and aarch64:
- Enable CONFIG_KEXEC_HANDOVER_READONLY=y alongside
CONFIG_KEXEC_HANDOVER=y in build_kernel().
- Pass KHO_RO_CMDLINE ('nokaslr kho_bootmem=64M,64M,64M' on x86_64) to
the first kernel in the read-only test run and configure 4G of RAM on
aarch64 so GFP_KERNEL KHO allocations are placed in ZONE_NORMAL above
4G, outside the read-only kernel's System RAM.
- When booted with 'kho_ro_test', read /debugfs/kho/out/fdt_phys in
init.c and kexec into the target kernel with 'kho_ro=<fdt_phys>' and
architecture-specific memory parameters
('memmap=792M$16M memmap=24M$1000M' on x86_64, 'mem=128M' 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 | 1 +
tools/testing/selftests/kho/init.c | 38 ++++++++++++++-
tools/testing/selftests/kho/vmtest.sh | 65 ++++++++++++++++----------
tools/testing/selftests/kho/x86.conf | 1 +
4 files changed, 79 insertions(+), 26 deletions(-)
diff --git a/tools/testing/selftests/kho/arm64.conf b/tools/testing/selftests/kho/arm64.conf
index ee696807cd35..202f92121695 100644
--- a/tools/testing/selftests/kho/arm64.conf
+++ b/tools/testing/selftests/kho/arm64.conf
@@ -1,4 +1,5 @@
QEMU_CMD="qemu-system-aarch64 -M virt -cpu max"
+QEMU_MEM="4G"
QEMU_KCONFIG="
CONFIG_SERIAL_AMBA_PL010=y
CONFIG_SERIAL_AMBA_PL010_CONSOLE=y
diff --git a/tools/testing/selftests/kho/init.c b/tools/testing/selftests/kho/init.c
index 88a41b6eba95..e2ebcf7ab307 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"
+#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..ca59a58ae2fb 100755
--- a/tools/testing/selftests/kho/vmtest.sh
+++ b/tools/testing/selftests/kho/vmtest.sh
@@ -35,17 +35,12 @@ 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
@@ -55,10 +50,11 @@ function build_kernel() {
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
+ # enable initrd, KHO, read-only KHO, and KHO test in kernel configuration
+ cat > "$kho_config" <<EOF
CONFIG_BLK_DEV_INITRD=y
CONFIG_KEXEC_HANDOVER=y
+CONFIG_KEXEC_HANDOVER_READONLY=y
CONFIG_KEXEC_HANDOVER_DEBUG=y
CONFIG_KEXEC_HANDOVER_DEBUGFS=y
CONFIG_TEST_KEXEC_HANDOVER=y
@@ -71,11 +67,25 @@ $arch_kconfig
EOF
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
+ # verify that kernel configuration 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"
@@ -107,18 +117,23 @@ function run_qemu() {
local qemu_cmd=$1
local cmdline=$2
local kernel=$3
+ local test_name=$4
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 "${QEMU_MEM:-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; then
+ ktap_test_pass "$test_name succeeded"
+ else
+ ktap_test_fail "$test_name failed"
+ fi
}
function target_to_arch() {
@@ -164,7 +179,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"
@@ -182,9 +197,11 @@ function main() {
local kernel="$build_dir/arch/$arch/boot/$KERNEL_IMAGE"
mkinitrd "$kernel"
- run_qemu "$QEMU_CMD" "$KERNEL_CMDLINE" "$kernel"
+ trap - ERR
- ktap_test_pass "KHO succeeded"
+ run_qemu "$QEMU_CMD" "$KERNEL_CMDLINE" "$kernel" "KHO"
+ run_qemu "$QEMU_CMD" "$KERNEL_CMDLINE ${KHO_RO_CMDLINE:-} kho_ro_test" \
+ "$kernel" "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.307.g5fe7bd680c-goog
^ permalink raw reply related [flat|nested] 19+ messages in thread* Re: [PATCH v2 9/9] selftests: kho: add read-only KHO test to vmtest.sh
2026-10-25 17:40 ` [PATCH v2 9/9] selftests: kho: add read-only KHO test to vmtest.sh Pasha Tatashin
@ 2026-10-25 17:57 ` sashiko-bot
0 siblings, 0 replies; 19+ messages in thread
From: sashiko-bot @ 2026-10-25 17:57 UTC (permalink / raw)
To: Pasha Tatashin, rppt, kuba, enelsonmoore, bp, shuah, david, akpm,
ljs, mhocko, skhan, vbabka, dapeng1.mi, corbet, surenb, kexec,
pratyush, rdunlap, elver, graf, lirongqing, liam, jlayton
> vmtest.sh currently only tests standard KHO restoration and does not
> exercise read-only KHO mode ('kho_ro=<fdt_phys>') in QEMU.
>
> Extend tools/testing/selftests/kho/vmtest.sh and init.c to enable both
> CONFIG_KEXEC_HANDOVER=y and CONFIG_KEXEC_HANDOVER_READONLY=y in the
> kernel configuration and run a second QEMU test verifying read-only KHO
> after the standard KHO test on both x86_64 and aarch64:
>
> - Enable CONFIG_KEXEC_HANDOVER_READONLY=y alongside
> CONFIG_KEXEC_HANDOVER=y in build_kernel().
> - Pass KHO_RO_CMDLINE ('nokaslr kho_bootmem=64M,64M,64M' on x86_64) to
> the first kernel in the read-only test run and configure 4G of RAM on
> aarch64 so GFP_KERNEL KHO allocations are placed in ZONE_NORMAL above
> 4G, outside the read-only kernel's System RAM.
> - When booted with 'kho_ro_test', read /debugfs/kho/out/fdt_phys in
> init.c and kexec into the target kernel with 'kho_ro=<fdt_phys>' and
> architecture-specific memory parameters
> ('memmap=792M$16M memmap=24M$1000M' on x86_64, 'mem=128M' 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>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261025174017.3870481-1-pasha.tatashin@soleen.com?part=9
^ permalink raw reply [flat|nested] 19+ messages in thread