* [RFC PATCH 1/4] kho: Move subtree blob logic to separate function
2026-07-31 21:52 [RFC PATCH 0/2] Add support for multiple versions to KHO Logan Odell
@ 2026-07-31 21:52 ` Logan Odell
2026-07-31 21:52 ` [RFC PATCH 2/4] kho: Add support for multiple versions in subtrees Logan Odell
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Logan Odell @ 2026-07-31 21:52 UTC (permalink / raw)
To: graf, rppt, pasha.tatashin
Cc: pratyush, akpm, kexec, linux-mm, linux-kernel, Logan Odell
Move the logic to set the blob data in a subtree node into a separate
function to allow for re-use. No functional change.
Signed-off-by: Logan Odell <loganodell@google.com>
---
kernel/liveupdate/kexec_handover.c | 23 ++++++++++++++++-------
1 file changed, 16 insertions(+), 7 deletions(-)
diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c
index cc68a3692905..29a05cec2625 100644
--- a/kernel/liveupdate/kexec_handover.c
+++ b/kernel/liveupdate/kexec_handover.c
@@ -722,6 +722,21 @@ static void __init kho_reserve_scratch(void)
kho_enable = false;
}
+static int kho_add_subtree_node(const char *name, void *root_fdt, void *fdt, int off)
+{
+ int err;
+ phys_addr_t phys = virt_to_phys(fdt);
+
+ err = fdt_setprop(root_fdt, off, KHO_FDT_SUB_TREE_PROP_NAME,
+ &phys, sizeof(phys));
+ if (err)
+ return err;
+
+ WARN_ON_ONCE(kho_debugfs_fdt_add(&kho_out.dbg, name, fdt, false));
+
+ return 0;
+}
+
/**
* kho_add_subtree - record the physical address of a sub FDT in KHO root tree.
* @name: name of the sub tree.
@@ -739,7 +754,6 @@ static void __init kho_reserve_scratch(void)
*/
int kho_add_subtree(const char *name, void *fdt)
{
- phys_addr_t phys = virt_to_phys(fdt);
void *root_fdt = kho_out.fdt;
int err = -ENOMEM;
int off, fdt_err;
@@ -757,12 +771,7 @@ int kho_add_subtree(const char *name, void *fdt)
goto out_pack;
}
- err = fdt_setprop(root_fdt, off, KHO_FDT_SUB_TREE_PROP_NAME,
- &phys, sizeof(phys));
- if (err < 0)
- goto out_pack;
-
- WARN_ON_ONCE(kho_debugfs_fdt_add(&kho_out.dbg, name, fdt, false));
+ err = kho_add_subtree_node(name, root_fdt, fdt, off);
out_pack:
fdt_pack(root_fdt);
--
2.55.0.508.g3f0d502094-goog
^ permalink raw reply related [flat|nested] 6+ messages in thread* [RFC PATCH 2/4] kho: Add support for multiple versions in subtrees
2026-07-31 21:52 [RFC PATCH 0/2] Add support for multiple versions to KHO Logan Odell
2026-07-31 21:52 ` [RFC PATCH 1/4] kho: Move subtree blob logic to separate function Logan Odell
@ 2026-07-31 21:52 ` Logan Odell
2026-07-31 21:52 ` [RFC PATCH 3/4] kho: Add support for multiple versions to debugfs Logan Odell
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Logan Odell @ 2026-07-31 21:52 UTC (permalink / raw)
To: graf, rppt, pasha.tatashin
Cc: pratyush, akpm, kexec, linux-mm, linux-kernel, Logan Odell
Add explicit version support to KHO by creating subnodes with
the version number. The first caller to kho_add_subtree_version will
create the parent node and versioned subnode. Subsequent calls with a
different version will add more subnodes with the version as the name.
Remove is updated to handle cleanup when the final version is removed.
Example Tree:
/ (Root Node)
├── compatible = "kho-v1"
├── preserved-memory-map = <0xphys_map>
├── some_legacy_node (Legacy Node)
│ ├── preserved-data = <0xphys_legacy>
│ └── blob-size = <0xsize_legacy>
└── LUO (Versioned Parent Node)
├── 1 (Version 1 Node)
│ ├── preserved-data = <0xphys_luo_v1>
│ └── blob-size = <0xsize_luo_v1>
└── 2 (Version 2 Node)
├── preserved-data = <0xphys_luo_v2>
└── blob-size = <0xsize_luo_v2>
Signed-off-by: Logan Odell <loganodell@google.com>
---
include/linux/kexec_handover.h | 14 ++
include/linux/kho/abi/kexec_handover.h | 16 ++
kernel/liveupdate/kexec_handover.c | 215 ++++++++++++++++++++++++-
3 files changed, 237 insertions(+), 8 deletions(-)
diff --git a/include/linux/kexec_handover.h b/include/linux/kexec_handover.h
index ac4129d1d741..1d47ab76b02b 100644
--- a/include/linux/kexec_handover.h
+++ b/include/linux/kexec_handover.h
@@ -33,8 +33,11 @@ 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 *fdt);
+int kho_add_subtree_version(const char *name, int version, void *fdt);
void kho_remove_subtree(void *fdt);
+void kho_remove_subtree_version(const char *name, int version, void *fdt);
int kho_retrieve_subtree(const char *name, phys_addr_t *phys);
+int kho_retrieve_subtree_version(const char *name, int version, phys_addr_t *phys);
void kho_memory_init(void);
@@ -102,13 +105,24 @@ static inline int kho_add_subtree(const char *name, void *fdt)
return -EOPNOTSUPP;
}
+static inline int kho_add_subtree_version(const char *name, int version, void *fdt)
+{
+ return -EOPNOTSUPP;
+}
+
static inline void kho_remove_subtree(void *fdt) { }
+static inline void kho_remove_subtree_version(const char *name, int version, void *fdt) { }
static inline int kho_retrieve_subtree(const char *name, phys_addr_t *phys)
{
return -EOPNOTSUPP;
}
+static inline int kho_retrieve_subtree_version(const char *name, int version, phys_addr_t *phys)
+{
+ return -EOPNOTSUPP;
+}
+
static inline void kho_memory_init(void) { }
static inline void kho_populate(phys_addr_t fdt_phys, u64 fdt_len,
diff --git a/include/linux/kho/abi/kexec_handover.h b/include/linux/kho/abi/kexec_handover.h
index 2201a0d2c159..55acb73b1d27 100644
--- a/include/linux/kho/abi/kexec_handover.h
+++ b/include/linux/kho/abi/kexec_handover.h
@@ -73,6 +73,22 @@
*
* Physical address pointing to a subnode FDT blob that is also
* being preserved.
+ *
+ * Versioned Subnodes (<subnode-name-N>):
+ * Alternatively, a subnode can group multiple versions of the same
+ * data. In this case, the subnode <subnode-name-N> does not contain
+ * an 'fdt' property directly. Instead, it contains child nodes named
+ * after the version number (e.g., '1', '2'), which in turn contain
+ * the 'fdt' property.
+ *
+ * <subnode-name-N> {
+ * <version-1> {
+ * fdt = <0x...>;
+ * };
+ * <version-2> {
+ * fdt = <0x...>;
+ * };
+ * };
*/
/* The compatible string for the KHO FDT root node. */
diff --git a/kernel/liveupdate/kexec_handover.c b/kernel/liveupdate/kexec_handover.c
index 29a05cec2625..6dc7c328a6e2 100644
--- a/kernel/liveupdate/kexec_handover.c
+++ b/kernel/liveupdate/kexec_handover.c
@@ -20,6 +20,7 @@
#include <linux/list.h>
#include <linux/memblock.h>
#include <linux/page-isolation.h>
+#include <linux/slab.h>
#include <linux/unaligned.h>
#include <linux/vmalloc.h>
@@ -722,15 +723,31 @@ static void __init kho_reserve_scratch(void)
kho_enable = false;
}
+static int fdt_err_to_errno(int fdt_err)
+{
+ switch (fdt_err) {
+ case 0:
+ return 0;
+ case -FDT_ERR_NOSPACE:
+ return -ENOSPC;
+ case -FDT_ERR_EXISTS:
+ return -EEXIST;
+ case -FDT_ERR_NOTFOUND:
+ return -ENOENT;
+ default:
+ return -EINVAL;
+ }
+}
+
static int kho_add_subtree_node(const char *name, void *root_fdt, void *fdt, int off)
{
int err;
- phys_addr_t phys = virt_to_phys(fdt);
+ u64 phys = virt_to_phys(fdt);
err = fdt_setprop(root_fdt, off, KHO_FDT_SUB_TREE_PROP_NAME,
&phys, sizeof(phys));
if (err)
- return err;
+ return fdt_err_to_errno(err);
WARN_ON_ONCE(kho_debugfs_fdt_add(&kho_out.dbg, name, fdt, false));
@@ -762,12 +779,11 @@ int kho_add_subtree(const char *name, void *fdt)
fdt_err = fdt_open_into(root_fdt, root_fdt, PAGE_SIZE);
if (fdt_err < 0)
- return err;
+ return fdt_err_to_errno(fdt_err);
off = fdt_add_subnode(root_fdt, 0, name);
if (off < 0) {
- if (off == -FDT_ERR_EXISTS)
- err = -EEXIST;
+ err = fdt_err_to_errno(off);
goto out_pack;
}
@@ -780,6 +796,93 @@ int kho_add_subtree(const char *name, void *fdt)
}
EXPORT_SYMBOL_GPL(kho_add_subtree);
+/**
+ * kho_add_subtree_version - record physical address of a sub FDT in KHO root tree with version.
+ * @name: name of the sub tree group.
+ * @version: version of the sub tree.
+ * @fdt: the sub tree blob.
+ *
+ * Finds or creates a child node named @name in KHO root FDT, and then
+ * creates a child node named @version under @name, and records
+ * the physical address of @fdt there.
+ *
+ * A debugfs blob entry is also created at
+ * ``/sys/kernel/debug/kho/out/sub_fdts/@name-@version`` when kernel is configured with
+ * CONFIG_KEXEC_HANDOVER_DEBUGFS
+ *
+ * Return: 0 on success, error code on failure
+ */
+int kho_add_subtree_version(const char *name, int version, void *fdt)
+{
+ void *root_fdt = kho_out.fdt;
+ int err = -ENOMEM;
+ int off, sub_off, fdt_err;
+ char version_str[12];
+ char *dbg_name = NULL;
+ bool created_parent = false;
+
+ mutex_lock(&kho_out.lock);
+
+ fdt_err = fdt_open_into(root_fdt, root_fdt, PAGE_SIZE);
+ if (fdt_err < 0) {
+ err = fdt_err_to_errno(fdt_err);
+ goto out;
+ }
+
+ off = fdt_subnode_offset(root_fdt, 0, name);
+ if (off == -FDT_ERR_NOTFOUND) {
+ off = fdt_add_subnode(root_fdt, 0, name);
+ if (off < 0) {
+ err = fdt_err_to_errno(off);
+ goto out_pack;
+ }
+ created_parent = true;
+ } else if (off < 0) {
+ err = fdt_err_to_errno(off);
+ goto out_pack;
+ }
+
+ snprintf(version_str, sizeof(version_str), "%d", version);
+ sub_off = fdt_add_subnode(root_fdt, off, version_str);
+ if (sub_off < 0) {
+ err = fdt_err_to_errno(sub_off);
+ goto err_del_parent;
+ }
+
+ dbg_name = kasprintf(GFP_KERNEL, "%s-%s", name, version_str);
+ if (!dbg_name) {
+ err = -ENOMEM;
+ goto err_del_subnode;
+ }
+
+ err = kho_add_subtree_node(dbg_name, root_fdt, fdt, sub_off);
+ if (err)
+ goto err_free_dbg_name;
+
+ kfree(dbg_name);
+
+out_pack:
+ fdt_pack(root_fdt);
+out:
+ mutex_unlock(&kho_out.lock);
+ return err;
+
+err_free_dbg_name:
+ kfree(dbg_name);
+err_del_subnode:
+ if (created_parent)
+ fdt_del_node(root_fdt, off);
+ else
+ fdt_del_node(root_fdt, sub_off);
+ goto out_pack;
+
+err_del_parent:
+ if (created_parent)
+ fdt_del_node(root_fdt, off);
+ goto out_pack;
+}
+EXPORT_SYMBOL_GPL(kho_add_subtree_version);
+
void kho_remove_subtree(void *fdt)
{
phys_addr_t target_phys = virt_to_phys(fdt);
@@ -799,10 +902,10 @@ void kho_remove_subtree(void *fdt)
int len;
val = fdt_getprop(root_fdt, off, KHO_FDT_SUB_TREE_PROP_NAME, &len);
- if (!val || len != sizeof(phys_addr_t))
+ if (!val || len != sizeof(*val))
continue;
- if ((phys_addr_t)*val == target_phys) {
+ if ((phys_addr_t)get_unaligned(val) == target_phys) {
fdt_del_node(root_fdt, off);
kho_debugfs_fdt_remove(&kho_out.dbg, fdt);
break;
@@ -813,6 +916,59 @@ void kho_remove_subtree(void *fdt)
}
EXPORT_SYMBOL_GPL(kho_remove_subtree);
+/**
+ * kho_remove_subtree_version - remove a versioned sub FDT from KHO root tree.
+ * @name: name of the sub tree group.
+ * @version: version of the sub tree.
+ * @fdt: the sub tree blob to remove.
+ *
+ * Removes the @version subnode under @name node and its associated debugfs entry.
+ * If @name node becomes empty, it is also removed.
+ */
+void kho_remove_subtree_version(const char *name, int version, void *fdt)
+{
+ phys_addr_t target_phys = virt_to_phys(fdt);
+ void *root_fdt = kho_out.fdt;
+ int off, sub_off;
+ int err;
+ char version_str[12];
+
+ mutex_lock(&kho_out.lock);
+
+ err = fdt_open_into(root_fdt, root_fdt, PAGE_SIZE);
+ if (err < 0)
+ goto out;
+
+ off = fdt_subnode_offset(root_fdt, 0, name);
+ if (off < 0)
+ goto out_pack;
+
+ snprintf(version_str, sizeof(version_str), "%d", version);
+ sub_off = fdt_subnode_offset(root_fdt, off, version_str);
+ if (sub_off < 0)
+ goto out_pack;
+
+ {
+ const u64 *val;
+ int len;
+
+ val = fdt_getprop(root_fdt, sub_off, KHO_FDT_SUB_TREE_PROP_NAME, &len);
+ if (val && len == sizeof(*val) && (phys_addr_t)get_unaligned(val) == target_phys) {
+ fdt_del_node(root_fdt, sub_off);
+ kho_debugfs_fdt_remove(&kho_out.dbg, fdt);
+
+ if (fdt_first_subnode(root_fdt, off) == -FDT_ERR_NOTFOUND)
+ fdt_del_node(root_fdt, off);
+ }
+ }
+
+out_pack:
+ fdt_pack(root_fdt);
+out:
+ mutex_unlock(&kho_out.lock);
+}
+EXPORT_SYMBOL_GPL(kho_remove_subtree_version);
+
/**
* kho_preserve_folio - preserve a folio across kexec.
* @folio: folio to preserve.
@@ -1336,12 +1492,55 @@ int kho_retrieve_subtree(const char *name, phys_addr_t *phys)
if (!val || len != sizeof(*val))
return -EINVAL;
- *phys = (phys_addr_t)*val;
+ *phys = (phys_addr_t)get_unaligned(val);
return 0;
}
EXPORT_SYMBOL_GPL(kho_retrieve_subtree);
+/**
+ * kho_retrieve_subtree_version - retrieve a preserved versioned sub FDT.
+ * @name: the name of the sub FDT group passed to kho_add_subtree_version().
+ * @version: the version of the sub FDT.
+ * @phys: if found, the physical address of the sub FDT is stored in @phys.
+ *
+ * Retrieve a preserved sub FDT named @version under @name and store its
+ * physical address in @phys.
+ *
+ * Return: 0 on success, error code on failure
+ */
+int kho_retrieve_subtree_version(const char *name, int version, phys_addr_t *phys)
+{
+ const void *fdt = kho_get_fdt();
+ const u64 *val;
+ int offset, sub_offset, len;
+ char version_str[12];
+
+ if (!fdt)
+ return -ENOENT;
+
+ if (!phys)
+ return -EINVAL;
+
+ offset = fdt_subnode_offset(fdt, 0, name);
+ if (offset < 0)
+ return -ENOENT;
+
+ snprintf(version_str, sizeof(version_str), "%d", version);
+ sub_offset = fdt_subnode_offset(fdt, offset, version_str);
+ if (sub_offset < 0)
+ return -ENOENT;
+
+ val = fdt_getprop(fdt, sub_offset, KHO_FDT_SUB_TREE_PROP_NAME, &len);
+ if (!val || len != sizeof(*val))
+ return -EINVAL;
+
+ *phys = (phys_addr_t)get_unaligned(val);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(kho_retrieve_subtree_version);
+
static __init int kho_out_fdt_setup(void)
{
void *root = kho_out.fdt;
--
2.55.0.508.g3f0d502094-goog
^ permalink raw reply related [flat|nested] 6+ messages in thread* [RFC PATCH 3/4] kho: Add support for multiple versions to debugfs
2026-07-31 21:52 [RFC PATCH 0/2] Add support for multiple versions to KHO Logan Odell
2026-07-31 21:52 ` [RFC PATCH 1/4] kho: Move subtree blob logic to separate function Logan Odell
2026-07-31 21:52 ` [RFC PATCH 2/4] kho: Add support for multiple versions in subtrees Logan Odell
@ 2026-07-31 21:52 ` Logan Odell
2026-07-31 21:52 ` [RFC PATCH 4/4] kho: Add test cases for versioned subtrees Logan Odell
2026-07-31 21:56 ` [RFC PATCH 0/2] Add support for multiple versions to KHO Logan Odell
4 siblings, 0 replies; 6+ messages in thread
From: Logan Odell @ 2026-07-31 21:52 UTC (permalink / raw)
To: graf, rppt, pasha.tatashin
Cc: pratyush, akpm, kexec, linux-mm, linux-kernel, Logan Odell
Update kho_in_debugfs_init to search for versioned subnodes
(depth 2) and expose them as name-version in the input debugfs.
This ensures versioned subtrees are visible to userspace for debugging,
matching the behavior of the output side.
Signed-off-by: Logan Odell <loganodell@google.com>
---
kernel/liveupdate/kexec_handover_debugfs.c | 57 +++++++++++++++++-----
1 file changed, 44 insertions(+), 13 deletions(-)
diff --git a/kernel/liveupdate/kexec_handover_debugfs.c b/kernel/liveupdate/kexec_handover_debugfs.c
index 2f93939168ab..2ca055219900 100644
--- a/kernel/liveupdate/kexec_handover_debugfs.c
+++ b/kernel/liveupdate/kexec_handover_debugfs.c
@@ -13,6 +13,7 @@
#include <linux/io.h>
#include <linux/libfdt.h>
#include <linux/mm.h>
+#include <linux/unaligned.h>
#include "kexec_handover_internal.h"
static struct dentry *debugfs_root;
@@ -140,19 +141,49 @@ __init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt)
const u64 *fdt_phys;
fdt_phys = fdt_getprop(fdt, child, "fdt", &len);
- if (!fdt_phys)
- continue;
- if (len != sizeof(*fdt_phys)) {
- pr_warn("node %s prop fdt has invalid length: %d\n",
- name, len);
- continue;
- }
- err = __kho_debugfs_fdt_add(&dbg->fdt_list, sub_fdt_dir, name,
- phys_to_virt(*fdt_phys));
- if (err) {
- pr_warn("failed to add fdt %s to debugfs: %pe\n", name,
- ERR_PTR(err));
- continue;
+ if (fdt_phys) {
+ if (len != sizeof(*fdt_phys)) {
+ pr_warn("node %s prop fdt has invalid length: %d\n",
+ name, len);
+ continue;
+ }
+ err = __kho_debugfs_fdt_add(&dbg->fdt_list, sub_fdt_dir, name,
+ phys_to_virt(get_unaligned(fdt_phys)));
+ if (err) {
+ pr_warn("failed to add fdt %s to debugfs: %pe\n", name,
+ ERR_PTR(err));
+ }
+ } else {
+ int grandchild;
+
+ fdt_for_each_subnode(grandchild, fdt, child) {
+ const char *gc_name = fdt_get_name(fdt, grandchild, NULL);
+ const u64 *gc_fdt_phys;
+ char *combined_name;
+
+ gc_fdt_phys = fdt_getprop(fdt, grandchild, "fdt", &len);
+ if (!gc_fdt_phys)
+ continue;
+
+ if (len != sizeof(*gc_fdt_phys)) {
+ pr_warn("node %s/%s prop fdt has invalid length: %d\n",
+ name, gc_name, len);
+ continue;
+ }
+
+ combined_name = kasprintf(GFP_KERNEL, "%s-%s", name, gc_name);
+ if (!combined_name)
+ continue;
+
+ err = __kho_debugfs_fdt_add(&dbg->fdt_list, sub_fdt_dir,
+ combined_name,
+ phys_to_virt(get_unaligned(gc_fdt_phys)));
+ kfree(combined_name);
+ if (err) {
+ pr_warn("failed to add fdt %s-%s to debugfs: %pe\n",
+ name, gc_name, ERR_PTR(err));
+ }
+ }
}
}
--
2.55.0.508.g3f0d502094-goog
^ permalink raw reply related [flat|nested] 6+ messages in thread* [RFC PATCH 4/4] kho: Add test cases for versioned subtrees
2026-07-31 21:52 [RFC PATCH 0/2] Add support for multiple versions to KHO Logan Odell
` (2 preceding siblings ...)
2026-07-31 21:52 ` [RFC PATCH 3/4] kho: Add support for multiple versions to debugfs Logan Odell
@ 2026-07-31 21:52 ` Logan Odell
2026-07-31 21:56 ` [RFC PATCH 0/2] Add support for multiple versions to KHO Logan Odell
4 siblings, 0 replies; 6+ messages in thread
From: Logan Odell @ 2026-07-31 21:52 UTC (permalink / raw)
To: graf, rppt, pasha.tatashin
Cc: pratyush, akpm, kexec, linux-mm, linux-kernel, Logan Odell
Add new test cases to test_kho.c to verify saving and restoring
multiple versions of a subtree. The test preserves versions 1 and 2
of "kho_test_ver" and retrieves them, verifying their contents.
It also ensures no memory leaks occur during the restore process.
Signed-off-by: Logan Odell <loganodell@google.com>
---
lib/test_kho.c | 266 +++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 245 insertions(+), 21 deletions(-)
diff --git a/lib/test_kho.c b/lib/test_kho.c
index 7ef9e4061869..db531da63576 100644
--- a/lib/test_kho.c
+++ b/lib/test_kho.c
@@ -310,52 +310,276 @@ static int kho_test_restore(phys_addr_t fdt_phys)
return 0;
}
+static void kho_test_cleanup(void)
+{
+ /* unpreserve and free the data stored in folios */
+ kho_test_unpreserve_data(&kho_test_state);
+ for (int i = 0; i < kho_test_state.nr_folios; i++)
+ folio_put(kho_test_state.folios[i]);
+
+ kvfree(kho_test_state.folios);
+
+ /* Unpreserve and release the FDT folio */
+ kho_unpreserve_folio(kho_test_state.fdt);
+ folio_put(kho_test_state.fdt);
+}
+
+#define KHO_TEST_VER_NAME "kho_test_ver"
+#define KHO_TEST_VER_1 1
+#define KHO_TEST_VER_2 2
+#define KHO_TEST_VER_MAGIC_1 0x11111111
+#define KHO_TEST_VER_MAGIC_2 0x22222222
+
+struct kho_test_versioned_state {
+ struct folio *fdt1;
+ struct folio *fdt2;
+};
+
+static struct kho_test_versioned_state kho_test_ver_state;
+
+static int kho_test_prepare_ver_fdt(struct folio *folio, int version, unsigned int magic)
+{
+ void *fdt = folio_address(folio);
+ int err;
+
+ err = fdt_create(fdt, PAGE_SIZE);
+ err |= fdt_finish_reservemap(fdt);
+ err |= fdt_begin_node(fdt, "");
+ err |= fdt_property(fdt, "compatible", KHO_TEST_COMPAT, sizeof(KHO_TEST_COMPAT));
+ err |= fdt_property(fdt, "magic", &magic, sizeof(magic));
+ err |= fdt_property(fdt, "version", &version, sizeof(version));
+ err |= fdt_end_node(fdt);
+ err |= fdt_finish(fdt);
+
+ return err;
+}
+
+static int kho_test_ver_save(void)
+{
+ struct kho_test_versioned_state *state = &kho_test_ver_state;
+ int err;
+
+ state->fdt1 = folio_alloc(GFP_KERNEL, 0);
+ if (!state->fdt1)
+ return -ENOMEM;
+
+ state->fdt2 = folio_alloc(GFP_KERNEL, 0);
+ if (!state->fdt2) {
+ err = -ENOMEM;
+ goto err_free_fdt1;
+ }
+
+ err = kho_preserve_folio(state->fdt1);
+ if (err)
+ goto err_free_fdt2;
+
+ err = kho_preserve_folio(state->fdt2);
+ if (err)
+ goto err_unpreserve_fdt1;
+
+ err = kho_test_prepare_ver_fdt(state->fdt1, KHO_TEST_VER_1, KHO_TEST_VER_MAGIC_1);
+ if (err)
+ goto err_unpreserve_fdt2;
+
+ err = kho_test_prepare_ver_fdt(state->fdt2, KHO_TEST_VER_2, KHO_TEST_VER_MAGIC_2);
+ if (err)
+ goto err_unpreserve_fdt2;
+
+ err = kho_add_subtree_version(KHO_TEST_VER_NAME, KHO_TEST_VER_1,
+ folio_address(state->fdt1));
+ if (err)
+ goto err_unpreserve_fdt2;
+
+ err = kho_add_subtree_version(KHO_TEST_VER_NAME, KHO_TEST_VER_2,
+ folio_address(state->fdt2));
+ if (err)
+ goto err_remove_ver1;
+
+ pr_info("KHO versioned test save succeeded\n");
+ return 0;
+
+err_remove_ver1:
+ kho_remove_subtree_version(KHO_TEST_VER_NAME, KHO_TEST_VER_1, folio_address(state->fdt1));
+err_unpreserve_fdt2:
+ kho_unpreserve_folio(state->fdt2);
+err_unpreserve_fdt1:
+ kho_unpreserve_folio(state->fdt1);
+err_free_fdt2:
+ folio_put(state->fdt2);
+ state->fdt2 = NULL;
+err_free_fdt1:
+ folio_put(state->fdt1);
+ state->fdt1 = NULL;
+ return err;
+}
+
+static int kho_test_ver_restore_one(phys_addr_t fdt_phys, int expected_version,
+ unsigned int expected_magic)
+{
+ void *fdt = phys_to_virt(fdt_phys);
+ const unsigned int *magic;
+ const int *version;
+ int node, len;
+
+ node = fdt_path_offset(fdt, "/");
+ if (node < 0)
+ return -EINVAL;
+
+ if (fdt_node_check_compatible(fdt, node, KHO_TEST_COMPAT))
+ return -EINVAL;
+
+ magic = fdt_getprop(fdt, node, "magic", &len);
+ if (!magic || len != sizeof(*magic) || *magic != expected_magic)
+ return -EINVAL;
+
+ version = fdt_getprop(fdt, node, "version", &len);
+ if (!version || len != sizeof(*version) || *version != expected_version)
+ return -EINVAL;
+
+ return 0;
+}
+
+static int kho_test_ver_restore(void)
+{
+ phys_addr_t fdt_phys1, fdt_phys2;
+ struct folio *fdt1 = NULL, *fdt2 = NULL;
+ int err;
+
+ err = kho_retrieve_subtree_version(KHO_TEST_VER_NAME, KHO_TEST_VER_1, &fdt_phys1);
+ if (err) {
+ if (err != -ENOENT)
+ pr_err("Failed to retrieve version 1: %d\n", err);
+ return err;
+ }
+
+ fdt1 = kho_restore_folio(fdt_phys1);
+ if (!fdt1) {
+ pr_err("Failed to restore FDT 1 folio\n");
+ return -EINVAL;
+ }
+
+ err = kho_retrieve_subtree_version(KHO_TEST_VER_NAME, KHO_TEST_VER_2, &fdt_phys2);
+ if (err) {
+ pr_err("Failed to retrieve version 2: %d\n", err);
+ goto out_put_fdt1;
+ }
+
+ fdt2 = kho_restore_folio(fdt_phys2);
+ if (!fdt2) {
+ pr_err("Failed to restore FDT 2 folio\n");
+ err = -EINVAL;
+ goto out_put_fdt1;
+ }
+
+ err = kho_test_ver_restore_one(fdt_phys1, KHO_TEST_VER_1, KHO_TEST_VER_MAGIC_1);
+ if (err) {
+ pr_err("Version 1 verification failed: %d\n", err);
+ goto out_put_fdt2;
+ }
+
+ err = kho_test_ver_restore_one(fdt_phys2, KHO_TEST_VER_2, KHO_TEST_VER_MAGIC_2);
+ if (err) {
+ pr_err("Version 2 verification failed: %d\n", err);
+ goto out_put_fdt2;
+ }
+
+ pr_info("KHO versioned test restore succeeded\n");
+
+out_put_fdt2:
+ folio_put(fdt2);
+out_put_fdt1:
+ folio_put(fdt1);
+ return err;
+}
+
+static void kho_test_ver_cleanup(void)
+{
+ struct kho_test_versioned_state *state = &kho_test_ver_state;
+
+ if (state->fdt1) {
+ kho_unpreserve_folio(state->fdt1);
+ folio_put(state->fdt1);
+ state->fdt1 = NULL;
+ }
+ if (state->fdt2) {
+ kho_unpreserve_folio(state->fdt2);
+ folio_put(state->fdt2);
+ state->fdt2 = NULL;
+ }
+}
+
+static void kho_test_ver_exit(void)
+{
+ struct kho_test_versioned_state *state = &kho_test_ver_state;
+
+ if (state->fdt1)
+ kho_remove_subtree_version(KHO_TEST_VER_NAME, KHO_TEST_VER_1,
+ folio_address(state->fdt1));
+ if (state->fdt2)
+ kho_remove_subtree_version(KHO_TEST_VER_NAME, KHO_TEST_VER_2,
+ folio_address(state->fdt2));
+ kho_test_ver_cleanup();
+}
+
static int __init kho_test_init(void)
{
phys_addr_t fdt_phys;
- int err;
+ int err, ver_err;
+ bool restored = false;
if (!kho_is_enabled())
return 0;
+ // Try to restore non-versioned
err = kho_retrieve_subtree(KHO_TEST_FDT, &fdt_phys);
if (!err) {
err = kho_test_restore(fdt_phys);
- if (err)
+ if (err) {
pr_err("KHO restore failed\n");
- else
- pr_info("KHO restore succeeded\n");
-
+ return err;
+ }
+ pr_info("KHO restore succeeded\n");
+ restored = true;
+ } else if (err != -ENOENT) {
+ pr_warn("failed to retrieve %s FDT: %d\n", KHO_TEST_FDT, err);
return err;
}
- if (err != -ENOENT) {
- pr_warn("failed to retrieve %s FDT: %d\n", KHO_TEST_FDT, err);
- return err;
+ // Try to restore versioned
+ ver_err = kho_test_ver_restore();
+ if (!ver_err) {
+ restored = true;
+ } else if (ver_err != -ENOENT) {
+ pr_warn("KHO versioned restore failed: %d\n", ver_err);
+ return ver_err;
}
- return kho_test_save();
-}
-module_init(kho_test_init);
+ if (restored)
+ return 0;
-static void kho_test_cleanup(void)
-{
- /* unpreserve and free the data stored in folios */
- kho_test_unpreserve_data(&kho_test_state);
- for (int i = 0; i < kho_test_state.nr_folios; i++)
- folio_put(kho_test_state.folios[i]);
+ // If we got here, it means both returned -ENOENT (first boot).
+ // So we save both.
- kvfree(kho_test_state.folios);
+ err = kho_test_save();
+ if (err)
+ return err;
- /* Unpreserve and release the FDT folio */
- kho_unpreserve_folio(kho_test_state.fdt);
- folio_put(kho_test_state.fdt);
+ ver_err = kho_test_ver_save();
+ if (ver_err) {
+ kho_remove_subtree(folio_address(kho_test_state.fdt));
+ kho_test_cleanup();
+ return ver_err;
+ }
+
+ return 0;
}
+module_init(kho_test_init);
static void __exit kho_test_exit(void)
{
kho_remove_subtree(folio_address(kho_test_state.fdt));
kho_test_cleanup();
+ kho_test_ver_exit();
}
module_exit(kho_test_exit);
--
2.55.0.508.g3f0d502094-goog
^ permalink raw reply related [flat|nested] 6+ messages in thread* Re: [RFC PATCH 0/2] Add support for multiple versions to KHO
2026-07-31 21:52 [RFC PATCH 0/2] Add support for multiple versions to KHO Logan Odell
` (3 preceding siblings ...)
2026-07-31 21:52 ` [RFC PATCH 4/4] kho: Add test cases for versioned subtrees Logan Odell
@ 2026-07-31 21:56 ` Logan Odell
4 siblings, 0 replies; 6+ messages in thread
From: Logan Odell @ 2026-07-31 21:56 UTC (permalink / raw)
To: graf, rppt, pasha.tatashin; +Cc: pratyush, akpm, kexec, linux-mm, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 2888 bytes --]
0/2 is a typo, should be 0/4 :-)
On Fri, Jul 31, 2026 at 2:53 PM Logan Odell <loganodell@google.com> wrote:
> The format of the preserved-data blob that LUO saves to KHO is expected
> to change as new features are added, bugs are fixed, etc. Currently,
> LUO handles this by embedding a compatibility string into struct luo_ser
> which can be used to compare two kernels. This works if the changes
> between kernels are fundamentally incompatible. In a case where a new
> feature is added, we may want to support both versions in the new
> kernel to retain compatibility.
>
> This series adds a KHO interface to embed multiple versions under a
> single node in a subtree.
>
> We go from something like:
>
> / (Root Node)
> └── LUO (Legacy Parent Node)
> ├── preserved-data = <0xphys_luo>
> └── blob-size = <0xsize_luo>
>
> To something like this:
>
> / (Root Node)
> └── LUO (Versioned Parent Node)
> ├── 1 (Version 1 Node)
> │ ├── preserved-data = <0xphys_luo_v1>
> │ └── blob-size = <0xsize_luo_v1>
> └── 2 (Version 2 Node)
> ├── preserved-data = <0xphys_luo_v2>
> └── blob-size = <0xsize_luo_v2>
>
> We can also support mixed mode, allowing backwards compatibility in case
> of a roll-back to a pre-multi version kernel:
>
> / (Root Node)
> └── LUO (Versioned Parent Node)
> ├── preserved-data = <0xphys_luo>
> ├── blob-size = <0xphys_luo>
> ├── 1 (Version 1 Node)
> │ ├── preserved-data = <0xphys_luo_v1>
> │ └── blob-size = <0xsize_luo_v1>
> └── 2 (Version 2 Node)
> ├── preserved-data = <0xphys_luo_v2>
> └── blob-size = <0xsize_luo_v2>
>
> The first patch moves some common functionality for preserving a node
> into a separate function. The second patch adds our new APIs. The third
> patch adds support for multiple versions to the debugfs entries. The
> forth patch adds new test cases to test_kho.
>
> Logan Odell (4):
> kho: Move subtree blob logic to separate function
> kho: Add support for multiple versions in subtrees
> kho: Add support for multiple versions to debugfs
> kho: Add test cases for versioned subtrees
>
> include/linux/kexec_handover.h | 14 ++
> include/linux/kho/abi/kexec_handover.h | 16 ++
> kernel/liveupdate/kexec_handover.c | 234 +++++++++++++++++-
> kernel/liveupdate/kexec_handover_debugfs.c | 57 ++++-
> lib/test_kho.c | 266 +++++++++++++++++++--
> 5 files changed, 540 insertions(+), 47 deletions(-)
>
> --
> 2.55.0.508.g3f0d502094-goog
>
>
[-- Attachment #2: Type: text/html, Size: 3577 bytes --]
^ permalink raw reply [flat|nested] 6+ messages in thread