| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * TDX host user interface driver | |
| * | |
| * Copyright (C) 2025 Intel Corporation | |
| */ | |
| #include <linux/device/faux.h> | |
| #include <linux/firmware.h> | |
| #include <linux/module.h> | |
| #include <linux/sysfs.h> | |
| #include <asm/cpu_device_id.h> | |
| #include <asm/seamldr.h> | |
| #include <asm/tdx.h> | |
| static const struct x86_cpu_id tdx_host_ids[] = { | |
| X86_MATCH_FEATURE(X86_FEATURE_TDX_HOST_PLATFORM, NULL), | |
| {} | |
| }; | |
| MODULE_DEVICE_TABLE(x86cpu, tdx_host_ids); | |
| static ssize_t version_show(struct device *dev, struct device_attribute *attr, | |
| char *buf) | |
| { | |
| const struct tdx_sys_info *tdx_sysinfo = tdx_get_sysinfo(); | |
| const struct tdx_sys_info_version *ver; | |
| int ret; | |
| if (!tdx_sysinfo) | |
| return -ENXIO; | |
| /* | |
| * The version number can change during an update. | |
| * Lock out updates while printing the version. | |
| */ | |
| seamldr_lock_module_update(); | |
| ver = &tdx_sysinfo->version; | |
| ret = sysfs_emit(buf, TDX_VERSION_FMT "\n", ver->major_version, | |
| ver->minor_version, | |
| ver->update_version); | |
| seamldr_unlock_module_update(); | |
| return ret; | |
| } | |
| static DEVICE_ATTR_RO(version); | |
| static struct attribute *tdx_host_attrs[] = { | |
| &dev_attr_version.attr, | |
| NULL, | |
| }; | |
| static const struct attribute_group tdx_host_group = { | |
| .attrs = tdx_host_attrs, | |
| }; | |
| static ssize_t seamldr_version_show(struct device *dev, struct device_attribute *attr, | |
| char *buf) | |
| { | |
| struct seamldr_info info; | |
| int ret; | |
| ret = seamldr_get_info(&info); | |
| if (ret) | |
| return ret; | |
| return sysfs_emit(buf, TDX_VERSION_FMT "\n", info.major_version, | |
| info.minor_version, | |
| info.update_version); | |
| } | |
| static ssize_t num_remaining_updates_show(struct device *dev, | |
| struct device_attribute *attr, | |
| char *buf) | |
| { | |
| struct seamldr_info info; | |
| int ret; | |
| ret = seamldr_get_info(&info); | |
| if (ret) | |
| return ret; | |
| 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); | |
| static struct attribute *seamldr_attrs[] = { | |
| &dev_attr_seamldr_version.attr, | |
| &dev_attr_num_remaining_updates.attr, | |
| NULL, | |
| }; | |
| static bool supports_runtime_update(void) | |
| { | |
| const struct tdx_sys_info *sysinfo = tdx_get_sysinfo(); | |
| if (!sysinfo) | |
| return false; | |
| if (!tdx_supports_runtime_update(sysinfo)) | |
| return false; | |
| /* | |
| * This bug makes P-SEAMLDR calls clobber the current VMCS | |
| * which breaks KVM. Avoid P-SEAMLDR calls by hiding all | |
| * attributes if the CPU has this bug. | |
| */ | |
| if (boot_cpu_has_bug(X86_BUG_SEAMRET_INVD_VMCS)) | |
| return false; | |
| return true; | |
| } | |
| static umode_t seamldr_group_visible(struct kobject *kobj, struct attribute *attr, int idx) | |
| { | |
| if (!supports_runtime_update()) | |
| return 0; | |
| return attr->mode; | |
| } | |
| static const struct attribute_group seamldr_group = { | |
| .attrs = seamldr_attrs, | |
| .is_visible = seamldr_group_visible, | |
| }; | |
| static const struct attribute_group *tdx_host_groups[] = { | |
| &tdx_host_group, | |
| &seamldr_group, | |
| NULL, | |
| }; | |
| static enum fw_upload_err tdx_fw_prepare(struct fw_upload *fwl, | |
| 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 data_len, u32 *written) | |
| { | |
| int ret; | |
| ret = seamldr_install_module(data, data_len); | |
| switch (ret) { | |
| case 0: | |
| *written = data_len; | |
| return FW_UPLOAD_ERR_NONE; | |
| default: | |
| return FW_UPLOAD_ERR_FW_INVALID; | |
| } | |
| } | |
| static enum fw_upload_err tdx_fw_poll_complete(struct fw_upload *fwl) | |
| { | |
| /* | |
| * The upload completed during tdx_fw_write(). | |
| * Never poll for completion. | |
| */ | |
| return FW_UPLOAD_ERR_NONE; | |
| } | |
| static void tdx_fw_cancel(struct fw_upload *fwl) | |
| { | |
| /* | |
| * TDX module updates are not cancellable. | |
| * Provide a no-op callback to satisfy fw_upload_ops. | |
| */ | |
| } | |
| 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_deinit(void *tdx_fwl) | |
| { | |
| firmware_upload_unregister(tdx_fwl); | |
| } | |
| static int seamldr_init(struct device *dev) | |
| { | |
| struct fw_upload *tdx_fwl; | |
| if (!supports_runtime_update()) | |
| return 0; | |
| tdx_fwl = firmware_upload_register(THIS_MODULE, dev, "tdx_module", | |
| &tdx_fw_ops, NULL); | |
| if (IS_ERR(tdx_fwl)) | |
| return PTR_ERR(tdx_fwl); | |
| return devm_add_action_or_reset(dev, seamldr_deinit, tdx_fwl); | |
| } | |
| static int tdx_host_probe(struct faux_device *fdev) | |
| { | |
| return seamldr_init(&fdev->dev); | |
| } | |
| static const struct faux_device_ops tdx_host_ops = { | |
| .probe = tdx_host_probe, | |
| }; | |
| static struct faux_device *fdev; | |
| 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, | |
| &tdx_host_ops, | |
| tdx_host_groups); | |
| if (!fdev) | |
| return -ENODEV; | |
| return 0; | |
| } | |
| module_init(tdx_host_init); | |
| static void __exit tdx_host_exit(void) | |
| { | |
| faux_device_destroy(fdev); | |
| } | |
| module_exit(tdx_host_exit); | |
| MODULE_DESCRIPTION("TDX Host Services"); | |
| MODULE_LICENSE("GPL"); |