kho: make debugfs interface optional

Patch series "liveupdate: Rework KHO for in-kernel users", v9.

This series refactors the KHO framework to better support in-kernel users
like the upcoming LUO.  The current design, which relies on a notifier
chain and debugfs for control, is too restrictive for direct programmatic
use.

The core of this rework is the removal of the notifier chain in favor of a
direct registration API.  This decouples clients from the shutdown-time
finalization sequence, allowing them to manage their preserved state more
flexibly and at any time.

In support of this new model, this series also:
 - Makes the debugfs interface optional.
 - Introduces APIs to unpreserve memory and fixes a bug in the abort
   path where client state was being incorrectly discarded. Note that
   this is an interim step, as a more comprehensive fix is planned as
   part of the stateless KHO work [1].
 - Moves all KHO code into a new kernel/liveupdate/ directory to
   consolidate live update components.


This patch (of 9):

Currently, KHO is controlled via debugfs interface, but once LUO is
introduced, it can control KHO, and the debug interface becomes optional.

Add a separate config CONFIG_KEXEC_HANDOVER_DEBUGFS that enables the
debugfs interface, and allows to inspect the tree.

Move all debugfs related code to a new file to keep the .c files clear of
ifdefs.

Link: https://lkml.kernel.org/r/20251101142325.1326536-1-pasha.tatashin@soleen.com
Link: https://lkml.kernel.org/r/20251101142325.1326536-2-pasha.tatashin@soleen.com
Link: https://lore.kernel.org/all/20251020100306.2709352-1-jasonmiu@google.com [1]
Co-developed-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
Signed-off-by: Mike Rapoport (Microsoft) <rppt@kernel.org>
Signed-off-by: Pasha Tatashin <pasha.tatashin@soleen.com>
Reviewed-by: Pratyush Yadav <pratyush@kernel.org>
Cc: Alexander Graf <graf@amazon.com>
Cc: Christian Brauner <brauner@kernel.org>
Cc: Jason Gunthorpe <jgg@ziepe.ca>
Cc: Jonathan Corbet <corbet@lwn.net>
Cc: Masahiro Yamada <masahiroy@kernel.org>
Cc: Miguel Ojeda <ojeda@kernel.org>
Cc: Randy Dunlap <rdunlap@infradead.org>
Cc: Tejun Heo <tj@kernel.org>
Cc: Changyuan Lyu <changyuanl@google.com>
Cc: Jason Gunthorpe <jgg@nvidia.com>
Cc: Simon Horman <horms@kernel.org>
Cc: Zhu Yanjun <yanjun.zhu@linux.dev>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
This commit is contained in:
Pasha Tatashin
2025-11-01 10:23:17 -04:00
committed by Andrew Morton
parent e6fbd1759c
commit 03d3963464
7 changed files with 314 additions and 222 deletions

View File

