Linux kernel supports two primary firmware update mechanisms: - request_firmware() - firmware upload (or fw_upload) The former is used by microcode updates, SEV firmware updates, etc. The latter is used by CXL and FPGA firmware updates. One key difference between them is: request_firmware() loads a named file from the filesystem, while fw_upload accepts a bitstream directly from userspace. Use fw_upload for TDX Module updates as loading a named file isn't suitable for TDX (see below for more reasons). Specifically, register TDX faux device with fw_upload framework to expose sysfs interfaces and implement operations to process data blobs supplied by userspace. Implementation notes: 1. P-SEAMLDR processes the entire update at once rather than chunk-by-chunk, so .write() is called only once per update; so the offset should be always 0. 2. An update completes synchronously within .write(), meaning .poll_complete() is only called after the update succeeds and so always returns success Why fw_upload instead of request_firmware()? ============================================ The explicit file selection capabilities of fw_upload is preferred over the implicit file selection of request_firmware() for the following reasons: a. Intel distributes all versions of the TDX Module, allowing admins to load any version rather than always defaulting to the latest. This flexibility is necessary because future extensions may require reverting to a previous version to clear fatal errors. b. Some module version series are platform-specific. For example, the 1.5.x series is for certain platform generations, while the 2.0.x series is intended for others. c. The update policy for TDX Module updates is non-linear at times. The latest TDX Module may not be compatible. For example, TDX Module 1.5.x may be updated to 1.5.y but not to 1.5.y+1. This policy is documented separately in a file released along with each TDX Module release. So, the default policy of "request_firmware()" of "always load latest", is not suitable for TDX. Userspace needs to deploy a more sophisticated policy check (e.g., latest may not be compatible), and there is potential operator choice to consider. Just have userspace pick rather than add kernel mechanism to change the default policy of request_firmware(). Signed-off-by: Chao Gao Reviewed-by: Tony Lindgren --- v4: - make tdx_fwl static [Kai] - don't support update canceling [Yilun] - explain why seamldr_init() doesn't return an error [Kai] - bail out if TDX module updates are not supported [Kai] - name the firmware "tdx_module" instead of "seamldr_upload" [Cedric] v3: - clear "cancel_request" in the "prepare" phase [Binbin] - Don't fail the whole tdx-host device if seamldr_init() met an error [Yilun] - Add kdoc for seamldr_install_module() and verify that the input buffer is vmalloc'd. [Yilun] --- arch/x86/include/asm/seamldr.h | 1 + arch/x86/include/asm/tdx.h | 5 ++ arch/x86/virt/vmx/tdx/seamldr.c | 19 +++++ drivers/virt/coco/tdx-host/Kconfig | 2 + drivers/virt/coco/tdx-host/tdx-host.c | 114 +++++++++++++++++++++++++- 5 files changed, 140 insertions(+), 1 deletion(-) diff --git a/arch/x86/include/asm/seamldr.h b/arch/x86/include/asm/seamldr.h index 954d850e34e3..e354d0bfc9f8 100644 --- a/arch/x86/include/asm/seamldr.h +++ b/arch/x86/include/asm/seamldr.h @@ -32,5 +32,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); #endif /* _ASM_X86_SEAMLDR_H */ diff --git a/arch/x86/include/asm/tdx.h b/arch/x86/include/asm/tdx.h index cb2219302dfc..ffadbf64d0c1 100644 --- a/arch/x86/include/asm/tdx.h +++ b/arch/x86/include/asm/tdx.h @@ -103,6 +103,11 @@ int tdx_enable(void); const char *tdx_dump_mce_info(struct mce *m); const struct tdx_sys_info *tdx_get_sysinfo(void); +static inline bool tdx_supports_runtime_update(const struct tdx_sys_info *sysinfo) +{ + return false; /* To be enabled when kernel is ready */ +} + int tdx_guest_keyid_alloc(void); u32 tdx_get_nr_guest_keyids(void); void tdx_guest_keyid_free(unsigned int keyid); diff --git a/arch/x86/virt/vmx/tdx/seamldr.c b/arch/x86/virt/vmx/tdx/seamldr.c index d17db3c0151e..4d40b08f9bed 100644 --- a/arch/x86/virt/vmx/tdx/seamldr.c +++ b/arch/x86/virt/vmx/tdx/seamldr.c @@ -6,6 +6,7 @@ */ #define pr_fmt(fmt) "seamldr: " fmt +#include #include #include @@ -38,3 +39,21 @@ int seamldr_get_info(struct seamldr_info *seamldr_info) return seamldr_call(P_SEAMLDR_INFO, &args); } EXPORT_SYMBOL_FOR_MODULES(seamldr_get_info, "tdx-host"); + +/** + * seamldr_install_module - Install a new TDX module + * @data: Pointer to the TDX module update blob. It should be vmalloc'd + * memory. + * @size: Size of the TDX module update blob + * + * Returns 0 on success, negative error code on failure. + */ +int seamldr_install_module(const u8 *data, u32 size) +{ + if (WARN_ON_ONCE(!is_vmalloc_addr(data))) + return -EINVAL; + + /* TODO: Update TDX Module here */ + return 0; +} +EXPORT_SYMBOL_FOR_MODULES(seamldr_install_module, "tdx-host"); diff --git a/drivers/virt/coco/tdx-host/Kconfig b/drivers/virt/coco/tdx-host/Kconfig index e58bad148a35..3d580d783106 100644 --- a/drivers/virt/coco/tdx-host/Kconfig +++ b/drivers/virt/coco/tdx-host/Kconfig @@ -1,6 +1,8 @@ config TDX_HOST_SERVICES tristate "TDX Host Services Driver" depends on INTEL_TDX_HOST + select FW_LOADER + select FW_UPLOAD default m help Enable access to TDX host services like module update and diff --git a/drivers/virt/coco/tdx-host/tdx-host.c b/drivers/virt/coco/tdx-host/tdx-host.c index fd6ffb4f2ff1..9ade3028a5bd 100644 --- a/drivers/virt/coco/tdx-host/tdx-host.c +++ b/drivers/virt/coco/tdx-host/tdx-host.c @@ -6,6 +6,7 @@ */ #include +#include #include #include #include @@ -20,6 +21,8 @@ static const struct x86_cpu_id tdx_host_ids[] = { }; MODULE_DEVICE_TABLE(x86cpu, tdx_host_ids); +static struct fw_upload *tdx_fwl; + static ssize_t version_show(struct device *dev, struct device_attribute *attr, char *buf) { @@ -103,6 +106,115 @@ static const struct attribute_group *tdx_host_groups[] = { NULL, }; +static enum fw_upload_err tdx_fw_prepare(struct fw_upload *fwl, + const u8 *data, u32 size) +{ + 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) +{ + int ret; + + /* + * tdx_fw_write() always processes all data on the first call with + * offset == 0. Since it never returns partial success (it either + * succeeds completely or fails), there is no subsequent call with + * non-zero offsets. + */ + WARN_ON_ONCE(offset); + ret = seamldr_install_module(data, size); + switch (ret) { + case 0: + *written = size; + return FW_UPLOAD_ERR_NONE; + case -EBUSY: + return FW_UPLOAD_ERR_BUSY; + case -EIO: + return FW_UPLOAD_ERR_HW_ERROR; + case -ENOSPC: + return FW_UPLOAD_ERR_WEAROUT; + case -ENOMEM: + return FW_UPLOAD_ERR_RW_ERROR; + default: + return FW_UPLOAD_ERR_FW_INVALID; + } +} + +static enum fw_upload_err tdx_fw_poll_complete(struct fw_upload *fwl) +{ + /* + * TDX Module updates are completed in the previous phase + * (tdx_fw_write()). If any error occurred, the previous phase + * would return an error code to abort the update process. In + * other words, reaching this point means the update succeeded. + */ + return FW_UPLOAD_ERR_NONE; +} + +/* + * TDX Module updates cannot be cancelled. Provide a stub function since + * the firmware upload framework requires a .cancel operation. + */ +static void tdx_fw_cancel(struct fw_upload *fwl) +{ +} + +static const struct fw_upload_ops tdx_fw_ops = { + .prepare = tdx_fw_prepare, + .write = tdx_fw_write, + .poll_complete = tdx_fw_poll_complete, + .cancel = tdx_fw_cancel, +}; + +static void seamldr_init(struct device *dev) +{ + const struct tdx_sys_info *tdx_sysinfo = tdx_get_sysinfo(); + int ret; + + if (WARN_ON_ONCE(!tdx_sysinfo)) + return; + + if (!tdx_supports_runtime_update(tdx_sysinfo)) { + pr_info("Current TDX Module cannot be updated. Consider BIOS updates\n"); + return; + } + + tdx_fwl = firmware_upload_register(THIS_MODULE, dev, "tdx_module", + &tdx_fw_ops, NULL); + ret = PTR_ERR_OR_ZERO(tdx_fwl); + if (ret) + pr_err("failed to register module uploader %d\n", ret); +} + +static void seamldr_deinit(void) +{ + if (tdx_fwl) + firmware_upload_unregister(tdx_fwl); +} + +static int tdx_host_probe(struct faux_device *fdev) +{ + /* + * P-SEAMLDR capabilities are optional. Don't fail the entire + * device probe if initialization fails. + */ + seamldr_init(&fdev->dev); + + return 0; +} + +static void tdx_host_remove(struct faux_device *fdev) +{ + seamldr_deinit(); +} + +static struct faux_device_ops tdx_host_ops = { + .probe = tdx_host_probe, + .remove = tdx_host_remove, +}; + static struct faux_device *fdev; static int __init tdx_host_init(void) @@ -110,7 +222,7 @@ static int __init tdx_host_init(void) if (!x86_match_cpu(tdx_host_ids) || !tdx_get_sysinfo()) return -ENODEV; - fdev = faux_device_create_with_groups(KBUILD_MODNAME, NULL, NULL, tdx_host_groups); + fdev = faux_device_create_with_groups(KBUILD_MODNAME, NULL, &tdx_host_ops, tdx_host_groups); if (!fdev) return -ENODEV; -- 2.47.3