[PATCH v2 3/9] kho: validate preservation metadata in kho_restore_vmalloc()
From: Pasha Tatashin
Date: Sun Oct 25 2026 - 13:44:29 EST
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@xxxxxxxxxx>
---
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