@@ -13799,7 +13799,7 @@ S: Maintained
F: Documentation/admin-guide/mm/kho.rst
F: Documentation/core-api/kho/*
F: include/linux/kexec_handover.h
F: kernel/kexec_handover.c
F: kernel/kexec_handover*
F: lib/test_kho.c
F: tools/testing/selftests/kho/

View File

@@ -100,7 +100,6 @@ config KEXEC_HANDOVER
depends on !DEFERRED_STRUCT_PAGE_INIT
select MEMBLOCK_KHO_SCRATCH
select KEXEC_FILE
select DEBUG_FS
select LIBFDT
select CMA
help
@@ -118,6 +117,17 @@ config KEXEC_HANDOVER_DEBUG
scenarios and the extra code might be adding overhead it is
only optionally enabled.
config KEXEC_HANDOVER_DEBUGFS
bool "kexec handover debugfs interface"
default KEXEC_HANDOVER
depends on KEXEC_HANDOVER
select DEBUG_FS
help
Allow to control kexec handover device tree via debugfs
interface, i.e. finalize the state or aborting the finalization.
Also, enables inspecting the KHO fdt trees with the debugfs binary
blobs.
config CRASH_DUMP
bool "kernel crash dumps"
default ARCH_DEFAULT_CRASH_DUMP

View File

@@ -84,6 +84,7 @@ obj-$(CONFIG_KEXEC_FILE) += kexec_file.o
obj-$(CONFIG_KEXEC_ELF) += kexec_elf.o
obj-$(CONFIG_KEXEC_HANDOVER) += kexec_handover.o
obj-$(CONFIG_KEXEC_HANDOVER_DEBUG) += kexec_handover_debug.o
obj-$(CONFIG_KEXEC_HANDOVER_DEBUGFS) += kexec_handover_debugfs.o
obj-$(CONFIG_BACKTRACE_SELF_TEST) += backtracetest.o
obj-$(CONFIG_COMPAT) += compat.o
obj-$(CONFIG_CGROUPS) += cgroup/

View File

@@ -11,7 +11,6 @@
#include <linux/cleanup.h>
#include <linux/cma.h>
#include <linux/count_zeros.h>
#include <linux/debugfs.h>
#include <linux/kexec.h>
#include <linux/kexec_handover.h>
#include <linux/libfdt.h>
@@ -30,6 +29,7 @@
*/
#include "../mm/internal.h"
#include "kexec_internal.h"
#include "kexec_handover_internal.h"
#define KHO_FDT_COMPATIBLE "kho-v1"
#define PROP_PRESERVED_MEMORY_MAP "preserved-memory-map"
@@ -105,8 +105,6 @@ struct khoser_mem_chunk;
struct kho_serialization {
struct page *fdt;
struct list_head fdt_list;
struct dentry *sub_fdt_dir;
struct kho_mem_track track;
/* First chunk of serialized preserved memory map */
struct khoser_mem_chunk *preserved_mem_map;
@@ -114,20 +112,16 @@ struct kho_serialization {
struct kho_out {
struct blocking_notifier_head chain_head;
struct dentry *dir;
struct mutex lock; /* protects KHO FDT finalization */
struct kho_serialization ser;
bool finalized;
struct kho_debugfs dbg;
};
static struct kho_out kho_out = {
.chain_head = BLOCKING_NOTIFIER_INIT(kho_out.chain_head),
.lock = __MUTEX_INITIALIZER(kho_out.lock),
.ser = {
.fdt_list = LIST_HEAD_INIT(kho_out.ser.fdt_list),
.track = {
.orders = XARRAY_INIT(kho_out.ser.track.orders, 0),
},
@@ -674,37 +668,6 @@ err_disable_kho:
kho_enable = false;
}
struct fdt_debugfs {
struct list_head list;
struct debugfs_blob_wrapper wrapper;
struct dentry *file;
};
static int kho_debugfs_fdt_add(struct list_head *list, struct dentry *dir,
const char *name, const void *fdt)
{
struct fdt_debugfs *f;
struct dentry *file;
f = kmalloc(sizeof(*f), GFP_KERNEL);
if (!f)
return -ENOMEM;
f->wrapper.data = (void *)fdt;
f->wrapper.size = fdt_totalsize(fdt);
file = debugfs_create_blob(name, 0400, dir, &f->wrapper);
if (IS_ERR(file)) {
kfree(f);
return PTR_ERR(file);
}
f->file = file;
list_add(&f->list, list);
return 0;
}
/**
* kho_add_subtree - record the physical address of a sub FDT in KHO root tree.
* @ser: serialization control object passed by KHO notifiers.
@@ -716,7 +679,8 @@ static int kho_debugfs_fdt_add(struct list_head *list, struct dentry *dir,
* by KHO for the new kernel to retrieve it after kexec.
*
* A debugfs blob entry is also created at
* ``/sys/kernel/debug/kho/out/sub_fdts/@name``.
* ``/sys/kernel/debug/kho/out/sub_fdts/@name`` when kernel is configured with
* CONFIG_KEXEC_HANDOVER_DEBUGFS
*
* Return: 0 on success, error code on failure
*/
@@ -733,7 +697,7 @@ int kho_add_subtree(struct kho_serialization *ser, const char *name, void *fdt)
if (err)
return err;
return kho_debugfs_fdt_add(&ser->fdt_list, ser->sub_fdt_dir, name, fdt);
return kho_debugfs_fdt_add(&kho_out.dbg, name, fdt, false);
}
EXPORT_SYMBOL_GPL(kho_add_subtree);
@@ -1065,30 +1029,7 @@ err_free_pages_array:
}
EXPORT_SYMBOL_GPL(kho_restore_vmalloc);
/* Handling for debug/kho/out */
static struct dentry *debugfs_root;
static int kho_out_update_debugfs_fdt(void)
{
int err = 0;
struct fdt_debugfs *ff, *tmp;
if (kho_out.finalized) {
err = kho_debugfs_fdt_add(&kho_out.ser.fdt_list, kho_out.dir,
"fdt", page_to_virt(kho_out.ser.fdt));
} else {
list_for_each_entry_safe(ff, tmp, &kho_out.ser.fdt_list, list) {
debugfs_remove(ff->file);
list_del(&ff->list);
kfree(ff);
}
}
return err;
}
static int kho_abort(void)
static int __kho_abort(void)
{
int err;
unsigned long order;
@@ -1121,7 +1062,28 @@ static int kho_abort(void)
return err;
}
static int kho_finalize(void)
int kho_abort(void)
{
int ret = 0;
if (!kho_enable)
return -EOPNOTSUPP;
guard(mutex)(&kho_out.lock);
if (!kho_out.finalized)
return -ENOENT;
ret = __kho_abort();
if (ret)
return ret;
kho_out.finalized = false;
kho_debugfs_cleanup(&kho_out.dbg);
return 0;
}
static int __kho_finalize(void)
{
int err = 0;
u64 *preserved_mem_map;
@@ -1164,118 +1126,46 @@ static int kho_finalize(void)
abort:
if (err) {
pr_err("Failed to convert KHO state tree: %d\n", err);
kho_abort();
__kho_abort();
}
return err;
}
static int kho_out_finalize_get(void *data, u64 *val)
int kho_finalize(void)
{
mutex_lock(&kho_out.lock);
*val = kho_out.finalized;
mutex_unlock(&kho_out.lock);
int ret;
return 0;
}
if (!kho_enable)
return -EOPNOTSUPP;
static int kho_out_finalize_set(void *data, u64 _val)
{
int ret = 0;
bool val = !!_val;
mutex_lock(&kho_out.lock);
if (val == kho_out.finalized) {
if (kho_out.finalized)
ret = -EEXIST;
else
ret = -ENOENT;
goto unlock;
}
if (val)
ret = kho_finalize();
else
ret = kho_abort();
guard(mutex)(&kho_out.lock);
if (kho_out.finalized)
return -EEXIST;
ret = __kho_finalize();
if (ret)
goto unlock;
return ret;
kho_out.finalized = val;
ret = kho_out_update_debugfs_fdt();
kho_out.finalized = true;
unlock:
mutex_unlock(&kho_out.lock);
return ret;
return kho_debugfs_fdt_add(&kho_out.dbg, "fdt",
page_to_virt(kho_out.ser.fdt), true);
}
DEFINE_DEBUGFS_ATTRIBUTE(fops_kho_out_finalize, kho_out_finalize_get,
kho_out_finalize_set, "%llu\n");
static int scratch_phys_show(struct seq_file *m, void *v)
bool kho_finalized(void)
{
for (int i = 0; i < kho_scratch_cnt; i++)
seq_printf(m, "0x%llx\n", kho_scratch[i].addr);
return 0;
}
DEFINE_SHOW_ATTRIBUTE(scratch_phys);
static int scratch_len_show(struct seq_file *m, void *v)
{
for (int i = 0; i < kho_scratch_cnt; i++)
seq_printf(m, "0x%llx\n", kho_scratch[i].size);
return 0;
}
DEFINE_SHOW_ATTRIBUTE(scratch_len);
static __init int kho_out_debugfs_init(void)
{
struct dentry *dir, *f, *sub_fdt_dir;
dir = debugfs_create_dir("out", debugfs_root);
if (IS_ERR(dir))
return -ENOMEM;
sub_fdt_dir = debugfs_create_dir("sub_fdts", dir);
if (IS_ERR(sub_fdt_dir))
goto err_rmdir;
f = debugfs_create_file("scratch_phys", 0400, dir, NULL,
&scratch_phys_fops);
if (IS_ERR(f))
goto err_rmdir;
f = debugfs_create_file("scratch_len", 0400, dir, NULL,
&scratch_len_fops);
if (IS_ERR(f))
goto err_rmdir;
f = debugfs_create_file("finalize", 0600, dir, NULL,
&fops_kho_out_finalize);
if (IS_ERR(f))
goto err_rmdir;
kho_out.dir = dir;
kho_out.ser.sub_fdt_dir = sub_fdt_dir;
return 0;
err_rmdir:
debugfs_remove_recursive(dir);
return -ENOENT;
guard(mutex)(&kho_out.lock);
return kho_out.finalized;
}
struct kho_in {
struct dentry *dir;
phys_addr_t fdt_phys;
phys_addr_t scratch_phys;
struct list_head fdt_list;
struct kho_debugfs dbg;
};
static struct kho_in kho_in = {
.fdt_list = LIST_HEAD_INIT(kho_in.fdt_list),
};
static const void *kho_get_fdt(void)
@@ -1339,56 +1229,6 @@ int kho_retrieve_subtree(const char *name, phys_addr_t *phys)
}
EXPORT_SYMBOL_GPL(kho_retrieve_subtree);
/* Handling for debugfs/kho/in */
static __init int kho_in_debugfs_init(const void *fdt)
{
struct dentry *sub_fdt_dir;
int err, child;
kho_in.dir = debugfs_create_dir("in", debugfs_root);
if (IS_ERR(kho_in.dir))
return PTR_ERR(kho_in.dir);
sub_fdt_dir = debugfs_create_dir("sub_fdts", kho_in.dir);
if (IS_ERR(sub_fdt_dir)) {
err = PTR_ERR(sub_fdt_dir);
goto err_rmdir;
}
err = kho_debugfs_fdt_add(&kho_in.fdt_list, kho_in.dir, "fdt", fdt);
if (err)
goto err_rmdir;
fdt_for_each_subnode(child, fdt, 0) {
int len = 0;
const char *name = fdt_get_name(fdt, child, NULL);
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`'s prop `fdt` has invalid length: %d\n",
name, len);
continue;
}
err = kho_debugfs_fdt_add(&kho_in.fdt_list, sub_fdt_dir, name,
phys_to_virt(*fdt_phys));
if (err) {
pr_warn("failed to add fdt `%s` to debugfs: %d\n", name,
err);
continue;
}
}
return 0;
err_rmdir:
debugfs_remove_recursive(kho_in.dir);
return err;
}
static __init int kho_init(void)
{
int err = 0;
@@ -1403,27 +1243,16 @@ static __init int kho_init(void)
goto err_free_scratch;
}
debugfs_root = debugfs_create_dir("kho", NULL);
if (IS_ERR(debugfs_root)) {
err = -ENOENT;
err = kho_debugfs_init();
if (err)
goto err_free_fdt;
}
err = kho_out_debugfs_init();
err = kho_out_debugfs_init(&kho_out.dbg);
if (err)
goto err_free_fdt;
if (fdt) {
err = kho_in_debugfs_init(fdt);
/*
* Failure to create /sys/kernel/debug/kho/in does not prevent
* reviving state from KHO and setting up KHO for the next
* kexec.
*/
if (err)
pr_err("failed exposing handover FDT in debugfs: %d\n",
err);
kho_in_debugfs_init(&kho_in.dbg, fdt);
return 0;
}

View File

@@ -0,0 +1,216 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* kexec_handover_debugfs.c - kexec handover debugfs interfaces
* Copyright (C) 2023 Alexander Graf <graf@amazon.com>
* Copyright (C) 2025 Microsoft Corporation, Mike Rapoport <rppt@kernel.org>
* Copyright (C) 2025 Google LLC, Changyuan Lyu <changyuanl@google.com>
* Copyright (C) 2025 Google LLC, Pasha Tatashin <pasha.tatashin@soleen.com>
*/
#define pr_fmt(fmt) "KHO: " fmt
#include <linux/init.h>
#include <linux/io.h>
#include <linux/libfdt.h>
#include <linux/mm.h>
#include "kexec_handover_internal.h"
static struct dentry *debugfs_root;
struct fdt_debugfs {
struct list_head list;
struct debugfs_blob_wrapper wrapper;
struct dentry *file;
};
static int __kho_debugfs_fdt_add(struct list_head *list, struct dentry *dir,
const char *name, const void *fdt)
{
struct fdt_debugfs *f;
struct dentry *file;
f = kmalloc(sizeof(*f), GFP_KERNEL);
if (!f)
return -ENOMEM;
f->wrapper.data = (void *)fdt;
f->wrapper.size = fdt_totalsize(fdt);
file = debugfs_create_blob(name, 0400, dir, &f->wrapper);
if (IS_ERR(file)) {
kfree(f);
return PTR_ERR(file);
}
f->file = file;
list_add(&f->list, list);
return 0;
}
int kho_debugfs_fdt_add(struct kho_debugfs *dbg, const char *name,
const void *fdt, bool root)
{
struct dentry *dir;
if (root)
dir = dbg->dir;
else
dir = dbg->sub_fdt_dir;
return __kho_debugfs_fdt_add(&dbg->fdt_list, dir, name, fdt);
}
void kho_debugfs_cleanup(struct kho_debugfs *dbg)
{
struct fdt_debugfs *ff, *tmp;
list_for_each_entry_safe(ff, tmp, &dbg->fdt_list, list) {
debugfs_remove(ff->file);
list_del(&ff->list);
kfree(ff);
}
}
static int kho_out_finalize_get(void *data, u64 *val)
{
*val = kho_finalized();
return 0;
}
static int kho_out_finalize_set(void *data, u64 val)
{
if (val)
return kho_finalize();
else
return kho_abort();
}
DEFINE_DEBUGFS_ATTRIBUTE(kho_out_finalize_fops, kho_out_finalize_get,
kho_out_finalize_set, "%llu\n");
static int scratch_phys_show(struct seq_file *m, void *v)
{
for (int i = 0; i < kho_scratch_cnt; i++)
seq_printf(m, "0x%llx\n", kho_scratch[i].addr);
return 0;
}
DEFINE_SHOW_ATTRIBUTE(scratch_phys);
static int scratch_len_show(struct seq_file *m, void *v)
{
for (int i = 0; i < kho_scratch_cnt; i++)
seq_printf(m, "0x%llx\n", kho_scratch[i].size);
return 0;
}
DEFINE_SHOW_ATTRIBUTE(scratch_len);
__init void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt)
{
struct dentry *dir, *sub_fdt_dir;
int err, child;
INIT_LIST_HEAD(&dbg->fdt_list);
dir = debugfs_create_dir("in", debugfs_root);
if (IS_ERR(dir)) {
err = PTR_ERR(dir);
goto err_out;
}
sub_fdt_dir = debugfs_create_dir("sub_fdts", dir);
if (IS_ERR(sub_fdt_dir)) {
err = PTR_ERR(sub_fdt_dir);
goto err_rmdir;
}
err = __kho_debugfs_fdt_add(&dbg->fdt_list, dir, "fdt", fdt);
if (err)
goto err_rmdir;
fdt_for_each_subnode(child, fdt, 0) {
int len = 0;
const char *name = fdt_get_name(fdt, child, NULL);
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: %d\n", name,
err);
continue;
}
}
dbg->dir = dir;
dbg->sub_fdt_dir = sub_fdt_dir;
return;
err_rmdir:
debugfs_remove_recursive(dir);
err_out:
/*
* Failure to create /sys/kernel/debug/kho/in does not prevent
* reviving state from KHO and setting up KHO for the next
* kexec.
*/
if (err)
pr_err("failed exposing handover FDT in debugfs: %d\n", err);
}
__init int kho_out_debugfs_init(struct kho_debugfs *dbg)
{
struct dentry *dir, *f, *sub_fdt_dir;
INIT_LIST_HEAD(&dbg->fdt_list);
dir = debugfs_create_dir("out", debugfs_root);
if (IS_ERR(dir))
return -ENOMEM;
sub_fdt_dir = debugfs_create_dir("sub_fdts", dir);
if (IS_ERR(sub_fdt_dir))
goto err_rmdir;
f = debugfs_create_file("scratch_phys", 0400, dir, NULL,
&scratch_phys_fops);
if (IS_ERR(f))
goto err_rmdir;
f = debugfs_create_file("scratch_len", 0400, dir, NULL,
&scratch_len_fops);
if (IS_ERR(f))
goto err_rmdir;
f = debugfs_create_file("finalize", 0600, dir, NULL,
&kho_out_finalize_fops);
if (IS_ERR(f))
goto err_rmdir;
dbg->dir = dir;
dbg->sub_fdt_dir = sub_fdt_dir;
return 0;
err_rmdir:
debugfs_remove_recursive(dir);
return -ENOENT;
}
__init int kho_debugfs_init(void)
{
debugfs_root = debugfs_create_dir("kho", NULL);
if (IS_ERR(debugfs_root))
return -ENOENT;
return 0;
}

View File

@@ -3,11 +3,46 @@
#define LINUX_KEXEC_HANDOVER_INTERNAL_H
#include <linux/kexec_handover.h>
#include <linux/list.h>
#include <linux/types.h>
#ifdef CONFIG_KEXEC_HANDOVER_DEBUGFS
#include <linux/debugfs.h>
struct kho_debugfs {
struct dentry *dir;
struct dentry *sub_fdt_dir;
struct list_head fdt_list;
};
#else
struct kho_debugfs {};
#endif
extern struct kho_scratch *kho_scratch;
extern unsigned int kho_scratch_cnt;
bool kho_finalized(void);
int kho_finalize(void);
int kho_abort(void);
#ifdef CONFIG_KEXEC_HANDOVER_DEBUGFS
int kho_debugfs_init(void);
void kho_in_debugfs_init(struct kho_debugfs *dbg, const void *fdt);
int kho_out_debugfs_init(struct kho_debugfs *dbg);
int kho_debugfs_fdt_add(struct kho_debugfs *dbg, const char *name,
const void *fdt, bool root);
void kho_debugfs_cleanup(struct kho_debugfs *dbg);
#else
static inline int kho_debugfs_init(void) { return 0; }
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_debugfs_fdt_add(struct kho_debugfs *dbg, const char *name,
const void *fdt, bool root) { return 0; }
static inline void kho_debugfs_cleanup(struct kho_debugfs *dbg) {}
#endif /* CONFIG_KEXEC_HANDOVER_DEBUGFS */
#ifdef CONFIG_KEXEC_HANDOVER_DEBUG
bool kho_scratch_overlap(phys_addr_t phys, size_t size);
#else

View File

@@ -59,6 +59,7 @@ function build_kernel() {
tee "$kconfig" > "$kho_config" <<EOF
CONFIG_BLK_DEV_INITRD=y
CONFIG_KEXEC_HANDOVER=y
CONFIG_KEXEC_HANDOVER_DEBUGFS=y
CONFIG_TEST_KEXEC_HANDOVER=y
CONFIG_DEBUG_KERNEL=y
CONFIG_DEBUG_VM=y