* [RFC PATCH v1 1/3] x86/virt/seamldr: Allocate 'struct seamldr_params' after image validation
2026-10-30 8:52 [RFC PATCH v1 0/3] TDX: P-SEAMLDR: Support large TDX modules and sigstructs Chao Gao
@ 2026-10-30 8:52 ` Chao Gao
2026-10-30 8:52 ` [RFC PATCH v1 2/3] x86/virt/seamldr: Support large TDX module binaries Chao Gao
2026-10-30 8:53 ` [RFC PATCH v1 3/3] x86/virt/seamldr: Support multiple sigstruct pages Chao Gao
2 siblings, 0 replies; 4+ messages in thread
From: Chao Gao @ 2026-10-30 8:52 UTC (permalink / raw)
To: linux-kernel, linux-coco, kvm
Cc: yilun.xu, arjan.van.de.ven, Chao Gao, Thomas Gleixner,
Ingo Molnar, Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Kiryl Shutsemau, Rick Edgecombe
'struct seamldr_params' is the in-memory ABI the kernel passes to P-SEAMLDR
to update the TDX module. It has a fixed size and is allocated before the
update image is validated.
To support larger TDX modules, the ABI was extended to let
'struct seamldr_params' link to extra pages. The number of extra pages
depends on the module page count, which can be trusted only after the image
header is validated.
Move the allocation after the image validation in init_seamldr_params(),
so a later change can allocate 'struct seamldr_params' and the extra
pages as one buffer. Rename the function to build_seamldr_params() since
it now allocates and returns a buffer rather than filling in one the
caller provides.
No functional change intended.
AI was used to review the code and workshop the logs. It suggested renaming
init_seamldr_params() and updating the comments above it and at its call
site.
Signed-off-by: Chao Gao <chao.gao@intel.com>
---
arch/x86/virt/vmx/tdx/seamldr.c | 40 ++++++++++++++++-----------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/arch/x86/virt/vmx/tdx/seamldr.c b/arch/x86/virt/vmx/tdx/seamldr.c
index b1137ca6150d..3513fd28536a 100644
--- a/arch/x86/virt/vmx/tdx/seamldr.c
+++ b/arch/x86/virt/vmx/tdx/seamldr.c
@@ -157,13 +157,14 @@ static void populate_seamldr_params(struct seamldr_params *params,
/*
* @image points to a vmalloc()'d 'struct tdx_image'. Transform
- * it into @params which is the P-SEAMLDR ABI format.
+ * it into a newly allocated 'struct seamldr_params', which is
+ * the P-SEAMLDR ABI format.
*/
-static int init_seamldr_params(struct seamldr_params *params,
- const struct tdx_image *image,
- u32 image_len)
+static struct seamldr_params *build_seamldr_params(const struct tdx_image *image,
+ u32 image_len)
{
const struct tdx_image_header *header = &image->header;
+ struct seamldr_params *params;
u32 sigstruct_len = header->sigstruct_nr_pages * PAGE_SIZE;
u32 module_len = header->module_nr_pages * PAGE_SIZE;
@@ -178,25 +179,29 @@ static int init_seamldr_params(struct seamldr_params *params,
/* Check the calculated payload size against the image size. */
if (TDX_IMAGE_HEADER_SIZE + sigstruct_len + module_len != image_len)
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
/* Reject unsupported tdx_image ABI versions. */
if (header->version != TDX_IMAGE_VERSION_2)
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
if (header->sigstruct_nr_pages > SEAMLDR_MAX_NR_SIG_PAGES ||
header->module_nr_pages > SEAMLDR_MAX_NR_MODULE_PAGES)
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
if (memcmp(header->signature, "TDX-BLOB", sizeof(header->signature)))
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
if (memchr_inv(header->reserved, 0, sizeof(header->reserved)))
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
+
+ params = kzalloc_obj(*params);
+ if (!params)
+ return ERR_PTR(-ENOMEM);
populate_seamldr_params(params, sigstruct_start, header->sigstruct_nr_pages,
module_start, header->module_nr_pages);
- return 0;
+ return params;
}
/*
@@ -321,7 +326,7 @@ int seamldr_install_module(const u8 *data, u32 data_len)
int ret;
/*
- * init_seamldr_params() reads the header early.
+ * build_seamldr_params() reads the header early.
* Ensure there is enough data to do at least that.
*/
if (data_len < TDX_IMAGE_HEADER_SIZE)
@@ -329,14 +334,10 @@ int seamldr_install_module(const u8 *data, u32 data_len)
image = (const struct tdx_image *)data;
- params = kzalloc_obj(*params);
- if (!params)
- return -ENOMEM;
-
- /* Populate 'params' from 'image'. */
- ret = init_seamldr_params(params, image, data_len);
- if (ret)
- goto out;
+ /* Build 'params' from 'image'. */
+ params = build_seamldr_params(image, data_len);
+ if (IS_ERR(params))
+ return PTR_ERR(params);
/* Ensure a stable set of online CPUs for the update process. */
cpus_read_lock();
@@ -345,7 +346,6 @@ int seamldr_install_module(const u8 *data, u32 data_len)
cpu_online_mask);
cpus_read_unlock();
-out:
kfree(params);
return ret;
}
--
2.52.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [RFC PATCH v1 2/3] x86/virt/seamldr: Support large TDX module binaries
2026-10-30 8:52 [RFC PATCH v1 0/3] TDX: P-SEAMLDR: Support large TDX modules and sigstructs Chao Gao
2026-10-30 8:52 ` [RFC PATCH v1 1/3] x86/virt/seamldr: Allocate 'struct seamldr_params' after image validation Chao Gao
@ 2026-10-30 8:52 ` Chao Gao
2026-10-30 8:53 ` [RFC PATCH v1 3/3] x86/virt/seamldr: Support multiple sigstruct pages Chao Gao
2 siblings, 0 replies; 4+ messages in thread
From: Chao Gao @ 2026-10-30 8:52 UTC (permalink / raw)
To: linux-kernel, linux-coco, kvm
Cc: yilun.xu, arjan.van.de.ven, Chao Gao, Thomas Gleixner,
Ingo Molnar, Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Kiryl Shutsemau, Rick Edgecombe
The module binary is handed to P-SEAMLDR as a list of page physical
addresses in 'struct seamldr_params'. The list has 496 entries, which
caps a module at 496 pages.
TDX modules are gaining features and are about to outgrow that limit.
The P-SEAMLDR ABI was extended for this. An upcoming revision of the
SEAMLDR specification adds the following to the description of the
module page address list in 'struct seamldr_params'.
For TDX module image with larger than 495 pages, the last entry will
point to a new 4 KB page that contains up to 511 additional HPAs. Each
page will use its last HPA to reference another page if required. This
chain continues until all module pages are referenced.
Allocate the needed chain pages together with 'struct seamldr_params',
as a flexible array at its end. Link them up at allocation time. Then
fill in the module page addresses as before, but skip the link entries.
A single allocation leaves nothing to unwind on failure and keeps the
existing kfree() as the only cleanup.
Do not query P-SEAMLDR for chaining support. Userspace selects the update
image and is responsible for ensuring the module is compatible with
P-SEAMLDR.
Chaining removes the 496 page cap, but the module page count still needs
a bound. Without one, a crafted header can overflow the u32 module length
('module_nr_pages * PAGE_SIZE') and pass the image size check. It can
also make the chain page allocation exceed KMALLOC_MAX_SIZE, which warns
in the page allocator.
Raise SEAMLDR_MAX_NR_MODULE_PAGES to a 1GB software limit to rule out
both problems. 1GB is far above the few MB of current modules.
The size of module_pages_pa_list[] no longer reflects the maximum module
page count. Define a new SEAMLDR_NR_MODULE_PA_ENTRIES for it.
Thanks to Yilun for the idea of allocating the chain pages together
with 'struct seamldr_params'.
AI was used under supervision to review code and workshop logs. It
spotted both problems that the software limit addresses.
Signed-off-by: Chao Gao <chao.gao@intel.com>
---
arch/x86/virt/vmx/tdx/seamldr.c | 95 +++++++++++++++++++++++++++++++--
1 file changed, 90 insertions(+), 5 deletions(-)
diff --git a/arch/x86/virt/vmx/tdx/seamldr.c b/arch/x86/virt/vmx/tdx/seamldr.c
index 3513fd28536a..09c8f6e30e9f 100644
--- a/arch/x86/virt/vmx/tdx/seamldr.c
+++ b/arch/x86/virt/vmx/tdx/seamldr.c
@@ -24,7 +24,14 @@
#define P_SEAMLDR_INFO 0x8000000000000000
#define P_SEAMLDR_INSTALL 0x8000000000000001
-#define SEAMLDR_MAX_NR_MODULE_PAGES 496
+/*
+ * Arbitrary software limit, far above the few MB of current modules.
+ * It keeps the module length within a u32, and 'struct seamldr_params'
+ * plus its chain pages within KMALLOC_MAX_SIZE.
+ */
+#define SEAMLDR_MAX_NR_MODULE_PAGES (SZ_1G / PAGE_SIZE)
+
+#define SEAMLDR_NR_MODULE_PA_ENTRIES 496
#define SEAMLDR_MAX_NR_SIG_PAGES 1
/*
@@ -34,6 +41,19 @@
*/
#define SEAMLDR_SCENARIO_UPDATE 1
+#define SEAMLDR_NR_CHAIN_PA_ENTRIES (PAGE_SIZE / sizeof(u64) - 1)
+/*
+ * A 4KB page holding the physical addresses of the module pages that do
+ * not fit in module_pages_pa_list[]. @next_page_pa points to the next
+ * page, forming a singly linked list.
+ */
+struct chain_page {
+ u64 pa[SEAMLDR_NR_CHAIN_PA_ENTRIES];
+ u64 next_page_pa;
+} __packed;
+
+static_assert(sizeof(struct chain_page) == 4096);
+
/*
* This is the "SEAMLDR_PARAMS" data structure defined in the
* "SEAM Loader (SEAMLDR) Interface Specification".
@@ -48,10 +68,13 @@ struct seamldr_params {
u64 sigstruct_pages_pa_list[SEAMLDR_MAX_NR_SIG_PAGES];
u8 reserved[104];
u64 module_nr_pages;
- u64 module_pages_pa_list[SEAMLDR_MAX_NR_MODULE_PAGES];
+ u64 module_pages_pa_list[SEAMLDR_NR_MODULE_PA_ENTRIES];
+
+ /* Hold module pages that cannot fit in module_pages_pa_list[]. */
+ struct chain_page chain_pages[];
} __packed;
-static_assert(sizeof(struct seamldr_params) == 4096);
+static_assert(offsetof(struct seamldr_params, chain_pages) == 4096);
/*
* Serialize P-SEAMLDR calls since the hardware only allows a single CPU to
@@ -143,6 +166,68 @@ static void populate_pa_list(u64 *pa_list, const u8 *vmalloc_addr, u32 vmalloc_l
}
}
+/*
+ * Like populate_pa_list(), but skip entries pointing to the next page.
+ */
+static void populate_module_pa_list(struct seamldr_params *params,
+ const u8 *vmalloc_addr, u32 vmalloc_len_pages)
+{
+ u64 *pa_list = params->module_pages_pa_list;
+ int i, j;
+
+ for (i = 0, j = 0; i < vmalloc_len_pages; i++, j++) {
+ unsigned long offset = i * PAGE_SIZE;
+ unsigned long pfn = vmalloc_to_pfn(&vmalloc_addr[offset]);
+
+ /*
+ * Skip non-zero entries. They point to the next page and
+ * are populated during allocation.
+ */
+ if (pa_list[j])
+ j++;
+
+ pa_list[j] = pfn << PAGE_SHIFT;
+ }
+}
+
+static struct seamldr_params *alloc_seamldr_params(u32 nr_module_pages)
+{
+ struct seamldr_params *params;
+ u32 nr_chain_pages;
+ u32 nr_direct;
+ u64 *link;
+
+ /*
+ * Number of module pages that fit directly in
+ * module_pages_pa_list[]. Minus one as the last entry is
+ * reserved to point to the first chain page.
+ */
+ nr_direct = SEAMLDR_NR_MODULE_PA_ENTRIES - 1;
+
+ /* Calculate the number of extra chain pages needed. */
+ nr_chain_pages = 0;
+ if (nr_module_pages > nr_direct) {
+ u32 nr_left = nr_module_pages - nr_direct;
+
+ nr_chain_pages = DIV_ROUND_UP(nr_left, SEAMLDR_NR_CHAIN_PA_ENTRIES);
+ }
+
+ params = kzalloc_flex(*params, chain_pages, nr_chain_pages);
+ if (!params)
+ return NULL;
+
+ /* Link all the chain pages. */
+ link = ¶ms->module_pages_pa_list[nr_direct];
+ for (int i = 0; i < nr_chain_pages; i++) {
+ struct chain_page *page = ¶ms->chain_pages[i];
+
+ *link = __pa(page);
+ link = &page->next_page_pa;
+ }
+
+ return params;
+}
+
static void populate_seamldr_params(struct seamldr_params *params,
const u8 *sig, u32 sig_nr_pages,
const u8 *mod, u32 mod_nr_pages)
@@ -152,7 +237,7 @@ static void populate_seamldr_params(struct seamldr_params *params,
params->module_nr_pages = mod_nr_pages;
populate_pa_list(params->sigstruct_pages_pa_list, sig, sig_nr_pages);
- populate_pa_list(params->module_pages_pa_list, mod, mod_nr_pages);
+ populate_module_pa_list(params, mod, mod_nr_pages);
}
/*
@@ -195,7 +280,7 @@ static struct seamldr_params *build_seamldr_params(const struct tdx_image *image
if (memchr_inv(header->reserved, 0, sizeof(header->reserved)))
return ERR_PTR(-EINVAL);
- params = kzalloc_obj(*params);
+ params = alloc_seamldr_params(header->module_nr_pages);
if (!params)
return ERR_PTR(-ENOMEM);
--
2.52.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [RFC PATCH v1 3/3] x86/virt/seamldr: Support multiple sigstruct pages
2026-10-30 8:52 [RFC PATCH v1 0/3] TDX: P-SEAMLDR: Support large TDX modules and sigstructs Chao Gao
2026-10-30 8:52 ` [RFC PATCH v1 1/3] x86/virt/seamldr: Allocate 'struct seamldr_params' after image validation Chao Gao
2026-10-30 8:52 ` [RFC PATCH v1 2/3] x86/virt/seamldr: Support large TDX module binaries Chao Gao
@ 2026-10-30 8:53 ` Chao Gao
2 siblings, 0 replies; 4+ messages in thread
From: Chao Gao @ 2026-10-30 8:53 UTC (permalink / raw)
To: linux-kernel, linux-coco, kvm
Cc: yilun.xu, arjan.van.de.ven, Chao Gao, Thomas Gleixner,
Ingo Molnar, Borislav Petkov, Dave Hansen, x86, H. Peter Anvin,
Kiryl Shutsemau, Rick Edgecombe
Each TDX module update image carries a sigstruct, which is passed along
with the module binary to P-SEAMLDR. 'struct seamldr_params' has a single
physical address slot for it, so a sigstruct can be at most one 4KB page.
TDX modules are moving to a post-quantum signature algorithm. Its
signatures are far larger than the ones in use today, so the sigstruct
no longer fits in 4KB.
The P-SEAMLDR ABI was extended for this. It now has four sigstruct
address slots instead of one, for sigstructs of up to 16KB. A new
'version' value of 1 tells P-SEAMLDR that this layout is in use.
Extend 'struct seamldr_params' to match the new ABI.
Set 'version' to 1 only when the sigstruct spans more than one page. Old
P-SEAMLDRs reject version 1, so this keeps updates with a single-page
sigstruct working on them.
Do not query P-SEAMLDR for version 1 support. Userspace selects the
update image and is responsible for ensuring the module is compatible
with P-SEAMLDR.
AI was used under supervision to review code and workshop logs
Signed-off-by: Chao Gao <chao.gao@intel.com>
---
arch/x86/virt/vmx/tdx/seamldr.c | 21 ++++++++++++++++++---
1 file changed, 18 insertions(+), 3 deletions(-)
diff --git a/arch/x86/virt/vmx/tdx/seamldr.c b/arch/x86/virt/vmx/tdx/seamldr.c
index 09c8f6e30e9f..04681e67072f 100644
--- a/arch/x86/virt/vmx/tdx/seamldr.c
+++ b/arch/x86/virt/vmx/tdx/seamldr.c
@@ -32,7 +32,15 @@
#define SEAMLDR_MAX_NR_MODULE_PAGES (SZ_1G / PAGE_SIZE)
#define SEAMLDR_NR_MODULE_PA_ENTRIES 496
-#define SEAMLDR_MAX_NR_SIG_PAGES 1
+#define SEAMLDR_MAX_NR_SIG_PAGES 4
+
+/*
+ * The seamldr_params "version" field specifies the layout of the
+ * structure. Version 0 supports only 1 sigstruct page while version 1
+ * supports up to 4.
+ */
+#define SEAMLDR_PARAMS_VERSION_0 0
+#define SEAMLDR_PARAMS_VERSION_1 1
/*
* The seamldr_params "scenario" field specifies the operation mode:
@@ -66,7 +74,7 @@ struct seamldr_params {
u32 version;
u32 scenario;
u64 sigstruct_pages_pa_list[SEAMLDR_MAX_NR_SIG_PAGES];
- u8 reserved[104];
+ u8 reserved[80];
u64 module_nr_pages;
u64 module_pages_pa_list[SEAMLDR_NR_MODULE_PA_ENTRIES];
@@ -232,7 +240,14 @@ static void populate_seamldr_params(struct seamldr_params *params,
const u8 *sig, u32 sig_nr_pages,
const u8 *mod, u32 mod_nr_pages)
{
- params->version = 0;
+ params->version = SEAMLDR_PARAMS_VERSION_0;
+ /*
+ * Use version 1 only for multi-page sigstructs, to stay
+ * compatible with old P-SEAMLDRs that lack version 1 support.
+ */
+ if (sig_nr_pages > 1)
+ params->version = SEAMLDR_PARAMS_VERSION_1;
+
params->scenario = SEAMLDR_SCENARIO_UPDATE;
params->module_nr_pages = mod_nr_pages;
--
2.52.0
^ permalink raw reply [flat|nested] 4+ messages in thread