* [PATCH v10 25/25] x86/virt/tdx: Document TDX module update
2026-05-20 13:38 [PATCH v10 00/25] Runtime TDX module update support Chao Gao
@ 2026-05-20 13:38 ` Chao Gao
2026-05-20 13:46 ` [PATCH v10 00/25] Runtime TDX module update support Chao Gao
2026-05-20 19:42 ` Dave Hansen
2 siblings, 0 replies; 4+ messages in thread
From: Chao Gao @ 2026-05-20 13:38 UTC (permalink / raw)
To: kvm, linux-coco, linux-kernel, linux-doc
Cc: binbin.wu, dave.hansen, djbw, ira.weiny, kai.huang, kas,
nik.borisov, paulmck, pbonzini, reinette.chatre, rick.p.edgecombe,
sagis, seanjc, tony.lindgren, vannapurve, vishal.l.verma,
yilun.xu, xiaoyao.li, yan.y.zhao, Chao Gao, Thomas Gleixner,
Ingo Molnar, Borislav Petkov, x86, H. Peter Anvin,
Jonathan Corbet, Shuah Khan
Document TDX module update as a subsection of "TDX Host Kernel Support" to
provide background information and cover key points that developers and
users may need to know, for example:
- update is done in stop_machine() context
- update instructions and results
- update policy and tooling
Signed-off-by: Chao Gao <chao.gao@intel.com>
Reviewed-by: Kai Huang <kai.huang@intel.com>
Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
Reviewed-by: Rick Edgecombe <rick.p.edgecombe@intel.com>
---
Documentation/arch/x86/tdx.rst | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/Documentation/arch/x86/tdx.rst b/Documentation/arch/x86/tdx.rst
index 1a3b5bac1021..9d2b7db166b5 100644
--- a/Documentation/arch/x86/tdx.rst
+++ b/Documentation/arch/x86/tdx.rst
@@ -73,6 +73,40 @@ initialize::
[..] virt/tdx: TDX-Module initialization failed ...
+TDX module Runtime Update
+-------------------------
+
+The TDX architecture includes a persistent SEAM loader (P-SEAMLDR) that
+runs in SEAM mode separately from the TDX module. The kernel can
+communicate with P-SEAMLDR to perform runtime updates of the TDX module.
+
+During updates, the TDX module becomes unresponsive to other TDX
+operations. To prevent components using TDX (such as KVM) from
+experiencing unexpected errors during updates, updates are performed in
+stop_machine() context.
+
+TDX module updates have complex compatibility requirements; the new module
+must be compatible with the current CPU, P-SEAMLDR, and running TDX module.
+Rather than implementing complex module selection and policy enforcement
+logic in the kernel, userspace is responsible for auditing and selecting
+appropriate updates.
+
+Updates use the standard firmware upload interface. See
+Documentation/driver-api/firmware/fw_upload.rst for detailed instructions.
+
+If updates failed, running TDs may be killed and further TDX operations may
+not be possible until reboot. For detailed error information, see
+Documentation/ABI/testing/sysfs-devices-faux-tdx-host.
+
+Given the risk of losing existing TDs, userspace should verify that the
+update is compatible with the current system and properly validated before
+applying it.
+
+A reference userspace tool that implements necessary checks is available
+at:
+
+ https://github.com/intel/tdx-module-binaries
+
TDX Interaction to Other Kernel Components
------------------------------------------
--
2.52.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH v10 00/25] Runtime TDX module update support
2026-05-20 13:38 [PATCH v10 00/25] Runtime TDX module update support Chao Gao
2026-05-20 13:38 ` [PATCH v10 25/25] x86/virt/tdx: Document TDX module update Chao Gao
@ 2026-05-20 13:46 ` Chao Gao
2026-05-20 19:42 ` Dave Hansen
2 siblings, 0 replies; 4+ messages in thread
From: Chao Gao @ 2026-05-20 13:46 UTC (permalink / raw)
To: kvm, linux-coco, x86, linux-kernel, linux-rt-devel, linux-doc
Cc: binbin.wu, dave.hansen, djbw, ira.weiny, kai.huang, kas,
nik.borisov, paulmck, pbonzini, reinette.chatre, rick.p.edgecombe,
sagis, seanjc, tony.lindgren, vannapurve, vishal.l.verma,
yilun.xu, xiaoyao.li, yan.y.zhao, Thomas Gleixner, Ingo Molnar,
Borislav Petkov, H. Peter Anvin, Sebastian Andrzej Siewior,
Clark Williams, Steven Rostedt, Jonathan Corbet, Shuah Khan
On Wed, May 20, 2026 at 06:38:03AM -0700, Chao Gao wrote:
>Hi Dave & Rick,
>
>Thanks for your thorough review of v9. This v10 addresses the issues you
>pointed out. The main changes in this version are polishing changelogs
>and variable renames to improve readability. Specifically:
>
> - Patches 1-2 (new): Split the original "Consolidate TDX global
> initialization states" into two steps — first move the statics to
> file scope, then clarify the result-caching logic in
> try_init_module_global().
> - Patch 6: Removed user-facing Kconfig help text for TDX_HOST_SERVICES
> (now a silent tristate auto-selected by INTEL_TDX_HOST).
> - Patch 13: Renamed "size" to "data_len" in seamldr_install_module()
> and init_seamldr_params(); renamed "HEADER_SIZE" to
> "TDX_IMAGE_HEADER_SIZE"; renamed "primary" to "is_lead_cpu" in the
> update state machine.
> - Patch 13: Added early data_len validation and explicit bounds checks
> on sigstruct_nr_pages/module_nr_pages against SEAMLDR_MAX_NR_*
> limits, removing the implicit clamping in populate_pa_list().
> - Patch 22: Fixed BIT(16) -> BIT_ULL(16) for
> TDX_SYS_SHUTDOWN_AVOID_COMPAT_SENSITIVE.
> - Patch 22: Removed unused TDX_FEATURES0_UPDATE_COMPAT definition.
> - Various patches: Shortened sysfs ABI descriptions, tightened
> comments across seamldr.h and seamldr.c, and minor style fixes
> (return 0 -> return false, unfolded conditionals)
FYI, below is the diff between v9 and v10:
diff --git a/Documentation/ABI/testing/sysfs-devices-faux-tdx-host b/Documentation/ABI/testing/sysfs-devices-faux-tdx-host
index 9e08db231da1..5f18ac972468 100644
--- a/Documentation/ABI/testing/sysfs-devices-faux-tdx-host
+++ b/Documentation/ABI/testing/sysfs-devices-faux-tdx-host
@@ -1,16 +1,14 @@
What: /sys/devices/faux/tdx_host/version
Contact: linux-coco@lists.linux.dev
-Description: (RO) Report the version of the loaded TDX module. The TDX module
- version is formatted as x.y.z, where "x" is the major version,
- "y" is the minor version and "z" is the update version. Versions
- are used for bug reporting, TDX module updates etc.
+Description: (RO) Report the version of the loaded TDX module.
+ Formatted as "major.minor.update". Used by TDX module
+ update tooling. Example: "1.2.03".
What: /sys/devices/faux/tdx_host/seamldr_version
Contact: linux-coco@lists.linux.dev
-Description: (RO) Report the version of the loaded P-SEAMLDR. The P-SEAMLDR
- version is formatted as x.y.z, where "x" is the major version,
- "y" is the minor version and "z" is the update version. Versions
- are used for bug reporting and compatibility checks.
+Description: (RO) Report the version of the loaded P-SEAMLDR.
+ Formatted as a TDX module version. Used by TDX module
+ update tooling.
What: /sys/devices/faux/tdx_host/num_remaining_updates
Contact: linux-coco@lists.linux.dev
diff --git a/arch/x86/include/asm/seamldr.h b/arch/x86/include/asm/seamldr.h
index ac6f80f7208b..43084e2daa2d 100644
--- a/arch/x86/include/asm/seamldr.h
+++ b/arch/x86/include/asm/seamldr.h
@@ -5,11 +5,10 @@
#include <linux/types.h>
/*
- * This is called the "SEAMLDR_INFO" data structure and is defined
- * in "SEAM Loader (SEAMLDR) Interface Specification".
+ * This is the "SEAMLDR_INFO" data structure defined in the
+ * "SEAM Loader (SEAMLDR) Interface Specification".
*
- * The SEAMLDR.INFO documentation requires this to be aligned to a
- * 256-byte boundary.
+ * Must be aligned to a 256-byte boundary.
*/
struct seamldr_info {
u32 version;
@@ -32,6 +31,6 @@ struct seamldr_info {
static_assert(sizeof(struct seamldr_info) == 256);
int seamldr_get_info(struct seamldr_info *seamldr_info);
-int seamldr_install_module(const u8 *data, u32 size);
+int seamldr_install_module(const u8 *data, u32 data_len);
#endif /* _ASM_X86_SEAMLDR_H */
diff --git a/arch/x86/include/asm/tdx.h b/arch/x86/include/asm/tdx.h
index ac042b369843..c848483d815f 100644
--- a/arch/x86/include/asm/tdx.h
+++ b/arch/x86/include/asm/tdx.h
@@ -36,7 +36,6 @@
/* Bit definitions of TDX_FEATURES0 metadata field */
#define TDX_FEATURES0_TD_PRESERVING BIT_ULL(1)
#define TDX_FEATURES0_NO_RBP_MOD BIT_ULL(18)
-#define TDX_FEATURES0_UPDATE_COMPAT BIT_ULL(47)
#ifndef __ASSEMBLER__
diff --git a/arch/x86/virt/vmx/tdx/seamldr.c b/arch/x86/virt/vmx/tdx/seamldr.c
index 6a39c9e3ef7d..ff95d8dd1162 100644
--- a/arch/x86/virt/vmx/tdx/seamldr.c
+++ b/arch/x86/virt/vmx/tdx/seamldr.c
@@ -32,10 +32,12 @@
#define SEAMLDR_SCENARIO_UPDATE 1
/*
- * This is called the "SEAMLDR_PARAMS" data structure and is defined
- * in "SEAM Loader (SEAMLDR) Interface Specification".
+ * This is the "SEAMLDR_PARAMS" data structure defined in the
+ * "SEAM Loader (SEAMLDR) Interface Specification".
*
- * It describes the TDX module that will be installed.
+ * It is the in-memory ABI that the kernel passes to the P-SEAMLDR
+ * to update the TDX module. It breaks the TDX module image up in
+ * page-size pieces.
*/
struct seamldr_params {
u32 version;
@@ -87,7 +89,7 @@ static int seamldr_install(const struct seamldr_params *params)
#define TDX_IMAGE_VERSION_2 0x200
struct tdx_image_header {
- u16 version; // This ABI is always 0x200
+ u16 version;
u16 checksum;
u8 signature[8];
u32 sigstruct_nr_pages;
@@ -95,23 +97,28 @@ struct tdx_image_header {
u8 reserved[4076];
} __packed;
-#define HEADER_SIZE sizeof(struct tdx_image_header)
-static_assert(HEADER_SIZE == 4096);
+#define TDX_IMAGE_HEADER_SIZE sizeof(struct tdx_image_header)
+static_assert(TDX_IMAGE_HEADER_SIZE == 4096);
-/* Intel TDX module update ABI structure. aka. "TDX module blob". */
+/*
+ * Intel TDX module update ABI structure. aka. "TDX module blob".
+ *
+ * @payload contains sigstruct pages followed by module pages.
+ */
struct tdx_image {
struct tdx_image_header header;
- u8 payload[]; // Contains sigstruct pages followed by module pages
+ u8 payload[];
};
-static void populate_pa_list(u64 *pa_list, u32 max_entries, const u8 *start, u32 nr_pages)
+static void populate_pa_list(u64 *pa_list, const u8 *vmalloc_addr, u32 vmalloc_len_pages)
{
int i;
- nr_pages = MIN(nr_pages, max_entries);
- for (i = 0; i < nr_pages; i++) {
- pa_list[i] = vmalloc_to_pfn(start) << PAGE_SHIFT;
- start += PAGE_SIZE;
+ for (i = 0; i < vmalloc_len_pages; i++) {
+ unsigned long offset = i * PAGE_SIZE;
+ unsigned long pfn = vmalloc_to_pfn(&vmalloc_addr[offset]);
+
+ pa_list[i] = pfn << PAGE_SHIFT;
}
}
@@ -123,39 +130,43 @@ static void populate_seamldr_params(struct seamldr_params *params,
params->scenario = SEAMLDR_SCENARIO_UPDATE;
params->module_nr_pages = mod_nr_pages;
- populate_pa_list(params->sigstruct_pages_pa_list, SEAMLDR_MAX_NR_SIG_PAGES,
- sig, sig_nr_pages);
- populate_pa_list(params->module_pages_pa_list, SEAMLDR_MAX_NR_MODULE_PAGES,
- mod, 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);
}
-static int init_seamldr_params(struct seamldr_params *params, const u8 *data, u32 size)
+/*
+ * @image points to a vmalloc()'d 'struct tdx_image'. Transform
+ * it into @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)
{
- const struct tdx_image *image = (const void *)data;
const struct tdx_image_header *header = &image->header;
u32 sigstruct_len = header->sigstruct_nr_pages * PAGE_SIZE;
u32 module_len = header->module_nr_pages * PAGE_SIZE;
u8 *header_start = (u8 *)header;
- u8 *header_end = header_start + HEADER_SIZE;
+ u8 *header_end = header_start + TDX_IMAGE_HEADER_SIZE;
u8 *sigstruct_start = header_end;
u8 *sigstruct_end = sigstruct_start + sigstruct_len;
u8 *module_start = sigstruct_end;
- /* Check the calculated payload size against the data size. */
- if (HEADER_SIZE + sigstruct_len + module_len != size)
+ /* Check the calculated payload size against the image size. */
+ if (TDX_IMAGE_HEADER_SIZE + sigstruct_len + module_len != image_len)
return -EINVAL;
- /*
- * Don't care about user passing the wrong file, but protect
- * kernel ABI by preventing accepting garbage.
- */
+ /* Reject unsupported tdx_image ABI versions. */
if (header->version != TDX_IMAGE_VERSION_2)
return -EINVAL;
+ if (header->sigstruct_nr_pages > SEAMLDR_MAX_NR_SIG_PAGES ||
+ header->module_nr_pages > SEAMLDR_MAX_NR_MODULE_PAGES)
+ return -EINVAL;
+
if (memcmp(header->signature, "TDX-BLOB", sizeof(header->signature)))
return -EINVAL;
@@ -163,7 +174,7 @@ static int init_seamldr_params(struct seamldr_params *params, const u8 *data, u3
return -EINVAL;
populate_seamldr_params(params, sigstruct_start, header->sigstruct_nr_pages,
- module_start, header->module_nr_pages);
+ module_start, header->module_nr_pages);
return 0;
}
@@ -230,14 +241,14 @@ static int do_seamldr_install_module(void *seamldr_params)
{
enum module_update_state newstate, curstate = MODULE_UPDATE_START;
int cpu = smp_processor_id();
- bool primary;
+ bool is_lead_cpu;
int ret = 0;
/*
- * Use CPU 0 to execute update steps that must run exactly once.
- * Note CPU 0 is always online.
+ * Some steps must be run on exactly one CPU. Pick a "lead" CPU to
+ * execute those steps. Use CPU 0 because it is always online.
*/
- primary = cpu == 0;
+ is_lead_cpu = cpu == 0;
do {
newstate = READ_ONCE(update_ctrl.state);
@@ -250,7 +261,7 @@ static int do_seamldr_install_module(void *seamldr_params)
curstate = newstate;
switch (curstate) {
case MODULE_UPDATE_SHUTDOWN:
- if (primary)
+ if (is_lead_cpu)
ret = tdx_module_shutdown();
break;
case MODULE_UPDATE_CPU_INSTALL:
@@ -260,7 +271,7 @@ static int do_seamldr_install_module(void *seamldr_params)
ret = tdx_cpu_enable();
break;
case MODULE_UPDATE_RUN_UPDATE:
- if (primary)
+ if (is_lead_cpu)
ret = tdx_module_run_update();
break;
default:
@@ -276,20 +287,27 @@ static int do_seamldr_install_module(void *seamldr_params)
/**
* seamldr_install_module - Install a new TDX module.
* @data: Pointer to the TDX module image.
- * @size: Size of the TDX module image.
+ * @data_len: Size of the TDX module image.
*
* Returns 0 on success, negative error code on failure.
*/
-int seamldr_install_module(const u8 *data, u32 size)
+int seamldr_install_module(const u8 *data, u32 data_len)
{
struct seamldr_params *params;
+ const struct tdx_image *image;
int ret;
+ if (data_len < TDX_IMAGE_HEADER_SIZE)
+ return -EINVAL;
+
+ image = (const struct tdx_image *)data;
+
params = kzalloc_obj(*params);
if (!params)
return -ENOMEM;
- ret = init_seamldr_params(params, data, size);
+ /* Populate 'params' from 'image'. */
+ ret = init_seamldr_params(params, image, data_len);
if (ret)
goto out;
diff --git a/arch/x86/virt/vmx/tdx/tdx.c b/arch/x86/virt/vmx/tdx/tdx.c
index 2ab6f6efe6d1..0c5660c9ab45 100644
--- a/arch/x86/virt/vmx/tdx/tdx.c
+++ b/arch/x86/virt/vmx/tdx/tdx.c
@@ -69,6 +69,8 @@ static LIST_HEAD(tdx_memlist);
static struct tdx_sys_info tdx_sysinfo;
+static DEFINE_RAW_SPINLOCK(sysinit_lock);
+
/*
* Do the module global initialization once and return its result.
* It can be done on any cpu, and from task or IRQ context.
@@ -76,29 +78,34 @@ static struct tdx_sys_info tdx_sysinfo;
static int try_init_module_global(void)
{
struct tdx_module_args args = {};
- static DEFINE_RAW_SPINLOCK(sysinit_lock);
+ int ret;
raw_spin_lock(&sysinit_lock);
- if (tdx_module_state.sysinit_done)
+ /* Return the "cached" return code. */
+ if (tdx_module_state.sysinit_done) {
+ ret = tdx_module_state.sysinit_ret;
goto out;
+ }
/* RCX is module attributes and all bits are reserved */
args.rcx = 0;
- tdx_module_state.sysinit_ret = seamcall_prerr(TDH_SYS_INIT, &args);
+ ret = seamcall_prerr(TDH_SYS_INIT, &args);
/*
* The first SEAMCALL also detects the TDX module, thus
* it can fail due to the TDX module is not loaded.
* Dump message to let the user know.
*/
- if (tdx_module_state.sysinit_ret == -ENODEV)
+ if (ret == -ENODEV)
pr_err("module not loaded\n");
+ /* Save the return code for later callers. */
tdx_module_state.sysinit_done = true;
+ tdx_module_state.sysinit_ret = ret;
out:
raw_spin_unlock(&sysinit_lock);
- return tdx_module_state.sysinit_ret;
+ return ret;
}
/**
@@ -1267,7 +1274,7 @@ static __init int tdx_enable(void)
}
subsys_initcall(tdx_enable);
-#define TDX_SYS_SHUTDOWN_AVOID_COMPAT_SENSITIVE BIT(16)
+#define TDX_SYS_SHUTDOWN_AVOID_COMPAT_SENSITIVE BIT_ULL(16)
int tdx_module_shutdown(void)
{
diff --git a/drivers/virt/coco/tdx-host/Kconfig b/drivers/virt/coco/tdx-host/Kconfig
index ca600a39d97b..57d0c01a4357 100644
--- a/drivers/virt/coco/tdx-host/Kconfig
+++ b/drivers/virt/coco/tdx-host/Kconfig
@@ -1,12 +1,6 @@
config TDX_HOST_SERVICES
- tristate "TDX Host Services Driver"
+ tristate
depends on INTEL_TDX_HOST
select FW_LOADER
select FW_UPLOAD
default m
- help
- Enable access to TDX host services like module update and
- extensions (e.g. TDX Connect).
-
- Say y or m if enabling support for confidential virtual machine
- support (CONFIG_INTEL_TDX_HOST). The module is called tdx_host.ko.
diff --git a/drivers/virt/coco/tdx-host/tdx-host.c b/drivers/virt/coco/tdx-host/tdx-host.c
index ad116e56aa1a..291464490fe0 100644
--- a/drivers/virt/coco/tdx-host/tdx-host.c
+++ b/drivers/virt/coco/tdx-host/tdx-host.c
@@ -76,6 +76,10 @@ static ssize_t num_remaining_updates_show(struct device *dev,
return sysfs_emit(buf, "%u\n", info.num_remaining_updates);
}
+/*
+ * These attributes are intended for managing TDX module updates. Reading
+ * them issues a slow, serialized P-SEAMLDR query, so keep them admin-only.
+ */
static DEVICE_ATTR_ADMIN_RO(seamldr_version);
static DEVICE_ATTR_ADMIN_RO(num_remaining_updates);
@@ -90,7 +94,10 @@ static bool supports_runtime_update(void)
const struct tdx_sys_info *sysinfo = tdx_get_sysinfo();
if (!sysinfo)
- return 0;
+ return false;
+
+ if (!tdx_supports_runtime_update(sysinfo))
+ return false;
/*
* Calling P-SEAMLDR on CPUs with the seamret_invd_vmcs bug clears
@@ -98,14 +105,17 @@ static bool supports_runtime_update(void)
* present before exposing P-SEAMLDR features.
*/
if (boot_cpu_has_bug(X86_BUG_SEAMRET_INVD_VMCS))
- return 0;
+ return false;
- return tdx_supports_runtime_update(sysinfo);
+ return true;
}
static umode_t seamldr_group_visible(struct kobject *kobj, struct attribute *attr, int idx)
{
- return supports_runtime_update() ? attr->mode : 0;
+ if (!supports_runtime_update())
+ return 0;
+
+ return attr->mode;
}
static const struct attribute_group seamldr_group = {
@@ -120,20 +130,20 @@ static const struct attribute_group *tdx_host_groups[] = {
};
static enum fw_upload_err tdx_fw_prepare(struct fw_upload *fwl,
- const u8 *data, u32 size)
+ const u8 *data, u32 data_len)
{
return FW_UPLOAD_ERR_NONE;
}
static enum fw_upload_err tdx_fw_write(struct fw_upload *fwl, const u8 *data,
- u32 offset, u32 size, u32 *written)
+ u32 offset, u32 data_len, u32 *written)
{
int ret;
- ret = seamldr_install_module(data, size);
+ ret = seamldr_install_module(data, data_len);
switch (ret) {
case 0:
- *written = size;
+ *written = data_len;
return FW_UPLOAD_ERR_NONE;
case -EBUSY:
return FW_UPLOAD_ERR_BUSY;
^ permalink raw reply related [flat|nested] 4+ messages in thread