Add CONFIG_KEXEC_HANDOVER_READONLY and the 'kho_ro=<pa>' kernel command-line parameter to support traversing a preserved KHO tree in read-only mode without taking ownership of or modifying KHO metadata or reclaiming preserved pages into the page allocator.
When booted with 'kho_ro=<pa>', preserved KHO metadata (FDT, radix-tree nodes, and vmalloc chunks) lies outside the kernel's direct map and is mapped read-only (PAGE_KERNEL_RO), with the FDT and radix-tree nodes mapped lazily on first visit and kept cached. This allows kho_retrieve_subtree(), kho_restore_vmalloc(), and kho_ro_lookup_folio_order() to inspect the preserved KHO state while leaving it intact. Also export kho_ro_map_folio() and kho_ro_unmap() so callers (such as kho_block, LUO, and file handlers) can map preserved folios verified against the KHO radix tree with the desired page protections. Signed-off-by: Pasha Tatashin <[email protected]> --- .../admin-guide/kernel-parameters.txt | 6 + include/linux/kexec_handover.h | 53 ++- kernel/liveupdate/Kconfig | 16 + kernel/liveupdate/Makefile | 1 + kernel/liveupdate/kexec_handover_ro.c | 378 ++++++++++++++++++ 5 files changed, 441 insertions(+), 13 deletions(-) create mode 100644 kernel/liveupdate/kexec_handover_ro.c diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt index 3b89b54db550..594d0b468f32 100644 --- a/Documentation/admin-guide/kernel-parameters.txt +++ b/Documentation/admin-guide/kernel-parameters.txt @@ -3051,6 +3051,12 @@ Kernel parameters bootmem areas. The form "nn%" defines scale factor (in percents) of memory that was used during boot. + kho_ro= [KEXEC,EARLY] + Format: <phys_addr> + Enables read-only Kexec HandOver mode, pointing to the + page-aligned physical address of a preserved KHO FDT + blob. Requires CONFIG_KEXEC_HANDOVER_READONLY. + kmac= [MIPS] Korina ethernet MAC address. Configure the RouterBoard 532 series on-chip Ethernet adapter MAC address. diff --git a/include/linux/kexec_handover.h b/include/linux/kexec_handover.h index ef2b188c8ed0..1f75c2f94be5 100644 --- a/include/linux/kexec_handover.h +++ b/include/linux/kexec_handover.h @@ -13,6 +13,46 @@ struct kho_vmalloc; struct folio; struct page; +#if defined(CONFIG_KEXEC_HANDOVER) || defined(CONFIG_KEXEC_HANDOVER_READONLY) +void *kho_restore_vmalloc(const struct kho_vmalloc *preservation); +int kho_retrieve_subtree(const char *name, phys_addr_t *phys, size_t *size); +#else +static inline void *kho_restore_vmalloc(const struct kho_vmalloc *preservation) +{ + return NULL; +} + +static inline int kho_retrieve_subtree(const char *name, phys_addr_t *phys, + size_t *size) +{ + return -EOPNOTSUPP; +} +#endif /* CONFIG_KEXEC_HANDOVER || CONFIG_KEXEC_HANDOVER_READONLY */ + +#ifdef CONFIG_KEXEC_HANDOVER_READONLY +bool kho_is_ro(void); +int kho_ro_lookup_folio_order(phys_addr_t pa); +void *kho_ro_map_folio(phys_addr_t pa, pgprot_t prot); +void kho_ro_unmap(const void *addr); +#else +static inline bool kho_is_ro(void) +{ + return false; +} + +static inline int kho_ro_lookup_folio_order(phys_addr_t pa) +{ + return -EOPNOTSUPP; +} + +static inline void *kho_ro_map_folio(phys_addr_t pa, pgprot_t prot) +{ + return NULL; +} + +static inline void kho_ro_unmap(const void *addr) { } +#endif /* CONFIG_KEXEC_HANDOVER_READONLY */ + #ifdef CONFIG_KEXEC_HANDOVER bool kho_is_enabled(void); bool is_kho_boot(void); @@ -28,10 +68,8 @@ void kho_unpreserve_free(void *mem); void kho_restore_free(void *mem); struct folio *kho_restore_folio(phys_addr_t phys); struct page *kho_restore_pages(phys_addr_t phys, unsigned long nr_pages); -void *kho_restore_vmalloc(const struct kho_vmalloc *preservation); int kho_add_subtree(const char *name, void *blob, size_t size); void kho_remove_subtree(void *blob); -int kho_retrieve_subtree(const char *name, phys_addr_t *phys, size_t *size); void kho_memory_init(void); void kho_memory_init_early(void); @@ -100,11 +138,6 @@ static inline struct page *kho_restore_pages(phys_addr_t phys, return NULL; } -static inline void *kho_restore_vmalloc(const struct kho_vmalloc *preservation) -{ - return NULL; -} - static inline int kho_add_subtree(const char *name, void *blob, size_t size) { return -EOPNOTSUPP; @@ -112,12 +145,6 @@ static inline int kho_add_subtree(const char *name, void *blob, size_t size) static inline void kho_remove_subtree(void *blob) { } -static inline int kho_retrieve_subtree(const char *name, phys_addr_t *phys, - size_t *size) -{ - return -EOPNOTSUPP; -} - static inline void kho_memory_init(void) { } static inline void kho_memory_init_early(void) { } diff --git a/kernel/liveupdate/Kconfig b/kernel/liveupdate/Kconfig index f50daee8eff0..362b7a3e2629 100644 --- a/kernel/liveupdate/Kconfig +++ b/kernel/liveupdate/Kconfig @@ -48,6 +48,22 @@ config KEXEC_HANDOVER_ENABLE_DEFAULT The default behavior can still be overridden at boot time by passing 'kho=off'. +config KEXEC_HANDOVER_READONLY + bool "Read-only kexec handover" + depends on ARCH_SUPPORTS_KEXEC_HANDOVER && !KEXEC_HANDOVER + select LIBFDT + help + Enable read-only Kexec Handover ('kho_ro=<pa>'). This allows a + kernel to traverse preserved KHO subtrees, radix trees, and + metadata in read-only mode without taking ownership of or + modifying the KHO metadata. + + Pages outside of KHO metadata can still be modified; for example, + a preserved file descriptor can be retrieved while remaining in + frozen mode (disallowing resize, hole punch, or faulting in new + memory), while the memory content of the file can still be + modified. + config LIVEUPDATE bool "Live Update Orchestrator" depends on KEXEC_HANDOVER diff --git a/kernel/liveupdate/Makefile b/kernel/liveupdate/Makefile index d9f469462556..f8b09e04bea2 100644 --- a/kernel/liveupdate/Makefile +++ b/kernel/liveupdate/Makefile @@ -8,6 +8,7 @@ luo-y := \ luo_session.o obj-$(CONFIG_KEXEC_HANDOVER) += kexec_handover.o +obj-$(CONFIG_KEXEC_HANDOVER_READONLY) += kexec_handover_ro.o obj-$(CONFIG_KEXEC_HANDOVER_DEBUGFS) += kexec_handover_debugfs.o obj-$(CONFIG_LIVEUPDATE) += luo.o diff --git a/kernel/liveupdate/kexec_handover_ro.c b/kernel/liveupdate/kexec_handover_ro.c new file mode 100644 index 000000000000..f5d5e9495778 --- /dev/null +++ b/kernel/liveupdate/kexec_handover_ro.c @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * kexec_handover_ro.c - Read-Only Kexec Handover backend + * + * Copyright (c) 2026, Google LLC. + * Pasha Tatashin <[email protected]> + * + * Implements the read-only KHO backend (kho_ro=<pa>). In read-only mode, KHO + * is not enabled for outgoing or incoming page ownership transfer and makes no + * changes to boot or struct page initialization. Instead, when subsystems + * retrieve preserved state, this backend traverses the KHO FDT, radix tree, + * and preserved metadata by mapping physical pages read-only (PAGE_KERNEL_RO) + * without modifying or freeing the KHO metadata or reclaiming the preserved + * pages. The KHO FDT and radix-tree page tables are mapped read-only lazily on + * first access and remain mapped for the lifetime of the kernel. + * + * Pages outside of KHO metadata can still be modified. For example, a + * preserved file descriptor can be retrieved while remaining in frozen mode: + * its layout cannot be changed (no resize, hole punch, or faulting in new + * memory), but the memory content of the file can still be modified. If a + * caller in LUO or a file handler needs read-write access to preserved pages, + * it can map them with PAGE_KERNEL via kho_ro_map_folio(). + */ + +#define pr_fmt(fmt) "KHO-RO: " fmt + +#include <linux/bitops.h> +#include <linux/cleanup.h> +#include <linux/err.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/ioport.h> +#include <linux/kexec_handover.h> +#include <linux/kho/abi/kexec_handover.h> +#include <linux/libfdt.h> +#include <linux/minmax.h> +#include <linux/mm.h> +#include <linux/mutex.h> +#include <linux/slab.h> +#include <linux/vmalloc.h> + +#include "kexec_handover_internal.h" + +struct kho_ro_radix_node { + const struct kho_radix_node *phys_node; + void *children[1 << KHO_TABLE_SIZE_LOG2]; +}; + +static bool kho_ro __ro_after_init; +static phys_addr_t kho_ro_fdt_phys __ro_after_init; +static DEFINE_MUTEX(kho_ro_lock); +static const void *kho_ro_fdt; +static struct kho_ro_radix_node *kho_ro_radix_root; + +/** + * kho_is_ro - Check if KHO is operating in read-only mode. + * + * Return: true if read-only KHO mode is active, false otherwise. + */ +bool kho_is_ro(void) +{ + return kho_ro; +} +EXPORT_SYMBOL_GPL(kho_is_ro); + +static int __init kho_parse_ro(char *p) +{ + unsigned long long pa; + int err; + + if (!p) + return -EINVAL; + + err = kstrtoull(p, 0, &pa); + if (err || !pa || !PAGE_ALIGNED(pa)) { + pr_err("Invalid kho_ro address '%s'\n", p); + return -EINVAL; + } + + kho_ro_fdt_phys = pa; + kho_ro = true; + pr_info("Read-only mode enabled with FDT at 0x%llx\n", pa); + return 0; +} +early_param("kho_ro", kho_parse_ro); + +static void *kho_ro_map(phys_addr_t pa, size_t size, pgprot_t prot) +{ + struct vm_struct *area; + unsigned long vaddr; + + if (!kho_ro || !pa || !size || !PAGE_ALIGNED(pa | size)) + return NULL; + + if (pa + size - 1 < pa) + return NULL; + + if (region_intersects(pa, size, IORESOURCE_SYSTEM_RAM, + IORES_DESC_NONE) != REGION_DISJOINT) { + pr_warn_once("Cannot map System RAM at %pa (size 0x%zx)\n", + &pa, size); + return NULL; + } + + area = get_vm_area_caller(size, VM_IOREMAP, __builtin_return_address(0)); + if (!area) + return NULL; + + vaddr = (unsigned long)area->addr; + area->phys_addr = pa; + + if (ioremap_page_range(vaddr, vaddr + size, pa, prot)) { + free_vm_area(area); + return NULL; + } + + return area->addr; +} + +/** + * kho_ro_unmap - Unmap a KHO read-only mode virtual mapping. + * @addr: Virtual address previously returned by kho_ro_map_folio(). + */ +void kho_ro_unmap(const void *addr) +{ + if (addr) + vunmap(addr); +} +EXPORT_SYMBOL_GPL(kho_ro_unmap); + +static const void *kho_ro_get_fdt(void) +{ + const void *fdt; + + if (!kho_ro || !kho_ro_fdt_phys) + return NULL; + + guard(mutex)(&kho_ro_lock); + if (kho_ro_fdt) + return kho_ro_fdt; + + fdt = kho_ro_map(kho_ro_fdt_phys, PAGE_SIZE, PAGE_KERNEL_RO); + if (!fdt) { + pr_err_once("Failed to map FDT at 0x%llx\n", + (unsigned long long)kho_ro_fdt_phys); + return NULL; + } + + if (!fdt_check_header(fdt) && fdt_totalsize(fdt) > PAGE_SIZE) { + pr_warn("handover FDT (0x%llx) totalsize %u exceeds PAGE_SIZE\n", + (unsigned long long)kho_ro_fdt_phys, fdt_totalsize(fdt)); + kho_ro_unmap(fdt); + return NULL; + } + + if (kho_check_fdt(fdt, kho_ro_fdt_phys)) { + kho_ro_unmap(fdt); + return NULL; + } + + kho_ro_fdt = fdt; + return fdt; +} + +static struct kho_ro_radix_node *kho_ro_alloc_radix_node(phys_addr_t pa) +{ + const struct kho_radix_node *phys_node; + struct kho_ro_radix_node *node; + + phys_node = kho_ro_map(pa, PAGE_SIZE, PAGE_KERNEL_RO); + if (!phys_node) + return NULL; + + node = kzalloc_obj(*node); + if (!node) { + kho_ro_unmap(phys_node); + return NULL; + } + + node->phys_node = phys_node; + return node; +} + +static bool kho_ro_radix_key_exists(struct kho_ro_radix_node *root, + unsigned long key) +{ + struct kho_ro_radix_node *node = root; + const struct kho_radix_leaf *leaf; + unsigned int i, idx; + + if (!node || unlikely(fls64(key) > KHO_RADIX_KEY_WIDTH)) + return false; + + for (i = KHO_TREE_MAX_DEPTH - 1; i > 0; i--) { + idx = kho_radix_get_table_index(key, i); + if (!node->children[idx]) { + phys_addr_t next_pa = node->phys_node->table[idx]; + + if (i == 1) + node->children[idx] = kho_ro_map(next_pa, PAGE_SIZE, + PAGE_KERNEL_RO); + else + node->children[idx] = kho_ro_alloc_radix_node(next_pa); + if (!node->children[idx]) + return false; + } + + if (i == 1) { + leaf = node->children[idx]; + return test_bit(kho_radix_get_bitmap_index(key), + leaf->bitmap); + } + + node = node->children[idx]; + } + + return false; +} + +/** + * kho_retrieve_subtree - Retrieve a preserved sub-blob by name from KHO root FDT. + * @name: Name of the subnode in the KHO root FDT. + * @phys: Output pointer for the physical address of the preserved sub-blob. + * @size: Optional output pointer for the size in bytes of the sub-blob. + * + * Return: 0 on success, or a negative errno on failure. + */ +int kho_retrieve_subtree(const char *name, phys_addr_t *phys, size_t *size) +{ + return __kho_retrieve_subtree(kho_ro_get_fdt(), name, phys, size); +} +EXPORT_SYMBOL_GPL(kho_retrieve_subtree); + +/** + * kho_ro_lookup_folio_order - Look up the preserved order of a physical page in KHO. + * @pa: Page-aligned physical address of the preserved folio. + * + * Queries the preserved KHO memory radix tree in read-only mode to determine + * the order at which @pa was preserved. + * + * Return: Non-negative page order on success, -ENOENT if @pa is not preserved, + * or -EOPNOTSUPP if read-only KHO is not active. + */ +int kho_ro_lookup_folio_order(phys_addr_t pa) +{ + unsigned int order, max_order; + const void *fdt; + + if (!kho_ro) + return -EOPNOTSUPP; + if (!pa || !PAGE_ALIGNED(pa)) + return -ENOENT; + + fdt = kho_ro_get_fdt(); + if (!fdt) + return -ENOENT; + + guard(mutex)(&kho_ro_lock); + if (!kho_ro_radix_root) { + kho_ro_radix_root = kho_ro_alloc_radix_node(kho_get_mem_map_phys(fdt)); + if (!kho_ro_radix_root) + return -ENOENT; + } + + max_order = min3((unsigned int)__ffs64(pa >> PAGE_SHIFT), + (unsigned int)(BITS_PER_LONG - PAGE_SHIFT - 1), + (unsigned int)(KHO_RADIX_KEY_WIDTH - 2)); + + for (order = 0; order <= max_order; order++) { + unsigned long key = kho_encode_radix_key(pa, order); + + if (kho_ro_radix_key_exists(kho_ro_radix_root, key)) + return (int)order; + } + + return -ENOENT; +} +EXPORT_SYMBOL_GPL(kho_ro_lookup_folio_order); + +/** + * kho_ro_map_folio - Map a preserved KHO folio into kernel virtual address space. + * @pa: Page-aligned physical address of the preserved folio. + * @prot: Page protection flags for the mapping (e.g. PAGE_KERNEL_RO or PAGE_KERNEL). + * + * Looks up @pa in the preserved KHO radix tree to determine its folio order + * and maps the entire folio (PAGE_SIZE << order) into kernel virtual address + * space with @prot permissions. Pair with kho_ro_unmap(). + * + * Return: Virtual pointer to the mapped folio on success, or NULL on failure. + */ +void *kho_ro_map_folio(phys_addr_t pa, pgprot_t prot) +{ + int order = kho_ro_lookup_folio_order(pa); + + if (order < 0) + return NULL; + + return kho_ro_map(pa, PAGE_SIZE << order, prot); +} +EXPORT_SYMBOL_GPL(kho_ro_map_folio); + +/** + * kho_restore_vmalloc - Reconstruct a preserved vmalloc region in read-only mode. + * @preservation: Pointer to the KHO vmalloc preservation metadata. + * + * Allocates a new vmalloc buffer and copies the preserved physical pages into + * it via temporary read-only mappings so that the caller can inspect and later + * vfree() the buffer without modifying or reclaiming the underlying preserved + * KHO pages. + * + * Return: Virtual pointer to the newly allocated copy on success, or NULL on + * failure. + */ +void *kho_restore_vmalloc(const struct kho_vmalloc *preservation) +{ + size_t total_size, contig_size, copied = 0; + const struct kho_vmalloc_chunk *chunk; + unsigned int order, vm_flags; + phys_addr_t chunk_pa; + void *buf; + + if (!kho_ro || !kho_vmalloc_validate(preservation, &vm_flags)) + return NULL; + + order = preservation->order; + contig_size = PAGE_SIZE << order; + total_size = preservation->total_pages * PAGE_SIZE; + + buf = vmalloc(total_size); + if (!buf) + return NULL; + + for (chunk_pa = preservation->first.phys; chunk_pa; ) { + unsigned int i; + + if (kho_ro_lookup_folio_order(chunk_pa) != 0) + goto err_free; + + chunk = kho_ro_map(chunk_pa, PAGE_SIZE, PAGE_KERNEL_RO); + if (!chunk) + goto err_free; + + for (i = 0; i < ARRAY_SIZE(chunk->phys) && chunk->phys[i]; i++) { + phys_addr_t phys = chunk->phys[i]; + const void *src; + + if (copied + contig_size > total_size || + kho_ro_lookup_folio_order(phys) != order) + goto err_unmap; + + src = kho_ro_map(phys, contig_size, PAGE_KERNEL_RO); + if (!src) + goto err_unmap; + + memcpy(buf + copied, src, contig_size); + kho_ro_unmap(src); + copied += contig_size; + } + + chunk_pa = chunk->hdr.next.phys; + if (chunk_pa && i < ARRAY_SIZE(chunk->phys)) + goto err_unmap; + + kho_ro_unmap(chunk); + } + + if (copied != total_size) + goto err_free; + + return buf; + +err_unmap: + kho_ro_unmap(chunk); +err_free: + vfree(buf); + return NULL; +} +EXPORT_SYMBOL_GPL(kho_restore_vmalloc); -- 2.56.0.176.g3575e65682-goog

